diff --git a/CHANGELOG.md b/CHANGELOG.md index 73e86ff0f4..74884dce39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Make Sentry Flutter multiview aware for the web platform and automatically disable `SentryScreenshotWidget`, `SentryUserInteractionWidget` and `WidgetsBindingIntegration` in multi-view applications.([#2366](https://github.com/getsentry/sentry-dart/pull/2366)) + ### Enhancements - Cache parsed DSN ([#2365](https://github.com/getsentry/sentry-dart/pull/2365)) diff --git a/flutter/lib/src/integrations/on_error_integration.dart b/flutter/lib/src/integrations/on_error_integration.dart index 365a3067ab..fad98e6d08 100644 --- a/flutter/lib/src/integrations/on_error_integration.dart +++ b/flutter/lib/src/integrations/on_error_integration.dart @@ -7,6 +7,8 @@ import '../sentry_flutter_options.dart'; // ignore: implementation_imports import 'package:sentry/src/utils/stacktrace_utils.dart'; +import '../utils/platform_dispatcher_wrapper.dart'; + typedef ErrorCallback = bool Function(Object exception, StackTrace stackTrace); /// Integration which captures `PlatformDispatcher.onError` @@ -109,45 +111,3 @@ class OnErrorIntegration implements Integration { } } } - -/// This class wraps the `this as dynamic` hack in a type-safe manner. -/// It helps to introduce code, which uses newer features from Flutter -/// without breaking Sentry on older versions of Flutter. -// Should not become part of public API. -@visibleForTesting -class PlatformDispatcherWrapper { - PlatformDispatcherWrapper(this._dispatcher); - - final PlatformDispatcher? _dispatcher; - - /// Should not be accessed if [isOnErrorSupported] == false - ErrorCallback? get onError => - (_dispatcher as dynamic)?.onError as ErrorCallback?; - - /// Should not be accessed if [isOnErrorSupported] == false - set onError(ErrorCallback? callback) { - (_dispatcher as dynamic)?.onError = callback; - } - - bool isOnErrorSupported(SentryFlutterOptions options) { - try { - onError; - } on NoSuchMethodError { - // This error is expected on pre 3.1 Flutter version - return false; - } catch (exception, stacktrace) { - // This error is neither expected on pre 3.1 nor on >= 3.1 Flutter versions - options.logger( - SentryLevel.debug, - 'An unexpected exception was thrown, please create an issue at https://github.com/getsentry/sentry-dart/issues', - exception: exception, - stackTrace: stacktrace, - ); - if (options.automatedTestMode) { - rethrow; - } - return false; - } - return true; - } -} diff --git a/flutter/lib/src/integrations/screenshot_integration.dart b/flutter/lib/src/integrations/screenshot_integration.dart index 10cf60228a..feb877d448 100644 --- a/flutter/lib/src/integrations/screenshot_integration.dart +++ b/flutter/lib/src/integrations/screenshot_integration.dart @@ -1,6 +1,7 @@ import 'package:sentry/sentry.dart'; import '../event_processor/screenshot_event_processor.dart'; import '../sentry_flutter_options.dart'; +import '../utils/multi_view_helper.dart'; /// Adds [ScreenshotEventProcessor] to options event processors if /// [SentryFlutterOptions.attachScreenshot] is true @@ -10,6 +11,15 @@ class ScreenshotIntegration implements Integration { @override void call(Hub hub, SentryFlutterOptions options) { + if (MultiViewHelper.isMultiViewEnabled()) { + // ignore: invalid_use_of_internal_member + options.logger( + SentryLevel.debug, + '`ScreenshotIntegration` is not available in multi-view applications.', + ); + return; + } + if (options.attachScreenshot) { _options = options; final screenshotEventProcessor = ScreenshotEventProcessor(options); diff --git a/flutter/lib/src/integrations/widgets_binding_integration.dart b/flutter/lib/src/integrations/widgets_binding_integration.dart index 8ebab7e7af..c020e96180 100644 --- a/flutter/lib/src/integrations/widgets_binding_integration.dart +++ b/flutter/lib/src/integrations/widgets_binding_integration.dart @@ -1,5 +1,6 @@ import 'package:sentry/sentry.dart'; import '../sentry_flutter_options.dart'; +import '../utils/multi_view_helper.dart'; import '../widgets_binding_observer.dart'; /// Integration that captures certain window and device events. @@ -12,6 +13,15 @@ class WidgetsBindingIntegration implements Integration { @override void call(Hub hub, SentryFlutterOptions options) { + if (MultiViewHelper.isMultiViewEnabled()) { + // ignore: invalid_use_of_internal_member + options.logger( + SentryLevel.debug, + '`WidgetsBindingIntegration` is not available in multi-view applications.', + ); + return; + } + _options = options; final observer = SentryWidgetsBindingObserver( hub: hub, diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index e63366c3f7..c863d56968 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -24,6 +24,7 @@ import 'native/sentry_native_binding.dart'; import 'profiling.dart'; import 'renderer/renderer.dart'; import 'span_frame_metrics_collector.dart'; +import 'utils/platform_dispatcher_wrapper.dart'; import 'version.dart'; import 'view_hierarchy/view_hierarchy_integration.dart'; diff --git a/flutter/lib/src/sentry_widget.dart b/flutter/lib/src/sentry_widget.dart index 40709a5465..f38ffbc4e0 100644 --- a/flutter/lib/src/sentry_widget.dart +++ b/flutter/lib/src/sentry_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:meta/meta.dart'; import '../sentry_flutter.dart'; +import 'utils/multi_view_helper.dart'; /// Key which is used to identify the [SentryWidget] @internal @@ -11,7 +12,13 @@ final sentryWidgetGlobalKey = GlobalKey(debugLabel: 'sentry_widget'); class SentryWidget extends StatefulWidget { final Widget child; - const SentryWidget({super.key, required this.child}); + SentryWidget({ + super.key, + required this.child, + @internal Hub? hub, + }); + + final bool _isMultiViewEnabled = MultiViewHelper.isMultiViewEnabled(); @override _SentryWidgetState createState() => _SentryWidgetState(); @@ -21,6 +28,14 @@ class _SentryWidgetState extends State { @override Widget build(BuildContext context) { Widget content = widget.child; + if (widget._isMultiViewEnabled) { + // ignore: invalid_use_of_internal_member + Sentry.currentHub.options.logger( + SentryLevel.debug, + '`SentryScreenshotWidget` and `SentryUserInteractionWidget` is not available in multi-view applications.', + ); + return content; + } content = SentryScreenshotWidget(child: content); content = SentryUserInteractionWidget(child: content); return Container( diff --git a/flutter/lib/src/utils/multi_view_helper.dart b/flutter/lib/src/utils/multi_view_helper.dart new file mode 100644 index 0000000000..6b40e74ddf --- /dev/null +++ b/flutter/lib/src/utils/multi_view_helper.dart @@ -0,0 +1,13 @@ +import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart'; +import 'platform_dispatcher_wrapper.dart'; + +@internal +class MultiViewHelper { + static PlatformDispatcherWrapper wrapper = + PlatformDispatcherWrapper(WidgetsBinding.instance.platformDispatcher); + + static bool isMultiViewEnabled() { + return wrapper.implicitView == null; + } +} diff --git a/flutter/lib/src/utils/platform_dispatcher_wrapper.dart b/flutter/lib/src/utils/platform_dispatcher_wrapper.dart new file mode 100644 index 0000000000..14a42ca2b0 --- /dev/null +++ b/flutter/lib/src/utils/platform_dispatcher_wrapper.dart @@ -0,0 +1,73 @@ +import 'dart:ui'; + +import 'package:meta/meta.dart'; +import 'package:sentry/sentry.dart'; +import '../sentry_flutter_options.dart'; + +/// This class wraps the `this as dynamic` hack in a type-safe manner. +/// It helps to introduce code, which uses newer features from Flutter +/// without breaking Sentry on older versions of Flutter. +// Should not become part of public API. +@internal +class PlatformDispatcherWrapper { + PlatformDispatcherWrapper(this._dispatcher); + + final PlatformDispatcher? _dispatcher; + + /// Should not be accessed if [isImplicitViewSupported] == false + FlutterView? get implicitView => + (_dispatcher as dynamic)?.implicitView as FlutterView?; + + bool isImplicitViewSupported(SentryFlutterOptions options) { + try { + implicitView; + } on NoSuchMethodError { + // This error is expected on pre 3.10.0 Flutter version + return false; + } catch (exception, stacktrace) { + // This error is neither expected on pre 3.10.0 nor on >= 3.10.0 Flutter versions + options.logger( + SentryLevel.debug, + 'An unexpected exception was thrown, please create an issue at https://github.com/getsentry/sentry-dart/issues', + exception: exception, + stackTrace: stacktrace, + ); + if (options.automatedTestMode) { + rethrow; + } + return false; + } + return true; + } + + /// Should not be accessed if [isOnErrorSupported] == false + ErrorCallback? get onError => + (_dispatcher as dynamic)?.onError as ErrorCallback?; + + /// Should not be accessed if [isOnErrorSupported] == false + set onError(ErrorCallback? callback) { + (_dispatcher as dynamic)?.onError = callback; + } + + bool isOnErrorSupported(SentryFlutterOptions options) { + try { + onError; + } on NoSuchMethodError { + // This error is expected on pre 3.1 Flutter version + return false; + } catch (exception, stacktrace) { + // This error is neither expected on pre 3.1 nor on >= 3.1 Flutter versions + options.logger( + SentryLevel.debug, + 'An unexpected exception was thrown, please create an issue at https://github.com/getsentry/sentry-dart/issues', + exception: exception, + stackTrace: stacktrace, + ); + if (options.automatedTestMode) { + rethrow; + } + return false; + } + return true; + } +} diff --git a/flutter/test/integrations/not_initialized_widgets_binding_on_error_integration_test.dart b/flutter/test/integrations/not_initialized_widgets_binding_on_error_integration_test.dart index 6df7df0d0f..789c21af79 100644 --- a/flutter/test/integrations/not_initialized_widgets_binding_on_error_integration_test.dart +++ b/flutter/test/integrations/not_initialized_widgets_binding_on_error_integration_test.dart @@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:sentry/sentry.dart'; import 'package:sentry_flutter/src/integrations/on_error_integration.dart'; +import 'package:sentry_flutter/src/utils/platform_dispatcher_wrapper.dart'; import '../mocks.dart'; import '../mocks.mocks.dart'; diff --git a/flutter/test/integrations/on_error_integration_test.dart b/flutter/test/integrations/on_error_integration_test.dart index f367916612..14e7abcb7b 100644 --- a/flutter/test/integrations/on_error_integration_test.dart +++ b/flutter/test/integrations/on_error_integration_test.dart @@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:sentry/sentry.dart'; import 'package:sentry_flutter/src/integrations/on_error_integration.dart'; +import 'package:sentry_flutter/src/utils/platform_dispatcher_wrapper.dart'; import '../mocks.dart'; import '../mocks.mocks.dart';