diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f24a7ed7e..89250d11c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- StackTraces in `PlatformException.message` will get nicely formatted too when present ([#1716](https://github.com/getsentry/sentry-dart/pull/1716)) - Breadcrumbs for database operations ([#1656](https://github.com/getsentry/sentry-dart/pull/1656)) ## 7.12.0 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 377af31d25..c784f5b221 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 @@ -15,9 +15,6 @@ class AndroidPlatformExceptionEventProcessor implements EventProcessor { final SentryFlutterOptions _options; - // Because of obfuscation, we need to dynamically get the name - static final _platformExceptionType = (PlatformException).toString(); - @override Future apply(SentryEvent event, {Hint? hint}) async { if (event is SentryTransaction) { @@ -29,19 +26,23 @@ class AndroidPlatformExceptionEventProcessor implements EventProcessor { return event; } - final nativeStackTrace = plaformException.stacktrace; - if (nativeStackTrace == null) { - return event; - } - try { // PackageInfo has an internal cache, so no need to do it ourselves. final packageInfo = await PackageInfo.fromPlatform(); + + final nativeStackTrace = + _tryParse(plaformException.stacktrace, packageInfo.packageName); + final messageStackTrace = + _tryParse(plaformException.message, packageInfo.packageName); + + if (nativeStackTrace == null && messageStackTrace == null) { + return event; + } + return _processPlatformException( event, - plaformException, nativeStackTrace, - packageInfo.packageName, + messageStackTrace, ); } catch (e, stackTrace) { _options.logger( @@ -55,25 +56,35 @@ class AndroidPlatformExceptionEventProcessor implements EventProcessor { } } - SentryEvent _processPlatformException( - SentryEvent event, - PlatformException exception, - String nativeStackTrace, + List>? _tryParse( + String? potentialStackTrace, String packageName, ) { - final jvmException = - _JvmExceptionFactory(packageName).fromJvmStackTrace(nativeStackTrace); + if (potentialStackTrace == null) { + return null; + } - final exceptions = _removePlatformExceptionStackTraceFromValue( - event.exceptions, - exception, - ); + return _JvmExceptionFactory(packageName) + .fromJvmStackTrace(potentialStackTrace); + } + SentryEvent _processPlatformException( + SentryEvent event, + List>? nativeStackTrace, + List>? messageStackTrace, + ) { final threads = _markDartThreadsAsNonCrashed(event.threads); - final jvmExceptions = jvmException.map((e) => e.key); + final jvmExceptions = [ + ...?nativeStackTrace?.map((e) => e.key), + ...?messageStackTrace?.map((e) => e.key) + ]; + + var jvmThreads = [ + ...?nativeStackTrace?.map((e) => e.value), + ...?messageStackTrace?.map((e) => e.value), + ]; - var jvmThreads = jvmException.map((e) => e.value).toList(growable: false); if (jvmThreads.isNotEmpty) { // filter potential duplicated threads final first = jvmThreads.first; @@ -84,13 +95,16 @@ class AndroidPlatformExceptionEventProcessor implements EventProcessor { jvmThreads.add(first); } - return event.copyWith(exceptions: [ - ...?exceptions, - ...jvmExceptions, - ], threads: [ - ...?threads, - if (_options.attachThreads) ...jvmThreads, - ]); + return event.copyWith( + exceptions: [ + ...?event.exceptions, + ...jvmExceptions, + ], + threads: [ + ...?threads, + if (_options.attachThreads) ...jvmThreads, + ], + ); } /// If the crash originated on Android, the Dart side didn't crash. @@ -99,60 +113,16 @@ class AndroidPlatformExceptionEventProcessor implements EventProcessor { List? threads, ) { return threads - ?.map((e) => e.copyWith( - crashed: false, - // Isolate is safe to use directly, - // because Android is only run in the dart:io context. - current: e.name == Isolate.current.debugName, - )) + ?.map( + (e) => e.copyWith( + crashed: false, + // Isolate is safe to use directly, + // because Android is only run in the dart:io context. + current: e.name == Isolate.current.debugName, + ), + ) .toList(growable: false); } - - /// Remove the StackTrace from [PlatformException] so the message on Sentry - /// looks much better. - List? _removePlatformExceptionStackTraceFromValue( - List? exceptions, - PlatformException platformException, - ) { - if (exceptions == null || exceptions.isEmpty) { - return null; - } - final exceptionCopy = List.from(exceptions); - - final sentryExceptions = exceptionCopy - .where((element) => element.type == _platformExceptionType); - if (sentryExceptions.isEmpty) { - return null; - } - var sentryException = sentryExceptions.first; - - final exceptionIndex = exceptionCopy.indexOf(sentryException); - exceptionCopy.remove(sentryException); - - // Remove stacktrace, so that the PlatformException value doesn't - // include the chained exception. - // PlatformException.stackTrace is an empty string so that - // PlatformException.toString() results in - // `PlatformException(error, Exception Message, null, )` - // instead of - // `PlatformException(error, Exception Message, null, null)`. - // While `null` for `PlatformException.stackTrace` is technically correct - // it's semantically wrong. - platformException = PlatformException( - code: platformException.code, - details: platformException.details, - message: platformException.message, - stacktrace: '', - ); - - sentryException = sentryException.copyWith( - value: platformException.toString(), - ); - - exceptionCopy.insert(exceptionIndex, sentryException); - - return exceptionCopy; - } } class _JvmExceptionFactory { @@ -161,7 +131,8 @@ class _JvmExceptionFactory { final String nativePackageName; List> fromJvmStackTrace( - String exceptionAsString) { + String exceptionAsString, + ) { final jvmException = JvmException.parse(exceptionAsString); final jvmExceptions = [ jvmException, diff --git a/flutter/test/android_platform_exception_event_processor_test.dart b/flutter/test/android_platform_exception_event_processor_test.dart index bfd8e2b4ce..f98bedbe26 100644 --- a/flutter/test/android_platform_exception_event_processor_test.dart +++ b/flutter/test/android_platform_exception_event_processor_test.dart @@ -30,7 +30,7 @@ void main() { await fixture.processor.apply(fixture.eventWithPlatformStackTrace); final exceptions = platformExceptionEvent!.exceptions!; - expect(exceptions.length, 2); + expect(exceptions.length, 3); final platformException = exceptions[1]; @@ -40,6 +40,15 @@ void main() { "Unsupported value: '[Ljava.lang.StackTraceElement;@ba6feed' of type 'class [Ljava.lang.StackTraceElement;'", ); expect(platformException.stackTrace!.frames.length, 18); + + final platformException_2 = exceptions[2]; + + expect(platformException_2.type, 'IllegalArgumentException'); + expect( + platformException_2.value, + "Unsupported value: '[Ljava.lang.StackTraceElement;@ba6feed' of type 'class [Ljava.lang.StackTraceElement;'", + ); + expect(platformException_2.stackTrace!.frames.length, 18); }); test( @@ -49,7 +58,7 @@ void main() { await fixture.processor.apply(fixture.eventWithPlatformStackTrace); final exceptions = platformExceptionEvent!.exceptions!; - expect(exceptions.length, 2); + expect(exceptions.length, 3); expect(platformExceptionEvent.threads?.first.current, true); expect(platformExceptionEvent.threads?.first.crashed, false); @@ -60,7 +69,7 @@ void main() { await fixture.processor.apply(fixture.eventWithPlatformStackTrace); final exceptions = platformExceptionEvent!.exceptions!; - expect(exceptions.length, 2); + expect(exceptions.length, 3); final platformException = exceptions[1]; final platformThread = platformExceptionEvent.threads?[1]; @@ -80,7 +89,7 @@ void main() { await fixture.processor.apply(fixture.eventWithPlatformStackTrace); final exceptions = platformExceptionEvent!.exceptions!; - expect(exceptions.length, 2); + expect(exceptions.length, 3); expect(platformExceptionEvent.threads?.length, threadCount); }); @@ -144,12 +153,23 @@ class Fixture { } final testPlatformException = PlatformException( + code: 'error', + details: + "Unsupported value: '[Ljava.lang.StackTraceElement;@fa902f1' of type 'class [Ljava.lang.StackTraceElement;'", + message: _jvmStackTrace, + stacktrace: _jvmStackTrace, +); + +final emptyPlatformException = PlatformException( code: 'error', details: "Unsupported value: '[Ljava.lang.StackTraceElement;@fa902f1' of type 'class [Ljava.lang.StackTraceElement;'", message: null, - stacktrace: - """java.lang.IllegalArgumentException: Unsupported value: '[Ljava.lang.StackTraceElement;@ba6feed' of type 'class [Ljava.lang.StackTraceElement;' + stacktrace: null, +); + +const _jvmStackTrace = + """java.lang.IllegalArgumentException: Unsupported value: '[Ljava.lang.StackTraceElement;@ba6feed' of type 'class [Ljava.lang.StackTraceElement;' at io.flutter.plugin.common.StandardMessageCodec.writeValue(StandardMessageCodec.java:292) at io.flutter.plugin.common.StandardMethodCodec.encodeSuccessEnvelope(StandardMethodCodec.java:59) at io.flutter.plugin.common.MethodChannel\$IncomingMethodCallHandler\$1.success(MethodChannel.java:267) @@ -167,13 +187,4 @@ final testPlatformException = PlatformException( at android.app.ActivityThread.main(ActivityThread.java:8138) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit\$MethodAndArgsCaller.run(RuntimeInit.java:556) - at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1037)""", -); - -final emptyPlatformException = PlatformException( - code: 'error', - details: - "Unsupported value: '[Ljava.lang.StackTraceElement;@fa902f1' of type 'class [Ljava.lang.StackTraceElement;'", - message: null, - stacktrace: null, -); + at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1037)""";