diff --git a/flutter/lib/sentry_flutter.dart b/flutter/lib/sentry_flutter.dart index c3e604e634..e902455ce6 100644 --- a/flutter/lib/sentry_flutter.dart +++ b/flutter/lib/sentry_flutter.dart @@ -12,7 +12,7 @@ export 'src/sentry_replay_options.dart'; export 'src/flutter_sentry_attachment.dart'; export 'src/sentry_asset_bundle.dart' show SentryAssetBundle; export 'src/integrations/on_error_integration.dart'; -export 'src/replay/masking_config.dart' show SentryMaskingDecision; +export 'src/screenshot/masking_config.dart' show SentryMaskingDecision; export 'src/screenshot/sentry_mask_widget.dart'; export 'src/screenshot/sentry_unmask_widget.dart'; export 'src/screenshot/sentry_screenshot_widget.dart'; diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index 2b9c80dc05..0e70c8ff5f 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -4,6 +4,8 @@ import 'dart:typed_data'; import 'dart:ui'; import 'package:sentry/sentry.dart'; +import '../screenshot/recorder.dart'; +import '../screenshot/recorder_config.dart'; import '../screenshot/sentry_screenshot_widget.dart'; import '../sentry_flutter_options.dart'; import 'package:flutter/rendering.dart'; @@ -19,6 +21,8 @@ class ScreenshotEventProcessor implements EventProcessor { bool get _hasSentryScreenshotWidget => sentryScreenshotWidgetGlobalKey.currentContext != null; + Uint8List? _screenshotCache; + @override Future apply(SentryEvent event, Hint hint) async { if (event is SentryTransaction) { @@ -75,83 +79,107 @@ class ScreenshotEventProcessor implements EventProcessor { return event; } - final bytes = await _createScreenshot(); - if (bytes != null) { - hint.screenshot = SentryAttachment.fromScreenshotData(bytes); + // ignore: deprecated_member_use + var recorder = ScreenshotRecorder( + ScreenshotRecorderConfig( + width: window.display.size.width.toInt(), + height: window.display.size.height.toInt()), + _options); + + await recorder.capture((Image image) async { + _screenshotCache = await _convertImageToUint8List(image); + }); + + if (_screenshotCache != null) { + hint.screenshot = SentryAttachment.fromScreenshotData(_screenshotCache!); } + _screenshotCache = null; return event; } - Future _createScreenshot() async { - try { - final renderObject = - sentryScreenshotWidgetGlobalKey.currentContext?.findRenderObject(); - if (renderObject is RenderRepaintBoundary) { - // ignore: deprecated_member_use - final pixelRatio = window.devicePixelRatio; - var imageResult = _getImage(renderObject, pixelRatio); - Image image; - if (imageResult is Future) { - image = await imageResult; - } else { - image = imageResult; - } - // At the time of writing there's no other image format available which - // Sentry understands. - - if (image.width == 0 || image.height == 0) { - _options.logger(SentryLevel.debug, - 'View\'s width and height is zeroed, not taking screenshot.'); - return null; - } + // Future _createScreenshot() async { + // try { + // final renderObject = + // sentryScreenshotWidgetGlobalKey.currentContext?.findRenderObject(); + // if (renderObject is RenderRepaintBoundary) { + // // ignore: deprecated_member_use + // final pixelRatio = window.devicePixelRatio; + // var imageResult = _getImage(renderObject, pixelRatio); + // Image image; + // if (imageResult is Future) { + // image = await imageResult; + // } else { + // image = imageResult; + // } + // // At the time of writing there's no other image format available which + // // Sentry understands. + // + // if (image.width == 0 || image.height == 0) { + // _options.logger(SentryLevel.debug, + // 'View\'s width and height is zeroed, not taking screenshot.'); + // return null; + // } + // + // final targetResolution = _options.screenshotQuality.targetResolution(); + // if (targetResolution != null) { + // var ratioWidth = targetResolution / image.width; + // var ratioHeight = targetResolution / image.height; + // var ratio = min(ratioWidth, ratioHeight); + // if (ratio > 0.0 && ratio < 1.0) { + // imageResult = _getImage(renderObject, ratio * pixelRatio); + // if (imageResult is Future) { + // image = await imageResult; + // } else { + // image = imageResult; + // } + // } + // } + // final byteData = await image.toByteData(format: ImageByteFormat.png); + // + // final bytes = byteData?.buffer.asUint8List(); + // if (bytes?.isNotEmpty == true) { + // return bytes; + // } else { + // _options.logger(SentryLevel.debug, + // 'Screenshot is 0 bytes, not attaching the image.'); + // return null; + // } + // } + // } catch (exception, stackTrace) { + // _options.logger( + // SentryLevel.error, + // 'Taking screenshot failed.', + // exception: exception, + // stackTrace: stackTrace, + // ); + // if (_options.automatedTestMode) { + // rethrow; + // } + // } + // return null; + // } - final targetResolution = _options.screenshotQuality.targetResolution(); - if (targetResolution != null) { - var ratioWidth = targetResolution / image.width; - var ratioHeight = targetResolution / image.height; - var ratio = min(ratioWidth, ratioHeight); - if (ratio > 0.0 && ratio < 1.0) { - imageResult = _getImage(renderObject, ratio * pixelRatio); - if (imageResult is Future) { - image = await imageResult; - } else { - image = imageResult; - } - } - } - final byteData = await image.toByteData(format: ImageByteFormat.png); + Future _convertImageToUint8List(Image image) async { + final byteData = await image.toByteData(format: ImageByteFormat.png); - final bytes = byteData?.buffer.asUint8List(); - if (bytes?.isNotEmpty == true) { - return bytes; - } else { - _options.logger(SentryLevel.debug, - 'Screenshot is 0 bytes, not attaching the image.'); - return null; - } - } - } catch (exception, stackTrace) { + final bytes = byteData?.buffer.asUint8List(); + if (bytes?.isNotEmpty == true) { + return bytes; + } else { _options.logger( - SentryLevel.error, - 'Taking screenshot failed.', - exception: exception, - stackTrace: stackTrace, - ); - if (_options.automatedTestMode) { - rethrow; - } - } - return null; - } - - FutureOr _getImage( - RenderRepaintBoundary repaintBoundary, double pixelRatio) { - // This one is a hack to use https://api.flutter.dev/flutter/rendering/RenderRepaintBoundary/toImage.html on versions older than 3.7 and https://api.flutter.dev/flutter/rendering/RenderRepaintBoundary/toImageSync.html on versions equal or newer than 3.7 - try { - return (repaintBoundary as dynamic).toImageSync(pixelRatio: pixelRatio) - as Image; - } on NoSuchMethodError catch (_) { - return repaintBoundary.toImage(pixelRatio: pixelRatio); + SentryLevel.debug, 'Screenshot is 0 bytes, not attaching the image.'); + return null; } } + // + // FutureOr _getImage( + // RenderRepaintBoundary repaintBoundary, double pixelRatio) { + // // This one is a hack to use https://api.flutter.dev/flutter/rendering/RenderRepaintBoundary/toImage.html on versions older than 3.7 and https://api.flutter.dev/flutter/rendering/RenderRepaintBoundary/toImageSync.html on versions equal or newer than 3.7 + // try { + // return (repaintBoundary as dynamic).toImageSync(pixelRatio: pixelRatio) + // as Image; + // } on NoSuchMethodError catch (_) { + // return repaintBoundary.toImage(pixelRatio: pixelRatio); + // } + // } } diff --git a/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart b/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart index a042f325ae..850165fcec 100644 --- a/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart +++ b/flutter/lib/src/native/cocoa/sentry_native_cocoa.dart @@ -6,9 +6,9 @@ import 'package:meta/meta.dart'; import '../../../sentry_flutter.dart'; import '../../event_processor/replay_event_processor.dart'; +import '../../screenshot/recorder.dart'; +import '../../screenshot/recorder_config.dart'; import '../../replay/integration.dart'; -import '../../replay/recorder.dart'; -import '../../replay/recorder_config.dart'; import '../sentry_native_channel.dart'; import 'binding.dart' as cocoa; diff --git a/flutter/lib/src/native/java/sentry_native_java.dart b/flutter/lib/src/native/java/sentry_native_java.dart index 0b8d2eb0f6..91c127de4f 100644 --- a/flutter/lib/src/native/java/sentry_native_java.dart +++ b/flutter/lib/src/native/java/sentry_native_java.dart @@ -7,7 +7,7 @@ import '../../../sentry_flutter.dart'; import '../../event_processor/replay_event_processor.dart'; import '../../replay/integration.dart'; import '../../replay/scheduled_recorder.dart'; -import '../../replay/recorder_config.dart'; +import '../../replay/scheduled_recorder_config.dart'; import '../sentry_native_channel.dart'; // Note: currently this doesn't do anything. Later, it shall be used with diff --git a/flutter/lib/src/replay/scheduled_recorder.dart b/flutter/lib/src/replay/scheduled_recorder.dart index c575278a74..5c040fc96e 100644 --- a/flutter/lib/src/replay/scheduled_recorder.dart +++ b/flutter/lib/src/replay/scheduled_recorder.dart @@ -2,10 +2,11 @@ import 'dart:async'; import 'dart:ui'; import 'package:meta/meta.dart'; +import 'package:sentry_flutter/src/replay/scheduled_recorder_config.dart'; import '../../sentry_flutter.dart'; -import 'recorder.dart'; -import 'recorder_config.dart'; +import '../screenshot/recorder.dart'; +import '../screenshot/recorder_config.dart'; import 'scheduler.dart'; @internal diff --git a/flutter/lib/src/replay/scheduled_recorder_config.dart b/flutter/lib/src/replay/scheduled_recorder_config.dart new file mode 100644 index 0000000000..67c5d672ef --- /dev/null +++ b/flutter/lib/src/replay/scheduled_recorder_config.dart @@ -0,0 +1,11 @@ +import '../screenshot/recorder_config.dart'; + +class ScheduledScreenshotRecorderConfig extends ScreenshotRecorderConfig { + final int frameRate; + + const ScheduledScreenshotRecorderConfig({ + super.width, + super.height, + required this.frameRate, + }); +} diff --git a/flutter/lib/src/replay/masking_config.dart b/flutter/lib/src/screenshot/masking_config.dart similarity index 100% rename from flutter/lib/src/replay/masking_config.dart rename to flutter/lib/src/screenshot/masking_config.dart diff --git a/flutter/lib/src/replay/recorder.dart b/flutter/lib/src/screenshot/recorder.dart similarity index 98% rename from flutter/lib/src/replay/recorder.dart rename to flutter/lib/src/screenshot/recorder.dart index f15b79a072..716dffa141 100644 --- a/flutter/lib/src/replay/recorder.dart +++ b/flutter/lib/src/screenshot/recorder.dart @@ -87,7 +87,7 @@ class ScreenshotRecorder { try { await callback(finalImage); } finally { - finalImage.dispose(); + finalImage.dispose(); // image needs to be disposed manually } } finally { picture.dispose(); diff --git a/flutter/lib/src/replay/recorder_config.dart b/flutter/lib/src/screenshot/recorder_config.dart similarity index 67% rename from flutter/lib/src/replay/recorder_config.dart rename to flutter/lib/src/screenshot/recorder_config.dart index 9649a33823..ff39ddf163 100644 --- a/flutter/lib/src/replay/recorder_config.dart +++ b/flutter/lib/src/screenshot/recorder_config.dart @@ -17,13 +17,3 @@ class ScreenshotRecorderConfig { return min(width! / srcWidth, height! / srcHeight); } } - -class ScheduledScreenshotRecorderConfig extends ScreenshotRecorderConfig { - final int frameRate; - - const ScheduledScreenshotRecorderConfig({ - super.width, - super.height, - required this.frameRate, - }); -} diff --git a/flutter/lib/src/replay/widget_filter.dart b/flutter/lib/src/screenshot/widget_filter.dart similarity index 100% rename from flutter/lib/src/replay/widget_filter.dart rename to flutter/lib/src/screenshot/widget_filter.dart diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index e400aa3536..073668e00b 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -15,6 +15,7 @@ import 'event_processor/screenshot_event_processor.dart'; import 'screenshot/sentry_screenshot_widget.dart'; import 'sentry_flutter.dart'; import 'sentry_replay_options.dart'; +import 'sentry_screenshot_options.dart'; import 'user_interaction/sentry_user_interaction_widget.dart'; /// This class adds options which are only available in a Flutter environment. @@ -380,6 +381,7 @@ class SentryFlutterOptions extends SentryOptions { class _SentryFlutterExperimentalOptions { /// Replay recording configuration. final replay = SentryReplayOptions(); + final screenshot = SentryScreenshotOptions(); } /// Callback being executed in [ScreenshotEventProcessor], deciding if a diff --git a/flutter/lib/src/sentry_replay_options.dart b/flutter/lib/src/sentry_replay_options.dart index a6e83fec4f..5b49a9cfc3 100644 --- a/flutter/lib/src/sentry_replay_options.dart +++ b/flutter/lib/src/sentry_replay_options.dart @@ -2,14 +2,15 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; -import 'replay/masking_config.dart'; -import 'replay/widget_filter.dart'; +import 'screenshot/masking_config.dart'; +import 'screenshot/widget_filter.dart'; import 'screenshot/sentry_mask_widget.dart'; import 'screenshot/sentry_unmask_widget.dart'; +import 'sentry_screenshot_options.dart'; /// Configuration of the experimental replay feature. @experimental -class SentryReplayOptions { +class SentryReplayOptions extends SentryScreenshotOptions { double? _sessionSampleRate; /// A percentage of sessions in which a replay will be created. @@ -32,110 +33,17 @@ class SentryReplayOptions { _onErrorSampleRate = value; } - /// Mask all text content. Draws a rectangle of text bounds with text color - /// on top. Currently, only [Text] and [EditableText] Widgets are redacted. - /// Default is enabled. - var maskAllText = true; - @Deprecated('Use maskAllText instead') bool get redactAllText => maskAllText; set redactAllText(bool value) => maskAllText = value; - /// Mask content of all images. Draws a rectangle of image bounds with image's - /// dominant color on top. Currently, only [Image] widgets are redacted. - /// Default is enabled (except for asset images, see [maskAssetImages]). - var maskAllImages = true; - @Deprecated('Use maskAllImages instead') bool get redactAllImages => maskAllImages; set redactAllImages(bool value) => maskAllImages = value; - /// Redact asset images coming from the root asset bundle. - var maskAssetImages = false; - final _userMaskingRules = []; - @internal - SentryMaskingConfig buildMaskingConfig() { - // First, we collect rules defined by the user (so they're applied first). - final rules = _userMaskingRules.toList(); - - // Then, we apply rules for [SentryMask] and [SentryUnmask]. - rules.add(const SentryMaskingConstantRule( - SentryMaskingDecision.mask)); - rules.add(const SentryMaskingConstantRule( - SentryMaskingDecision.unmask)); - - // Then, we apply apply rules based on the configuration. - if (maskAllImages) { - if (maskAssetImages) { - rules.add( - const SentryMaskingConstantRule(SentryMaskingDecision.mask)); - } else { - rules - .add(const SentryMaskingCustomRule(_maskImagesExceptAssets)); - } - } else { - assert(!maskAssetImages, - "maskAssetImages can't be true if maskAllImages is false"); - } - if (maskAllText) { - rules.add( - const SentryMaskingConstantRule(SentryMaskingDecision.mask)); - rules.add(const SentryMaskingConstantRule( - SentryMaskingDecision.mask)); - } - return SentryMaskingConfig(rules); - } - - /// Mask given widget type [T] (or subclasses of [T]) in the replay. - /// Note: masking rules are called in the order they're added so if a previous - /// rule already makes a decision, this rule won't be called. - void mask() { - assert(T != SentryMask); - assert(T != SentryUnmask); - _userMaskingRules - .add(SentryMaskingConstantRule(SentryMaskingDecision.mask)); - } - - /// Unmask given widget type [T] (or subclasses of [T]) in the replay. This is - /// useful to explicitly show certain widgets that would otherwise be masked - /// by other rules, for example default [maskAllText] or [maskAllImages]. - /// The [SentryMaskingDecision.unmask] will apply to the widget and its children, - /// so no other rules will be checked for the children. - /// Note: masking rules are called in the order they're added so if a previous - /// rule already makes a decision, this rule won't be called. - void unmask() { - assert(T != SentryMask); - assert(T != SentryUnmask); - _userMaskingRules - .add(SentryMaskingConstantRule(SentryMaskingDecision.unmask)); - } - - /// Provide a custom callback to decide whether to mask the widget of class - /// [T] (or subclasses of [T]). - /// Note: masking rules are called in the order they're added so if a previous - /// rule already makes a decision, this rule won't be called. - void maskCallback( - SentryMaskingDecision Function(Element, T) shouldMask) { - assert(T != SentryMask); - assert(T != SentryUnmask); - _userMaskingRules.add(SentryMaskingCustomRule(shouldMask)); - } - @internal bool get isEnabled => ((sessionSampleRate ?? 0) > 0) || ((onErrorSampleRate ?? 0) > 0); } - -SentryMaskingDecision _maskImagesExceptAssets(Element element, Widget widget) { - if (widget is Image) { - final image = widget.image; - if (image is AssetBundleImageProvider) { - if (WidgetFilter.isBuiltInAssetImage(image, rootBundle)) { - return SentryMaskingDecision.continueProcessing; - } - } - } - return SentryMaskingDecision.mask; -} diff --git a/flutter/lib/src/sentry_screenshot_options.dart b/flutter/lib/src/sentry_screenshot_options.dart new file mode 100644 index 0000000000..c42860be06 --- /dev/null +++ b/flutter/lib/src/sentry_screenshot_options.dart @@ -0,0 +1,107 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart'; + +import 'screenshot/masking_config.dart'; +import 'screenshot/widget_filter.dart'; +import 'screenshot/sentry_mask_widget.dart'; +import 'screenshot/sentry_unmask_widget.dart'; + +/// Configuration of the experimental screenshot feature. +@experimental +class SentryScreenshotOptions { + /// Mask all text content. Draws a rectangle of text bounds with text color + /// on top. Currently, only [Text] and [EditableText] Widgets are redacted. + /// Default is enabled. + var maskAllText = true; + + /// Mask content of all images. Draws a rectangle of image bounds with image's + /// dominant color on top. Currently, only [Image] widgets are redacted. + /// Default is enabled (except for asset images, see [maskAssetImages]). + var maskAllImages = true; + + /// Redact asset images coming from the root asset bundle. + var maskAssetImages = false; + + final _userMaskingRules = []; + + @internal + SentryMaskingConfig buildMaskingConfig() { + // First, we collect rules defined by the user (so they're applied first). + final rules = _userMaskingRules.toList(); + + // Then, we apply rules for [SentryMask] and [SentryUnmask]. + rules.add(const SentryMaskingConstantRule( + SentryMaskingDecision.mask)); + rules.add(const SentryMaskingConstantRule( + SentryMaskingDecision.unmask)); + + // Then, we apply apply rules based on the configuration. + if (maskAllImages) { + if (maskAssetImages) { + rules.add( + const SentryMaskingConstantRule(SentryMaskingDecision.mask)); + } else { + rules + .add(const SentryMaskingCustomRule(_maskImagesExceptAssets)); + } + } else { + assert(!maskAssetImages, + "maskAssetImages can't be true if maskAllImages is false"); + } + if (maskAllText) { + rules.add( + const SentryMaskingConstantRule(SentryMaskingDecision.mask)); + rules.add(const SentryMaskingConstantRule( + SentryMaskingDecision.mask)); + } + return SentryMaskingConfig(rules); + } + + /// Mask given widget type [T] (or subclasses of [T]) in the replay. + /// Note: masking rules are called in the order they're added so if a previous + /// rule already makes a decision, this rule won't be called. + void mask() { + assert(T != SentryMask); + assert(T != SentryUnmask); + _userMaskingRules + .add(SentryMaskingConstantRule(SentryMaskingDecision.mask)); + } + + /// Unmask given widget type [T] (or subclasses of [T]) in the replay. This is + /// useful to explicitly show certain widgets that would otherwise be masked + /// by other rules, for example default [maskAllText] or [maskAllImages]. + /// The [SentryMaskingDecision.unmask] will apply to the widget and its children, + /// so no other rules will be checked for the children. + /// Note: masking rules are called in the order they're added so if a previous + /// rule already makes a decision, this rule won't be called. + void unmask() { + assert(T != SentryMask); + assert(T != SentryUnmask); + _userMaskingRules + .add(SentryMaskingConstantRule(SentryMaskingDecision.unmask)); + } + + /// Provide a custom callback to decide whether to mask the widget of class + /// [T] (or subclasses of [T]). + /// Note: masking rules are called in the order they're added so if a previous + /// rule already makes a decision, this rule won't be called. + void maskCallback( + SentryMaskingDecision Function(Element, T) shouldMask) { + assert(T != SentryMask); + assert(T != SentryUnmask); + _userMaskingRules.add(SentryMaskingCustomRule(shouldMask)); + } +} + +SentryMaskingDecision _maskImagesExceptAssets(Element element, Widget widget) { + if (widget is Image) { + final image = widget.image; + if (image is AssetBundleImageProvider) { + if (WidgetFilter.isBuiltInAssetImage(image, rootBundle)) { + return SentryMaskingDecision.continueProcessing; + } + } + } + return SentryMaskingDecision.mask; +} diff --git a/flutter/test/replay/masking_config_test.dart b/flutter/test/replay/masking_config_test.dart index 46e6a99261..4323521541 100644 --- a/flutter/test/replay/masking_config_test.dart +++ b/flutter/test/replay/masking_config_test.dart @@ -1,7 +1,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:sentry_flutter/src/replay/masking_config.dart'; +import 'package:sentry_flutter/src/screenshot/masking_config.dart'; import 'test_widget.dart'; diff --git a/flutter/test/replay/recorder_config_test.dart b/flutter/test/replay/recorder_config_test.dart index d884073e91..7aa4612f7e 100644 --- a/flutter/test/replay/recorder_config_test.dart +++ b/flutter/test/replay/recorder_config_test.dart @@ -1,5 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:sentry_flutter/src/replay/recorder_config.dart'; +import 'package:sentry_flutter/src/screenshot/recorder_config.dart'; void main() async { group('$ScreenshotRecorderConfig', () { diff --git a/flutter/test/replay/recorder_test.dart b/flutter/test/replay/recorder_test.dart index 2df4334c5b..efef4137dd 100644 --- a/flutter/test/replay/recorder_test.dart +++ b/flutter/test/replay/recorder_test.dart @@ -6,8 +6,8 @@ library dart_test; import 'dart:ui'; import 'package:flutter_test/flutter_test.dart'; -import 'package:sentry_flutter/src/replay/recorder.dart'; -import 'package:sentry_flutter/src/replay/recorder_config.dart'; +import 'package:sentry_flutter/src/screenshot/recorder.dart'; +import 'package:sentry_flutter/src/screenshot/recorder_config.dart'; import '../mocks.dart'; import 'test_widget.dart'; diff --git a/flutter/test/replay/scheduled_recorder_test.dart b/flutter/test/replay/scheduled_recorder_test.dart index 7ace54c18e..59cf991fc6 100644 --- a/flutter/test/replay/scheduled_recorder_test.dart +++ b/flutter/test/replay/scheduled_recorder_test.dart @@ -7,7 +7,7 @@ import 'dart:ui'; import 'package:flutter_test/flutter_test.dart'; import 'package:sentry_flutter/src/replay/scheduled_recorder.dart'; -import 'package:sentry_flutter/src/replay/recorder_config.dart'; +import 'package:sentry_flutter/src/replay/scheduled_recorder_config.dart'; import '../mocks.dart'; import 'test_widget.dart'; diff --git a/flutter/test/replay/widget_filter_test.dart b/flutter/test/replay/widget_filter_test.dart index ad76e9bfaa..3d72c5a3e2 100644 --- a/flutter/test/replay/widget_filter_test.dart +++ b/flutter/test/replay/widget_filter_test.dart @@ -2,7 +2,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:sentry_flutter/src/replay/widget_filter.dart'; +import 'package:sentry_flutter/src/screenshot/widget_filter.dart'; import 'test_widget.dart';