sentry-dart
sentry-dart copied to clipboard
Calling Sentry capture methods within a new Isolate does not work
Description
Works fine:
throwingClosure(String message) async {
throw StateError(message);
}
final isolate = await Isolate.spawn(throwingClosure, "message", paused: true);
isolate.addSentryErrorListener();
isolate.resume(isolate.pauseCapability!);
Does not work:
throwingClosure(String message) async {
Sentry.captureException(message);
}
final isolate = await Isolate.spawn(throwingClosure, "message", paused: true);
isolate.addSentryErrorListener();
isolate.resume(isolate.pauseCapability!);
In this case, when you call any capture method, the Hub
is NoOpHub
, it's like the SDK isn't initialized within a different isolate, most likely because isolates are isolated :D and don't share any memory.
A workaround, for now, is to really n to call Sentry.captureX
but rather let exceptions be thrown and bubble down to the global error or via addSentryErrorListener
.
If you wanna capture a message
, you can also just do throw Exception('Your message')
, it's not the same as captureMessage
but that's the only option until we figure this out.
If it's a Flutter background isolate (see this guide on how to set it up), you can just initialize Sentry again. In that case, you also don't need to attach an error listener.
throwingClosure(RootIsolateToken rootIsolateToken) async {
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
SentryFlutter.init(...)
throw StateError(message);
}
RootIsolateToken rootIsolateToken = RootIsolateToken.instance!;
final isolate = await Isolate.spawn(throwingClosure, rootIsolateToken, paused: true);
isolate.resume(isolate.pauseCapability!);
I don't think it's specifically to background isolates, but rather any new Isolate, for example, using Isolate.spawn
.
I'd like to find a way where SentryFlutter.init
isn't necessary, if possible. :(
We can either:
- init the Sentry SDK every time a new background isolate is spawned
- send the hub to the isolate. This requires the hub to be sendable. Also, we should check for minimum Dart version required
If it's a Flutter background isolate (see this guide on how to set it up), you can just initialize Sentry again. In that case, you also don't need to attach an error listener.
throwingClosure(RootIsolateToken rootIsolateToken) async { BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken); SentryFlutter.init(...) throw StateError(message); } RootIsolateToken rootIsolateToken = RootIsolateToken.instance!; final isolate = await Isolate.spawn(throwingClosure, rootIsolateToken, paused: true); isolate.resume(isolate.pauseCapability!);
Calling SentryFlutter.init(...)
in an isolate other than root causes an error:
UI actions are only available on root isolate.
#0 FfiTrampoline____nativeSetNeedsReportTimings$Method$FfiNative$Ptr (dart:ffi)
#1 PlatformDispatcher.__nativeSetNeedsReportTimings (dart:ui/platform_dispatcher.dart:546:24)
#2 PlatformDispatcher._nativeSetNeedsReportTimings (dart:ui/platform_dispatcher.dart:543:52)
#3 PlatformDispatcher.onReportTimings= (dart:ui/platform_dispatcher.dart:535:29)
#4 SchedulerBinding.addTimingsCallback (package:flutter/src/scheduler/binding.dart:308:26)
#5 SchedulerBinding.initInstances (package:flutter/src/scheduler/binding.dart:240:7)
#6 ServicesBinding.initInstances (package:flutter/src/services/binding.dart:37:11)
#7 PaintingBinding.initInstances (package:flutter/src/painting/binding.dart:20:11)
#8 SemanticsBinding.initInstances (package:flutter/src/semantics/binding.dart:18:11)
#9 RendererBinding.initInstances (package:flutter/src/rendering/binding.dart:30:11)
#10 WidgetsBinding.initInstances (package:flutter/src/widgets/binding.dart:263:11)
#11 new BindingBase (package:flutter/src/foundation/binding.dart:151:5)
#12 new _WidgetsFlutterBinding&BindingBase&GestureBinding (package:flutter/src/widgets/binding.dart)
#13 new _WidgetsFlutterBinding&BindingBase&GestureBinding&SchedulerBinding (package:flutter/src/widgets/binding.dart)
#14 new _WidgetsFlutterBinding&BindingBase&GestureBinding&SchedulerBinding&ServicesBinding (package:flutter/src/widgets/binding.dart)
#15 new _WidgetsFlutterBinding&BindingBase&GestureBinding&SchedulerBinding&ServicesBinding&PaintingBinding (package:flutter/src/widgets/binding.dart)
#16 new _WidgetsFlutterBinding&BindingBase&GestureBinding&SchedulerBinding&ServicesBinding&PaintingBinding&SemanticsBinding (package:flutter/src/widgets/binding.dart)
#17 new _WidgetsFlutterBinding&BindingBase&GestureBinding&SchedulerBinding&ServicesBinding&PaintingBinding&SemanticsBinding&RendererBinding (package:flutter/src/widgets/binding.dart)
#18 new _WidgetsFlutterBinding&BindingBase&GestureBinding&SchedulerBinding&ServicesBinding&PaintingBinding&SemanticsBinding&RendererBinding&WidgetsBinding (package:flutter/src/widgets/binding.dart)
#19 new WidgetsFlutterBinding (package:flutter/src/widgets/binding.dart)
#20 WidgetsFlutterBinding.ensureInitialized (package:flutter/src/widgets/binding.dart:1306:7)
#21 BindingWrapper.ensureInitialized (package:sentry_flutter/src/binding_wrapper.dart:39:29)
#22 WidgetsFlutterBindingIntegration.call (package:sentry_flutter/src/integrations/widgets_flutter_binding_integration.dart:13:26)
#23 Sentry._callIntegrations (package:sentry/src/sentry.dart:159:34)
#24 Sentry._init (package:sentry/src/sentry.dart:152:13)
<asynchronous suspension>
#25 Sentry.init (package:sentry/src/sentry.dart:70:5)
<asynchronous suspension>
#26 SentryFlutter.init (package:sentry_flutter/src/sentry_flutter.dart:83:5)
At least on Flutter 3.10.5, 3.10.6 with Sentry 7.8.0 and 7.10.1.
Calling Sentry.init(...)
(not SentryFlutter) in the spawned isolates works, however the crash reports don't contain any information about the device, release, etc.
Is there any workaround to get as much information as we have with SentryFlutter called in root isolate?
~I've found out that if I call SentryFlutter.init()
in the root isolate and Sentry.init()
in other spawned isolates, the crash reports do contain release and device information even if they happen in non-root isolates. Which is great.~
@RustamG Thanks for the update, we'll take a look.
I've found out that if I call
SentryFlutter.init()
in the root isolate andSentry.init()
in other spawned isolates, the crash reports do contain release and device information even if they happen in non-root isolates. Which is great.
Actually, in my case the errors were passed from the spawned isolate to the root. So those reports were sent from the root isolate (where sentry was initialized via SentryFlutter.init).
The bad news is that reports sent from the spawned isolate do not contain release and device information.
One other observation is that the breadcrumbs are not shared between isolates. So if the report is sent from the root isolate, it won't contain the breadcrumbs recorded in spawned isolate and vice versa.
Does it make sense?
So you are doing Sentry.init
within spawned isolates and SentryFlutter.init
in the root?
Have you tried throwing the exception and catch them as global error then send that to Sentry as described above?
But other than that we are still investigating how to solve this without these workarounds.
So you are doing
Sentry.init
within spawned isolates andSentryFlutter.init
in the root?
Right.
Have you tried throwing the exception and catch them as global error then send that to Sentry as described above?
What do you mean by "catch them as global error"? How would I do that? Could you please point me to what you are referencing as "above"?
But other than that we are still investigating how to solve this without these workarounds.
Very nice 🙏
@RustamG
let exceptions be thrown and bubble down to the global error or via
addSentryErrorListener.
instead of initializing Sentry in your isolate, you can for example use addSentryErrorListener
which is a Sentry extension on Isolate
.
Thanks for the clarification. I will keep the manual passing the error to root isolate for now.
Any updates on this? Being able to send info to Sentry on an isolate doesn't seem like a novel use-case.
There's unfortunately nothing that can be done to make it more automatic as of now since that's a limitation of Dart (& Flutter), not of Sentry.
That being said, you can actually send reports on an Isolate as described in replies earlier in this thread. It's just not as automatic as one would wish.
@ueman Perhaps I'm missing something. But addSentryErrorListener
doesn't seem to be available. Maybe sentry_isolate_extension.dart should be exported in sentry.dart
import 'package:sentry/sentry_io.dart';
Isolate.current.addSentryErrorListener();
try this out