From bd65decc8b22029e6221426f6baad7bec6320a16 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 18 Mar 2025 13:54:11 +0100 Subject: [PATCH 01/23] add source to exception cause, populate source from sentry exception factory --- dart/lib/src/exception_cause.dart | 3 ++- .../recursive_exception_cause_extractor.dart | 6 +++-- dart/lib/src/sentry_exception_factory.dart | 14 +++++++++-- ...ursive_exception_cause_extractor_test.dart | 24 +++++++++++++++++-- dart/test/sentry_exception_factory_test.dart | 24 +++++++++++++++++++ dio/lib/src/dio_error_extractor.dart | 1 + dio/test/dio_error_extractor_test.dart | 2 ++ 7 files changed, 67 insertions(+), 7 deletions(-) diff --git a/dart/lib/src/exception_cause.dart b/dart/lib/src/exception_cause.dart index 522244eb93..60bf12e31e 100644 --- a/dart/lib/src/exception_cause.dart +++ b/dart/lib/src/exception_cause.dart @@ -1,7 +1,8 @@ /// Holds inner exception and stackTrace combinations contained in other exceptions class ExceptionCause { - ExceptionCause(this.exception, this.stackTrace); + ExceptionCause(this.exception, this.stackTrace, {this.source}); dynamic exception; dynamic stackTrace; + String? source; } diff --git a/dart/lib/src/recursive_exception_cause_extractor.dart b/dart/lib/src/recursive_exception_cause_extractor.dart index 5636af84a5..360044f0bc 100644 --- a/dart/lib/src/recursive_exception_cause_extractor.dart +++ b/dart/lib/src/recursive_exception_cause_extractor.dart @@ -16,8 +16,10 @@ class RecursiveExceptionCauseExtractor { final circularityDetector = {}; var currentException = exception; - ExceptionCause? currentExceptionCause = - ExceptionCause(exception, stackTrace); + ExceptionCause? currentExceptionCause = ExceptionCause( + exception, + stackTrace, + ); while (currentException != null && currentExceptionCause != null && diff --git a/dart/lib/src/sentry_exception_factory.dart b/dart/lib/src/sentry_exception_factory.dart index 9263f07bf6..9c78de027e 100644 --- a/dart/lib/src/sentry_exception_factory.dart +++ b/dart/lib/src/sentry_exception_factory.dart @@ -1,3 +1,4 @@ +import '../sentry.dart'; import 'protocol.dart'; import 'recursive_exception_cause_extractor.dart'; import 'sentry_options.dart'; @@ -20,13 +21,16 @@ class SentryExceptionFactory { dynamic stackTrace, bool? removeSentryFrames, }) { - var throwable = exception; Mechanism? mechanism; + var throwable = exception; + bool? snapshot; if (exception is ThrowableMechanism) { - throwable = exception.throwable; mechanism = exception.mechanism; + throwable = exception.throwable; snapshot = exception.snapshot; + } else { + mechanism = Mechanism(type: "generic"); } if (throwable is Error) { @@ -74,6 +78,12 @@ class SentryExceptionFactory { } } + if (throwable is ExceptionCause) { + mechanism = mechanism.copyWith( + source: throwable.source, + ); + } + // if --obfuscate feature is enabled, 'type' won't be human readable. // https://flutter.dev/docs/deployment/obfuscate#caveat return SentryException( diff --git a/dart/test/recursive_exception_cause_extractor_test.dart b/dart/test/recursive_exception_cause_extractor_test.dart index b2da696998..e30b8fb36e 100644 --- a/dart/test/recursive_exception_cause_extractor_test.dart +++ b/dart/test/recursive_exception_cause_extractor_test.dart @@ -33,6 +33,26 @@ void main() { expect(actual, [errorA, errorB, errorC]); }); + test('parent (source) references', () { + final errorC = ExceptionC(); + final errorB = ExceptionB(errorC); + final errorA = ExceptionA(errorB); + + fixture.options.addExceptionCauseExtractor( + ExceptionACauseExtractor(false), + ); + + fixture.options.addExceptionCauseExtractor( + ExceptionBCauseExtractor(), + ); + + final sut = fixture.getSut(); + + final flattened = sut.flatten(errorA, null); + final actual = flattened.map((exceptionCause) => exceptionCause.source).toList(); + expect(actual, [null, "other", "anotherOther"]); + }); + test('flatten breaks circularity', () { final a = ExceptionCircularA(); final b = ExceptionCircularB(); @@ -132,14 +152,14 @@ class ExceptionACauseExtractor extends ExceptionCauseExtractor { if (throwing) { throw StateError("Unexpected exception"); } - return ExceptionCause(error.other, null); + return ExceptionCause(error.other, null, source: "other"); } } class ExceptionBCauseExtractor extends ExceptionCauseExtractor { @override ExceptionCause? cause(ExceptionB error) { - return ExceptionCause(error.anotherOther, null); + return ExceptionCause(error.anotherOther, null, source: "anotherOther"); } } diff --git a/dart/test/sentry_exception_factory_test.dart b/dart/test/sentry_exception_factory_test.dart index ad6df2a724..3004f0d3c9 100644 --- a/dart/test/sentry_exception_factory_test.dart +++ b/dart/test/sentry_exception_factory_test.dart @@ -248,6 +248,30 @@ void main() { expect(sentryStackTrace.frames[16].package, 'sentry_flutter_example'); expect(sentryStackTrace.frames[15].package, 'flutter'); }); + + test('adds generic type mechanism if there is none', () { + final sentryException = + fixture.getSut(attachStacktrace: false).getSentryException( + SentryStackTraceError(), + stackTrace: SentryStackTrace(), + ); + + expect(sentryException.mechanism, isNotNull); + expect(sentryException.mechanism?.type, 'generic'); + }); + + test('adds source for exception cause exception', () { + final exception = SentryStackTraceError(); + final stackTrace = SentryStackTrace(); + final cause = ExceptionCause(exception, stackTrace, source: "fixture-source"); + + final sentryException = fixture.getSut(attachStacktrace: false) + .getSentryException(cause); + + expect(sentryException.mechanism, isNotNull); + expect(sentryException.mechanism?.type, 'generic'); + expect(sentryException.mechanism?.source, 'fixture-source'); + }); } class CustomError extends Error {} diff --git a/dio/lib/src/dio_error_extractor.dart b/dio/lib/src/dio_error_extractor.dart index 6accbc17c7..3509cbea9a 100644 --- a/dio/lib/src/dio_error_extractor.dart +++ b/dio/lib/src/dio_error_extractor.dart @@ -16,6 +16,7 @@ class DioErrorExtractor extends ExceptionCauseExtractor { // A custom [ExceptionStackTraceExtractor] can be // used to extract the inner stacktrace in other cases cause is Error ? cause.stackTrace : null, + source: 'error', ); } } diff --git a/dio/test/dio_error_extractor_test.dart b/dio/test/dio_error_extractor_test.dart index 63dc4a46ee..bee490d196 100644 --- a/dio/test/dio_error_extractor_test.dart +++ b/dio/test/dio_error_extractor_test.dart @@ -29,6 +29,7 @@ void main() { expect(cause?.exception, error); expect(cause?.stackTrace, error.stackTrace); + expect(cause?.source, 'error'); }); test('extracts exception', () { @@ -43,6 +44,7 @@ void main() { expect(cause?.exception, 'Some error'); expect(cause?.stackTrace, isNull); + expect(cause?.source, 'error'); }); test('extracts nothing with missing cause', () { From b95004336c7e27d8337c0767f791f3f6e4ef65d9 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 18 Mar 2025 14:00:13 +0100 Subject: [PATCH 02/23] add source for osError --- .../exception/io_exception_event_processor.dart | 9 +++++---- .../exception/io_exception_event_processor_test.dart | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/dart/lib/src/event_processor/exception/io_exception_event_processor.dart b/dart/lib/src/event_processor/exception/io_exception_event_processor.dart index 55677a938c..5e1675b7bd 100644 --- a/dart/lib/src/event_processor/exception/io_exception_event_processor.dart +++ b/dart/lib/src/event_processor/exception/io_exception_event_processor.dart @@ -53,7 +53,7 @@ class IoExceptionEventProcessor implements ExceptionEventProcessor { // OSError is the underlying error // https://api.dart.dev/stable/dart-io/SocketException/osError.html // https://api.dart.dev/stable/dart-io/OSError-class.html - if (osError != null) _sentryExceptionfromOsError(osError), + if (osError != null) _sentryExceptionFromOsError(osError), ...?event.exceptions, ], ); @@ -80,7 +80,7 @@ class IoExceptionEventProcessor implements ExceptionEventProcessor { // OSError is the underlying error // https://api.dart.dev/stable/dart-io/SocketException/osError.html // https://api.dart.dev/stable/dart-io/OSError-class.html - if (osError != null) _sentryExceptionfromOsError(osError), + if (osError != null) _sentryExceptionFromOsError(osError), ...?event.exceptions, ], ); @@ -97,14 +97,14 @@ class IoExceptionEventProcessor implements ExceptionEventProcessor { // OSError is the underlying error // https://api.dart.dev/stable/dart-io/FileSystemException/osError.html // https://api.dart.dev/stable/dart-io/OSError-class.html - if (osError != null) _sentryExceptionfromOsError(osError), + if (osError != null) _sentryExceptionFromOsError(osError), ...?event.exceptions, ], ); } } -SentryException _sentryExceptionfromOsError(OSError osError) { +SentryException _sentryExceptionFromOsError(OSError osError) { return SentryException( type: osError.runtimeType.toString(), value: osError.toString(), @@ -115,6 +115,7 @@ SentryException _sentryExceptionfromOsError(OSError osError) { meta: { 'errno': {'number': osError.errorCode}, }, + source: 'osError' ), ); } diff --git a/dart/test/event_processor/exception/io_exception_event_processor_test.dart b/dart/test/event_processor/exception/io_exception_event_processor_test.dart index 335f45ab2b..d239cb5988 100644 --- a/dart/test/event_processor/exception/io_exception_event_processor_test.dart +++ b/dart/test/event_processor/exception/io_exception_event_processor_test.dart @@ -73,6 +73,7 @@ void main() { ); expect(event?.exceptions?.first.mechanism?.type, 'OSError'); expect(event?.exceptions?.first.mechanism?.meta['errno']['number'], 54); + expect(event?.exceptions?.first.mechanism?.source, 'osError'); }); test('adds OSError SentryException for $FileSystemException', () { @@ -97,6 +98,7 @@ void main() { ); expect(event?.exceptions?.first.mechanism?.type, 'OSError'); expect(event?.exceptions?.first.mechanism?.meta['errno']['number'], 42); + expect(event?.exceptions?.first.mechanism?.source, 'osError'); }); }); } From 34869588cd1d816f244516ff97ca983705df991b Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 18 Mar 2025 18:25:17 +0100 Subject: [PATCH 03/23] revert sentry exception cause factory changes --- dart/lib/src/sentry_exception_factory.dart | 8 -------- dart/test/sentry_exception_factory_test.dart | 13 ------------- 2 files changed, 21 deletions(-) diff --git a/dart/lib/src/sentry_exception_factory.dart b/dart/lib/src/sentry_exception_factory.dart index 9c78de027e..933fecf20d 100644 --- a/dart/lib/src/sentry_exception_factory.dart +++ b/dart/lib/src/sentry_exception_factory.dart @@ -29,8 +29,6 @@ class SentryExceptionFactory { mechanism = exception.mechanism; throwable = exception.throwable; snapshot = exception.snapshot; - } else { - mechanism = Mechanism(type: "generic"); } if (throwable is Error) { @@ -78,12 +76,6 @@ class SentryExceptionFactory { } } - if (throwable is ExceptionCause) { - mechanism = mechanism.copyWith( - source: throwable.source, - ); - } - // if --obfuscate feature is enabled, 'type' won't be human readable. // https://flutter.dev/docs/deployment/obfuscate#caveat return SentryException( diff --git a/dart/test/sentry_exception_factory_test.dart b/dart/test/sentry_exception_factory_test.dart index 3004f0d3c9..25657ca2ac 100644 --- a/dart/test/sentry_exception_factory_test.dart +++ b/dart/test/sentry_exception_factory_test.dart @@ -259,19 +259,6 @@ void main() { expect(sentryException.mechanism, isNotNull); expect(sentryException.mechanism?.type, 'generic'); }); - - test('adds source for exception cause exception', () { - final exception = SentryStackTraceError(); - final stackTrace = SentryStackTrace(); - final cause = ExceptionCause(exception, stackTrace, source: "fixture-source"); - - final sentryException = fixture.getSut(attachStacktrace: false) - .getSentryException(cause); - - expect(sentryException.mechanism, isNotNull); - expect(sentryException.mechanism?.type, 'generic'); - expect(sentryException.mechanism?.source, 'fixture-source'); - }); } class CustomError extends Error {} From fd73eaff286c748325abfb08a2e40b50d31d0bdc Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 18 Mar 2025 18:25:57 +0100 Subject: [PATCH 04/23] extract soruce from android platform exceptions --- ...id_platform_exception_event_processor_test.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/flutter/test/android_platform_exception_event_processor_test.dart b/flutter/test/android_platform_exception_event_processor_test.dart index c42505584f..0c8d4615f4 100644 --- a/flutter/test/android_platform_exception_event_processor_test.dart +++ b/flutter/test/android_platform_exception_event_processor_test.dart @@ -12,6 +12,7 @@ import 'mocks.dart'; void main() { late Fixture fixture; + setUp(() { fixture = Fixture(); @@ -34,6 +35,9 @@ void main() { final exceptions = platformExceptionEvent!.exceptions!; expect(exceptions.length, 3); + final exception = exceptions[0]; + expect(exception.mechanism?.source, isNull); + final platformException_1 = exceptions[1]; expect(platformException_1.type, 'IllegalArgumentException'); @@ -42,6 +46,7 @@ void main() { "Unsupported value: '[Ljava.lang.StackTraceElement;@ba6feed' of type 'class [Ljava.lang.StackTraceElement;'", ); expect(platformException_1.stackTrace!.frames.length, 18); + expect(platformException_1.mechanism?.source, "stackTrace"); final platformException_2 = exceptions[2]; @@ -51,6 +56,7 @@ void main() { "Unsupported value: '[Ljava.lang.StackTraceElement;@ba6feed' of type 'class [Ljava.lang.StackTraceElement;'", ); expect(platformException_2.stackTrace!.frames.length, 18); + expect(platformException_2.mechanism?.source, "details"); }); test('platform exception with details correctly parsed', () async { @@ -60,6 +66,9 @@ void main() { final exceptions = platformExceptionEvent!.exceptions!; expect(exceptions.length, 2); + final exception = exceptions[0]; + expect(exception.mechanism?.source, isNull); + final platformException_1 = exceptions[1]; expect(platformException_1.type, 'Resources\$NotFoundException'); @@ -69,6 +78,7 @@ void main() { "Unable to find resource ID #0x7f14000d", ); expect(platformException_1.stackTrace!.frames.length, 19); + expect(platformException_1.mechanism?.source, "details"); }); test('platform exception with stackTrace correctly parsed', () async { @@ -78,6 +88,9 @@ void main() { final exceptions = platformExceptionEvent!.exceptions!; expect(exceptions.length, 2); + final exception = exceptions[0]; + expect(exception.mechanism?.source, isNull); + final platformException_1 = exceptions[1]; expect(platformException_1.type, 'IllegalArgumentException'); @@ -87,6 +100,7 @@ void main() { "Not supported, use openfile", ); expect(platformException_1.stackTrace!.frames.length, 22); + expect(platformException_1.mechanism?.source, "stackTrace"); }); test( From 07087aae9addecba99eff867a0f10390ceb6a4e3 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 18 Mar 2025 18:26:05 +0100 Subject: [PATCH 05/23] add ExceptionGroupEventProcessor --- .../exception_group_event_processor.dart | 38 +++++++++ .../exception_group_event_processor_test.dart | 77 +++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 flutter/lib/src/event_processor/exception_group_event_processor.dart create mode 100644 flutter/test/event_processor/exception_group_event_processor_test.dart diff --git a/flutter/lib/src/event_processor/exception_group_event_processor.dart b/flutter/lib/src/event_processor/exception_group_event_processor.dart new file mode 100644 index 0000000000..bcd0263a10 --- /dev/null +++ b/flutter/lib/src/event_processor/exception_group_event_processor.dart @@ -0,0 +1,38 @@ +import 'package:flutter/services.dart'; +import 'package:sentry/sentry.dart'; + +/// Add code & message from [PlatformException] to [SentryException] +class ExceptionGroupEventProcessor implements EventProcessor { + @override + SentryEvent? apply(SentryEvent event, Hint hint) { + final sentryExceptions = event.exceptions ?? []; + if (sentryExceptions.isEmpty) { + return event; + } + + final updatedSentryExceptions = []; + + int exceptionId = sentryExceptions.length - 1; + + for (SentryException sentryException in sentryExceptions) { + final mechanism = sentryException.mechanism ?? Mechanism(type: "generic"); + + final isChild = exceptionId > 0; + final isOriginal = !isChild; + + sentryException = sentryException.copyWith( + mechanism: mechanism.copyWith( + type: isChild ? 'chained' : null, + isExceptionGroup: isOriginal ? true : null, + exceptionId: exceptionId, + parentId: isChild ? exceptionId - 1 : null, + ), + ); + updatedSentryExceptions.add(sentryException); + + exceptionId -= 1; + } + + return event.copyWith(exceptions: updatedSentryExceptions); + } +} diff --git a/flutter/test/event_processor/exception_group_event_processor_test.dart b/flutter/test/event_processor/exception_group_event_processor_test.dart new file mode 100644 index 0000000000..fe5deff05f --- /dev/null +++ b/flutter/test/event_processor/exception_group_event_processor_test.dart @@ -0,0 +1,77 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry/sentry.dart'; +import 'package:sentry_flutter/src/event_processor/exception_group_event_processor.dart'; + +void main() { + group(ExceptionGroupEventProcessor, () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('applies grouping to exception cause exceptions', () { + final exceptionC = ExceptionC(); // Original + final exceptionB = ExceptionB(exceptionC); + final exceptionA = ExceptionA(exceptionB); + + // Would be the result of RecursiveExceptionCauseExtractor + final causes = [ + ExceptionCause(exceptionA, null, source: "anotherOther"), + ExceptionCause(exceptionB, null, source: "other"), + ExceptionCause(exceptionC, null, source: null), + ]; + + final sentryExceptions = causes.map((e) { + return SentryException( + type: 'fixture-original-type', + value: 'fixture-original-value', + throwable: e.exception, + mechanism: Mechanism(type: "fixture-type", source: e.source), + ); + }).toList(); + var event = SentryEvent(exceptions: sentryExceptions); + + final sut = fixture.getSut(); + event = (sut.apply(event, Hint()))!; + + final sentryExceptionC = event.exceptions![0]; + expect(sentryExceptionC.mechanism?.type, "chained"); + expect(sentryExceptionC.mechanism?.isExceptionGroup, isNull); + expect(sentryExceptionC.mechanism?.exceptionId, 2); + expect(sentryExceptionC.mechanism?.parentId, 1); + + final sentryExceptionB = event.exceptions![1]; + expect(sentryExceptionB.mechanism?.type, "chained"); + expect(sentryExceptionB.mechanism?.isExceptionGroup, isNull); + expect(sentryExceptionB.mechanism?.exceptionId, 1); + expect(sentryExceptionB.mechanism?.parentId, 0); + + final sentryExceptionA = event.exceptions![2]; + expect(sentryExceptionA.mechanism?.type, "fixture-type"); + expect(sentryExceptionA.mechanism?.isExceptionGroup, true); + expect(sentryExceptionA.mechanism?.exceptionId, 0); + expect(sentryExceptionA.mechanism?.parentId, isNull); + }); + }); +} + +class Fixture { + ExceptionGroupEventProcessor getSut() { + return ExceptionGroupEventProcessor(); + } +} + +class ExceptionA { + ExceptionA(this.other); + final ExceptionB? other; +} + +class ExceptionB { + ExceptionB(this.anotherOther); + final ExceptionC? anotherOther; +} + +class ExceptionC { + // I am empty inside +} From 2e189f24d82467d07d5fe4f67c3058657da3f0eb Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 18 Mar 2025 18:29:45 +0100 Subject: [PATCH 06/23] format --- .../exception/io_exception_event_processor.dart | 2 +- dart/lib/src/sentry_exception_factory.dart | 7 +++---- dart/test/recursive_exception_cause_extractor_test.dart | 3 ++- dart/test/sentry_exception_factory_test.dart | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/dart/lib/src/event_processor/exception/io_exception_event_processor.dart b/dart/lib/src/event_processor/exception/io_exception_event_processor.dart index 5e1675b7bd..885ccba3b2 100644 --- a/dart/lib/src/event_processor/exception/io_exception_event_processor.dart +++ b/dart/lib/src/event_processor/exception/io_exception_event_processor.dart @@ -115,7 +115,7 @@ SentryException _sentryExceptionFromOsError(OSError osError) { meta: { 'errno': {'number': osError.errorCode}, }, - source: 'osError' + source: 'osError', ), ); } diff --git a/dart/lib/src/sentry_exception_factory.dart b/dart/lib/src/sentry_exception_factory.dart index 933fecf20d..ce16a86c46 100644 --- a/dart/lib/src/sentry_exception_factory.dart +++ b/dart/lib/src/sentry_exception_factory.dart @@ -1,4 +1,3 @@ -import '../sentry.dart'; import 'protocol.dart'; import 'recursive_exception_cause_extractor.dart'; import 'sentry_options.dart'; @@ -21,13 +20,13 @@ class SentryExceptionFactory { dynamic stackTrace, bool? removeSentryFrames, }) { - Mechanism? mechanism; var throwable = exception; - + Mechanism? mechanism; + bool? snapshot; if (exception is ThrowableMechanism) { - mechanism = exception.mechanism; throwable = exception.throwable; + mechanism = exception.mechanism; snapshot = exception.snapshot; } diff --git a/dart/test/recursive_exception_cause_extractor_test.dart b/dart/test/recursive_exception_cause_extractor_test.dart index e30b8fb36e..9eeaf63697 100644 --- a/dart/test/recursive_exception_cause_extractor_test.dart +++ b/dart/test/recursive_exception_cause_extractor_test.dart @@ -49,7 +49,8 @@ void main() { final sut = fixture.getSut(); final flattened = sut.flatten(errorA, null); - final actual = flattened.map((exceptionCause) => exceptionCause.source).toList(); + final actual = + flattened.map((exceptionCause) => exceptionCause.source).toList(); expect(actual, [null, "other", "anotherOther"]); }); diff --git a/dart/test/sentry_exception_factory_test.dart b/dart/test/sentry_exception_factory_test.dart index 25657ca2ac..2508f58056 100644 --- a/dart/test/sentry_exception_factory_test.dart +++ b/dart/test/sentry_exception_factory_test.dart @@ -251,10 +251,10 @@ void main() { test('adds generic type mechanism if there is none', () { final sentryException = - fixture.getSut(attachStacktrace: false).getSentryException( - SentryStackTraceError(), - stackTrace: SentryStackTrace(), - ); + fixture.getSut(attachStacktrace: false).getSentryException( + SentryStackTraceError(), + stackTrace: SentryStackTrace(), + ); expect(sentryException.mechanism, isNotNull); expect(sentryException.mechanism?.type, 'generic'); From 27ab973d01e5c340249225a2a1f374e418460ffc Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 19 Mar 2025 10:28:28 +0100 Subject: [PATCH 07/23] reverse order according to rfc --- .../exception_group_event_processor.dart | 2 +- .../exception_group_event_processor_test.dart | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/flutter/lib/src/event_processor/exception_group_event_processor.dart b/flutter/lib/src/event_processor/exception_group_event_processor.dart index bcd0263a10..cd8353c191 100644 --- a/flutter/lib/src/event_processor/exception_group_event_processor.dart +++ b/flutter/lib/src/event_processor/exception_group_event_processor.dart @@ -14,7 +14,7 @@ class ExceptionGroupEventProcessor implements EventProcessor { int exceptionId = sentryExceptions.length - 1; - for (SentryException sentryException in sentryExceptions) { + for (SentryException sentryException in sentryExceptions.reversed) { final mechanism = sentryException.mechanism ?? Mechanism(type: "generic"); final isChild = exceptionId > 0; diff --git a/flutter/test/event_processor/exception_group_event_processor_test.dart b/flutter/test/event_processor/exception_group_event_processor_test.dart index fe5deff05f..bdac0b688e 100644 --- a/flutter/test/event_processor/exception_group_event_processor_test.dart +++ b/flutter/test/event_processor/exception_group_event_processor_test.dart @@ -17,9 +17,9 @@ void main() { // Would be the result of RecursiveExceptionCauseExtractor final causes = [ - ExceptionCause(exceptionA, null, source: "anotherOther"), + ExceptionCause(exceptionA, null, source: null), ExceptionCause(exceptionB, null, source: "other"), - ExceptionCause(exceptionC, null, source: null), + ExceptionCause(exceptionC, null, source: "anotherOther"), ]; final sentryExceptions = causes.map((e) { @@ -36,22 +36,28 @@ void main() { event = (sut.apply(event, Hint()))!; final sentryExceptionC = event.exceptions![0]; + expect(sentryExceptionC.throwable, exceptionC); expect(sentryExceptionC.mechanism?.type, "chained"); expect(sentryExceptionC.mechanism?.isExceptionGroup, isNull); expect(sentryExceptionC.mechanism?.exceptionId, 2); expect(sentryExceptionC.mechanism?.parentId, 1); + expect(sentryExceptionC.mechanism?.source, "anotherOther"); final sentryExceptionB = event.exceptions![1]; + expect(sentryExceptionB.throwable, exceptionB); expect(sentryExceptionB.mechanism?.type, "chained"); expect(sentryExceptionB.mechanism?.isExceptionGroup, isNull); expect(sentryExceptionB.mechanism?.exceptionId, 1); expect(sentryExceptionB.mechanism?.parentId, 0); + expect(sentryExceptionB.mechanism?.source, "other"); final sentryExceptionA = event.exceptions![2]; + expect(sentryExceptionA.throwable, exceptionA); expect(sentryExceptionA.mechanism?.type, "fixture-type"); expect(sentryExceptionA.mechanism?.isExceptionGroup, true); expect(sentryExceptionA.mechanism?.exceptionId, 0); expect(sentryExceptionA.mechanism?.parentId, isNull); + expect(sentryExceptionA.mechanism?.source, isNull); }); }); } From c888e409a37f79805bf68b80e6dced4341107097 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 19 Mar 2025 10:41:41 +0100 Subject: [PATCH 08/23] iterate seperatley --- ...id_platform_exception_event_processor.dart | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart b/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart index edb5773bbc..605c7610e4 100644 --- a/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart +++ b/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart @@ -143,15 +143,23 @@ class _JvmExceptionFactory { String exceptionAsString, ) { final jvmException = JvmException.parse(exceptionAsString); - final jvmExceptions = [ - jvmException, - ...?jvmException.causes, - ...?jvmException.suppressed, - ]; - return jvmExceptions.map((exception) { - return exception.toSentryException(nativePackageName); - }).toList(growable: false); + List> sentryExceptions = []; + + final sentryException = jvmException.toSentryException(nativePackageName); + sentryExceptions.add(sentryException); + + for (final cause in jvmException.causes ?? []) { + final causeSentryException = cause.toSentryException(nativePackageName); + sentryExceptions.add(causeSentryException); + } + + for (final suppressed in jvmException.suppressed ?? []) { + final suppressedSentryException = suppressed.toSentryException(nativePackageName); + sentryExceptions.add(suppressedSentryException); + } + + return sentryExceptions.toList(growable: false); } } From d317c844e0679d2e6c5b75308643678a2c7c3493 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 19 Mar 2025 10:41:47 +0100 Subject: [PATCH 09/23] update test --- .../android_platform_exception_event_processor_test.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/test/android_platform_exception_event_processor_test.dart b/flutter/test/android_platform_exception_event_processor_test.dart index 0c8d4615f4..d59c5603d6 100644 --- a/flutter/test/android_platform_exception_event_processor_test.dart +++ b/flutter/test/android_platform_exception_event_processor_test.dart @@ -46,7 +46,7 @@ void main() { "Unsupported value: '[Ljava.lang.StackTraceElement;@ba6feed' of type 'class [Ljava.lang.StackTraceElement;'", ); expect(platformException_1.stackTrace!.frames.length, 18); - expect(platformException_1.mechanism?.source, "stackTrace"); + expect(platformException_1.mechanism?.source, "stackTrace[0]"); final platformException_2 = exceptions[2]; @@ -56,7 +56,7 @@ void main() { "Unsupported value: '[Ljava.lang.StackTraceElement;@ba6feed' of type 'class [Ljava.lang.StackTraceElement;'", ); expect(platformException_2.stackTrace!.frames.length, 18); - expect(platformException_2.mechanism?.source, "details"); + expect(platformException_2.mechanism?.source, "details[0]"); }); test('platform exception with details correctly parsed', () async { @@ -78,7 +78,7 @@ void main() { "Unable to find resource ID #0x7f14000d", ); expect(platformException_1.stackTrace!.frames.length, 19); - expect(platformException_1.mechanism?.source, "details"); + expect(platformException_1.mechanism?.source, "details[0]"); }); test('platform exception with stackTrace correctly parsed', () async { @@ -100,7 +100,7 @@ void main() { "Not supported, use openfile", ); expect(platformException_1.stackTrace!.frames.length, 22); - expect(platformException_1.mechanism?.source, "stackTrace"); + expect(platformException_1.mechanism?.source, "stackTrace[0]"); }); test( From 9a23c4a6a90d4801268fdc2b4c258bcf699066fe Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 19 Mar 2025 16:07:13 +0100 Subject: [PATCH 10/23] introduce parent/child relationship in exceptions, flatten exceptions according to rfc --- .../exception_group_event_processor.dart | 23 ++ .../io_exception_event_processor.dart | 53 ++-- dart/lib/src/protocol/sentry_exception.dart | 48 +++- dart/lib/src/sentry.dart | 4 + .../exception_group_event_processor_test.dart | 108 +++++++ .../io_exception_event_processor_test.dart | 69 +++-- dart/test/protocol/sentry_exception_test.dart | 269 ++++++++++++++++++ ...id_platform_exception_event_processor.dart | 112 +++++--- .../exception_group_event_processor.dart | 38 --- ...atform_exception_event_processor_test.dart | 31 +- .../exception_group_event_processor_test.dart | 83 ------ 11 files changed, 618 insertions(+), 220 deletions(-) create mode 100644 dart/lib/src/event_processor/exception/exception_group_event_processor.dart create mode 100644 dart/test/event_processor/exception/exception_group_event_processor_test.dart delete mode 100644 flutter/lib/src/event_processor/exception_group_event_processor.dart delete mode 100644 flutter/test/event_processor/exception_group_event_processor_test.dart diff --git a/dart/lib/src/event_processor/exception/exception_group_event_processor.dart b/dart/lib/src/event_processor/exception/exception_group_event_processor.dart new file mode 100644 index 0000000000..af867c22fb --- /dev/null +++ b/dart/lib/src/event_processor/exception/exception_group_event_processor.dart @@ -0,0 +1,23 @@ +import '../../event_processor.dart'; +import '../../protocol.dart'; +import '../../hint.dart'; + +/// Group exceptions into a flat list with references to hierarchy. +class ExceptionGroupEventProcessor implements EventProcessor { + @override + SentryEvent? apply(SentryEvent event, Hint hint) { + final sentryExceptions = event.exceptions ?? []; + if (sentryExceptions.isEmpty) { + return event; + } + final firstException = sentryExceptions.first; + + if (sentryExceptions.length > 1) { + // Somehow already a list here, no grouping possible, as there is no root exception. + return event; + } else { + final grouped = firstException.flatten().reversed.toList(growable: false); + return event.copyWith(exceptions: grouped); + } + } +} diff --git a/dart/lib/src/event_processor/exception/io_exception_event_processor.dart b/dart/lib/src/event_processor/exception/io_exception_event_processor.dart index 885ccba3b2..790a9f707a 100644 --- a/dart/lib/src/event_processor/exception/io_exception_event_processor.dart +++ b/dart/lib/src/event_processor/exception/io_exception_event_processor.dart @@ -45,17 +45,24 @@ class IoExceptionEventProcessor implements ExceptionEventProcessor { SocketException exception, SentryEvent event, ) { - final address = exception.address; final osError = exception.osError; + SentryException? osException; + List? exceptions; + if (osError != null) { + // OSError is the underlying error + // https://api.dart.dev/stable/dart-io/SocketException/osError.html + // https://api.dart.dev/stable/dart-io/OSError-class.html + osException = _sentryExceptionFromOsError(osError); + osException.exceptions = event.exceptions; + exceptions = [osException]; + } else { + exceptions = event.exceptions; + } + + final address = exception.address; if (address == null) { return event.copyWith( - exceptions: [ - // OSError is the underlying error - // https://api.dart.dev/stable/dart-io/SocketException/osError.html - // https://api.dart.dev/stable/dart-io/OSError-class.html - if (osError != null) _sentryExceptionFromOsError(osError), - ...?event.exceptions, - ], + exceptions: exceptions, ); } SentryRequest? request; @@ -76,13 +83,7 @@ class IoExceptionEventProcessor implements ExceptionEventProcessor { return event.copyWith( request: event.request ?? request, - exceptions: [ - // OSError is the underlying error - // https://api.dart.dev/stable/dart-io/SocketException/osError.html - // https://api.dart.dev/stable/dart-io/OSError-class.html - if (osError != null) _sentryExceptionFromOsError(osError), - ...?event.exceptions, - ], + exceptions: exceptions, ); } @@ -92,14 +93,22 @@ class IoExceptionEventProcessor implements ExceptionEventProcessor { SentryEvent event, ) { final osError = exception.osError; + + SentryException? osException; + List? exceptions; + if (osError != null) { + // OSError is the underlying error + // https://api.dart.dev/stable/dart-io/SocketException/osError.html + // https://api.dart.dev/stable/dart-io/OSError-class.html + osException = _sentryExceptionFromOsError(osError); + osException.exceptions = event.exceptions; + exceptions = [osException]; + } else { + exceptions = event.exceptions; + } + return event.copyWith( - exceptions: [ - // OSError is the underlying error - // https://api.dart.dev/stable/dart-io/FileSystemException/osError.html - // https://api.dart.dev/stable/dart-io/OSError-class.html - if (osError != null) _sentryExceptionFromOsError(osError), - ...?event.exceptions, - ], + exceptions: exceptions, ); } } diff --git a/dart/lib/src/protocol/sentry_exception.dart b/dart/lib/src/protocol/sentry_exception.dart index 9bf5f3fa13..f7c406abeb 100644 --- a/dart/lib/src/protocol/sentry_exception.dart +++ b/dart/lib/src/protocol/sentry_exception.dart @@ -29,7 +29,22 @@ class SentryException { @internal final Map? unknown; - const SentryException({ + List? _exceptions; + + List? get exceptions => _exceptions; + + @internal + set exceptions(List? value) { + _exceptions = value; + } + + @internal + void addException(SentryException exception) { + _exceptions ??= []; + _exceptions!.add(exception); + } + + SentryException({ required this.type, required this.value, this.module, @@ -92,4 +107,35 @@ class SentryException { throwable: throwable ?? this.throwable, unknown: unknown, ); + + List flatten({int? parentId, int id = 0}) { + final exceptions = this.exceptions ?? []; + + var mechanism = this.mechanism ?? Mechanism(type: "generic"); + mechanism = mechanism.copyWith( + type: id > 0 ? "chained" : null, + parentId: parentId, + exceptionId: id, + isExceptionGroup: exceptions.length > 1 ? true : null, + ); + + final exception = copyWith( + mechanism: mechanism, + ); + + var all = []; + all.add(exception); + + if (exceptions.isNotEmpty) { + final parentId = id; + for (var exception in exceptions) { + id++; + final flattenedExceptions = + exception.flatten(parentId: parentId, id: id); + id = flattenedExceptions.lastOrNull?.mechanism?.exceptionId ?? id; + all.addAll(flattenedExceptions); + } + } + return all.toList(growable: false); + } } diff --git a/dart/lib/src/sentry.dart b/dart/lib/src/sentry.dart index d89d26c46c..ff49fe0c46 100644 --- a/dart/lib/src/sentry.dart +++ b/dart/lib/src/sentry.dart @@ -7,6 +7,7 @@ import 'environment/environment_variables.dart'; import 'event_processor/deduplication_event_processor.dart'; import 'event_processor/enricher/enricher_event_processor.dart'; import 'event_processor/exception/exception_event_processor.dart'; +import 'event_processor/exception/exception_group_event_processor.dart'; import 'hint.dart'; import 'hub.dart'; import 'hub_adapter.dart'; @@ -110,6 +111,9 @@ class Sentry { options.addEventProcessor(DeduplicationEventProcessor(options)); options.prependExceptionTypeIdentifier(DartExceptionTypeIdentifier()); + + // Added last to ensure all error events have correct parent/child relationships + options.addEventProcessor(ExceptionGroupEventProcessor()); } /// This method reads available environment variables and uses them diff --git a/dart/test/event_processor/exception/exception_group_event_processor_test.dart b/dart/test/event_processor/exception/exception_group_event_processor_test.dart new file mode 100644 index 0000000000..cf74e28ea3 --- /dev/null +++ b/dart/test/event_processor/exception/exception_group_event_processor_test.dart @@ -0,0 +1,108 @@ +import 'package:sentry/sentry.dart'; +import 'package:test/test.dart'; + +import 'package:sentry/src/event_processor/exception/exception_group_event_processor.dart'; + +void main() { + group(ExceptionGroupEventProcessor, () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('applies grouping to exception with children', () { + final throwableA = Exception('ExceptionA'); + final exceptionA = SentryException( + type: 'ExceptionA', + value: 'ExceptionA', + throwable: throwableA, + ); + final throwableB = Exception('ExceptionB'); + final exceptionB = SentryException( + type: 'ExceptionB', + value: 'ExceptionB', + throwable: throwableB, + ); + exceptionA.addException(exceptionB); + + var event = SentryEvent( + throwable: throwableA, + exceptions: [exceptionA], + ); + + final sut = fixture.getSut(); + event = (sut.apply(event, Hint()))!; + + final sentryExceptionB = event.exceptions![0]; + final sentryExceptionA = event.exceptions![1]; + + expect(sentryExceptionB.throwable, throwableB); + expect(sentryExceptionB.mechanism?.type, "chained"); + expect(sentryExceptionB.mechanism?.isExceptionGroup, isNull); + expect(sentryExceptionB.mechanism?.exceptionId, 1); + expect(sentryExceptionB.mechanism?.parentId, 0); + + expect(sentryExceptionA.throwable, throwableA); + expect(sentryExceptionA.mechanism?.type, "generic"); + expect(sentryExceptionA.mechanism?.isExceptionGroup, isNull); + expect(sentryExceptionA.mechanism?.exceptionId, 0); + expect(sentryExceptionA.mechanism?.parentId, isNull); + }); + + test('applies no grouping if there is no exception', () { + final event = SentryEvent(); + final sut = fixture.getSut(); + + final result = sut.apply(event, Hint()); + + expect(result, event); + expect(event.throwable, isNull); + expect(event.exceptions, isNull); + }); + + test('applies no grouping if there is already a list of exceptions', () { + final event = SentryEvent( + exceptions: [ + SentryException( + type: 'ExceptionA', + value: 'ExceptionA', + throwable: Exception('ExceptionA')), + SentryException( + type: 'ExceptionB', + value: 'ExceptionB', + throwable: Exception('ExceptionB')), + ], + ); + final sut = fixture.getSut(); + + final result = sut.apply(event, Hint()); + + final sentryExceptionA = result?.exceptions![0]; + final sentryExceptionB = result?.exceptions![1]; + + expect(sentryExceptionA?.type, 'ExceptionA'); + expect(sentryExceptionB?.type, 'ExceptionB'); + }); + }); +} + +class Fixture { + ExceptionGroupEventProcessor getSut() { + return ExceptionGroupEventProcessor(); + } +} + +class ExceptionA { + ExceptionA(this.other); + final ExceptionB? other; +} + +class ExceptionB { + ExceptionB(this.anotherOther); + final ExceptionC? anotherOther; +} + +class ExceptionC { + // I am empty inside +} diff --git a/dart/test/event_processor/exception/io_exception_event_processor_test.dart b/dart/test/event_processor/exception/io_exception_event_processor_test.dart index d239cb5988..4e0d318b68 100644 --- a/dart/test/event_processor/exception/io_exception_event_processor_test.dart +++ b/dart/test/event_processor/exception/io_exception_event_processor_test.dart @@ -6,6 +6,9 @@ import 'dart:io'; import 'package:sentry/sentry.dart'; import 'package:sentry/src/event_processor/exception/io_exception_event_processor.dart'; import 'package:test/test.dart'; +import 'package:sentry/src/sentry_exception_factory.dart'; + +import '../../test_utils.dart'; void main() { group(IoExceptionEventProcessor, () { @@ -46,17 +49,22 @@ void main() { test('adds $SentryRequest for $SocketException with addresses', () { final enricher = fixture.getSut(); + final throwable = SocketException( + 'Exception while connecting', + osError: OSError('Connection reset by peer', 54), + port: 12345, + address: InternetAddress( + '127.0.0.1', + type: InternetAddressType.IPv4, + ), + ); + final sentryException = + fixture.exceptionFactory.getSentryException(throwable); + final event = enricher.apply( SentryEvent( - throwable: SocketException( - 'Exception while connecting', - osError: OSError('Connection reset by peer', 54), - port: 12345, - address: InternetAddress( - '127.0.0.1', - type: InternetAddressType.IPv4, - ), - ), + throwable: throwable, + exceptions: [sentryException], ), Hint(), ); @@ -64,27 +72,32 @@ void main() { expect(event?.request, isNotNull); expect(event?.request?.url, '127.0.0.1'); - // Due to the test setup, there's no SentryException for the SocketException. - // And thus only one entry for the added OSError - expect(event?.exceptions?.first.type, 'OSError'); - expect( - event?.exceptions?.first.value, - 'OS Error: Connection reset by peer, errno = 54', - ); - expect(event?.exceptions?.first.mechanism?.type, 'OSError'); - expect(event?.exceptions?.first.mechanism?.meta['errno']['number'], 54); - expect(event?.exceptions?.first.mechanism?.source, 'osError'); + final rootException = event?.exceptions?.first; + expect(rootException?.type, 'OSError'); + expect(rootException?.value, + 'OS Error: Connection reset by peer, errno = 54'); + expect(rootException?.mechanism?.type, 'OSError'); + expect(rootException?.mechanism?.meta['errno']['number'], 54); + expect(rootException?.mechanism?.source, 'osError'); + + final childException = rootException?.exceptions?.first; + expect(childException, sentryException); }); test('adds OSError SentryException for $FileSystemException', () { final enricher = fixture.getSut(); + final throwable = FileSystemException( + 'message', + 'path', + OSError('Oh no :(', 42), + ); + final sentryException = + fixture.exceptionFactory.getSentryException(throwable); + final event = enricher.apply( SentryEvent( - throwable: FileSystemException( - 'message', - 'path', - OSError('Oh no :(', 42), - ), + throwable: throwable, + exceptions: [sentryException], ), Hint(), ); @@ -99,11 +112,19 @@ void main() { expect(event?.exceptions?.first.mechanism?.type, 'OSError'); expect(event?.exceptions?.first.mechanism?.meta['errno']['number'], 42); expect(event?.exceptions?.first.mechanism?.source, 'osError'); + + final childException = event?.exceptions?.first.exceptions?.first; + expect(childException, sentryException); }); }); } class Fixture { + final SentryOptions options = defaultTestOptions(); + + // ignore: invalid_use_of_internal_member + SentryExceptionFactory get exceptionFactory => options.exceptionFactory; + IoExceptionEventProcessor getSut() { return IoExceptionEventProcessor(SentryOptions.empty()); } diff --git a/dart/test/protocol/sentry_exception_test.dart b/dart/test/protocol/sentry_exception_test.dart index 4541e67332..d2cfa70563 100644 --- a/dart/test/protocol/sentry_exception_test.dart +++ b/dart/test/protocol/sentry_exception_test.dart @@ -155,4 +155,273 @@ void main() { expect(stackTrace.toJson(), copy.stackTrace!.toJson()); }); }); + + group('flatten', () { + test('flatten exception without nested exceptions', () { + final origin = sentryException.copyWith( + value: 'origin', + ); + + final flattened = origin.flatten(); + + expect(flattened.length, 1); + expect(flattened.first.value, 'origin'); + + expect(flattened.first.mechanism?.source, isNull); + expect(flattened.first.mechanism?.exceptionId, 0); + expect(flattened.first.mechanism?.parentId, null); + }); + + test('flatten exception with nested chained exceptions', () { + final origin = sentryException.copyWith( + value: 'origin', + ); + final originChild = sentryException.copyWith( + value: 'originChild', + ); + origin.addException(originChild); + final originChildChild = sentryException.copyWith( + value: 'originChildChild', + ); + originChild.addException(originChildChild); + + final flattened = origin.flatten(); + + expect(flattened.length, 3); + + expect(flattened[0].value, 'origin'); + expect(flattened[0].mechanism?.isExceptionGroup, isNull); + expect(flattened[0].mechanism?.source, isNull); + expect(flattened[0].mechanism?.exceptionId, 0); + expect(flattened[0].mechanism?.parentId, null); + + expect(flattened[1].value, 'originChild'); + expect(flattened[1].mechanism?.source, isNull); + expect(flattened[1].mechanism?.exceptionId, 1); + expect(flattened[1].mechanism?.parentId, 0); + + expect(flattened[2].value, 'originChildChild'); + expect(flattened[2].mechanism?.source, isNull); + expect(flattened[2].mechanism?.exceptionId, 2); + expect(flattened[2].mechanism?.parentId, 1); + }); + + test('flatten exception with nested parallel exceptions', () { + final origin = sentryException.copyWith( + value: 'origin', + ); + final originChild = sentryException.copyWith( + value: 'originChild', + ); + origin.addException(originChild); + final originChild2 = sentryException.copyWith( + value: 'originChild2', + ); + origin.addException(originChild2); + + final flattened = origin.flatten(); + + expect(flattened.length, 3); + + expect(flattened[0].value, 'origin'); + expect(flattened[0].mechanism?.isExceptionGroup, true); + expect(flattened[0].mechanism?.source, isNull); + expect(flattened[0].mechanism?.exceptionId, 0); + expect(flattened[0].mechanism?.parentId, null); + + expect(flattened[1].value, 'originChild'); + expect(flattened[1].mechanism?.source, isNull); + expect(flattened[1].mechanism?.exceptionId, 1); + expect(flattened[1].mechanism?.parentId, 0); + + expect(flattened[2].value, 'originChild2'); + expect(flattened[2].mechanism?.source, isNull); + expect(flattened[2].mechanism?.exceptionId, 2); + expect(flattened[2].mechanism?.parentId, 0); + }); + + test('flatten rfc example', () { + // try: + // raise RuntimeError("something") + // except: + // raise ExceptionGroup("nested", + // [ + // ValueError(654), + // ExceptionGroup("imports", + // [ + // ImportError("no_such_module"), + // ModuleNotFoundError("another_module"), + // ] + // ), + // TypeError("int"), + // ] + // ) + + // https://github.com/getsentry/rfcs/blob/main/text/0079-exception-groups.md#example-event + // In the example, the runtime error is inserted as the first exception in the outer exception group. + + final exceptionGroupNested = sentryException.copyWith( + value: 'ExceptionGroup', + ); + final runtimeError = sentryException.copyWith( + value: 'RuntimeError', + mechanism: sentryException.mechanism?.copyWith(source: '__source__'), + ); + exceptionGroupNested.addException(runtimeError); + final valueError = sentryException.copyWith( + value: 'ValueError', + mechanism: sentryException.mechanism?.copyWith(source: 'exceptions[0]'), + ); + exceptionGroupNested.addException(valueError); + + final exceptionGroupImports = sentryException.copyWith( + value: 'ExceptionGroup', + mechanism: sentryException.mechanism?.copyWith(source: 'exceptions[1]'), + ); + exceptionGroupNested.addException(exceptionGroupImports); + + final importError = sentryException.copyWith( + value: 'ImportError', + mechanism: sentryException.mechanism?.copyWith(source: 'exceptions[0]'), + ); + exceptionGroupImports.addException(importError); + final moduleNotFoundError = sentryException.copyWith( + value: 'ModuleNotFoundError', + mechanism: sentryException.mechanism?.copyWith(source: 'exceptions[1]'), + ); + exceptionGroupImports.addException(moduleNotFoundError); + + final typeError = sentryException.copyWith( + value: 'TypeError', + mechanism: sentryException.mechanism?.copyWith(source: 'exceptions[2]'), + ); + exceptionGroupNested.addException(typeError); + + final flattened = + exceptionGroupNested.flatten().reversed.toList(growable: false); + + expect(flattened.length, 7); + + // { + // "exception": { + // "values": [ + // { + // "type": "TypeError", + // "value": "int", + // "mechanism": { + // "type": "chained", + // "source": "exceptions[2]", + // "exception_id": 6, + // "parent_id": 0 + // } + // }, + // { + // "type": "ModuleNotFoundError", + // "value": "another_module", + // "mechanism": { + // "type": "chained", + // "source": "exceptions[1]", + // "exception_id": 5, + // "parent_id": 3 + // } + // }, + // { + // "type": "ImportError", + // "value": "no_such_module", + // "mechanism": { + // "type": "chained", + // "source": "exceptions[0]", + // "exception_id": 4, + // "parent_id": 3 + // } + // }, + // { + // "type": "ExceptionGroup", + // "value": "imports", + // "mechanism": { + // "type": "chained", + // "source": "exceptions[1]", + // "is_exception_group": true, + // "exception_id": 3, + // "parent_id": 0 + // } + // }, + // { + // "type": "ValueError", + // "value": "654", + // "mechanism": { + // "type": "chained", + // "source": "exceptions[0]", + // "exception_id": 2, + // "parent_id": 0 + // } + // }, + // { + // "type": "RuntimeError", + // "value": "something", + // "mechanism": { + // "type": "chained", + // "source": "__context__", + // "exception_id": 1, + // "parent_id": 0 + // } + // }, + // { + // "type": "ExceptionGroup", + // "value": "nested", + // "mechanism": { + // "type": "exceptionhook", + // "handled": false, + // "is_exception_group": true, + // "exception_id": 0 + // } + // }, + // ] + // } + // } + + expect(flattened[0].value, 'TypeError'); + expect(flattened[0].mechanism?.source, 'exceptions[2]'); + expect(flattened[0].mechanism?.exceptionId, 6); + expect(flattened[0].mechanism?.parentId, 0); + expect(flattened[0].mechanism?.type, 'chained'); + + expect(flattened[1].value, 'ModuleNotFoundError'); + expect(flattened[1].mechanism?.source, 'exceptions[1]'); + expect(flattened[1].mechanism?.exceptionId, 5); + expect(flattened[1].mechanism?.parentId, 3); + expect(flattened[1].mechanism?.type, 'chained'); + + expect(flattened[2].value, 'ImportError'); + expect(flattened[2].mechanism?.source, 'exceptions[0]'); + expect(flattened[2].mechanism?.exceptionId, 4); + expect(flattened[2].mechanism?.parentId, 3); + expect(flattened[2].mechanism?.type, 'chained'); + + expect(flattened[3].value, 'ExceptionGroup'); + expect(flattened[3].mechanism?.source, 'exceptions[1]'); + expect(flattened[3].mechanism?.isExceptionGroup, true); + expect(flattened[3].mechanism?.exceptionId, 3); + expect(flattened[3].mechanism?.parentId, 0); + expect(flattened[3].mechanism?.type, 'chained'); + + expect(flattened[4].value, 'ValueError'); + expect(flattened[4].mechanism?.source, 'exceptions[0]'); + expect(flattened[4].mechanism?.exceptionId, 2); + expect(flattened[4].mechanism?.parentId, 0); + expect(flattened[4].mechanism?.type, 'chained'); + + expect(flattened[5].value, 'RuntimeError'); + expect(flattened[5].mechanism?.exceptionId, 1); + expect(flattened[5].mechanism?.parentId, 0); + expect(flattened[5].mechanism?.type, 'chained'); + + expect(flattened[6].value, 'ExceptionGroup'); + expect(flattened[6].mechanism?.isExceptionGroup, true); + expect(flattened[6].mechanism?.exceptionId, 0); + expect(flattened[6].mechanism?.parentId, isNull); + expect( + flattened[6].mechanism?.type, exceptionGroupNested.mechanism?.type); + }); + }); } diff --git a/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart b/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart index 605c7610e4..410ebeb6a4 100644 --- a/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart +++ b/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart @@ -30,8 +30,8 @@ class AndroidPlatformExceptionEventProcessor implements EventProcessor { // PackageInfo has an internal cache, so no need to do it ourselves. final packageInfo = await PackageInfo.fromPlatform(); - final nativeStackTrace = - _tryParse(platformException.stacktrace, packageInfo.packageName); + final nativeStackTrace = _tryParse( + platformException.stacktrace, packageInfo.packageName, "stackTrace",); final details = platformException.details; String? detailsString; @@ -39,7 +39,7 @@ class AndroidPlatformExceptionEventProcessor implements EventProcessor { detailsString = details; } final detailsStackTrace = - _tryParse(detailsString, packageInfo.packageName); + _tryParse(detailsString, packageInfo.packageName, "details",); if (nativeStackTrace == null && detailsStackTrace == null) { return event; @@ -65,34 +65,43 @@ class AndroidPlatformExceptionEventProcessor implements EventProcessor { } } - List>? _tryParse( + MapEntry>? _tryParse( String? potentialStackTrace, String packageName, + String source, ) { if (potentialStackTrace == null) { return null; } - return _JvmExceptionFactory(packageName) - .fromJvmStackTrace(potentialStackTrace); + .fromJvmStackTrace(potentialStackTrace, source); } SentryEvent _processPlatformException( SentryEvent event, - List>? nativeStackTrace, - List>? detailsStackTrace, + MapEntry>? nativeStackTrace, + MapEntry>? detailsStackTrace, ) { final threads = _markDartThreadsAsNonCrashed(event.threads); + final exception = event.exceptions?.firstOrNull; - final jvmExceptions = [ - ...?nativeStackTrace?.map((e) => e.key), - ...?detailsStackTrace?.map((e) => e.key) - ]; + // Assumption is that the first exception is the original exception and there is only one. + if (exception == null) { + return event; + } + + var jvmThreads = []; + if (nativeStackTrace != null) { + // ignore: invalid_use_of_internal_member + exception.addException(nativeStackTrace.key); + jvmThreads.addAll(nativeStackTrace.value); + } - var jvmThreads = [ - ...?nativeStackTrace?.map((e) => e.value), - ...?detailsStackTrace?.map((e) => e.value), - ]; + if (detailsStackTrace != null) { + // ignore: invalid_use_of_internal_member + exception.addException(detailsStackTrace.key); + jvmThreads.addAll(detailsStackTrace.value); + } if (jvmThreads.isNotEmpty) { // filter potential duplicated threads @@ -105,10 +114,7 @@ class AndroidPlatformExceptionEventProcessor implements EventProcessor { } return event.copyWith( - exceptions: [ - ...?event.exceptions, - ...jvmExceptions, - ], + exceptions: [exception], threads: [ ...?threads, if (_options.attachThreads) ...jvmThreads, @@ -139,33 +145,66 @@ class _JvmExceptionFactory { final String nativePackageName; - List> fromJvmStackTrace( + MapEntry> fromJvmStackTrace( String exceptionAsString, + String source, ) { final jvmException = JvmException.parse(exceptionAsString); - List> sentryExceptions = []; + var sentryException = jvmException.toSentryException(nativePackageName); + sentryException = sentryException.copyWith( + mechanism: + (sentryException.mechanism ?? Mechanism(type: "generic")).copyWith( + source: source, + )); - final sentryException = jvmException.toSentryException(nativePackageName); - sentryExceptions.add(sentryException); + List sentryThreads = []; + int causeIndex = 0; for (final cause in jvmException.causes ?? []) { - final causeSentryException = cause.toSentryException(nativePackageName); - sentryExceptions.add(causeSentryException); + var causeSentryException = cause.toSentryException(nativePackageName); + final causeSentryThread = cause.toSentryThread(); + + var mechanism = + causeSentryException.mechanism ?? Mechanism(type: "generic"); + mechanism = mechanism.copyWith(source: 'causes[$causeIndex]'); + + sentryThreads.add(causeSentryThread); + + causeSentryException = causeSentryException.copyWith( + threadId: causeSentryThread.id, + mechanism: mechanism, + ); + + // ignore: invalid_use_of_internal_member + sentryException.addException(causeSentryException); + causeIndex++; } + int suppressedIndex = 0; for (final suppressed in jvmException.suppressed ?? []) { - final suppressedSentryException = suppressed.toSentryException(nativePackageName); - sentryExceptions.add(suppressedSentryException); - } + var suppressedSentryException = + suppressed.toSentryException(nativePackageName); + final suppressedSentryThread = suppressed.toSentryThread(); + + var mechanism = + suppressedSentryException.mechanism ?? Mechanism(type: "generic"); + mechanism = mechanism.copyWith(source: 'suppressed[$suppressedIndex]'); + + sentryThreads.add(suppressedSentryThread); - return sentryExceptions.toList(growable: false); + suppressedSentryException = suppressedSentryException.copyWith( + threadId: suppressedSentryThread.id, + mechanism: mechanism, + ); + suppressedIndex++; + } + return MapEntry(sentryException, sentryThreads.toList(growable: false)); } } extension on JvmException { - MapEntry toSentryException( - String nativePackageName) { + SentryException toSentryException(String nativePackageName) { String? exceptionType; String? module; final typeParts = type?.split('.'); @@ -183,7 +222,7 @@ extension on JvmException { return entry.value.toSentryStackFrame(entry.key, nativePackageName); }).toList(growable: false); - var exception = SentryException( + return SentryException( value: description, type: exceptionType, module: module, @@ -191,7 +230,9 @@ extension on JvmException { frames: stackFrames.reversed.toList(growable: false), ), ); + } + SentryThread toSentryThread() { String threadName; if (thread != null) { // Needs to be prefixed with 'Android', otherwise this thread id might @@ -204,15 +245,12 @@ extension on JvmException { } final threadId = threadName.hashCode; - final sentryThread = SentryThread( + return SentryThread( crashed: true, current: false, name: threadName, id: threadId, ); - exception = exception.copyWith(threadId: threadId); - - return MapEntry(exception, sentryThread); } } diff --git a/flutter/lib/src/event_processor/exception_group_event_processor.dart b/flutter/lib/src/event_processor/exception_group_event_processor.dart deleted file mode 100644 index cd8353c191..0000000000 --- a/flutter/lib/src/event_processor/exception_group_event_processor.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:sentry/sentry.dart'; - -/// Add code & message from [PlatformException] to [SentryException] -class ExceptionGroupEventProcessor implements EventProcessor { - @override - SentryEvent? apply(SentryEvent event, Hint hint) { - final sentryExceptions = event.exceptions ?? []; - if (sentryExceptions.isEmpty) { - return event; - } - - final updatedSentryExceptions = []; - - int exceptionId = sentryExceptions.length - 1; - - for (SentryException sentryException in sentryExceptions.reversed) { - final mechanism = sentryException.mechanism ?? Mechanism(type: "generic"); - - final isChild = exceptionId > 0; - final isOriginal = !isChild; - - sentryException = sentryException.copyWith( - mechanism: mechanism.copyWith( - type: isChild ? 'chained' : null, - isExceptionGroup: isOriginal ? true : null, - exceptionId: exceptionId, - parentId: isChild ? exceptionId - 1 : null, - ), - ); - updatedSentryExceptions.add(sentryException); - - exceptionId -= 1; - } - - return event.copyWith(exceptions: updatedSentryExceptions); - } -} diff --git a/flutter/test/android_platform_exception_event_processor_test.dart b/flutter/test/android_platform_exception_event_processor_test.dart index d59c5603d6..8dd6c3e444 100644 --- a/flutter/test/android_platform_exception_event_processor_test.dart +++ b/flutter/test/android_platform_exception_event_processor_test.dart @@ -33,12 +33,12 @@ void main() { .apply(fixture.eventWithPlatformDetailsAndStackTrace, Hint()); final exceptions = platformExceptionEvent!.exceptions!; - expect(exceptions.length, 3); + expect(exceptions.length, 1); final exception = exceptions[0]; expect(exception.mechanism?.source, isNull); - final platformException_1 = exceptions[1]; + final platformException_1 = exception.exceptions![0]; expect(platformException_1.type, 'IllegalArgumentException'); expect( @@ -46,9 +46,9 @@ void main() { "Unsupported value: '[Ljava.lang.StackTraceElement;@ba6feed' of type 'class [Ljava.lang.StackTraceElement;'", ); expect(platformException_1.stackTrace!.frames.length, 18); - expect(platformException_1.mechanism?.source, "stackTrace[0]"); + expect(platformException_1.mechanism?.source, "stackTrace"); - final platformException_2 = exceptions[2]; + final platformException_2 = exception.exceptions![1]; expect(platformException_2.type, 'IllegalArgumentException'); expect( @@ -56,7 +56,7 @@ void main() { "Unsupported value: '[Ljava.lang.StackTraceElement;@ba6feed' of type 'class [Ljava.lang.StackTraceElement;'", ); expect(platformException_2.stackTrace!.frames.length, 18); - expect(platformException_2.mechanism?.source, "details[0]"); + expect(platformException_2.mechanism?.source, "details"); }); test('platform exception with details correctly parsed', () async { @@ -64,12 +64,12 @@ void main() { .apply(fixture.eventWithPlatformDetails, Hint()); final exceptions = platformExceptionEvent!.exceptions!; - expect(exceptions.length, 2); + expect(exceptions.length, 1); final exception = exceptions[0]; expect(exception.mechanism?.source, isNull); - final platformException_1 = exceptions[1]; + final platformException_1 = exception.exceptions![0]; expect(platformException_1.type, 'Resources\$NotFoundException'); expect(platformException_1.module, 'android.content.res'); @@ -78,7 +78,7 @@ void main() { "Unable to find resource ID #0x7f14000d", ); expect(platformException_1.stackTrace!.frames.length, 19); - expect(platformException_1.mechanism?.source, "details[0]"); + expect(platformException_1.mechanism?.source, "details"); }); test('platform exception with stackTrace correctly parsed', () async { @@ -86,12 +86,12 @@ void main() { .apply(fixture.eventWithPlatformStackTrace, Hint()); final exceptions = platformExceptionEvent!.exceptions!; - expect(exceptions.length, 2); + expect(exceptions.length, 1); final exception = exceptions[0]; expect(exception.mechanism?.source, isNull); - final platformException_1 = exceptions[1]; + final platformException_1 = exception.exceptions![0]; expect(platformException_1.type, 'IllegalArgumentException'); expect(platformException_1.module, 'java.lang'); @@ -100,7 +100,7 @@ void main() { "Not supported, use openfile", ); expect(platformException_1.stackTrace!.frames.length, 22); - expect(platformException_1.mechanism?.source, "stackTrace[0]"); + expect(platformException_1.mechanism?.source, "stackTrace"); }); test( @@ -110,7 +110,7 @@ void main() { .apply(fixture.eventWithPlatformDetailsAndStackTrace, Hint()); final exceptions = platformExceptionEvent!.exceptions!; - expect(exceptions.length, 3); + expect(exceptions.length, 1); expect(platformExceptionEvent.threads?.first.current, true); expect(platformExceptionEvent.threads?.first.crashed, false); @@ -121,9 +121,10 @@ void main() { .apply(fixture.eventWithPlatformDetailsAndStackTrace, Hint()); final exceptions = platformExceptionEvent!.exceptions!; - expect(exceptions.length, 3); + expect(exceptions.length, 1); - final platformException = exceptions[1]; + final exception = exceptions[0]; + final platformException = exception.exceptions![0]; final platformThread = platformExceptionEvent.threads?[1]; expect(platformException.threadId, platformThread?.id); @@ -142,7 +143,7 @@ void main() { .apply(fixture.eventWithPlatformDetailsAndStackTrace, Hint()); final exceptions = platformExceptionEvent!.exceptions!; - expect(exceptions.length, 3); + expect(exceptions.length, 1); expect(platformExceptionEvent.threads?.length, threadCount); }); diff --git a/flutter/test/event_processor/exception_group_event_processor_test.dart b/flutter/test/event_processor/exception_group_event_processor_test.dart deleted file mode 100644 index bdac0b688e..0000000000 --- a/flutter/test/event_processor/exception_group_event_processor_test.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:sentry/sentry.dart'; -import 'package:sentry_flutter/src/event_processor/exception_group_event_processor.dart'; - -void main() { - group(ExceptionGroupEventProcessor, () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - test('applies grouping to exception cause exceptions', () { - final exceptionC = ExceptionC(); // Original - final exceptionB = ExceptionB(exceptionC); - final exceptionA = ExceptionA(exceptionB); - - // Would be the result of RecursiveExceptionCauseExtractor - final causes = [ - ExceptionCause(exceptionA, null, source: null), - ExceptionCause(exceptionB, null, source: "other"), - ExceptionCause(exceptionC, null, source: "anotherOther"), - ]; - - final sentryExceptions = causes.map((e) { - return SentryException( - type: 'fixture-original-type', - value: 'fixture-original-value', - throwable: e.exception, - mechanism: Mechanism(type: "fixture-type", source: e.source), - ); - }).toList(); - var event = SentryEvent(exceptions: sentryExceptions); - - final sut = fixture.getSut(); - event = (sut.apply(event, Hint()))!; - - final sentryExceptionC = event.exceptions![0]; - expect(sentryExceptionC.throwable, exceptionC); - expect(sentryExceptionC.mechanism?.type, "chained"); - expect(sentryExceptionC.mechanism?.isExceptionGroup, isNull); - expect(sentryExceptionC.mechanism?.exceptionId, 2); - expect(sentryExceptionC.mechanism?.parentId, 1); - expect(sentryExceptionC.mechanism?.source, "anotherOther"); - - final sentryExceptionB = event.exceptions![1]; - expect(sentryExceptionB.throwable, exceptionB); - expect(sentryExceptionB.mechanism?.type, "chained"); - expect(sentryExceptionB.mechanism?.isExceptionGroup, isNull); - expect(sentryExceptionB.mechanism?.exceptionId, 1); - expect(sentryExceptionB.mechanism?.parentId, 0); - expect(sentryExceptionB.mechanism?.source, "other"); - - final sentryExceptionA = event.exceptions![2]; - expect(sentryExceptionA.throwable, exceptionA); - expect(sentryExceptionA.mechanism?.type, "fixture-type"); - expect(sentryExceptionA.mechanism?.isExceptionGroup, true); - expect(sentryExceptionA.mechanism?.exceptionId, 0); - expect(sentryExceptionA.mechanism?.parentId, isNull); - expect(sentryExceptionA.mechanism?.source, isNull); - }); - }); -} - -class Fixture { - ExceptionGroupEventProcessor getSut() { - return ExceptionGroupEventProcessor(); - } -} - -class ExceptionA { - ExceptionA(this.other); - final ExceptionB? other; -} - -class ExceptionB { - ExceptionB(this.anotherOther); - final ExceptionC? anotherOther; -} - -class ExceptionC { - // I am empty inside -} From 576672c7cd0b0efd25424a4af8332f1f451f3deb Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Thu, 27 Mar 2025 10:18:46 +0100 Subject: [PATCH 11/23] mark methods as internal --- dart/lib/src/protocol/sentry_exception.dart | 29 ++++++++++--------- ...id_platform_exception_event_processor.dart | 12 ++++++-- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/dart/lib/src/protocol/sentry_exception.dart b/dart/lib/src/protocol/sentry_exception.dart index f7c406abeb..201d52288c 100644 --- a/dart/lib/src/protocol/sentry_exception.dart +++ b/dart/lib/src/protocol/sentry_exception.dart @@ -31,19 +31,6 @@ class SentryException { List? _exceptions; - List? get exceptions => _exceptions; - - @internal - set exceptions(List? value) { - _exceptions = value; - } - - @internal - void addException(SentryException exception) { - _exceptions ??= []; - _exceptions!.add(exception); - } - SentryException({ required this.type, required this.value, @@ -108,6 +95,22 @@ class SentryException { unknown: unknown, ); + @internal + List? get exceptions => + _exceptions != null ? List.unmodifiable(_exceptions!) : null; + + @internal + set exceptions(List? value) { + _exceptions = value; + } + + @internal + void addException(SentryException exception) { + _exceptions ??= []; + _exceptions!.add(exception); + } + + @internal List flatten({int? parentId, int id = 0}) { final exceptions = this.exceptions ?? []; diff --git a/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart b/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart index 410ebeb6a4..77415dbb44 100644 --- a/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart +++ b/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart @@ -31,15 +31,21 @@ class AndroidPlatformExceptionEventProcessor implements EventProcessor { final packageInfo = await PackageInfo.fromPlatform(); final nativeStackTrace = _tryParse( - platformException.stacktrace, packageInfo.packageName, "stackTrace",); + platformException.stacktrace, + packageInfo.packageName, + "stackTrace", + ); final details = platformException.details; String? detailsString; if (details is String) { detailsString = details; } - final detailsStackTrace = - _tryParse(detailsString, packageInfo.packageName, "details",); + final detailsStackTrace = _tryParse( + detailsString, + packageInfo.packageName, + "details", + ); if (nativeStackTrace == null && detailsStackTrace == null) { return event; From 28a7e06126173961342679917f61b46cef77fa11 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Thu, 27 Mar 2025 10:20:41 +0100 Subject: [PATCH 12/23] add cl entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1c9c4026e..9ff53b89f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ - Set sentry-native backend to `crashpad` by default and `breakpad` for Windows ARM64 ([#2791](https://github.com/getsentry/sentry-dart/pull/2791)) - Setting the `SENTRY_NATIVE_BACKEND` environment variable will override the defaults. - Remove renderer from `flutter_context` ([#2751](https://github.com/getsentry/sentry-dart/pull/2751)) +- Parent-child relationship for the PlatformExceptions and Cause ([#2803](https://github.com/getsentry/sentry-dart/pull/2803)) + - Improves and changes exception grouping ### API changes From d74c081db28a8f06fd8f55e0104631bc0e46ff44 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Thu, 27 Mar 2025 10:34:42 +0100 Subject: [PATCH 13/23] fix integration test --- dart/lib/src/protocol/sentry_exception.dart | 1 - flutter/example/integration_test/integration_test.dart | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dart/lib/src/protocol/sentry_exception.dart b/dart/lib/src/protocol/sentry_exception.dart index 201d52288c..aa9027d6f7 100644 --- a/dart/lib/src/protocol/sentry_exception.dart +++ b/dart/lib/src/protocol/sentry_exception.dart @@ -4,7 +4,6 @@ import '../protocol.dart'; import 'access_aware_map.dart'; /// The Exception Interface specifies an exception or error that occurred in a program. -@immutable class SentryException { /// Required. The type of exception final String? type; diff --git a/flutter/example/integration_test/integration_test.dart b/flutter/example/integration_test/integration_test.dart index 03321ea39d..f8c8478b7d 100644 --- a/flutter/example/integration_test/integration_test.dart +++ b/flutter/example/integration_test/integration_test.dart @@ -66,8 +66,10 @@ void main() { await setupSentryAndApp(tester); try { - throw const SentryException( - type: 'StarError', value: 'I have a bad feeling about this...'); + throw SentryException( + type: 'StarError', + value: 'I have a bad feeling about this...', + ); } catch (exception, stacktrace) { final sentryId = await Sentry.captureException(exception, stackTrace: stacktrace); From 4f6a74f55a9b31be8d5c4e5f8cc6f3f3cf2bca21 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Thu, 27 Mar 2025 10:37:48 +0100 Subject: [PATCH 14/23] fix cl --- CHANGELOG.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ff53b89f4..c128b0e58f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +### Behavioral changes + +- Parent-child relationship for the PlatformExceptions and Cause ([#2803](https://github.com/getsentry/sentry-dart/pull/2803)) + - Improves and changes exception grouping + ## 9.0.0-alpha.2 ### Features @@ -21,8 +28,6 @@ - Set sentry-native backend to `crashpad` by default and `breakpad` for Windows ARM64 ([#2791](https://github.com/getsentry/sentry-dart/pull/2791)) - Setting the `SENTRY_NATIVE_BACKEND` environment variable will override the defaults. - Remove renderer from `flutter_context` ([#2751](https://github.com/getsentry/sentry-dart/pull/2751)) -- Parent-child relationship for the PlatformExceptions and Cause ([#2803](https://github.com/getsentry/sentry-dart/pull/2803)) - - Improves and changes exception grouping ### API changes From 5e0236b7eee2523c1fce7c4a08d88cac2efcbf9e Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Thu, 27 Mar 2025 10:39:26 +0100 Subject: [PATCH 15/23] =?UTF-8?q?don=E2=80=99t=20try=20grouping=20if=20the?= =?UTF-8?q?re=20are=20no=20children?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/exception_group_event_processor.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dart/lib/src/event_processor/exception/exception_group_event_processor.dart b/dart/lib/src/event_processor/exception/exception_group_event_processor.dart index af867c22fb..e3e1d451c7 100644 --- a/dart/lib/src/event_processor/exception/exception_group_event_processor.dart +++ b/dart/lib/src/event_processor/exception/exception_group_event_processor.dart @@ -12,8 +12,8 @@ class ExceptionGroupEventProcessor implements EventProcessor { } final firstException = sentryExceptions.first; - if (sentryExceptions.length > 1) { - // Somehow already a list here, no grouping possible, as there is no root exception. + if (sentryExceptions.length > 1 || firstException.exceptions == null) { + // If already a list or no child exceptions, no grouping possible/needed. return event; } else { final grouped = firstException.flatten().reversed.toList(growable: false); From 31c1a248c4b8ac2ad1dac10156cfdf0edab6ea72 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Thu, 27 Mar 2025 11:23:11 +0100 Subject: [PATCH 16/23] sentry client builds correct hierarchy for exception causes --- dart/lib/src/sentry_client.dart | 30 ++++++++++---- dart/test/sentry_client_test.dart | 66 ++++++++++++++++++++++--------- 2 files changed, 70 insertions(+), 26 deletions(-) diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index ff677fadfc..cd5976e8b1 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -237,18 +237,27 @@ class SentryClient { final isolateId = isolateName?.hashCode; if (event.throwableMechanism != null) { - final extractedExceptions = _exceptionFactory.extractor + final extractedExceptionCauses = _exceptionFactory.extractor .flatten(event.throwableMechanism, stackTrace); - final sentryExceptions = []; + SentryException? rootException; + SentryException? currentException; final sentryThreads = []; - for (final extractedException in extractedExceptions) { + for (final extractedExceptionCause in extractedExceptionCauses) { var sentryException = _exceptionFactory.getSentryException( - extractedException.exception, - stackTrace: extractedException.stackTrace, + extractedExceptionCause.exception, + stackTrace: extractedExceptionCause.stackTrace, removeSentryFrames: hint.get(TypeCheckHint.currentStackTrace), ); + if (extractedExceptionCause.source != null) { + var mechanism = + sentryException.mechanism ?? Mechanism(type: "generic"); + mechanism = mechanism.copyWith( + source: extractedExceptionCause.source, + ); + sentryException = sentryException.copyWith(mechanism: mechanism); + } SentryThread? sentryThread; @@ -264,14 +273,21 @@ class SentryClient { ); } - sentryExceptions.add(sentryException); + rootException ??= sentryException; + currentException?.addException(sentryException); + currentException = sentryException; + if (sentryThread != null) { sentryThreads.add(sentryThread); } } + final exceptions = [...?event.exceptions]; + if (rootException != null) { + exceptions.add(rootException); + } return event.copyWith( - exceptions: [...?event.exceptions, ...sentryExceptions], + exceptions: exceptions, threads: [ ...?event.threads, ...sentryThreads, diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 6f18cb00c8..538d429853 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -15,8 +15,9 @@ import 'package:sentry/src/transport/data_category.dart'; import 'package:sentry/src/transport/noop_transport.dart'; import 'package:sentry/src/transport/spotlight_http_transport.dart'; import 'package:sentry/src/utils/iterable_utils.dart'; -import 'package:test/test.dart'; +import 'package:sentry/src/event_processor/exception/exception_group_event_processor.dart'; +import 'package:test/test.dart'; import 'mocks.dart'; import 'mocks/mock_client_report_recorder.dart'; import 'mocks/mock_hub.dart'; @@ -256,14 +257,23 @@ void main() { final cause = Object(); exception = ExceptionWithCause(cause, null); - final client = fixture.getSut(); + final client = fixture.getSut( + eventProcessor: ExceptionGroupEventProcessor(), + ); 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); + expect(capturedEvent.exceptions?.length, 2); + + final firstException = capturedEvent.exceptions?[0]; + expect(firstException is SentryException, true); + expect(firstException?.mechanism?.source, "cause"); + + final secondException = capturedEvent.exceptions?[1]; + expect(secondException is SentryException, true); + expect(secondException?.mechanism?.source, null); }); test('should capture cause stacktrace', () async { @@ -280,17 +290,20 @@ void main() { exception = ExceptionWithCause(cause, stackTrace); - final client = fixture.getSut(attachStacktrace: true); + final client = fixture.getSut( + attachStacktrace: true, + eventProcessor: ExceptionGroupEventProcessor(), + ); 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, + expect(capturedEvent.exceptions?[0].stackTrace, isNotNull); + expect(capturedEvent.exceptions?[0].stackTrace!.frames.first.fileName, 'test.dart'); - expect(capturedEvent.exceptions?[1].stackTrace!.frames.first.lineNo, 46); - expect(capturedEvent.exceptions?[1].stackTrace!.frames.first.colNo, 9); + expect(capturedEvent.exceptions?[0].stackTrace!.frames.first.lineNo, 46); + expect(capturedEvent.exceptions?[0].stackTrace!.frames.first.colNo, 9); }); test('should capture custom stacktrace', () async { @@ -306,7 +319,10 @@ void main() { exception = ExceptionWithStackTrace(stackTrace); - final client = fixture.getSut(attachStacktrace: true); + final client = fixture.getSut( + attachStacktrace: true, + eventProcessor: ExceptionGroupEventProcessor(), + ); await client.captureException(exception, stackTrace: null); final capturedEnvelope = (fixture.transport).envelopes.first; @@ -328,13 +344,16 @@ void main() { final cause = Object(); exception = ExceptionWithCause(cause, null); - final client = fixture.getSut(attachStacktrace: false); + final client = fixture.getSut( + attachStacktrace: false, + eventProcessor: ExceptionGroupEventProcessor(), + ); await client.captureException(exception, stackTrace: null); final capturedEnvelope = (fixture.transport).envelopes.first; final capturedEvent = await eventFromEnvelope(capturedEnvelope); - expect(capturedEvent.exceptions?[1].stackTrace, isNull); + expect(capturedEvent.exceptions?[0].stackTrace, isNull); }); test( @@ -347,13 +366,16 @@ void main() { final cause = Object(); exception = ExceptionWithCause(cause, StackTrace.empty); - final client = fixture.getSut(attachStacktrace: false); + final client = fixture.getSut( + attachStacktrace: false, + eventProcessor: ExceptionGroupEventProcessor(), + ); await client.captureException(exception, stackTrace: null); final capturedEnvelope = (fixture.transport).envelopes.first; final capturedEvent = await eventFromEnvelope(capturedEnvelope); - expect(capturedEvent.exceptions?[1].stackTrace, isNull); + expect(capturedEvent.exceptions?[0].stackTrace, isNull); }); test('should capture cause exception with Stackframe.current', () async { @@ -364,13 +386,16 @@ void main() { final cause = Object(); exception = ExceptionWithCause(cause, null); - final client = fixture.getSut(attachStacktrace: true); + final client = fixture.getSut( + attachStacktrace: true, + eventProcessor: ExceptionGroupEventProcessor(), + ); 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?[0].stackTrace, isNotNull); }); test('should capture sentry frames exception', () async { @@ -387,13 +412,16 @@ void main() { '''; exception = ExceptionWithCause(cause, stackTrace); - final client = fixture.getSut(attachStacktrace: true); + final client = fixture.getSut( + attachStacktrace: true, + eventProcessor: ExceptionGroupEventProcessor(), + ); await client.captureException(exception, stackTrace: null); final capturedEnvelope = (fixture.transport).envelopes.first; final capturedEvent = await eventFromEnvelope(capturedEnvelope); - final sentryFramesCount = capturedEvent.exceptions?[1].stackTrace!.frames + final sentryFramesCount = capturedEvent.exceptions?[0].stackTrace!.frames .where((frame) => frame.package == 'sentry') .length; @@ -2442,7 +2470,7 @@ class ExceptionWithCauseExtractor extends ExceptionCauseExtractor { @override ExceptionCause? cause(ExceptionWithCause error) { - return ExceptionCause(error.cause, error.stackTrace); + return ExceptionCause(error.cause, error.stackTrace, source: "cause"); } } From 277bc213ecab05fb10da936afa2423cb0f7dd538 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Thu, 27 Mar 2025 11:32:33 +0100 Subject: [PATCH 17/23] reumove unused test --- dart/test/sentry_exception_factory_test.dart | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/dart/test/sentry_exception_factory_test.dart b/dart/test/sentry_exception_factory_test.dart index 2508f58056..ad6df2a724 100644 --- a/dart/test/sentry_exception_factory_test.dart +++ b/dart/test/sentry_exception_factory_test.dart @@ -248,17 +248,6 @@ void main() { expect(sentryStackTrace.frames[16].package, 'sentry_flutter_example'); expect(sentryStackTrace.frames[15].package, 'flutter'); }); - - test('adds generic type mechanism if there is none', () { - final sentryException = - fixture.getSut(attachStacktrace: false).getSentryException( - SentryStackTraceError(), - stackTrace: SentryStackTrace(), - ); - - expect(sentryException.mechanism, isNotNull); - expect(sentryException.mechanism?.type, 'generic'); - }); } class CustomError extends Error {} From a57fa2bba0a2c1a8f487d24ed825af0c8d5a403b Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Thu, 27 Mar 2025 11:48:51 +0100 Subject: [PATCH 18/23] attach missing thread to root exception --- ...id_platform_exception_event_processor.dart | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart b/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart index 77415dbb44..6b5b07733c 100644 --- a/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart +++ b/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart @@ -157,26 +157,31 @@ class _JvmExceptionFactory { ) { final jvmException = JvmException.parse(exceptionAsString); + List sentryThreads = []; + var sentryException = jvmException.toSentryException(nativePackageName); - sentryException = sentryException.copyWith( - mechanism: - (sentryException.mechanism ?? Mechanism(type: "generic")).copyWith( - source: source, - )); + final sentryThread = jvmException.toSentryThread(); + sentryThreads.add(sentryThread); - List sentryThreads = []; + var mechanism = + (sentryException.mechanism ?? Mechanism(type: "generic")).copyWith( + source: source, + ); + sentryException = sentryException.copyWith( + threadId: sentryThread.id, + mechanism: mechanism, + ); int causeIndex = 0; for (final cause in jvmException.causes ?? []) { var causeSentryException = cause.toSentryException(nativePackageName); final causeSentryThread = cause.toSentryThread(); + sentryThreads.add(causeSentryThread); var mechanism = causeSentryException.mechanism ?? Mechanism(type: "generic"); mechanism = mechanism.copyWith(source: 'causes[$causeIndex]'); - sentryThreads.add(causeSentryThread); - causeSentryException = causeSentryException.copyWith( threadId: causeSentryThread.id, mechanism: mechanism, @@ -192,13 +197,12 @@ class _JvmExceptionFactory { var suppressedSentryException = suppressed.toSentryException(nativePackageName); final suppressedSentryThread = suppressed.toSentryThread(); + sentryThreads.add(suppressedSentryThread); var mechanism = suppressedSentryException.mechanism ?? Mechanism(type: "generic"); mechanism = mechanism.copyWith(source: 'suppressed[$suppressedIndex]'); - sentryThreads.add(suppressedSentryThread); - suppressedSentryException = suppressedSentryException.copyWith( threadId: suppressedSentryThread.id, mechanism: mechanism, From 23d5f2459be31295be08eba52edfa0bce3440a4b Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 2 Apr 2025 13:20:32 +0200 Subject: [PATCH 19/23] move flatten to event processor --- .../exception_group_event_processor.dart | 33 +++ dart/lib/src/protocol/sentry_exception.dart | 32 -- .../exception_group_event_processor_test.dart | 277 ++++++++++++++++++ dart/test/protocol/sentry_exception_test.dart | 269 ----------------- 4 files changed, 310 insertions(+), 301 deletions(-) diff --git a/dart/lib/src/event_processor/exception/exception_group_event_processor.dart b/dart/lib/src/event_processor/exception/exception_group_event_processor.dart index e3e1d451c7..5b0bcab84f 100644 --- a/dart/lib/src/event_processor/exception/exception_group_event_processor.dart +++ b/dart/lib/src/event_processor/exception/exception_group_event_processor.dart @@ -21,3 +21,36 @@ class ExceptionGroupEventProcessor implements EventProcessor { } } } + +extension _SentryExceptionFlatten on SentryException { + List flatten({int? parentId, int id = 0}) { + final exceptions = this.exceptions ?? []; + + var mechanism = this.mechanism ?? Mechanism(type: "generic"); + mechanism = mechanism.copyWith( + type: id > 0 ? "chained" : null, + parentId: parentId, + exceptionId: id, + isExceptionGroup: exceptions.length > 1 ? true : null, + ); + + final exception = copyWith( + mechanism: mechanism, + ); + + var all = []; + all.add(exception); + + if (exceptions.isNotEmpty) { + final parentId = id; + for (var exception in exceptions) { + id++; + final flattenedExceptions = + exception.flatten(parentId: parentId, id: id); + id = flattenedExceptions.lastOrNull?.mechanism?.exceptionId ?? id; + all.addAll(flattenedExceptions); + } + } + return all.toList(growable: false); + } +} diff --git a/dart/lib/src/protocol/sentry_exception.dart b/dart/lib/src/protocol/sentry_exception.dart index aa9027d6f7..fa19a439b7 100644 --- a/dart/lib/src/protocol/sentry_exception.dart +++ b/dart/lib/src/protocol/sentry_exception.dart @@ -108,36 +108,4 @@ class SentryException { _exceptions ??= []; _exceptions!.add(exception); } - - @internal - List flatten({int? parentId, int id = 0}) { - final exceptions = this.exceptions ?? []; - - var mechanism = this.mechanism ?? Mechanism(type: "generic"); - mechanism = mechanism.copyWith( - type: id > 0 ? "chained" : null, - parentId: parentId, - exceptionId: id, - isExceptionGroup: exceptions.length > 1 ? true : null, - ); - - final exception = copyWith( - mechanism: mechanism, - ); - - var all = []; - all.add(exception); - - if (exceptions.isNotEmpty) { - final parentId = id; - for (var exception in exceptions) { - id++; - final flattenedExceptions = - exception.flatten(parentId: parentId, id: id); - id = flattenedExceptions.lastOrNull?.mechanism?.exceptionId ?? id; - all.addAll(flattenedExceptions); - } - } - return all.toList(growable: false); - } } diff --git a/dart/test/event_processor/exception/exception_group_event_processor_test.dart b/dart/test/event_processor/exception/exception_group_event_processor_test.dart index cf74e28ea3..49c0dde1c4 100644 --- a/dart/test/event_processor/exception/exception_group_event_processor_test.dart +++ b/dart/test/event_processor/exception/exception_group_event_processor_test.dart @@ -85,6 +85,283 @@ void main() { expect(sentryExceptionB?.type, 'ExceptionB'); }); }); + + group('flatten', () { + late Fixture fixture; + + final sentryException = SentryException( + type: 'type', + value: 'value', + module: 'module', + stackTrace: SentryStackTrace(frames: [SentryStackFrame(absPath: 'abs')]), + mechanism: Mechanism(type: 'type'), + threadId: 1, + ); + + setUp(() { + fixture = Fixture(); + }); + + test('will flatten exception with nested chained exceptions', () { + final origin = sentryException.copyWith( + value: 'origin', + ); + final originChild = sentryException.copyWith( + value: 'originChild', + ); + origin.addException(originChild); + final originChildChild = sentryException.copyWith( + value: 'originChildChild', + ); + originChild.addException(originChildChild); + + final sut = fixture.getSut(); + var event = SentryEvent(exceptions: [origin]); + event = sut.apply(event, Hint())!; + final flattened = event.exceptions ?? []; + + expect(flattened.length, 3); + + expect(flattened[2].value, 'origin'); + expect(flattened[2].mechanism?.isExceptionGroup, isNull); + expect(flattened[2].mechanism?.source, isNull); + expect(flattened[2].mechanism?.exceptionId, 0); + expect(flattened[2].mechanism?.parentId, null); + + expect(flattened[1].value, 'originChild'); + expect(flattened[1].mechanism?.source, isNull); + expect(flattened[1].mechanism?.exceptionId, 1); + expect(flattened[1].mechanism?.parentId, 0); + + expect(flattened[0].value, 'originChildChild'); + expect(flattened[0].mechanism?.source, isNull); + expect(flattened[0].mechanism?.exceptionId, 2); + expect(flattened[0].mechanism?.parentId, 1); + }); + + test('will flatten exception with nested parallel exceptions', () { + final origin = sentryException.copyWith( + value: 'origin', + ); + final originChild = sentryException.copyWith( + value: 'originChild', + ); + origin.addException(originChild); + final originChild2 = sentryException.copyWith( + value: 'originChild2', + ); + origin.addException(originChild2); + + final sut = fixture.getSut(); + var event = SentryEvent(exceptions: [origin]); + event = sut.apply(event, Hint())!; + final flattened = event.exceptions ?? []; + + expect(flattened.length, 3); + + expect(flattened[2].value, 'origin'); + expect(flattened[2].mechanism?.isExceptionGroup, true); + expect(flattened[2].mechanism?.source, isNull); + expect(flattened[2].mechanism?.exceptionId, 0); + expect(flattened[2].mechanism?.parentId, null); + + expect(flattened[1].value, 'originChild'); + expect(flattened[1].mechanism?.source, isNull); + expect(flattened[1].mechanism?.exceptionId, 1); + expect(flattened[1].mechanism?.parentId, 0); + + expect(flattened[0].value, 'originChild2'); + expect(flattened[0].mechanism?.source, isNull); + expect(flattened[0].mechanism?.exceptionId, 2); + expect(flattened[0].mechanism?.parentId, 0); + }); + + test('will flatten rfc example', () { + // try: + // raise RuntimeError("something") + // except: + // raise ExceptionGroup("nested", + // [ + // ValueError(654), + // ExceptionGroup("imports", + // [ + // ImportError("no_such_module"), + // ModuleNotFoundError("another_module"), + // ] + // ), + // TypeError("int"), + // ] + // ) + + // https://github.com/getsentry/rfcs/blob/main/text/0079-exception-groups.md#example-event + // In the example, the runtime error is inserted as the first exception in the outer exception group. + + final exceptionGroupNested = sentryException.copyWith( + value: 'ExceptionGroup', + ); + final runtimeError = sentryException.copyWith( + value: 'RuntimeError', + mechanism: sentryException.mechanism?.copyWith(source: '__source__'), + ); + exceptionGroupNested.addException(runtimeError); + final valueError = sentryException.copyWith( + value: 'ValueError', + mechanism: sentryException.mechanism?.copyWith(source: 'exceptions[0]'), + ); + exceptionGroupNested.addException(valueError); + + final exceptionGroupImports = sentryException.copyWith( + value: 'ExceptionGroup', + mechanism: sentryException.mechanism?.copyWith(source: 'exceptions[1]'), + ); + exceptionGroupNested.addException(exceptionGroupImports); + + final importError = sentryException.copyWith( + value: 'ImportError', + mechanism: sentryException.mechanism?.copyWith(source: 'exceptions[0]'), + ); + exceptionGroupImports.addException(importError); + final moduleNotFoundError = sentryException.copyWith( + value: 'ModuleNotFoundError', + mechanism: sentryException.mechanism?.copyWith(source: 'exceptions[1]'), + ); + exceptionGroupImports.addException(moduleNotFoundError); + + final typeError = sentryException.copyWith( + value: 'TypeError', + mechanism: sentryException.mechanism?.copyWith(source: 'exceptions[2]'), + ); + exceptionGroupNested.addException(typeError); + + final sut = fixture.getSut(); + var event = SentryEvent(exceptions: [exceptionGroupNested]); + event = sut.apply(event, Hint())!; + final flattened = event.exceptions ?? []; + + expect(flattened.length, 7); + + // { + // "exception": { + // "values": [ + // { + // "type": "TypeError", + // "value": "int", + // "mechanism": { + // "type": "chained", + // "source": "exceptions[2]", + // "exception_id": 6, + // "parent_id": 0 + // } + // }, + // { + // "type": "ModuleNotFoundError", + // "value": "another_module", + // "mechanism": { + // "type": "chained", + // "source": "exceptions[1]", + // "exception_id": 5, + // "parent_id": 3 + // } + // }, + // { + // "type": "ImportError", + // "value": "no_such_module", + // "mechanism": { + // "type": "chained", + // "source": "exceptions[0]", + // "exception_id": 4, + // "parent_id": 3 + // } + // }, + // { + // "type": "ExceptionGroup", + // "value": "imports", + // "mechanism": { + // "type": "chained", + // "source": "exceptions[1]", + // "is_exception_group": true, + // "exception_id": 3, + // "parent_id": 0 + // } + // }, + // { + // "type": "ValueError", + // "value": "654", + // "mechanism": { + // "type": "chained", + // "source": "exceptions[0]", + // "exception_id": 2, + // "parent_id": 0 + // } + // }, + // { + // "type": "RuntimeError", + // "value": "something", + // "mechanism": { + // "type": "chained", + // "source": "__context__", + // "exception_id": 1, + // "parent_id": 0 + // } + // }, + // { + // "type": "ExceptionGroup", + // "value": "nested", + // "mechanism": { + // "type": "exceptionhook", + // "handled": false, + // "is_exception_group": true, + // "exception_id": 0 + // } + // }, + // ] + // } + // } + + expect(flattened[0].value, 'TypeError'); + expect(flattened[0].mechanism?.source, 'exceptions[2]'); + expect(flattened[0].mechanism?.exceptionId, 6); + expect(flattened[0].mechanism?.parentId, 0); + expect(flattened[0].mechanism?.type, 'chained'); + + expect(flattened[1].value, 'ModuleNotFoundError'); + expect(flattened[1].mechanism?.source, 'exceptions[1]'); + expect(flattened[1].mechanism?.exceptionId, 5); + expect(flattened[1].mechanism?.parentId, 3); + expect(flattened[1].mechanism?.type, 'chained'); + + expect(flattened[2].value, 'ImportError'); + expect(flattened[2].mechanism?.source, 'exceptions[0]'); + expect(flattened[2].mechanism?.exceptionId, 4); + expect(flattened[2].mechanism?.parentId, 3); + expect(flattened[2].mechanism?.type, 'chained'); + + expect(flattened[3].value, 'ExceptionGroup'); + expect(flattened[3].mechanism?.source, 'exceptions[1]'); + expect(flattened[3].mechanism?.isExceptionGroup, true); + expect(flattened[3].mechanism?.exceptionId, 3); + expect(flattened[3].mechanism?.parentId, 0); + expect(flattened[3].mechanism?.type, 'chained'); + + expect(flattened[4].value, 'ValueError'); + expect(flattened[4].mechanism?.source, 'exceptions[0]'); + expect(flattened[4].mechanism?.exceptionId, 2); + expect(flattened[4].mechanism?.parentId, 0); + expect(flattened[4].mechanism?.type, 'chained'); + + expect(flattened[5].value, 'RuntimeError'); + expect(flattened[5].mechanism?.exceptionId, 1); + expect(flattened[5].mechanism?.parentId, 0); + expect(flattened[5].mechanism?.type, 'chained'); + + expect(flattened[6].value, 'ExceptionGroup'); + expect(flattened[6].mechanism?.isExceptionGroup, true); + expect(flattened[6].mechanism?.exceptionId, 0); + expect(flattened[6].mechanism?.parentId, isNull); + expect( + flattened[6].mechanism?.type, exceptionGroupNested.mechanism?.type); + }); + }); } class Fixture { diff --git a/dart/test/protocol/sentry_exception_test.dart b/dart/test/protocol/sentry_exception_test.dart index d2cfa70563..4541e67332 100644 --- a/dart/test/protocol/sentry_exception_test.dart +++ b/dart/test/protocol/sentry_exception_test.dart @@ -155,273 +155,4 @@ void main() { expect(stackTrace.toJson(), copy.stackTrace!.toJson()); }); }); - - group('flatten', () { - test('flatten exception without nested exceptions', () { - final origin = sentryException.copyWith( - value: 'origin', - ); - - final flattened = origin.flatten(); - - expect(flattened.length, 1); - expect(flattened.first.value, 'origin'); - - expect(flattened.first.mechanism?.source, isNull); - expect(flattened.first.mechanism?.exceptionId, 0); - expect(flattened.first.mechanism?.parentId, null); - }); - - test('flatten exception with nested chained exceptions', () { - final origin = sentryException.copyWith( - value: 'origin', - ); - final originChild = sentryException.copyWith( - value: 'originChild', - ); - origin.addException(originChild); - final originChildChild = sentryException.copyWith( - value: 'originChildChild', - ); - originChild.addException(originChildChild); - - final flattened = origin.flatten(); - - expect(flattened.length, 3); - - expect(flattened[0].value, 'origin'); - expect(flattened[0].mechanism?.isExceptionGroup, isNull); - expect(flattened[0].mechanism?.source, isNull); - expect(flattened[0].mechanism?.exceptionId, 0); - expect(flattened[0].mechanism?.parentId, null); - - expect(flattened[1].value, 'originChild'); - expect(flattened[1].mechanism?.source, isNull); - expect(flattened[1].mechanism?.exceptionId, 1); - expect(flattened[1].mechanism?.parentId, 0); - - expect(flattened[2].value, 'originChildChild'); - expect(flattened[2].mechanism?.source, isNull); - expect(flattened[2].mechanism?.exceptionId, 2); - expect(flattened[2].mechanism?.parentId, 1); - }); - - test('flatten exception with nested parallel exceptions', () { - final origin = sentryException.copyWith( - value: 'origin', - ); - final originChild = sentryException.copyWith( - value: 'originChild', - ); - origin.addException(originChild); - final originChild2 = sentryException.copyWith( - value: 'originChild2', - ); - origin.addException(originChild2); - - final flattened = origin.flatten(); - - expect(flattened.length, 3); - - expect(flattened[0].value, 'origin'); - expect(flattened[0].mechanism?.isExceptionGroup, true); - expect(flattened[0].mechanism?.source, isNull); - expect(flattened[0].mechanism?.exceptionId, 0); - expect(flattened[0].mechanism?.parentId, null); - - expect(flattened[1].value, 'originChild'); - expect(flattened[1].mechanism?.source, isNull); - expect(flattened[1].mechanism?.exceptionId, 1); - expect(flattened[1].mechanism?.parentId, 0); - - expect(flattened[2].value, 'originChild2'); - expect(flattened[2].mechanism?.source, isNull); - expect(flattened[2].mechanism?.exceptionId, 2); - expect(flattened[2].mechanism?.parentId, 0); - }); - - test('flatten rfc example', () { - // try: - // raise RuntimeError("something") - // except: - // raise ExceptionGroup("nested", - // [ - // ValueError(654), - // ExceptionGroup("imports", - // [ - // ImportError("no_such_module"), - // ModuleNotFoundError("another_module"), - // ] - // ), - // TypeError("int"), - // ] - // ) - - // https://github.com/getsentry/rfcs/blob/main/text/0079-exception-groups.md#example-event - // In the example, the runtime error is inserted as the first exception in the outer exception group. - - final exceptionGroupNested = sentryException.copyWith( - value: 'ExceptionGroup', - ); - final runtimeError = sentryException.copyWith( - value: 'RuntimeError', - mechanism: sentryException.mechanism?.copyWith(source: '__source__'), - ); - exceptionGroupNested.addException(runtimeError); - final valueError = sentryException.copyWith( - value: 'ValueError', - mechanism: sentryException.mechanism?.copyWith(source: 'exceptions[0]'), - ); - exceptionGroupNested.addException(valueError); - - final exceptionGroupImports = sentryException.copyWith( - value: 'ExceptionGroup', - mechanism: sentryException.mechanism?.copyWith(source: 'exceptions[1]'), - ); - exceptionGroupNested.addException(exceptionGroupImports); - - final importError = sentryException.copyWith( - value: 'ImportError', - mechanism: sentryException.mechanism?.copyWith(source: 'exceptions[0]'), - ); - exceptionGroupImports.addException(importError); - final moduleNotFoundError = sentryException.copyWith( - value: 'ModuleNotFoundError', - mechanism: sentryException.mechanism?.copyWith(source: 'exceptions[1]'), - ); - exceptionGroupImports.addException(moduleNotFoundError); - - final typeError = sentryException.copyWith( - value: 'TypeError', - mechanism: sentryException.mechanism?.copyWith(source: 'exceptions[2]'), - ); - exceptionGroupNested.addException(typeError); - - final flattened = - exceptionGroupNested.flatten().reversed.toList(growable: false); - - expect(flattened.length, 7); - - // { - // "exception": { - // "values": [ - // { - // "type": "TypeError", - // "value": "int", - // "mechanism": { - // "type": "chained", - // "source": "exceptions[2]", - // "exception_id": 6, - // "parent_id": 0 - // } - // }, - // { - // "type": "ModuleNotFoundError", - // "value": "another_module", - // "mechanism": { - // "type": "chained", - // "source": "exceptions[1]", - // "exception_id": 5, - // "parent_id": 3 - // } - // }, - // { - // "type": "ImportError", - // "value": "no_such_module", - // "mechanism": { - // "type": "chained", - // "source": "exceptions[0]", - // "exception_id": 4, - // "parent_id": 3 - // } - // }, - // { - // "type": "ExceptionGroup", - // "value": "imports", - // "mechanism": { - // "type": "chained", - // "source": "exceptions[1]", - // "is_exception_group": true, - // "exception_id": 3, - // "parent_id": 0 - // } - // }, - // { - // "type": "ValueError", - // "value": "654", - // "mechanism": { - // "type": "chained", - // "source": "exceptions[0]", - // "exception_id": 2, - // "parent_id": 0 - // } - // }, - // { - // "type": "RuntimeError", - // "value": "something", - // "mechanism": { - // "type": "chained", - // "source": "__context__", - // "exception_id": 1, - // "parent_id": 0 - // } - // }, - // { - // "type": "ExceptionGroup", - // "value": "nested", - // "mechanism": { - // "type": "exceptionhook", - // "handled": false, - // "is_exception_group": true, - // "exception_id": 0 - // } - // }, - // ] - // } - // } - - expect(flattened[0].value, 'TypeError'); - expect(flattened[0].mechanism?.source, 'exceptions[2]'); - expect(flattened[0].mechanism?.exceptionId, 6); - expect(flattened[0].mechanism?.parentId, 0); - expect(flattened[0].mechanism?.type, 'chained'); - - expect(flattened[1].value, 'ModuleNotFoundError'); - expect(flattened[1].mechanism?.source, 'exceptions[1]'); - expect(flattened[1].mechanism?.exceptionId, 5); - expect(flattened[1].mechanism?.parentId, 3); - expect(flattened[1].mechanism?.type, 'chained'); - - expect(flattened[2].value, 'ImportError'); - expect(flattened[2].mechanism?.source, 'exceptions[0]'); - expect(flattened[2].mechanism?.exceptionId, 4); - expect(flattened[2].mechanism?.parentId, 3); - expect(flattened[2].mechanism?.type, 'chained'); - - expect(flattened[3].value, 'ExceptionGroup'); - expect(flattened[3].mechanism?.source, 'exceptions[1]'); - expect(flattened[3].mechanism?.isExceptionGroup, true); - expect(flattened[3].mechanism?.exceptionId, 3); - expect(flattened[3].mechanism?.parentId, 0); - expect(flattened[3].mechanism?.type, 'chained'); - - expect(flattened[4].value, 'ValueError'); - expect(flattened[4].mechanism?.source, 'exceptions[0]'); - expect(flattened[4].mechanism?.exceptionId, 2); - expect(flattened[4].mechanism?.parentId, 0); - expect(flattened[4].mechanism?.type, 'chained'); - - expect(flattened[5].value, 'RuntimeError'); - expect(flattened[5].mechanism?.exceptionId, 1); - expect(flattened[5].mechanism?.parentId, 0); - expect(flattened[5].mechanism?.type, 'chained'); - - expect(flattened[6].value, 'ExceptionGroup'); - expect(flattened[6].mechanism?.isExceptionGroup, true); - expect(flattened[6].mechanism?.exceptionId, 0); - expect(flattened[6].mechanism?.parentId, isNull); - expect( - flattened[6].mechanism?.type, exceptionGroupNested.mechanism?.type); - }); - }); } From 6fdbf5b6105b6e182a8e306fddd8c287dddcdcb7 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 7 Apr 2025 14:16:39 +0200 Subject: [PATCH 20/23] format --- .../exception_group_event_processor.dart | 24 +++++++++---------- dart/lib/src/protocol/sentry_exception.dart | 4 ++-- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/dart/lib/src/event_processor/exception/exception_group_event_processor.dart b/dart/lib/src/event_processor/exception/exception_group_event_processor.dart index 5b0bcab84f..5f8a11c3c5 100644 --- a/dart/lib/src/event_processor/exception/exception_group_event_processor.dart +++ b/dart/lib/src/event_processor/exception/exception_group_event_processor.dart @@ -16,8 +16,9 @@ class ExceptionGroupEventProcessor implements EventProcessor { // If already a list or no child exceptions, no grouping possible/needed. return event; } else { - final grouped = firstException.flatten().reversed.toList(growable: false); - return event.copyWith(exceptions: grouped); + event.exceptions = + firstException.flatten().reversed.toList(growable: false); + return event; } } } @@ -26,20 +27,17 @@ extension _SentryExceptionFlatten on SentryException { List flatten({int? parentId, int id = 0}) { final exceptions = this.exceptions ?? []; - var mechanism = this.mechanism ?? Mechanism(type: "generic"); - mechanism = mechanism.copyWith( - type: id > 0 ? "chained" : null, - parentId: parentId, - exceptionId: id, - isExceptionGroup: exceptions.length > 1 ? true : null, - ); + final newMechanism = mechanism ?? Mechanism(type: "generic"); + newMechanism + ..type = id > 0 ? "chained" : newMechanism.type + ..parentId = parentId + ..exceptionId = id + ..isExceptionGroup = exceptions.length > 1 ? true : null; - final exception = copyWith( - mechanism: mechanism, - ); + mechanism = newMechanism; var all = []; - all.add(exception); + all.add(this); if (exceptions.isNotEmpty) { final parentId = id; diff --git a/dart/lib/src/protocol/sentry_exception.dart b/dart/lib/src/protocol/sentry_exception.dart index 6125d77cfe..a27d86c140 100644 --- a/dart/lib/src/protocol/sentry_exception.dart +++ b/dart/lib/src/protocol/sentry_exception.dart @@ -88,8 +88,8 @@ class SentryException { type: type ?? this.type, value: value ?? this.value, module: module ?? this.module, - stackTrace: stackTrace ?? this.stackTrace, - mechanism: mechanism ?? this.mechanism, + stackTrace: stackTrace ?? this.stackTrace?.copyWith(), + mechanism: mechanism ?? this.mechanism?.copyWith(), threadId: threadId ?? this.threadId, throwable: throwable ?? this.throwable, unknown: unknown, From a19c9aa0a9f607e376c2ac4dced075b1f2fed6d0 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 8 Apr 2025 14:43:08 +0200 Subject: [PATCH 21/23] handle copyWith usage --- .../exception_group_event_processor_test.dart | 21 +++++++++++ ...id_platform_exception_event_processor.dart | 37 ++++++++----------- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/dart/test/event_processor/exception/exception_group_event_processor_test.dart b/dart/test/event_processor/exception/exception_group_event_processor_test.dart index 49c0dde1c4..6cfa505ca8 100644 --- a/dart/test/event_processor/exception/exception_group_event_processor_test.dart +++ b/dart/test/event_processor/exception/exception_group_event_processor_test.dart @@ -103,13 +103,17 @@ void main() { }); test('will flatten exception with nested chained exceptions', () { + // ignore: deprecated_member_use_from_same_package final origin = sentryException.copyWith( value: 'origin', ); + // ignore: deprecated_member_use_from_same_package final originChild = sentryException.copyWith( value: 'originChild', ); origin.addException(originChild); + + // ignore: deprecated_member_use_from_same_package final originChildChild = sentryException.copyWith( value: 'originChildChild', ); @@ -140,13 +144,16 @@ void main() { }); test('will flatten exception with nested parallel exceptions', () { + // ignore: deprecated_member_use_from_same_package final origin = sentryException.copyWith( value: 'origin', ); + // ignore: deprecated_member_use_from_same_package final originChild = sentryException.copyWith( value: 'originChild', ); origin.addException(originChild); + // ignore: deprecated_member_use_from_same_package final originChild2 = sentryException.copyWith( value: 'originChild2', ); @@ -196,39 +203,53 @@ void main() { // https://github.com/getsentry/rfcs/blob/main/text/0079-exception-groups.md#example-event // In the example, the runtime error is inserted as the first exception in the outer exception group. + // ignore: deprecated_member_use_from_same_package final exceptionGroupNested = sentryException.copyWith( value: 'ExceptionGroup', ); + // ignore: deprecated_member_use_from_same_package final runtimeError = sentryException.copyWith( value: 'RuntimeError', + // ignore: deprecated_member_use_from_same_package mechanism: sentryException.mechanism?.copyWith(source: '__source__'), ); exceptionGroupNested.addException(runtimeError); + // ignore: deprecated_member_use_from_same_package final valueError = sentryException.copyWith( value: 'ValueError', + // ignore: deprecated_member_use_from_same_package mechanism: sentryException.mechanism?.copyWith(source: 'exceptions[0]'), ); exceptionGroupNested.addException(valueError); + // ignore: deprecated_member_use_from_same_package final exceptionGroupImports = sentryException.copyWith( value: 'ExceptionGroup', + // ignore: deprecated_member_use_from_same_package mechanism: sentryException.mechanism?.copyWith(source: 'exceptions[1]'), ); exceptionGroupNested.addException(exceptionGroupImports); + // ignore: deprecated_member_use_from_same_package final importError = sentryException.copyWith( value: 'ImportError', + // ignore: deprecated_member_use_from_same_package mechanism: sentryException.mechanism?.copyWith(source: 'exceptions[0]'), ); exceptionGroupImports.addException(importError); + + // ignore: deprecated_member_use_from_same_package final moduleNotFoundError = sentryException.copyWith( value: 'ModuleNotFoundError', + // ignore: deprecated_member_use_from_same_package mechanism: sentryException.mechanism?.copyWith(source: 'exceptions[1]'), ); exceptionGroupImports.addException(moduleNotFoundError); + // ignore: deprecated_member_use_from_same_package final typeError = sentryException.copyWith( value: 'TypeError', + // ignore: deprecated_member_use_from_same_package mechanism: sentryException.mechanism?.copyWith(source: 'exceptions[2]'), ); exceptionGroupNested.addException(typeError); diff --git a/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart b/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart index 6987732074..60dbadc378 100644 --- a/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart +++ b/flutter/lib/src/event_processor/android_platform_exception_event_processor.dart @@ -154,18 +154,14 @@ class _JvmExceptionFactory { List sentryThreads = []; - var sentryException = jvmException.toSentryException(nativePackageName); + final sentryException = jvmException.toSentryException(nativePackageName); final sentryThread = jvmException.toSentryThread(); sentryThreads.add(sentryThread); - var mechanism = - (sentryException.mechanism ?? Mechanism(type: "generic")).copyWith( - source: source, - ); - sentryException = sentryException.copyWith( - threadId: sentryThread.id, - mechanism: mechanism, - ); + final mechanism = sentryException.mechanism ?? Mechanism(type: "generic"); + mechanism.source = source; + sentryException.threadId = sentryThread.id; + sentryException.mechanism = mechanism; int causeIndex = 0; for (final cause in jvmException.causes ?? []) { @@ -173,14 +169,12 @@ class _JvmExceptionFactory { final causeSentryThread = cause.toSentryThread(); sentryThreads.add(causeSentryThread); - var mechanism = + final causeMechanism = causeSentryException.mechanism ?? Mechanism(type: "generic"); - mechanism = mechanism.copyWith(source: 'causes[$causeIndex]'); + causeMechanism.source = 'causes[$causeIndex]'; - causeSentryException = causeSentryException.copyWith( - threadId: causeSentryThread.id, - mechanism: mechanism, - ); + causeSentryException.threadId = causeSentryThread.id; + causeSentryException.mechanism = causeMechanism; // ignore: invalid_use_of_internal_member sentryException.addException(causeSentryException); @@ -194,14 +188,15 @@ class _JvmExceptionFactory { final suppressedSentryThread = suppressed.toSentryThread(); sentryThreads.add(suppressedSentryThread); - var mechanism = + final suppressedMechanism = suppressedSentryException.mechanism ?? Mechanism(type: "generic"); - mechanism = mechanism.copyWith(source: 'suppressed[$suppressedIndex]'); + suppressedMechanism.source = 'suppressed[$suppressedIndex]'; - suppressedSentryException = suppressedSentryException.copyWith( - threadId: suppressedSentryThread.id, - mechanism: mechanism, - ); + suppressedSentryException.threadId = suppressedSentryThread.id; + suppressedSentryException.mechanism = suppressedMechanism; + + // ignore: invalid_use_of_internal_member + sentryException.addException(suppressedSentryException); suppressedIndex++; } return MapEntry(sentryException, sentryThreads.toList(growable: false)); From 3a6af6817d59b4a55cec71d368d444fe8b4f9aca Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 9 Apr 2025 15:33:11 +0200 Subject: [PATCH 22/23] update exception grouping --- .../exception_group_event_processor.dart | 2 +- .../io_exception_event_processor.dart | 18 ++++++++--- .../exception_group_event_processor_test.dart | 6 ++-- .../io_exception_event_processor_test.dart | 31 ++++++++++--------- dart/test/sentry_client_test.dart | 6 ++++ 5 files changed, 40 insertions(+), 23 deletions(-) diff --git a/dart/lib/src/event_processor/exception/exception_group_event_processor.dart b/dart/lib/src/event_processor/exception/exception_group_event_processor.dart index 5f8a11c3c5..6141452315 100644 --- a/dart/lib/src/event_processor/exception/exception_group_event_processor.dart +++ b/dart/lib/src/event_processor/exception/exception_group_event_processor.dart @@ -32,7 +32,7 @@ extension _SentryExceptionFlatten on SentryException { ..type = id > 0 ? "chained" : newMechanism.type ..parentId = parentId ..exceptionId = id - ..isExceptionGroup = exceptions.length > 1 ? true : null; + ..isExceptionGroup = exceptions.isNotEmpty ? true : null; mechanism = newMechanism; diff --git a/dart/lib/src/event_processor/exception/io_exception_event_processor.dart b/dart/lib/src/event_processor/exception/io_exception_event_processor.dart index 3cab8bb512..40aa5fa9a9 100644 --- a/dart/lib/src/event_processor/exception/io_exception_event_processor.dart +++ b/dart/lib/src/event_processor/exception/io_exception_event_processor.dart @@ -45,14 +45,18 @@ class IoExceptionEventProcessor implements ExceptionEventProcessor { ) { final osError = exception.osError; SentryException? osException; - List? exceptions; + List? exceptions = event.exceptions; if (osError != null) { // OSError is the underlying error // https://api.dart.dev/stable/dart-io/SocketException/osError.html // https://api.dart.dev/stable/dart-io/OSError-class.html osException = _sentryExceptionFromOsError(osError); - osException.exceptions = event.exceptions; - exceptions = [osException]; + final exception = event.exceptions?.firstOrNull; + if (exception != null) { + exception.addException(osException); + } else { + exceptions = [osException]; + } } else { exceptions = event.exceptions; } @@ -95,8 +99,12 @@ class IoExceptionEventProcessor implements ExceptionEventProcessor { // https://api.dart.dev/stable/dart-io/SocketException/osError.html // https://api.dart.dev/stable/dart-io/OSError-class.html final osException = _sentryExceptionFromOsError(osError); - osException.exceptions = event.exceptions; - event.exceptions = [osException]; + final exception = event.exceptions?.firstOrNull; + if (exception != null) { + exception.addException(osException); + } else { + event.exceptions = [osException]; + } } return event; } diff --git a/dart/test/event_processor/exception/exception_group_event_processor_test.dart b/dart/test/event_processor/exception/exception_group_event_processor_test.dart index 6cfa505ca8..32332ab8f5 100644 --- a/dart/test/event_processor/exception/exception_group_event_processor_test.dart +++ b/dart/test/event_processor/exception/exception_group_event_processor_test.dart @@ -45,7 +45,7 @@ void main() { expect(sentryExceptionA.throwable, throwableA); expect(sentryExceptionA.mechanism?.type, "generic"); - expect(sentryExceptionA.mechanism?.isExceptionGroup, isNull); + expect(sentryExceptionA.mechanism?.isExceptionGroup, isTrue); expect(sentryExceptionA.mechanism?.exceptionId, 0); expect(sentryExceptionA.mechanism?.parentId, isNull); }); @@ -127,17 +127,19 @@ void main() { expect(flattened.length, 3); expect(flattened[2].value, 'origin'); - expect(flattened[2].mechanism?.isExceptionGroup, isNull); + expect(flattened[2].mechanism?.isExceptionGroup, isTrue); expect(flattened[2].mechanism?.source, isNull); expect(flattened[2].mechanism?.exceptionId, 0); expect(flattened[2].mechanism?.parentId, null); expect(flattened[1].value, 'originChild'); + expect(flattened[1].mechanism?.isExceptionGroup, isTrue); expect(flattened[1].mechanism?.source, isNull); expect(flattened[1].mechanism?.exceptionId, 1); expect(flattened[1].mechanism?.parentId, 0); expect(flattened[0].value, 'originChildChild'); + expect(flattened[0].mechanism?.isExceptionGroup, isNull); expect(flattened[0].mechanism?.source, isNull); expect(flattened[0].mechanism?.exceptionId, 2); expect(flattened[0].mechanism?.parentId, 1); diff --git a/dart/test/event_processor/exception/io_exception_event_processor_test.dart b/dart/test/event_processor/exception/io_exception_event_processor_test.dart index 4e0d318b68..109b07b8eb 100644 --- a/dart/test/event_processor/exception/io_exception_event_processor_test.dart +++ b/dart/test/event_processor/exception/io_exception_event_processor_test.dart @@ -73,15 +73,15 @@ void main() { expect(event?.request?.url, '127.0.0.1'); final rootException = event?.exceptions?.first; - expect(rootException?.type, 'OSError'); - expect(rootException?.value, - 'OS Error: Connection reset by peer, errno = 54'); - expect(rootException?.mechanism?.type, 'OSError'); - expect(rootException?.mechanism?.meta['errno']['number'], 54); - expect(rootException?.mechanism?.source, 'osError'); + expect(rootException, sentryException); final childException = rootException?.exceptions?.first; - expect(childException, sentryException); + expect(childException?.type, 'OSError'); + expect(childException?.value, + 'OS Error: Connection reset by peer, errno = 54'); + expect(childException?.mechanism?.type, 'OSError'); + expect(childException?.mechanism?.meta['errno']['number'], 54); + expect(childException?.mechanism?.source, 'osError'); }); test('adds OSError SentryException for $FileSystemException', () { @@ -102,19 +102,20 @@ void main() { Hint(), ); + final rootException = event?.exceptions?.first; + expect(rootException, sentryException); + + final childException = rootException?.exceptions?.firstOrNull; // Due to the test setup, there's no SentryException for the FileSystemException. // And thus only one entry for the added OSError - expect(event?.exceptions?.first.type, 'OSError'); + expect(childException?.type, 'OSError'); expect( - event?.exceptions?.first.value, + childException?.value, 'OS Error: Oh no :(, errno = 42', ); - expect(event?.exceptions?.first.mechanism?.type, 'OSError'); - expect(event?.exceptions?.first.mechanism?.meta['errno']['number'], 42); - expect(event?.exceptions?.first.mechanism?.source, 'osError'); - - final childException = event?.exceptions?.first.exceptions?.first; - expect(childException, sentryException); + expect(childException?.mechanism?.type, 'OSError'); + expect(childException?.mechanism?.meta['errno']['number'], 42); + expect(childException?.mechanism?.source, 'osError'); }); }); } diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 2f8471a659..729b5349df 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -270,10 +270,16 @@ void main() { final firstException = capturedEvent.exceptions?[0]; expect(firstException is SentryException, true); expect(firstException?.mechanism?.source, "cause"); + expect(firstException?.mechanism?.parentId, 0); + expect(firstException?.mechanism?.exceptionId, 1); + expect(firstException?.mechanism?.isExceptionGroup, isNull); final secondException = capturedEvent.exceptions?[1]; expect(secondException is SentryException, true); expect(secondException?.mechanism?.source, null); + expect(secondException?.mechanism?.parentId, null); + expect(secondException?.mechanism?.exceptionId, 0); + expect(secondException?.mechanism?.isExceptionGroup, isTrue); }); test('should capture cause stacktrace', () async { From cd9e4aefe894c396c74d2a91fd696926574ede18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Andra=C5=A1ec?= Date: Thu, 10 Apr 2025 12:51:10 +0200 Subject: [PATCH 23/23] Update dart/lib/src/sentry_exception_factory.dart remove newline Co-authored-by: Giancarlo Buenaflor --- dart/lib/src/sentry_exception_factory.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/dart/lib/src/sentry_exception_factory.dart b/dart/lib/src/sentry_exception_factory.dart index 1e18a9f06d..15d474bf4f 100644 --- a/dart/lib/src/sentry_exception_factory.dart +++ b/dart/lib/src/sentry_exception_factory.dart @@ -22,7 +22,6 @@ class SentryExceptionFactory { }) { var throwable = exception; Mechanism? mechanism; - bool? snapshot; if (exception is ThrowableMechanism) { throwable = exception.throwable;