diff --git a/CHANGELOG.md b/CHANGELOG.md index 622edb2c2f..5849227004 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +* Feat: Attach Isolate name to thread context (#847) * Fix: Fix `SentryAssetBundle` on Flutter >= 3.1 (#877) * Feat: Add Android thread to platform stacktraces (#853) * Fix: Rename auto initialize property (#857) diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 5de96d0502..e07dc168c2 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -12,6 +12,7 @@ import 'sentry_options.dart'; import 'sentry_stack_trace_factory.dart'; import 'transport/http_transport.dart'; import 'transport/noop_transport.dart'; +import 'utils/isolate_utils.dart'; import 'version.dart'; import 'sentry_envelope.dart'; import 'client_reports/client_report_recorder.dart'; @@ -140,18 +141,48 @@ class SentryClient { return event; } - if (event.exceptions?.isNotEmpty ?? false) return event; + if (event.exceptions?.isNotEmpty ?? false) { + return event; + } + + final isolateName = getIsolateName(); + // Isolates have no id, so the hashCode of the name will be used as id + final isolateId = isolateName?.hashCode; if (event.throwableMechanism != null) { - final sentryException = _exceptionFactory.getSentryException( + var sentryException = _exceptionFactory.getSentryException( event.throwableMechanism, stackTrace: stackTrace, ); - return event.copyWith(exceptions: [ - ...(event.exceptions ?? []), - sentryException, - ]); + if (_options.platformChecker.isWeb) { + return event.copyWith( + exceptions: [ + ...?event.exceptions, + sentryException, + ], + ); + } + + SentryThread? thread; + + if (isolateName != null && _options.attachThreads) { + sentryException = sentryException.copyWith(threadId: isolateId); + thread = SentryThread( + id: isolateId, + name: isolateName, + crashed: true, + current: true, + ); + } + + return event.copyWith( + exceptions: [...?event.exceptions, sentryException], + threads: [ + ...?event.threads, + if (thread != null) thread, + ], + ); } // The stacktrace is not part of an exception, @@ -163,8 +194,10 @@ class SentryClient { if (frames.isNotEmpty) { event = event.copyWith(threads: [ - ...(event.threads ?? []), + ...?event.threads, SentryThread( + name: isolateName, + id: isolateId, crashed: false, current: true, stacktrace: SentryStackTrace(frames: frames), diff --git a/dart/lib/src/sentry_exception_factory.dart b/dart/lib/src/sentry_exception_factory.dart index de10b06a84..b59ab6da18 100644 --- a/dart/lib/src/sentry_exception_factory.dart +++ b/dart/lib/src/sentry_exception_factory.dart @@ -45,13 +45,11 @@ class SentryExceptionFactory { // if --obfuscate feature is enabled, 'type' won't be human readable. // https://flutter.dev/docs/deployment/obfuscate#caveat - final sentryException = SentryException( - type: '${throwable.runtimeType}', - value: '$throwable', + return SentryException( + type: (throwable.runtimeType).toString(), + value: throwable.toString(), mechanism: mechanism, stackTrace: sentryStackTrace, ); - - return sentryException; } } diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index 2612dafe9c..f03a137218 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -218,7 +218,15 @@ class SentryOptions { /// variables. This is useful in tests. EnvironmentVariables environmentVariables = EnvironmentVariables.instance(); - /// When enabled, all the threads are automatically attached to all logged events (Android). + /// When enabled, the current isolate will be attached to the event. + /// This only applies to Dart:io platforms and only the current isolate. + /// The Dart runtime doesn't provide information about other active isolates. + /// + /// When running on web, this option has no effect at all. + /// + /// When running in the Flutter context, this enables attaching of threads + /// for native events, if supported for the native platform. + /// Currently, this is only supported on Android. bool attachThreads = false; /// Whether to send personal identifiable information along with events diff --git a/dart/lib/src/utils/_io_get_isolate_name.dart b/dart/lib/src/utils/_io_get_isolate_name.dart new file mode 100644 index 0000000000..93a7c993d0 --- /dev/null +++ b/dart/lib/src/utils/_io_get_isolate_name.dart @@ -0,0 +1,3 @@ +import 'dart:isolate'; + +String? getIsolateName() => Isolate.current.debugName; diff --git a/dart/lib/src/utils/_web_get_isolate_name.dart b/dart/lib/src/utils/_web_get_isolate_name.dart new file mode 100644 index 0000000000..0db3f82b99 --- /dev/null +++ b/dart/lib/src/utils/_web_get_isolate_name.dart @@ -0,0 +1 @@ +String? getIsolateName() => null; diff --git a/dart/lib/src/utils/isolate_utils.dart b/dart/lib/src/utils/isolate_utils.dart new file mode 100644 index 0000000000..ce523b0d63 --- /dev/null +++ b/dart/lib/src/utils/isolate_utils.dart @@ -0,0 +1,4 @@ +import '_io_get_isolate_name.dart' + if (dart.library.html) '_web_get_isolate_name.dart' as isolate_getter; + +String? getIsolateName() => isolate_getter.getIsolateName(); diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 2aab69c9d3..33c5047ccf 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -110,6 +110,50 @@ void main() { expect(capturedEvent.exceptions?.first.stackTrace, isNotNull); }); + test( + 'should attach isolate info in thread', + () async { + final client = fixture.getSut(attachThreads: true); + + await client.captureException( + Exception(), + stackTrace: StackTrace.current, + ); + + final capturedEnvelope = (fixture.transport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); + + expect(capturedEvent.threads?.first.current, true); + expect(capturedEvent.threads?.first.crashed, true); + expect(capturedEvent.threads?.first.name, isNotNull); + expect(capturedEvent.threads?.first.id, isNotNull); + + expect( + capturedEvent.exceptions?.first.threadId, + capturedEvent.threads?.first.id, + ); + }, + onPlatform: {'js': Skip("Isolates don't exist on the web")}, + ); + + test( + 'should not attach isolate info in thread if disabled', + () async { + final client = fixture.getSut(attachThreads: false); + + await client.captureException( + Exception(), + stackTrace: StackTrace.current, + ); + + final capturedEnvelope = (fixture.transport).envelopes.first; + final capturedEvent = await eventFromEnvelope(capturedEnvelope); + + expect(capturedEvent.threads, null); + }, + onPlatform: {'js': Skip("Isolates don't exist on the web")}, + ); + test('should capture message', () async { final client = fixture.getSut(); await client.captureMessage( @@ -1014,6 +1058,7 @@ class Fixture { SentryClient getSut({ bool sendDefaultPii = false, bool attachStacktrace = true, + bool attachThreads = false, double? sampleRate, BeforeSendCallback? beforeSend, EventProcessor? eventProcessor, @@ -1029,14 +1074,16 @@ class Fixture { options.tracesSampleRate = 1.0; options.sendDefaultPii = sendDefaultPii; options.attachStacktrace = attachStacktrace; + options.attachThreads = attachThreads; options.sampleRate = sampleRate; options.beforeSend = beforeSend; + if (eventProcessor != null) { options.addEventProcessor(eventProcessor); } options.transport = transport; final client = SentryClient(options); - // hub.bindClient(client); + if (provideMockRecorder) { options.recorder = recorder; } diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index bbb59ee0bf..e194e8d08c 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -26,6 +26,7 @@ Future main() async { options.reportPackages = false; options.addInAppInclude('sentry_flutter_example'); options.considerInAppFramesByDefault = false; + options.attachThreads = true; }, // Init your App. appRunner: () => runApp(