Skip to content

Commit

Permalink
Merge branch 'main' into chore/sqflite
Browse files Browse the repository at this point in the history
  • Loading branch information
marandaneto committed Mar 17, 2023
2 parents 8daa8e6 + 5aab4c5 commit 232f68e
Show file tree
Hide file tree
Showing 12 changed files with 254 additions and 5 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@
### Features

- sqflite Support for Flutter ([#1306](https://github.com/getsentry/sentry-dart/pull/1306))
- Exception StackTrace Extractor ([#1335](https://github.com/getsentry/sentry-dart/pull/1335))

### Dependencies

- Bump Cocoa SDK from v8.0.0 to v8.3.1 ([#1331](https://github.com/getsentry/sentry-dart/pull/1331))
- [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#831)
- [diff](https://github.com/getsentry/sentry-cocoa/compare/8.0.0...8.3.1)

### Fixes

- SentryUserInteractionWidget checks if the Elements are mounted before comparing them ([#1339](https://github.com/getsentry/sentry-dart/pull/1339))

## 7.0.0

### Features
Expand Down
1 change: 1 addition & 0 deletions dart/lib/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export 'src/type_check_hint.dart';
// exception extraction
export 'src/exception_cause_extractor.dart';
export 'src/exception_cause.dart';
export 'src/exception_stacktrace_extractor.dart';
// Isolates
export 'src/sentry_isolate_extension.dart';
export 'src/sentry_isolate.dart';
35 changes: 35 additions & 0 deletions dart/lib/src/exception_stacktrace_extractor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'protocol.dart';
import 'sentry_options.dart';

/// Sentry handles [Error.stackTrace] by default. For other cases
/// extend this abstract class and return a custom [StackTrace] of your
/// exceptions.
///
/// Implementing an extractor and providing it through
/// [SentryOptions.addExceptionStackTraceExtractor] will enable the framework to
/// extract the inner stacktrace and add it to [SentryException] when no other
/// stacktrace was provided while capturing the event.
///
/// For an example on how to use the API refer to dio/DioStackTraceExtractor or the
/// code below:
///
/// ```dart
/// class ExceptionWithInner {
/// ExceptionWithInner(this.innerException, this.innerStackTrace);
/// Object innerException;
/// dynamic innerStackTrace;
/// }
///
/// class ExceptionWithInnerStackTraceExtractor extends ExceptionStackTraceExtractor<ExceptionWithInner> {
/// @override
/// dynamic cause(ExceptionWithInner error) {
/// return error.innerStackTrace;
/// }
/// }
///
/// options.addExceptionStackTraceExtractor(ExceptionWithInnerStackTraceExtractor());
/// ```
abstract class ExceptionStackTraceExtractor<T> {
dynamic stackTrace(T error);
Type get exceptionType => T;
}
4 changes: 4 additions & 0 deletions dart/lib/src/sentry_exception_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ class SentryExceptionFactory {
if (throwable is Error) {
stackTrace ??= throwable.stackTrace;
}
stackTrace ??= _options
.exceptionStackTraceExtractor(throwable.runtimeType)
?.stackTrace(throwable);

// throwable.stackTrace is null if its an exception that was never thrown
// hence we check again if stackTrace is null and if not, read the current stack trace
// but only if attachStacktrace is enabled
Expand Down
18 changes: 15 additions & 3 deletions dart/lib/src/sentry_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -331,16 +331,28 @@ class SentryOptions {
/// The default is 3 seconds.
Duration? idleTimeout = Duration(seconds: 3);

final _extractorsByType = <Type, ExceptionCauseExtractor>{};
final _causeExtractorsByType = <Type, ExceptionCauseExtractor>{};

final _stackTraceExtractorsByType = <Type, ExceptionStackTraceExtractor>{};

/// Returns a previously added [ExceptionCauseExtractor] by type
ExceptionCauseExtractor? exceptionCauseExtractor(Type type) {
return _extractorsByType[type];
return _causeExtractorsByType[type];
}

/// Adds [ExceptionCauseExtractor] in order to extract inner exceptions
void addExceptionCauseExtractor(ExceptionCauseExtractor extractor) {
_extractorsByType[extractor.exceptionType] = extractor;
_causeExtractorsByType[extractor.exceptionType] = extractor;
}

/// Returns a previously added [ExceptionStackTraceExtractor] by type
ExceptionStackTraceExtractor? exceptionStackTraceExtractor(Type type) {
return _stackTraceExtractorsByType[type];
}

/// Adds [ExceptionStackTraceExtractor] in order to extract inner exceptions
void addExceptionStackTraceExtractor(ExceptionStackTraceExtractor extractor) {
_stackTraceExtractorsByType[extractor.exceptionType] = extractor;
}

/// Changed SDK behaviour when set to true:
Expand Down
39 changes: 39 additions & 0 deletions dart/test/sentry_client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,32 @@ void main() {
expect(capturedEvent.exceptions?[1].stackTrace!.frames.first.colNo, 9);
});

test('should capture custom stacktrace', () async {
fixture.options.addExceptionStackTraceExtractor(
ExceptionWithStackTraceExtractor(),
);

final stackTrace = StackTrace.fromString('''
#0 baz (file:///pathto/test.dart:50:3)
<asynchronous suspension>
#1 bar (file:///pathto/test.dart:46:9)
''');

exception = ExceptionWithStackTrace(stackTrace);

final client = fixture.getSut(attachStacktrace: true);
await client.captureException(exception, stackTrace: null);

final capturedEnvelope = (fixture.transport).envelopes.first;
final capturedEvent = await eventFromEnvelope(capturedEnvelope);

expect(capturedEvent.exceptions?[0].stackTrace, isNotNull);
expect(capturedEvent.exceptions?[0].stackTrace!.frames.first.fileName,
'test.dart');
expect(capturedEvent.exceptions?[0].stackTrace!.frames.first.lineNo, 46);
expect(capturedEvent.exceptions?[0].stackTrace!.frames.first.colNo, 9);
});

test('should not capture cause stacktrace when attachStacktrace is false',
() async {
fixture.options.addExceptionCauseExtractor(
Expand Down Expand Up @@ -1638,3 +1664,16 @@ class ExceptionWithCauseExtractor
return ExceptionCause(error.cause, error.stackTrace);
}
}

class ExceptionWithStackTrace {
ExceptionWithStackTrace(this.stackTrace);
final StackTrace stackTrace;
}

class ExceptionWithStackTraceExtractor
extends ExceptionStackTraceExtractor<ExceptionWithStackTrace> {
@override
StackTrace? stackTrace(ExceptionWithStackTrace error) {
return error.stackTrace;
}
}
51 changes: 51 additions & 0 deletions dart/test/sentry_exception_factory_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,43 @@ void main() {
expect(sentryException.stackTrace!.frames.first.fileName, 'test.dart');
});

test('should extract stackTrace from custom exception', () {
fixture.options
.addExceptionStackTraceExtractor(CustomExceptionStackTraceExtractor());

SentryException sentryException;
try {
throw CustomException(StackTrace.fromString('''
#0 baz (file:///pathto/test.dart:50:3)
<asynchronous suspension>
#1 bar (file:///pathto/test.dart:46:9)
'''));
} catch (err, _) {
sentryException = fixture.getSut().getSentryException(
err,
);
}

expect(sentryException.type, 'CustomException');
expect(sentryException.stackTrace!.frames.first.lineNo, 46);
expect(sentryException.stackTrace!.frames.first.colNo, 9);
expect(sentryException.stackTrace!.frames.first.fileName, 'test.dart');
});

test('should not fail when stackTrace property does not exist', () {
SentryException sentryException;
try {
throw Object();
} catch (err, _) {
sentryException = fixture.getSut().getSentryException(
err,
);
}

expect(sentryException.type, 'Object');
expect(sentryException.stackTrace, isNotNull);
});

test('getSentryException with not thrown Error and frames', () {
final sentryException = fixture.getSut().getSentryException(
CustomError(),
Expand Down Expand Up @@ -136,6 +173,20 @@ void main() {

class CustomError extends Error {}

class CustomException implements Exception {
final StackTrace stackTrace;

CustomException(this.stackTrace);
}

class CustomExceptionStackTraceExtractor
extends ExceptionStackTraceExtractor<CustomException> {
@override
StackTrace? stackTrace(CustomException error) {
return error.stackTrace;
}
}

class Fixture {
final options = SentryOptions(dsn: fakeDsn);

Expand Down
10 changes: 10 additions & 0 deletions dio/lib/src/dio_stacktrace_extractor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'package:dio/dio.dart';
import 'package:sentry/sentry.dart';

/// Extracts the inner stacktrace from [DioError]
class DioStackTraceExtractor extends ExceptionStackTraceExtractor<DioError> {
@override
StackTrace? stackTrace(DioError error) {
return error.stackTrace;
}
}
8 changes: 7 additions & 1 deletion dio/lib/src/sentry_dio_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
import 'package:sentry/sentry.dart';
import 'dio_error_extractor.dart';
import 'dio_event_processor.dart';
import 'dio_stacktrace_extractor.dart';
import 'failed_request_interceptor.dart';
import 'sentry_transformer.dart';
import 'sentry_dio_client_adapter.dart';
Expand Down Expand Up @@ -51,11 +52,16 @@ extension SentryDioExtension on Dio {
// ignore: invalid_use_of_internal_member
final options = hub.options;

// Add to get inner exception & stacktrace
// Add to get inner exception
if (options.exceptionCauseExtractor(DioError) == null) {
options.addExceptionCauseExtractor(DioErrorExtractor());
}

// Add to get inner stacktrace
if (options.exceptionStackTraceExtractor(DioError) == null) {
options.addExceptionStackTraceExtractor(DioStackTraceExtractor());
}

// Add DioEventProcessor when it's not already present
if (options.eventProcessors.whereType<DioEventProcessor>().isEmpty) {
options.sdk.addIntegration('sentry_dio');
Expand Down
49 changes: 49 additions & 0 deletions dio/test/dio_stacktrace_extractor_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import 'package:dio/dio.dart';
import 'package:sentry_dio/src/dio_stacktrace_extractor.dart';
import 'package:test/test.dart';

void main() {
late Fixture fixture;

setUp(() {
fixture = Fixture();
});

group(DioStackTraceExtractor, () {
test('extracts stacktrace', () {
final sut = fixture.getSut();
final exception = Exception('foo bar');
final stacktrace = StackTrace.current;

final dioError = DioError(
error: exception,
requestOptions: RequestOptions(path: '/foo/bar'),
stackTrace: stacktrace,
);

final result = sut.stackTrace(dioError);

expect(result, stacktrace);
});

test('extracts nothing with missing stacktrace', () {
final sut = fixture.getSut();
final exception = Exception('foo bar');

final dioError = DioError(
error: exception,
requestOptions: RequestOptions(path: '/foo/bar'),
);

final result = sut.stackTrace(dioError);

expect(result, isNull);
});
});
}

class Fixture {
DioStackTraceExtractor getSut() {
return DioStackTraceExtractor();
}
}
12 changes: 12 additions & 0 deletions dio/test/sentry_dio_extension_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:dio/dio.dart';
import 'package:sentry_dio/sentry_dio.dart';
import 'package:sentry_dio/src/dio_error_extractor.dart';
import 'package:sentry_dio/src/dio_stacktrace_extractor.dart';
import 'package:sentry_dio/src/sentry_dio_client_adapter.dart';
import 'package:sentry_dio/src/sentry_dio_extension.dart';
import 'package:sentry_dio/src/sentry_transformer.dart';
Expand Down Expand Up @@ -70,6 +71,17 @@ void main() {
);
});

test('addSentry adds $DioStackTraceExtractor', () {
final dio = fixture.getSut();

dio.addSentry(hub: fixture.hub);

expect(
fixture.hub.options.exceptionStackTraceExtractor(DioError),
isNotNull,
);
});

test('addSentry adds integration to sdk', () {
final dio = fixture.getSut();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,11 @@ class _SentryUserInteractionWidgetState
);

final activeTransaction = _activeTransaction;
final lastElement = _lastTappedWidget?.element;
if (activeTransaction != null) {
if (_lastTappedWidget?.element.widget == element.widget &&
if (_isElementMounted(lastElement) &&
_isElementMounted(element) &&
lastElement?.widget == element.widget &&
_lastTappedWidget?.eventType == tappedWidget.eventType &&
!activeTransaction.finished) {
// ignore: invalid_use_of_internal_member
Expand Down Expand Up @@ -339,4 +342,26 @@ class _SentryUserInteractionWidgetState

return null;
}

bool _isElementMounted(Element? element) {
if (element == null) {
return false;
}
try {
// ignore: return_of_invalid_type
return (element as dynamic).mounted;
} on NoSuchMethodError catch (_) {
// mounted checks if the widget is not null.

try {
// Flutter 3.0.0 does `_widget!` and if `_widget` is null it throws.

// ignore: unnecessary_null_comparison
return element.widget != null;
} catch (_) {
// if it throws, the `_widget` is null and not mounted.
return false;
}
}
}
}

0 comments on commit 232f68e

Please sign in to comment.