From 3bbfb14710811468c5aace40bcca274a752ee9b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Andra=C5=A1ec?= Date: Mon, 23 Jan 2023 15:12:57 +0000 Subject: [PATCH] Feat: Error Cause Extractor (#1198) --- CHANGELOG.md | 5 + dart/lib/sentry.dart | 3 + dart/lib/src/exception_cause.dart | 6 + dart/lib/src/exception_cause_extractor.dart | 40 +++++ dart/lib/src/protocol/sentry_exception.dart | 5 + dart/lib/src/sentry_client.dart | 50 +++--- dart/lib/src/sentry_exception_factory.dart | 4 + dart/lib/src/sentry_options.dart | 10 ++ dart/test/exception_cause_extractor_test.dart | 140 ++++++++++++++++ dart/test/sentry_client_test.dart | 150 ++++++++++++++++++ dart/test/sentry_exception_factory_test.dart | 16 ++ dio/lib/src/dio_error_extractor.dart | 16 ++ dio/lib/src/dio_event_processor.dart | 87 ++-------- dio/lib/src/sentry_dio_extension.dart | 4 + dio/test/dio_error_extractor_test.dart | 61 +++++++ dio/test/dio_event_processor_test.dart | 141 ++++++++++------ 16 files changed, 594 insertions(+), 144 deletions(-) create mode 100644 dart/lib/src/exception_cause.dart create mode 100644 dart/lib/src/exception_cause_extractor.dart create mode 100644 dart/test/exception_cause_extractor_test.dart create mode 100644 dio/lib/src/dio_error_extractor.dart create mode 100644 dio/test/dio_error_extractor_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 90bad25ce5..6e0fee0e70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +### Features + +- Error Cause Extractor ([#1198](https://github.com/getsentry/sentry-dart/pull/1198)) + - Add `throwable` to `SentryException` + ### Fixes - Don't suppress error logs ([#1228](https://github.com/getsentry/sentry-dart/pull/1228)) diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index 4c0633a17e..c5439d6329 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -32,3 +32,6 @@ export 'src/utils/tracing_utils.dart'; export 'src/tracing.dart'; export 'src/hint.dart'; export 'src/type_check_hint.dart'; +// exception extraction +export 'src/exception_cause_extractor.dart'; +export 'src/exception_cause.dart'; diff --git a/dart/lib/src/exception_cause.dart b/dart/lib/src/exception_cause.dart new file mode 100644 index 0000000000..94be20e65a --- /dev/null +++ b/dart/lib/src/exception_cause.dart @@ -0,0 +1,6 @@ +class ExceptionCause { + ExceptionCause(this.exception, this.stackTrace); + + dynamic exception; + dynamic stackTrace; +} diff --git a/dart/lib/src/exception_cause_extractor.dart b/dart/lib/src/exception_cause_extractor.dart new file mode 100644 index 0000000000..bdd8535e48 --- /dev/null +++ b/dart/lib/src/exception_cause_extractor.dart @@ -0,0 +1,40 @@ +import 'exception_cause.dart'; +import 'sentry_options.dart'; +import 'throwable_mechanism.dart'; + +abstract class ExceptionCauseExtractor { + ExceptionCause? cause(T error); + Type get exceptionType => T; +} + +class RecursiveExceptionCauseExtractor { + RecursiveExceptionCauseExtractor(this._options); + + final SentryOptions _options; + + List flatten(exception, stackTrace) { + final allExceptionCauses = []; + final circularityDetector = {}; + + var currentException = exception; + ExceptionCause? currentExceptionCause = + ExceptionCause(exception, stackTrace); + + while (currentException != null && + currentExceptionCause != null && + circularityDetector.add(currentException)) { + allExceptionCauses.add(currentExceptionCause); + + final extractionSourceSource = currentException is ThrowableMechanism + ? currentException.throwable + : currentException; + + final extractor = + _options.exceptionCauseExtractor(extractionSourceSource.runtimeType); + + currentExceptionCause = extractor?.cause(extractionSourceSource); + currentException = currentExceptionCause?.exception; + } + return allExceptionCauses; + } +} diff --git a/dart/lib/src/protocol/sentry_exception.dart b/dart/lib/src/protocol/sentry_exception.dart index 3b3bab7924..45de1b5c9c 100644 --- a/dart/lib/src/protocol/sentry_exception.dart +++ b/dart/lib/src/protocol/sentry_exception.dart @@ -23,6 +23,8 @@ class SentryException { /// Represents a [SentryThread.id]. final int? threadId; + final dynamic throwable; + const SentryException({ required this.type, required this.value, @@ -30,6 +32,7 @@ class SentryException { this.stackTrace, this.mechanism, this.threadId, + this.throwable, }); /// Deserializes a [SentryException] from JSON [Map]. @@ -68,6 +71,7 @@ class SentryException { SentryStackTrace? stackTrace, Mechanism? mechanism, int? threadId, + dynamic throwable, }) => SentryException( type: type ?? this.type, @@ -76,5 +80,6 @@ class SentryException { stackTrace: stackTrace ?? this.stackTrace, mechanism: mechanism ?? this.mechanism, threadId: threadId ?? this.threadId, + throwable: throwable ?? this.throwable, ); } diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 5988999673..75698d8322 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -182,37 +182,43 @@ class SentryClient { final isolateId = isolateName?.hashCode; if (event.throwableMechanism != null) { - var sentryException = _exceptionFactory.getSentryException( - event.throwableMechanism, - stackTrace: stackTrace, - ); + final extractedExceptions = _exceptionFactory.extractor + .flatten(event.throwableMechanism, stackTrace); + + final sentryExceptions = []; + final sentryThreads = []; - if (_options.platformChecker.isWeb) { - return event.copyWith( - exceptions: [ - ...?event.exceptions, - sentryException, - ], + for (final extractedException in extractedExceptions) { + var sentryException = _exceptionFactory.getSentryException( + extractedException.exception, + stackTrace: extractedException.stackTrace, ); - } - SentryThread? thread; + SentryThread? sentryThread; - if (isolateName != null && _options.attachThreads) { - sentryException = sentryException.copyWith(threadId: isolateId); - thread = SentryThread( - id: isolateId, - name: isolateName, - crashed: true, - current: true, - ); + if (!_options.platformChecker.isWeb && + isolateName != null && + _options.attachThreads) { + sentryException = sentryException.copyWith(threadId: isolateId); + sentryThread = SentryThread( + id: isolateId, + name: isolateName, + crashed: true, + current: true, + ); + } + + sentryExceptions.add(sentryException); + if (sentryThread != null) { + sentryThreads.add(sentryThread); + } } return event.copyWith( - exceptions: [...?event.exceptions, sentryException], + exceptions: [...?event.exceptions, ...sentryExceptions], threads: [ ...?event.threads, - if (thread != null) thread, + ...sentryThreads, ], ); } diff --git a/dart/lib/src/sentry_exception_factory.dart b/dart/lib/src/sentry_exception_factory.dart index 575ed4f0a9..7058c7d793 100644 --- a/dart/lib/src/sentry_exception_factory.dart +++ b/dart/lib/src/sentry_exception_factory.dart @@ -1,3 +1,4 @@ +import 'exception_cause_extractor.dart'; import 'protocol.dart'; import 'sentry_options.dart'; import 'sentry_stack_trace_factory.dart'; @@ -9,6 +10,8 @@ class SentryExceptionFactory { SentryStackTraceFactory get _stacktraceFactory => _options.stackTraceFactory; + late final extractor = RecursiveExceptionCauseExtractor(_options); + SentryExceptionFactory(this._options); SentryException getSentryException( @@ -57,6 +60,7 @@ class SentryExceptionFactory { value: throwable.toString(), mechanism: mechanism, stackTrace: sentryStackTrace, + throwable: throwable, ); } } diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index ca04f83da8..745a964a58 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -327,6 +327,16 @@ class SentryOptions { /// The default is 3 seconds. Duration? idleTimeout = Duration(seconds: 3); + final _extractorsByType = {}; + + ExceptionCauseExtractor? exceptionCauseExtractor(Type type) { + return _extractorsByType[type]; + } + + void addExceptionCauseExtractor(ExceptionCauseExtractor extractor) { + _extractorsByType[extractor.exceptionType] = extractor; + } + SentryOptions({this.dsn, PlatformChecker? checker}) { if (checker != null) { platformChecker = checker; diff --git a/dart/test/exception_cause_extractor_test.dart b/dart/test/exception_cause_extractor_test.dart new file mode 100644 index 0000000000..7fb36cd0cf --- /dev/null +++ b/dart/test/exception_cause_extractor_test.dart @@ -0,0 +1,140 @@ +import 'package:sentry/src/exception_cause.dart'; +import 'package:sentry/src/exception_cause_extractor.dart'; +import 'package:sentry/src/protocol/mechanism.dart'; +import 'package:sentry/src/sentry_options.dart'; +import 'package:sentry/src/throwable_mechanism.dart'; +import 'package:test/test.dart'; +import 'mocks.dart'; + +void main() { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('flatten', () { + final errorC = ExceptionC(); + final errorB = ExceptionB(errorC); + final errorA = ExceptionA(errorB); + + fixture.options.addExceptionCauseExtractor( + ExceptionACauseExtractor(), + ); + + fixture.options.addExceptionCauseExtractor( + ExceptionBCauseExtractor(), + ); + + final sut = fixture.getSut(); + + final flattened = sut.flatten(errorA, null); + final actual = flattened.map((exceptionCause) => exceptionCause.exception); + expect(actual, [errorA, errorB, errorC]); + }); + + test('flatten breaks circularity', () { + final a = ExceptionCircularA(); + final b = ExceptionCircularB(); + a.other = b; + b.other = a; + + fixture.options.addExceptionCauseExtractor( + ExceptionCircularAExtractor(), + ); + + fixture.options.addExceptionCauseExtractor( + ExceptionCircularBExtractor(), + ); + + final sut = fixture.getSut(); + + final flattened = sut.flatten(a, null); + final actual = flattened.map((exceptionCause) => exceptionCause.exception); + + expect(actual, [a, b]); + }); + + test('flatten preserves throwable mechanism', () { + final errorC = ExceptionC(); + final errorB = ExceptionB(errorC); + final errorA = ExceptionA(errorB); + + fixture.options.addExceptionCauseExtractor( + ExceptionACauseExtractor(), + ); + + fixture.options.addExceptionCauseExtractor( + ExceptionBCauseExtractor(), + ); + + final mechanism = Mechanism(type: "foo"); + final throwableMechanism = ThrowableMechanism(mechanism, errorA); + + final sut = fixture.getSut(); + final flattened = sut.flatten(throwableMechanism, null); + + final actual = flattened.map((exceptionCause) => exceptionCause.exception); + expect(actual, [throwableMechanism, errorB, errorC]); + }); +} + +class Fixture { + final options = SentryOptions(dsn: fakeDsn); + + RecursiveExceptionCauseExtractor getSut() { + return RecursiveExceptionCauseExtractor(options); + } +} + +class ExceptionA { + ExceptionA(this.other); + final ExceptionB? other; +} + +class ExceptionB { + ExceptionB(this.anotherOther); + final ExceptionC? anotherOther; +} + +class ExceptionC { + // I am empty inside +} + +class ExceptionACauseExtractor extends ExceptionCauseExtractor { + @override + ExceptionCause? cause(ExceptionA error) { + return ExceptionCause(error.other, null); + } +} + +class ExceptionBCauseExtractor extends ExceptionCauseExtractor { + @override + ExceptionCause? cause(ExceptionB error) { + return ExceptionCause(error.anotherOther, null); + } +} + +class ExceptionCircularA { + ExceptionCircularB? other; +} + +class ExceptionCircularB { + ExceptionCircularA? other; +} + +class ExceptionCircularAExtractor + extends ExceptionCauseExtractor { + @override + ExceptionCause? cause(ExceptionCircularA error) { + return ExceptionCause(error.other, null); + } +} + +class ExceptionCircularBExtractor + extends ExceptionCauseExtractor { + @override + ExceptionCause? cause(ExceptionCircularB error) { + return ExceptionCause(error.other, null); + } +} diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 53c1122eb2..f5f9263824 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -237,6 +237,142 @@ void main() { }); }); + group('SentryClient captures exception cause', () { + dynamic exception; + + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('should capture exception cause', () async { + fixture.options.addExceptionCauseExtractor( + ExceptionWithCauseExtractor(), + ); + + final cause = Object(); + exception = ExceptionWithCause(cause, null); + + final client = fixture.getSut(); + await client.captureException(exception, stackTrace: null); + + final capturedEnvelope = (fixture.transport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); + + expect(capturedEvent.exceptions?[0] is SentryException, true); + expect(capturedEvent.exceptions?[1] is SentryException, true); + }); + + test('should capture cause stacktrace', () async { + fixture.options.addExceptionCauseExtractor( + ExceptionWithCauseExtractor(), + ); + + final cause = Object(); + final stackTrace = ''' +#0 baz (file:///pathto/test.dart:50:3) + +#1 bar (file:///pathto/test.dart:46:9) + '''; + + exception = ExceptionWithCause(cause, 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?[1].stackTrace, isNotNull); + expect(capturedEvent.exceptions?[1].stackTrace!.frames.first.fileName, + 'test.dart'); + expect(capturedEvent.exceptions?[1].stackTrace!.frames.first.lineNo, 46); + expect(capturedEvent.exceptions?[1].stackTrace!.frames.first.colNo, 9); + }); + + test('should not capture cause stacktrace when attachStacktrace is false', + () async { + fixture.options.addExceptionCauseExtractor( + ExceptionWithCauseExtractor(), + ); + + final cause = Object(); + exception = ExceptionWithCause(cause, null); + + final client = fixture.getSut(attachStacktrace: false); + await client.captureException(exception, stackTrace: null); + + final capturedEnvelope = (fixture.transport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); + + expect(capturedEvent.exceptions?[1].stackTrace, isNull); + }); + + test( + 'should not capture cause stacktrace when attachStacktrace is false and StackTrace.empty', + () async { + fixture.options.addExceptionCauseExtractor( + ExceptionWithCauseExtractor(), + ); + + final cause = Object(); + exception = ExceptionWithCause(cause, StackTrace.empty); + + final client = fixture.getSut(attachStacktrace: false); + await client.captureException(exception, stackTrace: null); + + final capturedEnvelope = (fixture.transport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); + + expect(capturedEvent.exceptions?[1].stackTrace, isNull); + }); + + test('should capture cause exception with Stackframe.current', () async { + fixture.options.addExceptionCauseExtractor( + ExceptionWithCauseExtractor(), + ); + + final cause = Object(); + exception = ExceptionWithCause(cause, null); + + 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?[1].stackTrace, isNotNull); + }); + + test('should not capture sentry frames exception', () async { + fixture.options.addExceptionCauseExtractor( + ExceptionWithCauseExtractor(), + ); + + final cause = Object(); + final stackTrace = ''' +#0 init (package:sentry/sentry.dart:46:9) +#1 bar (file:///pathto/test.dart:46:9) + +#2 capture (package:sentry/sentry.dart:46:9) + '''; + exception = ExceptionWithCause(cause, 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?[1].stackTrace!.frames + .every((frame) => frame.package != 'sentry'), + true, + ); + }); + }); + group('SentryClient captures exception and stacktrace', () { late Fixture fixture; @@ -1389,3 +1525,17 @@ class Fixture { loggedException = exception; } } + +class ExceptionWithCause { + ExceptionWithCause(this.cause, this.stackTrace); + final dynamic cause; + final dynamic stackTrace; +} + +class ExceptionWithCauseExtractor + extends ExceptionCauseExtractor { + @override + ExceptionCause? cause(ExceptionWithCause error) { + return ExceptionCause(error.cause, error.stackTrace); + } +} diff --git a/dart/test/sentry_exception_factory_test.dart b/dart/test/sentry_exception_factory_test.dart index 23f577173e..70d1d30e5f 100644 --- a/dart/test/sentry_exception_factory_test.dart +++ b/dart/test/sentry_exception_factory_test.dart @@ -116,6 +116,22 @@ void main() { expect(sentryException.stackTrace!.snapshot, true); }); + + test('getSentryException adds throwable', () { + SentryException sentryException; + dynamic throwable; + try { + throw StateError('a state error'); + } catch (err, stacktrace) { + throwable = err; + sentryException = fixture.getSut().getSentryException( + err, + stackTrace: stacktrace, + ); + } + + expect(sentryException.throwable, throwable); + }); } class CustomError extends Error {} diff --git a/dio/lib/src/dio_error_extractor.dart b/dio/lib/src/dio_error_extractor.dart new file mode 100644 index 0000000000..a16284f9fd --- /dev/null +++ b/dio/lib/src/dio_error_extractor.dart @@ -0,0 +1,16 @@ +import 'package:dio/dio.dart'; +import 'package:sentry/sentry.dart'; + +/// Extracts the inner exception and stacktrace from [DioError] +class DioErrorExtractor extends ExceptionCauseExtractor { + @override + ExceptionCause? cause(DioError error) { + if (error.stackTrace == null) { + return null; + } + return ExceptionCause( + error.error ?? 'DioError inner stacktrace', + error.stackTrace, + ); + } +} diff --git a/dio/lib/src/dio_event_processor.dart b/dio/lib/src/dio_event_processor.dart index 7f17a4bc69..3213ae0960 100644 --- a/dio/lib/src/dio_event_processor.dart +++ b/dio/lib/src/dio_event_processor.dart @@ -2,29 +2,29 @@ import 'dart:async'; import 'package:dio/dio.dart'; import 'package:sentry/sentry.dart'; -// ignore: implementation_imports -import 'package:sentry/src/sentry_exception_factory.dart'; /// This is an [EventProcessor], which improves crash reports of [DioError]s. /// It adds information about [DioError.requestOptions] if present and also about /// the inner exceptions. class DioEventProcessor implements EventProcessor { - // Because of obfuscation, we need to dynamically get the name - static final _dioErrorType = (DioError).toString(); - /// This is an [EventProcessor], which improves crash reports of [DioError]s. DioEventProcessor(this._options); final SentryOptions _options; - SentryExceptionFactory get _sentryExceptionFactory => - // ignore: invalid_use_of_internal_member - _options.exceptionFactory; - @override FutureOr apply(SentryEvent event, {Hint? hint}) { - final dynamic dioError = event.throwable; - if (dioError is! DioError) { + DioError? dioError; + + for (final exception in event.exceptions ?? []) { + final throwable = exception.throwable; + if (throwable is DioError) { + dioError = throwable; + break; + } + } + + if (dioError == null) { return event; } @@ -41,74 +41,9 @@ class DioEventProcessor implements EventProcessor { contexts: contexts, ); - final innerDioStackTrace = dioError.stackTrace; - final innerDioErrorException = dioError.error as Object?; - - // If the inner errors stacktrace is null, - // there's nothing to create chained exception - if (innerDioStackTrace == null) { - return event; - } - - try { - final innerException = _sentryExceptionFactory.getSentryException( - innerDioErrorException ?? 'DioError inner stacktrace', - stackTrace: innerDioStackTrace, - ); - - final exceptions = _removeDioErrorStackTraceFromValue( - List.from(event.exceptions ?? []), - dioError, - ); - - return event.copyWith( - exceptions: [ - innerException, - ...exceptions, - ], - ); - } catch (e, stackTrace) { - _options.logger( - SentryLevel.debug, - 'Could not convert DioError to SentryException', - exception: e, - stackTrace: stackTrace, - ); - } return event; } - /// Remove the StackTrace from [dioError] so the message on Sentry looks - /// much better. - List _removeDioErrorStackTraceFromValue( - List exceptions, - DioError dioError, - ) { - final dioSentryExceptions = - exceptions.where((element) => element.type == _dioErrorType); - - if (dioSentryExceptions.isEmpty) { - return exceptions; - } - var dioSentryException = dioSentryExceptions.first; - - final exceptionIndex = exceptions.indexOf(dioSentryException); - exceptions.remove(dioSentryException); - - // Remove error and stacktrace, so that the DioError value doesn't - // include the chained exception. - dioError.stackTrace = null; - dioError.error = null; - - dioSentryException = dioSentryException.copyWith( - value: dioError.toString(), - ); - - exceptions.insert(exceptionIndex, dioSentryException); - - return exceptions; - } - SentryRequest? _requestFrom(DioError dioError) { final options = dioError.requestOptions; final headers = options.headers diff --git a/dio/lib/src/sentry_dio_extension.dart b/dio/lib/src/sentry_dio_extension.dart index bd12c242f6..658c808751 100644 --- a/dio/lib/src/sentry_dio_extension.dart +++ b/dio/lib/src/sentry_dio_extension.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:sentry/sentry.dart'; +import 'dio_error_extractor.dart'; import 'dio_event_processor.dart'; import 'failed_request_interceptor.dart'; import 'sentry_transformer.dart'; @@ -19,6 +20,9 @@ extension SentryDioExtension on Dio { // ignore: invalid_use_of_internal_member final options = hub.options; + // Add to get inner exception & stacktrace + options.addExceptionCauseExtractor(DioErrorExtractor()); + // Add DioEventProcessor when it's not already present if (options.eventProcessors.whereType().isEmpty) { options.sdk.addIntegration('sentry_dio'); diff --git a/dio/test/dio_error_extractor_test.dart b/dio/test/dio_error_extractor_test.dart new file mode 100644 index 0000000000..94e9fda8f4 --- /dev/null +++ b/dio/test/dio_error_extractor_test.dart @@ -0,0 +1,61 @@ +import 'package:dio/dio.dart'; +import 'package:sentry_dio/src/dio_error_extractor.dart'; +import 'package:test/test.dart'; + +void main() { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('$DioErrorExtractor extracts error and 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 cause = sut.cause(dioError); + + expect(cause?.exception, exception); + expect(cause?.stackTrace, stacktrace); + }); + + test('$DioErrorExtractor extracts stacktrace only', () { + final sut = fixture.getSut(); + final stacktrace = StackTrace.current; + + final dioError = DioError( + requestOptions: RequestOptions(path: '/foo/bar'), + )..stackTrace = stacktrace; + + final cause = sut.cause(dioError); + + expect(cause?.exception, 'DioError inner stacktrace'); + expect(cause?.stackTrace, stacktrace); + }); + + test('$DioErrorExtractor 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 cause = sut.cause(dioError); + + expect(cause, isNull); + }); +} + +class Fixture { + DioErrorExtractor getSut() { + return DioErrorExtractor(); + } +} diff --git a/dio/test/dio_event_processor_test.dart b/dio/test/dio_event_processor_test.dart index bf333329b5..26d442ca1b 100644 --- a/dio/test/dio_event_processor_test.dart +++ b/dio/test/dio_event_processor_test.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; import 'package:sentry/sentry.dart'; import 'package:sentry_dio/sentry_dio.dart'; +import 'package:sentry_dio/src/dio_error_extractor.dart'; import 'package:test/test.dart'; import 'package:sentry/src/sentry_exception_factory.dart'; @@ -16,7 +17,11 @@ void main() { test('$DioEventProcessor only processes ${DioError}s', () { final sut = fixture.getSut(); - final event = SentryEvent(throwable: Exception()); + final throwable = Exception(); + final event = SentryEvent( + throwable: Exception(), + exceptions: [fixture.sentryError(throwable)], + ); final processedEvent = sut.apply(event) as SentryEvent; expect(event, processedEvent); @@ -27,11 +32,13 @@ void main() { 'if stacktrace is null and a request is present', () { final sut = fixture.getSut(); + final dioError = DioError( + requestOptions: RequestOptions(path: '/foo/bar'), + ); final event = SentryEvent( - throwable: DioError( - requestOptions: RequestOptions(path: '/foo/bar'), - ), + throwable: dioError, request: SentryRequest(), + exceptions: [fixture.sentryError(dioError)], ); final processedEvent = sut.apply(event) as SentryEvent; @@ -47,14 +54,20 @@ void main() { method: 'POST', data: 'foobar', ); - final event = SentryEvent( - throwable: DioError( + final throwable = Exception(); + final dioError = DioError( + requestOptions: request, + response: Response( requestOptions: request, - response: Response( - requestOptions: request, - ), ), ); + final event = SentryEvent( + throwable: throwable, + exceptions: [ + fixture.sentryError(throwable), + fixture.sentryError(dioError) + ], + ); final processedEvent = sut.apply(event) as SentryEvent; expect(processedEvent.throwable, event.throwable); @@ -70,15 +83,21 @@ void main() { test('$DioEventProcessor adds request without pii', () { final sut = fixture.getSut(sendDefaultPii: false); - final event = SentryEvent( - throwable: DioError( + final throwable = Exception(); + final dioError = DioError( + requestOptions: requestOptions, + response: Response( requestOptions: requestOptions, - response: Response( - requestOptions: requestOptions, - data: 'foobar', - ), + data: 'foobar', ), ); + final event = SentryEvent( + throwable: throwable, + exceptions: [ + fixture.sentryError(throwable), + fixture.sentryError(dioError) + ], + ); final processedEvent = sut.apply(event) as SentryEvent; expect(processedEvent.throwable, event.throwable); @@ -96,22 +115,28 @@ void main() { final request = requestOptions.copyWith( method: 'POST', ); - final event = SentryEvent( - throwable: DioError( + final throwable = Exception(); + final dioError = DioError( + requestOptions: request, + response: Response( + data: 'foobar', + headers: Headers.fromMap(>{ + 'foo': ['bar'], + 'set-cookie': ['foo=bar'] + }), requestOptions: request, - response: Response( - data: 'foobar', - headers: Headers.fromMap(>{ - 'foo': ['bar'], - 'set-cookie': ['foo=bar'] - }), - requestOptions: request, - isRedirect: true, - statusCode: 200, - statusMessage: 'OK', - ), + isRedirect: true, + statusCode: 200, + statusMessage: 'OK', ), ); + final event = SentryEvent( + throwable: throwable, + exceptions: [ + fixture.sentryError(throwable), + fixture.sentryError(dioError) + ], + ); final processedEvent = sut.apply(event) as SentryEvent; expect(processedEvent.throwable, event.throwable); @@ -131,21 +156,27 @@ void main() { final request = requestOptions.copyWith( method: 'POST', ); - final event = SentryEvent( - throwable: DioError( + final throwable = Exception(); + final dioError = DioError( + requestOptions: request, + response: Response( + data: 'foobar', + headers: Headers.fromMap(>{ + 'foo': ['bar'] + }), requestOptions: request, - response: Response( - data: 'foobar', - headers: Headers.fromMap(>{ - 'foo': ['bar'] - }), - requestOptions: request, - isRedirect: true, - statusCode: 200, - statusMessage: 'OK', - ), + isRedirect: true, + statusCode: 200, + statusMessage: 'OK', ), ); + final event = SentryEvent( + throwable: throwable, + exceptions: [ + fixture.sentryError(throwable), + fixture.sentryError(dioError) + ], + ); final processedEvent = sut.apply(event) as SentryEvent; expect(processedEvent.throwable, event.throwable); @@ -157,6 +188,8 @@ void main() { }); test('$DioEventProcessor adds chained stacktraces', () { + fixture.options.addExceptionCauseExtractor(DioErrorExtractor()); + final sut = fixture.getSut(sendDefaultPii: false); final exception = Exception('foo bar'); final dioError = DioError( @@ -164,20 +197,28 @@ void main() { requestOptions: requestOptions, )..stackTrace = StackTrace.current; + final extracted = + fixture.exceptionFactory.extractor.flatten(dioError, null); + final exceptions = extracted.map((element) { + return fixture.exceptionFactory.getSentryException( + element.exception, + stackTrace: element.stackTrace, + ); + }).toList(); + final event = SentryEvent( throwable: dioError, - exceptions: [fixture.exceptionFactory.getSentryException(dioError)], + exceptions: exceptions, ); final processedEvent = sut.apply(event) as SentryEvent; expect(processedEvent.exceptions?.length, 2); - expect(processedEvent.exceptions?[0].value, exception.toString()); + + expect(processedEvent.exceptions?[0].value, dioError.toString()); expect(processedEvent.exceptions?[0].stackTrace, isNotNull); - expect( - processedEvent.exceptions?[1].value, - (dioError..stackTrace = null).toString(), - ); + + expect(processedEvent.exceptions?[1].value, exception.toString()); expect(processedEvent.exceptions?[1].stackTrace, isNotNull); }); } @@ -206,4 +247,12 @@ class Fixture { ..maxResponseBodySize = MaxResponseBodySize.always, ); } + + SentryException sentryError(dynamic throwable) { + return SentryException( + type: throwable.runtimeType.toString(), + value: throwable.toString(), + throwable: throwable, + ); + } }