sentry-dart
sentry-dart copied to clipboard
Zone missmatch on flutter web
Platform
Flutter Web
Obfuscation
Disabled
Debug Info
Disabled
Doctor
[√] Flutter (Channel stable, 3.19.3, on Microsoft Windows [Version 10.0.19045.4046], locale de-DE) • Flutter version 3.19.3 on channel stable at C:\Users\R081094\dev\flutter • Upstream repository https://github.com/flutter/flutter.git • Framework revision ba39319843 (13 days ago), 2024-03-07 15:22:21 -0600 • Engine revision 2e4ba9c6fb • Dart version 3.3.1 • DevTools version 2.31.1
[√] Windows Version (Installed version of Windows is version 10 or higher)
[√] Android toolchain - develop for Android devices (Android SDK version 34.0.0) • Android SDK at C:\Users\R081094\AppData\Local\Android\Sdk • Platform android-34, build-tools 34.0.0 • ANDROID_HOME = C:\Users\R081094\AppData\Local\Android\Sdk • Java binary at: C:\Program Files\Android\Android Studio\jbr\bin\java • Java version OpenJDK Runtime Environment (build 17.0.9+0--11185874) • All Android licenses accepted.
[√] Chrome - develop for the web • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe
[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.9.3) • Visual Studio at C:\Program Files\Microsoft Visual Studio\2022\Community • Visual Studio Community 2022 version 17.9.34701.34 • Windows 10 SDK version 10.0.20348.0
[√] Android Studio (version 2023.2) • Android Studio at C:\Program Files\Android\Android Studio • Flutter plugin can be installed from: https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: https://plugins.jetbrains.com/plugin/6351-dart • Java version OpenJDK Runtime Environment (build 17.0.9+0--11185874)
[√] VS Code (version 1.87.2) • VS Code at C:\Users\R081094\AppData\Local\Programs\Microsoft VS Code • Flutter extension version 3.84.0
[√] Connected device (3 available) • Windows (desktop) • windows • windows-x64 • Microsoft Windows [Version 10.0.19045.4046] • Chrome (web) • chrome • web-javascript • Google Chrome 122.0.6261.129 • Edge (web) • edge • web-javascript • Microsoft Edge 122.0.2365.92
[√] Network resources • All expected network resources are available.
• No issues found!
Version
7.18.0
Steps to Reproduce
- create a Flutter project using all available platforms (iOS, MacOS, Windows, Android) and Web
- implement a
Future<void> init()method wich is called inmain()and initializes some stuff asynch. - Your
main()should look like this:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await init(); // initializes the stuff asynch
await SentryFlutter.init(
(options) => options
..dsn = 'dsn'
..debug = kDebugMode
..attachViewHierarchy = true
..sampleRate = settings.general.acceptAnalysis == true ? 1 : 0, // Enables or disables error sending according to user settings
appRunner: () => runApp(const ProviderScope(child: SchunkControlCenterApp())), // Riverpod should not care here
);
}
- run your app in debug mode
Expected Result
I expected that the app starts normally without throwing an exception.
Actual Result
Following Exception is thrown:
The following assertion was thrown during runApp:
Zone mismatch.
The Flutter bindings were initialized in a different zone than is now being used. This will likely
cause confusion and bugs as any zone-specific configuration will inconsistently use the
configuration of the original binding initialization zone or this zone based on hard-to-predict
factors such as which zone was active when a particular callback was set.
It is important to use the same zone when calling `ensureInitialized` on the binding as when calling
`runApp` later.
To make this warning fatal, set BindingBase.debugZoneErrorsAreFatal to true before the bindings are
initialized (i.e. as the first statement in `void main() { }`).
When the exception was thrown, this was the stack:
dart-sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart 945:28 get current
packages/flutter/src/foundation/binding.dart 495:29 <fn>
packages/flutter/src/foundation/binding.dart 499:14 debugCheckZone
packages/flutter/src/widgets/binding.dart 1212:17 runApp
packages/schunk_cc/main.dart 34:22 <fn>
packages/sentry/src/sentry.dart 136:26 <fn>
dart-sdk/lib/_internal/js_dev_runtime/patch/async_patch.dart 45:50 <fn>
dart-sdk/lib/async/zone.dart 1407:47 _rootRunUnary
dart-sdk/lib/async/zone.dart 1308:19 runUnary
dart-sdk/lib/async/future_impl.dart 162:18 handleValue
dart-sdk/lib/async/future_impl.dart 838:44 handleValueCallback
dart-sdk/lib/async/future_impl.dart 867:13 _propagateToListeners
dart-sdk/lib/async/future_impl.dart 643:5 [_completeWithValue]
dart-sdk/lib/async/future_impl.dart 713:7 <fn>
dart-sdk/lib/async/zone.dart 1399:13 _rootRun
dart-sdk/lib/async/zone.dart 1301:19 run
dart-sdk/lib/async/zone.dart 1209:7 runGuarded
dart-sdk/lib/async/zone.dart 1249:23 callback
dart-sdk/lib/async/schedule_microtask.dart 40:11 _microtaskLoop
dart-sdk/lib/async/schedule_microtask.dart 49:5 _startMicrotaskLoop
dart-sdk/lib/_internal/js_dev_runtime/patch/async_patch.dart 181:7 <fn>
Are you willing to submit a PR?
No
I also tried to wrap everythin inside main() with a runZoneGuarded() but that didn't work...
I think the problen is that The appRunner is initialized with a runZoneGuarded automatically on Web. There is no option to disable this ro run my own guarded zone on the web.
I cannot init my data inside the app runner because i need to give the user the option to diable Sentry (mandatory from my companys legal team). If there is a workaround by disabling Sentry programatically on another way, i'd be happy to use this.
Same here, all combinations result in that mismatch
hi @kay4ik and @r-dev-limited You can find the reason in the docs.
The SDK already runs your init callback on an error handler, such as runZonedGuarded on Flutter versions prior to 3.3, or PlatformDispatcher.onError on Flutter versions 3.3 and higher, so that errors are automatically captured.
This means that WidgetsFlutterBinding.ensureInitialized() should be called within the runZonedGuarded
Please, let me know if this works for you.
void main() async {
await runZonedGuarded(() async {
await init(); // initializes the stuff asynch
await SentryFlutter.init(
(options) => options
..dsn = 'dsn'
..debug = kDebugMode
..attachViewHierarchy = true
..sampleRate = settings.general.acceptAnalysis == true ? 1 : 0, // Enables or disables error sending according to user settings
appRunner: () async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const ProviderScope(child: SchunkControlCenterApp())), // Riverpod should not care here
});
}, (exception, stackTrace) async {
await Sentry.captureException(exception, stackTrace: stackTrace);
});
}
As a workaround, you could use the beforeSend callback to drop all data sent to Sentry based on a flag reflecting users consent.
Something like
await SentryFlutter.init(
(options) => options
..beforeSend = (event, {hint}) => userConsent ? event : null,
...
@stefanosiano
This means that WidgetsFlutterBinding.ensureInitialized() should be called within the runZonedGuarded Please, let me know if this works for you.
No this is not working. The Zone missmatch occurs either way.
Thank you for the workaround idea! I will try it :)
So the workaround is working fine but i think it should be possible to initialize the app before sentry.
Let's add the workaround to the troubleshooting section of the docs. Let's also see how we can initialize the app before Sentry
I run into the same issue but for me it is not user consent that I run before await SentryFlutter.init but some other async initialization logic that determines the Sentry dsn property. Can I ignore the crash or is there another workaround available?
@flodaniel
do errors etc.. still log to sentry despite the zone mismatch error?
The problem is that we always run runZonedGuarded on SentryFlutter.init (on web).
The way to fix the issues with runZonedGuarded would be to automatically check if we are inside a guarded zone during SentryFlutter.init, and skip the zone creation. Let's see if we can do it.
Otherwise we may add a flag - something like inGuardedZone or avoidZoneCreation - to pass to SentryFlutter.init to disable the zone creation
Should be possible by checking if Zone.current == Zone.root then we at least know that no custom Zone has been created
FWIW, Zone creation is skipped when no appRunner callback is passed.
So, as of now, you should not use the appRunner callback when creating the zone yourself. If you do not create the zone yourself, you should use the appRunner callback.
(On io platform zones and the appRunner callback are not needed)
@ueman oh yeah, was there a specific reason why it was implement this way? (creating zone if appRunner callback is used otherwise skip)
No clue, since that's code from before I started contributing.
But it's somewhat logical to do it that way since Sentry can only create a zone for you if Sentry is executing the appRunner callback. If the callback is not used, creating a zone doesn't actually do anything.
You need to be aware of this even if there would be an option for creating a zone. So adding an option would probably lead to just as much confusion, since you're still required to use the appRunner callback correctly.
Totally agree. I was confused, too 😅 we should add it to the troubleshoot in our docs, since the only place we wrote it is the github repo, I think And then link it to other places like https://docs.sentry.io/platforms/flutter/usage/#capturing-errors
Thoughts on skipping zone creation on web if the current zone != root zone in .init? (see https://github.com/getsentry/sentry-dart/pull/2088)
Or shall we rather keep it as it is and tell users to just not use the appRunner callback in case they want to have a guarded zone themselves?
i think it still makes sense, to make the SDK simpler to use
Thinking more about it, if we don't create a zone, the user will be responsible to manually add Sentry.captureException in his own zone creation, right?
So, the difference with now would be that there is no zone mismatch error anymore, but the user needs to be aware he has to add the captureException call.
Not a real problem, but we should make it crystal clear in the docs.
What do you think?
Is it better to:
- have an error that makes you more "aware" of the problem
- handle it gracefully with the users potentially not seeing their errors on sentry?
In both cases we should improve docs.
Thinking more about it, if we don't create a zone, the user will be responsible to manually add Sentry.captureException in his own zone creation, right?
it's possible to decorate the user's onError callback with ours on top of it - so it would be possible to keep the default behaviour (reporting the error to Sentry) on top of the user's custom onError. Just need to add proper docs in that case that the user shouldn't report that exception again otherwise it would be sent twice
Another problem with internal Sentry's call to WidgetsFlutterBinding.ensureInitialized() is that it is called after the (options) { ... } callback is invoked, so you can't use any code in that callback that requires bindings to be initialized. For example, can't use the PackageInfo.fromPlatform() in order to set options.release.
On a side note. When code in the (options) { ... } callback throws any exception - nothing is shown on the console. Even when you have debug build running in the IDE. It makes it hard to catch these errors.
@ekuleshov
You can run your own runZonedGuarded and call WidgetsFlutterBinding.ensureInitialized()
The only thing is that when you init Sentry you don't have to pass the appRunner argument, instead run your app directly.
Something like this
void main() async {
await runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized();
await SentryFlutter.init(
(options) => options
..dsn = 'dsn'
...,
);
runApp(MyApp()),
}, (exception, stackTrace) async {
Sentry.captureException(exception, stackTrace: stackTrace);
});
}
We are going to make it work even when passing the appRunner parameter and without the need to call Sentry.captureException(exception, stackTrace: stackTrace); in newer versions
@stefanosiano, thx its work for me.
As a (potential) customer, I'd encourage someone to consider the DX while this issue is prioritised. In spite of the onboarding flow I did find my way here today. On a day with worse google-fu, I may have just binned this off and used something/body else ;)
That said, it's certainly a positive to find https://github.com/getsentry/sentry-dart/issues/1943#issuecomment-2185880234 and unblock my eval :)
@ewann that's fair feedback, thanks 👍
@ewann thx for the feedback, I agree this needs to addressed.
I'll have a look here and see how we can improve the dx
@ewann thx for the feedback, I agree this needs to addressed.
I'll have a look here and see how we can improve the dx
@buenaflor 👋
If memory serves, I followed some sort of onboarding in the sentry portal/ui. I'm not sure how to get back there to share more concrete info, but I remember because my thought was along the lines of "that would have been wonderful if it worked" 😅
imo it would be enough to update that onboarding guidance to include the workaround/solution in this thread, or at least link to that info at someplace.
fwiw & in case it helps, I ended up doing something very close to the following in my eval:
Future<void> main() async {
Future<void> initialSetup() async {
WidgetsFlutterBinding.ensureInitialized();
// <snip>
return Future<void>.value(null);
}
if (patrolIsRunning) {
serverpodIsEnabled = true;
await initialSetup();
await serverpodInit();
runApp(
ProviderScope(observers: [
RiverpodProviderObserver(),
], child: const MyApp()),
);
} else {
await runZonedGuarded(() async {
await initialSetup();
await SentryFlutter.init(
(options) {
options.dsn =
'<snip>'
options.tracesSampleRate = 1.0;
options.profilesSampleRate = 1.0;
options.experimental.replay.sessionSampleRate = 0.0;
options.experimental.replay.onErrorSampleRate = 0.0;
},
);
runApp(
ProviderScope(observers: [
RiverpodProviderObserver(),
], child: const SentryWidget(child: MyApp())),
);
}, (exception, stackTrace) async {
await Sentry.captureException(exception, stackTrace: stackTrace);
});
}
It smells a little funny, but I didn't come up with a better solution to getting it working with https://patrol.leancode.co - as well as realising I didn't want sentry in a dev env, I also failed to correctly pass exceptions to patrol in a way that didn't break tests in there.
I'm not expecting a fix or even really feedback here, but it seems a useful opportunity to share how (at least one) folks are trying to use this, and the small challenges faced :)
Thanks a lot for the feedback, what we would do going forward is this
- provide a custom sentry runZonedGuarded which needs to be inserted by the user, this will allow us to catch and send errors caught by runZonedGuarded and the user can still do their own e.g logs on top.
// or Sentry.runZonedGuarded? api tbd
sentryRunZonedGuarded {
WidgetsBinding.ensureInitialized()
SentryFlutter.init(..., runApp(...));
} (e, stackTrace) {
// automatically sends errors to Sentry, no need for the user to do any captureException calls
// optionally: on top of that the user can do their custom stuff on top
}
- document this properly in our getting started page docs
Hopefully this is then helpful for everyone struggling with this