sentry-dart icon indicating copy to clipboard operation
sentry-dart copied to clipboard

Calling Sentry capture methods within a new Isolate does not work

Open marandaneto opened this issue 1 year ago • 17 comments

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.

marandaneto avatar Jul 21 '23 06:07 marandaneto

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.

marandaneto avatar Jul 26 '23 10:07 marandaneto

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!);

ueman avatar Jul 26 '23 12:07 ueman

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. :(

marandaneto avatar Jul 26 '23 12:07 marandaneto

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

stefanosiano avatar Sep 01 '23 13:09 stefanosiano

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?

RustamG avatar Oct 23 '23 19:10 RustamG

~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 avatar Oct 24 '23 08:10 RustamG

@RustamG Thanks for the update, we'll take a look.

krystofwoldrich avatar Oct 25 '23 11:10 krystofwoldrich

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.

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?

RustamG avatar Nov 04 '23 09:11 RustamG

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.

buenaflor avatar Nov 07 '23 12:11 buenaflor

So you are doing Sentry.init within spawned isolates and SentryFlutter.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 avatar Nov 07 '23 17:11 RustamG

@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.

buenaflor avatar Nov 08 '23 10:11 buenaflor

Thanks for the clarification. I will keep the manual passing the error to root isolate for now.

RustamG avatar Nov 09 '23 08:11 RustamG

Any updates on this? Being able to send info to Sentry on an isolate doesn't seem like a novel use-case.

jjonte avatar Jul 17 '24 18:07 jjonte

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 avatar Jul 17 '24 18:07 ueman

@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

RustamG avatar Jul 18 '24 08:07 RustamG

import 'package:sentry/sentry_io.dart';

Isolate.current.addSentryErrorListener();

try this out

buenaflor avatar Jul 18 '24 10:07 buenaflor