From 42be7f3c66b3db16730e7daf953592efb76d072c Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Tue, 15 Oct 2024 10:19:04 +0200 Subject: [PATCH 01/51] use view hierachy for screenshots --- flutter/lib/sentry_flutter.dart | 2 +- .../screenshot_event_processor.dart | 168 ++++++++++-------- .../src/native/cocoa/sentry_native_cocoa.dart | 4 +- .../src/native/java/sentry_native_java.dart | 2 +- .../lib/src/replay/scheduled_recorder.dart | 5 +- .../src/replay/scheduled_recorder_config.dart | 11 ++ .../masking_config.dart | 0 .../src/{replay => screenshot}/recorder.dart | 2 +- .../recorder_config.dart | 10 -- .../{replay => screenshot}/widget_filter.dart | 0 flutter/lib/src/sentry_flutter_options.dart | 2 + flutter/lib/src/sentry_replay_options.dart | 100 +---------- .../lib/src/sentry_screenshot_options.dart | 107 +++++++++++ flutter/test/replay/masking_config_test.dart | 2 +- flutter/test/replay/recorder_config_test.dart | 2 +- flutter/test/replay/recorder_test.dart | 4 +- .../test/replay/scheduled_recorder_test.dart | 2 +- flutter/test/replay/widget_filter_test.dart | 2 +- 18 files changed, 236 insertions(+), 189 deletions(-) create mode 100644 flutter/lib/src/replay/scheduled_recorder_config.dart rename flutter/lib/src/{replay => screenshot}/masking_config.dart (100%) rename flutter/lib/src/{replay => screenshot}/recorder.dart (98%) rename flutter/lib/src/{replay => screenshot}/recorder_config.dart (67%) rename flutter/lib/src/{replay => screenshot}/widget_filter.dart (100%) create mode 100644 flutter/lib/src/sentry_screenshot_options.dart 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'; From 49a413b86e3fa5c120cae0ec24e5435b182d0e9e Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Tue, 15 Oct 2024 16:42:16 +0200 Subject: [PATCH 02/51] fix tests and distinguish between ScheduledScreenshotRecorderConfig and ScreenshotRecorderConfig. --- .../screenshot_event_processor.dart | 4 +- flutter/lib/src/screenshot/recorder.dart | 23 +++++++-- .../lib/src/screenshot/recorder_config.dart | 9 +++- .../screenshot/sentry_screenshot_quality.dart | 26 ++++++++++ flutter/test/replay/recorder_test.dart | 50 +++++++++++++++++-- .../sentry_screenshot_quality_test.dart | 45 +++++++++++++++++ 6 files changed, 146 insertions(+), 11 deletions(-) create mode 100644 flutter/test/screenshot/sentry_screenshot_quality_test.dart diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index 0e70c8ff5f..2f72d7f743 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -81,9 +81,7 @@ class ScreenshotEventProcessor implements EventProcessor { // ignore: deprecated_member_use var recorder = ScreenshotRecorder( - ScreenshotRecorderConfig( - width: window.display.size.width.toInt(), - height: window.display.size.height.toInt()), + ScreenshotRecorderConfig(quality: _options.screenshotQuality), _options); await recorder.capture((Image image) async { diff --git a/flutter/lib/src/screenshot/recorder.dart b/flutter/lib/src/screenshot/recorder.dart index 716dffa141..e6e6dc5c6f 100644 --- a/flutter/lib/src/screenshot/recorder.dart +++ b/flutter/lib/src/screenshot/recorder.dart @@ -5,6 +5,8 @@ import 'package:flutter/rendering.dart'; import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; +import '../replay/scheduled_recorder_config.dart'; +import 'masking_config.dart'; import 'recorder_config.dart'; import 'widget_filter.dart'; @@ -21,7 +23,13 @@ class ScreenshotRecorder { bool warningLogged = false; ScreenshotRecorder(this.config, this.options) { - final maskingConfig = options.experimental.replay.buildMaskingConfig(); + SentryMaskingConfig maskingConfig; + if (config is ScheduledScreenshotRecorderConfig) { + maskingConfig = options.experimental.replay.buildMaskingConfig(); + } else { + maskingConfig = options.experimental.screenshot.buildMaskingConfig(); + } + if (maskingConfig.length > 0) { _widgetFilter = WidgetFilter(maskingConfig, options.logger); } @@ -82,8 +90,17 @@ class ScreenshotRecorder { final picture = recorder.endRecording(); try { - final finalImage = await picture.toImage( - (srcWidth * pixelRatio).round(), (srcHeight * pixelRatio).round()); + Image finalImage; + if (config is ScheduledScreenshotRecorderConfig) { + finalImage = await picture.toImage((srcWidth * pixelRatio).round(), + (srcHeight * pixelRatio).round()); + } else { + final targetHeight = config.quality + .calculateHeight(srcWidth.toInt(), srcHeight.toInt()); + final targetWidth = config.quality + .calculateWidth(srcWidth.toInt(), srcHeight.toInt()); + finalImage = await picture.toImage(targetWidth, targetHeight); + } try { await callback(finalImage); } finally { diff --git a/flutter/lib/src/screenshot/recorder_config.dart b/flutter/lib/src/screenshot/recorder_config.dart index ff39ddf163..64a1d7d183 100644 --- a/flutter/lib/src/screenshot/recorder_config.dart +++ b/flutter/lib/src/screenshot/recorder_config.dart @@ -2,12 +2,19 @@ import 'dart:math'; import 'package:meta/meta.dart'; +import '../../sentry_flutter.dart'; + @internal class ScreenshotRecorderConfig { final int? width; final int? height; + final SentryScreenshotQuality quality; - const ScreenshotRecorderConfig({this.width, this.height}); + const ScreenshotRecorderConfig({ + this.width, + this.height, + this.quality = SentryScreenshotQuality.full, + }); double getPixelRatio(double srcWidth, double srcHeight) { assert((width == null) == (height == null)); diff --git a/flutter/lib/src/screenshot/sentry_screenshot_quality.dart b/flutter/lib/src/screenshot/sentry_screenshot_quality.dart index d42e622966..2b97791983 100644 --- a/flutter/lib/src/screenshot/sentry_screenshot_quality.dart +++ b/flutter/lib/src/screenshot/sentry_screenshot_quality.dart @@ -17,4 +17,30 @@ enum SentryScreenshotQuality { return 854; } } + + int calculateHeight(int width, int height) { + if (this == SentryScreenshotQuality.full) { + return height; + } else { + if (height > width) { + return targetResolution()!; + } else { + var ratio = targetResolution()! / width; + return (height * ratio).round(); + } + } + } + + int calculateWidth(int width, int height) { + if (this == SentryScreenshotQuality.full) { + return width; + } else { + if (width > height) { + return targetResolution()!; + } else { + var ratio = targetResolution()! / height; + return (width * ratio).round(); + } + } + } } diff --git a/flutter/test/replay/recorder_test.dart b/flutter/test/replay/recorder_test.dart index efef4137dd..fd9987cff6 100644 --- a/flutter/test/replay/recorder_test.dart +++ b/flutter/test/replay/recorder_test.dart @@ -6,6 +6,7 @@ library dart_test; import 'dart:ui'; import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/screenshot/recorder.dart'; import 'package:sentry_flutter/src/screenshot/recorder_config.dart'; @@ -19,20 +20,61 @@ void main() async { final fixture = await _Fixture.create(tester); expect(fixture.capture(), completion('800x600')); }); + + testWidgets('captures full resolution images - portrait', (tester) async { + await tester.binding.setSurfaceSize(Size(2000, 4000)); + final fixture = await _Fixture.create(tester); + expect(fixture.capture(), completion('2000x4000')); + }); + + testWidgets('captures full resolution images - landscape', (tester) async { + await tester.binding.setSurfaceSize(Size(4000, 2000)); + final fixture = await _Fixture.create(tester); + expect(fixture.capture(), completion('4000x2000')); + }); + + testWidgets('captures high resolution images - portrait', (tester) async { + await tester.binding.setSurfaceSize(Size(2000, 4000)); + final fixture = + await _Fixture.create(tester, quality: SentryScreenshotQuality.high); + expect(fixture.capture(), completion('960x1920')); + }); + + testWidgets('captures high resolution images - landscape', (tester) async { + await tester.binding.setSurfaceSize(Size(4000, 2000)); + final fixture = + await _Fixture.create(tester, quality: SentryScreenshotQuality.high); + expect(fixture.capture(), completion('1920x960')); + }); + + testWidgets('captures medium resolution images', (tester) async { + await tester.binding.setSurfaceSize(Size(2000, 4000)); + final fixture = + await _Fixture.create(tester, quality: SentryScreenshotQuality.medium); + expect(fixture.capture(), completion('640x1280')); + }); + + testWidgets('captures low resolution images', (tester) async { + await tester.binding.setSurfaceSize(Size(2000, 4000)); + final fixture = + await _Fixture.create(tester, quality: SentryScreenshotQuality.low); + expect(fixture.capture(), completion('427x854')); + }); } class _Fixture { late final ScreenshotRecorder sut; - _Fixture._() { + _Fixture({SentryScreenshotQuality quality = SentryScreenshotQuality.full}) { sut = ScreenshotRecorder( - ScreenshotRecorderConfig(), + ScreenshotRecorderConfig(quality: quality), defaultTestOptions()..bindingUtils = TestBindingWrapper(), ); } - static Future<_Fixture> create(WidgetTester tester) async { - final fixture = _Fixture._(); + static Future<_Fixture> create(WidgetTester tester, + {SentryScreenshotQuality quality = SentryScreenshotQuality.full}) async { + final fixture = _Fixture(quality: quality); await pumpTestElement(tester); return fixture; } diff --git a/flutter/test/screenshot/sentry_screenshot_quality_test.dart b/flutter/test/screenshot/sentry_screenshot_quality_test.dart new file mode 100644 index 0000000000..a4663bdf0f --- /dev/null +++ b/flutter/test/screenshot/sentry_screenshot_quality_test.dart @@ -0,0 +1,45 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +void main() async { + group('$SentryScreenshotQuality', () { + test('test quality: full', () { + final sut = SentryScreenshotQuality.full; + expect(sut.targetResolution(), isNull); + expect(sut.calculateHeight(2000, 4000), 4000); + expect(sut.calculateWidth(2000, 4000), 2000); + expect(sut.calculateHeight(4000, 2000), 2000); + expect(sut.calculateWidth(4000, 2000), 4000); + }); + + test('test quality: high', () { + final sut = SentryScreenshotQuality.high; + final res = sut.targetResolution()!; + expect(res, 1920); + expect(sut.calculateHeight(2000, 4000), res); + expect(sut.calculateWidth(2000, 4000), res / 2); + expect(sut.calculateHeight(4000, 2000), res / 2); + expect(sut.calculateWidth(4000, 2000), res); + }); + + test('test quality: medium', () { + final sut = SentryScreenshotQuality.medium; + final res = sut.targetResolution()!; + expect(res, 1280); + expect(sut.calculateHeight(2000, 4000), res); + expect(sut.calculateWidth(2000, 4000), res / 2); + expect(sut.calculateHeight(4000, 2000), res / 2); + expect(sut.calculateWidth(4000, 2000), res); + }); + + test('test quality: low', () { + final sut = SentryScreenshotQuality.low; + final res = sut.targetResolution()!; + expect(res, 854); + expect(sut.calculateHeight(2000, 4000), res); + expect(sut.calculateWidth(2000, 4000), res / 2); + expect(sut.calculateHeight(4000, 2000), res / 2); + expect(sut.calculateWidth(4000, 2000), res); + }); + }); +} From fdc7d6d0cc06a244314a57c72dbf94a4ddc01c58 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Wed, 16 Oct 2024 16:06:49 +0200 Subject: [PATCH 03/51] removed unused imports --- .../screenshot_event_processor.dart | 81 +------------------ .../lib/src/replay/scheduled_recorder.dart | 3 +- flutter/lib/src/sentry_replay_options.dart | 8 -- 3 files changed, 4 insertions(+), 88 deletions(-) diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index 2f72d7f743..b53d614e6f 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:math'; import 'dart:typed_data'; import 'dart:ui'; @@ -8,7 +7,6 @@ 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'; import '../renderer/renderer.dart'; import 'package:flutter/widgets.dart' as widget; @@ -21,8 +19,6 @@ class ScreenshotEventProcessor implements EventProcessor { bool get _hasSentryScreenshotWidget => sentryScreenshotWidgetGlobalKey.currentContext != null; - Uint8List? _screenshotCache; - @override Future apply(SentryEvent event, Hint hint) async { if (event is SentryTransaction) { @@ -84,6 +80,8 @@ class ScreenshotEventProcessor implements EventProcessor { ScreenshotRecorderConfig(quality: _options.screenshotQuality), _options); + Uint8List? _screenshotCache; + await recorder.capture((Image image) async { _screenshotCache = await _convertImageToUint8List(image); }); @@ -91,72 +89,10 @@ class ScreenshotEventProcessor implements EventProcessor { 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; - // } - // - // 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; - // } - Future _convertImageToUint8List(Image image) async { final byteData = await image.toByteData(format: ImageByteFormat.png); @@ -169,15 +105,4 @@ class ScreenshotEventProcessor implements EventProcessor { 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/replay/scheduled_recorder.dart b/flutter/lib/src/replay/scheduled_recorder.dart index 5c040fc96e..a8a29a276f 100644 --- a/flutter/lib/src/replay/scheduled_recorder.dart +++ b/flutter/lib/src/replay/scheduled_recorder.dart @@ -2,11 +2,10 @@ import 'dart:async'; import 'dart:ui'; import 'package:meta/meta.dart'; -import 'package:sentry_flutter/src/replay/scheduled_recorder_config.dart'; +import 'scheduled_recorder_config.dart'; import '../../sentry_flutter.dart'; import '../screenshot/recorder.dart'; -import '../screenshot/recorder_config.dart'; import 'scheduler.dart'; @internal diff --git a/flutter/lib/src/sentry_replay_options.dart b/flutter/lib/src/sentry_replay_options.dart index 5b49a9cfc3..4cf6d664ca 100644 --- a/flutter/lib/src/sentry_replay_options.dart +++ b/flutter/lib/src/sentry_replay_options.dart @@ -1,11 +1,5 @@ -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'; import 'sentry_screenshot_options.dart'; /// Configuration of the experimental replay feature. @@ -41,8 +35,6 @@ class SentryReplayOptions extends SentryScreenshotOptions { bool get redactAllImages => maskAllImages; set redactAllImages(bool value) => maskAllImages = value; - final _userMaskingRules = []; - @internal bool get isEnabled => ((sessionSampleRate ?? 0) > 0) || ((onErrorSampleRate ?? 0) > 0); From 981039c3839b662cc22403bca88c8258f75a7669 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Wed, 16 Oct 2024 16:11:40 +0200 Subject: [PATCH 04/51] remove unused test --- flutter/test/replay/recorder_test.dart | 5 ----- 1 file changed, 5 deletions(-) diff --git a/flutter/test/replay/recorder_test.dart b/flutter/test/replay/recorder_test.dart index fd9987cff6..b01fd2872c 100644 --- a/flutter/test/replay/recorder_test.dart +++ b/flutter/test/replay/recorder_test.dart @@ -16,11 +16,6 @@ import 'test_widget.dart'; void main() async { TestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('captures images', (tester) async { - final fixture = await _Fixture.create(tester); - expect(fixture.capture(), completion('800x600')); - }); - testWidgets('captures full resolution images - portrait', (tester) async { await tester.binding.setSurfaceSize(Size(2000, 4000)); final fixture = await _Fixture.create(tester); From 4671c53e3a929890af6f1db6b1a102a82754735a Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Wed, 16 Oct 2024 16:51:15 +0200 Subject: [PATCH 05/51] add changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38f93e41b2..2e34c42ae7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Switching from traditional screenshot to view hierarchy for screenshots which allows redacting ([#2361](https://github.com/getsentry/sentry-dart/pull/2361)) + ## 8.10.0-beta.2 ### Fixes From f7221910ce3921286c53e3eb09a79af67ef01833 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Tue, 22 Oct 2024 14:55:42 +0200 Subject: [PATCH 06/51] rename variable --- .../src/event_processor/screenshot_event_processor.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index b53d614e6f..8b4140f75d 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -80,14 +80,14 @@ class ScreenshotEventProcessor implements EventProcessor { ScreenshotRecorderConfig(quality: _options.screenshotQuality), _options); - Uint8List? _screenshotCache; + Uint8List? _screenshotData; await recorder.capture((Image image) async { - _screenshotCache = await _convertImageToUint8List(image); + _screenshotData = await _convertImageToUint8List(image); }); - if (_screenshotCache != null) { - hint.screenshot = SentryAttachment.fromScreenshotData(_screenshotCache!); + if (_screenshotData != null) { + hint.screenshot = SentryAttachment.fromScreenshotData(_screenshotData!); } return event; From 22f22b04b0a307204d8471c7140dd9d6ace78fd6 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Tue, 22 Oct 2024 14:55:51 +0200 Subject: [PATCH 07/51] add internal --- flutter/lib/src/screenshot/sentry_screenshot_quality.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/flutter/lib/src/screenshot/sentry_screenshot_quality.dart b/flutter/lib/src/screenshot/sentry_screenshot_quality.dart index 2b97791983..2110a1ec49 100644 --- a/flutter/lib/src/screenshot/sentry_screenshot_quality.dart +++ b/flutter/lib/src/screenshot/sentry_screenshot_quality.dart @@ -1,3 +1,5 @@ +import 'package:meta/meta.dart'; + /// The quality of the attached screenshot enum SentryScreenshotQuality { full, @@ -18,6 +20,7 @@ enum SentryScreenshotQuality { } } + @internal int calculateHeight(int width, int height) { if (this == SentryScreenshotQuality.full) { return height; @@ -31,6 +34,7 @@ enum SentryScreenshotQuality { } } + @internal int calculateWidth(int width, int height) { if (this == SentryScreenshotQuality.full) { return width; From 9bb14ad7b14e8133db84cfa4ebc796ca39f38cff Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Thu, 24 Oct 2024 17:01:55 +0200 Subject: [PATCH 08/51] split into screenshot, screenreplay and redaction options --- flutter/lib/src/screenshot/recorder.dart | 28 ++-- flutter/lib/src/sentry_flutter.dart | 16 +++ flutter/lib/src/sentry_flutter_options.dart | 35 +++-- flutter/lib/src/sentry_redaction_options.dart | 111 +++++++++++++++ flutter/lib/src/sentry_replay_options.dart | 11 +- .../lib/src/sentry_screenshot_options.dart | 130 ++++-------------- flutter/test/replay/masking_config_test.dart | 27 ++-- .../test/replay/scheduled_recorder_test.dart | 1 + flutter/test/replay/widget_filter_test.dart | 7 +- 9 files changed, 206 insertions(+), 160 deletions(-) create mode 100644 flutter/lib/src/sentry_redaction_options.dart diff --git a/flutter/lib/src/screenshot/recorder.dart b/flutter/lib/src/screenshot/recorder.dart index e6e6dc5c6f..e8eb3f0038 100644 --- a/flutter/lib/src/screenshot/recorder.dart +++ b/flutter/lib/src/screenshot/recorder.dart @@ -3,9 +3,9 @@ import 'dart:ui'; import 'package:flutter/rendering.dart'; import 'package:meta/meta.dart'; +import '../sentry_redaction_options.dart'; import '../../sentry_flutter.dart'; -import '../replay/scheduled_recorder_config.dart'; import 'masking_config.dart'; import 'recorder_config.dart'; import 'widget_filter.dart'; @@ -23,12 +23,11 @@ class ScreenshotRecorder { bool warningLogged = false; ScreenshotRecorder(this.config, this.options) { - SentryMaskingConfig maskingConfig; - if (config is ScheduledScreenshotRecorderConfig) { - maskingConfig = options.experimental.replay.buildMaskingConfig(); - } else { - maskingConfig = options.experimental.screenshot.buildMaskingConfig(); - } + /// TODO: Rewrite when default redaction value are synced with SS & SR + final SentryMaskingConfig maskingConfig = + (options.experimental.sentryRedactingOptions ?? + SentryRedactingOptions()) + .buildMaskingConfig(); if (maskingConfig.length > 0) { _widgetFilter = WidgetFilter(maskingConfig, options.logger); @@ -91,16 +90,11 @@ class ScreenshotRecorder { try { Image finalImage; - if (config is ScheduledScreenshotRecorderConfig) { - finalImage = await picture.toImage((srcWidth * pixelRatio).round(), - (srcHeight * pixelRatio).round()); - } else { - final targetHeight = config.quality - .calculateHeight(srcWidth.toInt(), srcHeight.toInt()); - final targetWidth = config.quality - .calculateWidth(srcWidth.toInt(), srcHeight.toInt()); - finalImage = await picture.toImage(targetWidth, targetHeight); - } + final targetHeight = + config.quality.calculateHeight(srcWidth.toInt(), srcHeight.toInt()); + final targetWidth = + config.quality.calculateWidth(srcWidth.toInt(), srcHeight.toInt()); + finalImage = await picture.toImage(targetWidth, targetHeight); try { await callback(finalImage); } finally { diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index e63366c3f7..33b7cbf289 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -23,6 +23,7 @@ import 'native/native_scope_observer.dart'; import 'native/sentry_native_binding.dart'; import 'profiling.dart'; import 'renderer/renderer.dart'; +import 'sentry_redaction_options.dart'; import 'span_frame_metrics_collector.dart'; import 'version.dart'; import 'view_hierarchy/view_hierarchy_integration.dart'; @@ -149,6 +150,8 @@ mixin SentryFlutter { SentryFlutterOptions options, bool isOnErrorSupported, ) { + _setRedactionOptions(options); + final integrations = []; final platformChecker = options.platformChecker; @@ -243,6 +246,19 @@ mixin SentryFlutter { options.sdk = sdk; } + static void _setRedactionOptions(SentryFlutterOptions options) { + if (options.experimental.sentryRedactingOptions != null) { + return; + } else if (options.screenshot.attachScreenshot == true && + !options.experimental.replay.isEnabled) { + options.experimental.sentryRedactingOptions = SentryRedactingOptions() + ..maskAllText = false + ..maskAllImages = false; + } else { + options.experimental.sentryRedactingOptions = SentryRedactingOptions(); + } + } + /// Reports the time it took for the screen to be fully displayed. /// This requires the [SentryFlutterOptions.enableTimeToFullDisplayTracing] option to be set to `true`. static Future reportFullyDisplayed() async { diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index 073668e00b..e9c9f034ce 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:file/file.dart'; import 'package:file/local.dart'; import 'package:flutter/services.dart'; @@ -11,9 +9,9 @@ import 'binding_wrapper.dart'; import 'navigation/time_to_display_tracker.dart'; import 'renderer/renderer.dart'; import 'screenshot/sentry_screenshot_quality.dart'; -import 'event_processor/screenshot_event_processor.dart'; import 'screenshot/sentry_screenshot_widget.dart'; import 'sentry_flutter.dart'; +import 'sentry_redaction_options.dart'; import 'sentry_replay_options.dart'; import 'sentry_screenshot_options.dart'; import 'user_interaction/sentry_user_interaction_widget.dart'; @@ -188,18 +186,32 @@ class SentryFlutterOptions extends SentryOptions { /// Example: /// runApp(SentryScreenshotWidget(child: App())); /// The [SentryScreenshotWidget] has to be the root widget of the app. - bool attachScreenshot = false; + @Deprecated('Use `screenshot.attachScreenshot` instead') + bool get attachScreenshot => screenshot.attachScreenshot; + set attachScreenshot(bool value) => screenshot.attachScreenshot = value; /// The quality of the attached screenshot - SentryScreenshotQuality screenshotQuality = SentryScreenshotQuality.high; + @Deprecated('Use `screenshot.screenshotQuality` instead') + SentryScreenshotQuality get screenshotQuality => screenshot.screenshotQuality; + set screenshotQuality(SentryScreenshotQuality value) => + screenshot.screenshotQuality = value; /// Only attach a screenshot when the app is resumed. - bool attachScreenshotOnlyWhenResumed = false; + @Deprecated('Use `screenshot.attachScreenshotOnlyWhenResumed` instead') + bool get attachScreenshotOnlyWhenResumed => + screenshot.attachScreenshotOnlyWhenResumed; + set attachScreenshotOnlyWhenResumed(bool value) => + screenshot.attachScreenshotOnlyWhenResumed = value; /// Sets a callback which is executed before capturing screenshots. Only /// relevant if `attachScreenshot` is set to true. When false is returned /// from the function, no screenshot will be attached. - BeforeScreenshotCallback? beforeScreenshot; + @Deprecated('Use `screenshot.beforeScreenshot` instead') + BeforeScreenshotCallback? get beforeScreenshot => screenshot.beforeScreenshot; + set beforeScreenshot(BeforeScreenshotCallback? value) => + screenshot.beforeScreenshot = value; + + final screenshot = SentryScreenshotOptions(); /// Enable or disable automatic breadcrumbs for User interactions Using [Listener] /// @@ -381,12 +393,5 @@ class SentryFlutterOptions extends SentryOptions { class _SentryFlutterExperimentalOptions { /// Replay recording configuration. final replay = SentryReplayOptions(); - final screenshot = SentryScreenshotOptions(); + SentryRedactingOptions? sentryRedactingOptions; } - -/// Callback being executed in [ScreenshotEventProcessor], deciding if a -/// screenshot should be recorded and attached. -typedef BeforeScreenshotCallback = FutureOr Function( - SentryEvent event, { - Hint? hint, -}); diff --git a/flutter/lib/src/sentry_redaction_options.dart b/flutter/lib/src/sentry_redaction_options.dart new file mode 100644 index 0000000000..0438cf3d85 --- /dev/null +++ b/flutter/lib/src/sentry_redaction_options.dart @@ -0,0 +1,111 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart'; + +import '../sentry_flutter.dart'; +import 'screenshot/masking_config.dart'; +import 'screenshot/widget_filter.dart'; + +/// Configuration of the experimental screenshot feature. +class SentryRedactingOptions { + /// 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. + @experimental + 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]). + @experimental + var maskAllImages = true; + + /// Redact asset images coming from the root asset bundle. + @experimental + 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. + @experimental + 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. + @experimental + 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. + @experimental + 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/lib/src/sentry_replay_options.dart b/flutter/lib/src/sentry_replay_options.dart index 4cf6d664ca..2631f78554 100644 --- a/flutter/lib/src/sentry_replay_options.dart +++ b/flutter/lib/src/sentry_replay_options.dart @@ -1,10 +1,8 @@ import 'package:meta/meta.dart'; -import 'sentry_screenshot_options.dart'; - /// Configuration of the experimental replay feature. @experimental -class SentryReplayOptions extends SentryScreenshotOptions { +class SentryReplayOptions { double? _sessionSampleRate; /// A percentage of sessions in which a replay will be created. @@ -27,13 +25,6 @@ class SentryReplayOptions extends SentryScreenshotOptions { _onErrorSampleRate = value; } - @Deprecated('Use maskAllText instead') - bool get redactAllText => maskAllText; - set redactAllText(bool value) => maskAllText = value; - - @Deprecated('Use maskAllImages instead') - bool get redactAllImages => maskAllImages; - set redactAllImages(bool value) => maskAllImages = value; @internal bool get isEnabled => diff --git a/flutter/lib/src/sentry_screenshot_options.dart b/flutter/lib/src/sentry_screenshot_options.dart index c42860be06..30eaf7122b 100644 --- a/flutter/lib/src/sentry_screenshot_options.dart +++ b/flutter/lib/src/sentry_screenshot_options.dart @@ -1,107 +1,33 @@ -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:meta/meta.dart'; +import 'dart:async'; -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)); +import '../sentry_flutter.dart'; - // 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)); - } +/// Configuration of the screenshot feature. +class SentryScreenshotOptions { + /// Automatically attaches a screenshot when capturing an error or exception. + /// + /// Requires adding the [SentryScreenshotWidget] to the widget tree. + /// Example: + /// runApp(SentryScreenshotWidget(child: App())); + /// The [SentryScreenshotWidget] has to be the root widget of the app. + bool attachScreenshot = false; + + /// Sets a callback which is executed before capturing screenshots. Only + /// relevant if `attachScreenshot` is set to true. When false is returned + /// from the function, no screenshot will be attached. + BeforeScreenshotCallback? beforeScreenshot; + + /// Only attach a screenshot when the app is resumed. + bool attachScreenshotOnlyWhenResumed = false; + + /// The quality of the attached screenshot + SentryScreenshotQuality screenshotQuality = SentryScreenshotQuality.high; } -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; -} +/// Callback being executed in [ScreenshotEventProcessor], deciding if a +/// screenshot should be recorded and attached. +typedef BeforeScreenshotCallback = FutureOr Function( + SentryEvent event, { + Hint? hint, +}); diff --git a/flutter/test/replay/masking_config_test.dart b/flutter/test/replay/masking_config_test.dart index 4323521541..1f1a02120e 100644 --- a/flutter/test/replay/masking_config_test.dart +++ b/flutter/test/replay/masking_config_test.dart @@ -2,6 +2,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/screenshot/masking_config.dart'; +import 'package:sentry_flutter/src/sentry_redaction_options.dart'; import 'test_widget.dart'; @@ -114,7 +115,7 @@ void main() async { }); group('$SentryReplayOptions.buildMaskingConfig()', () { - List rulesAsStrings(SentryReplayOptions options) { + List rulesAsStrings(SentryRedactingOptions options) { final config = options.buildMaskingConfig(); return config.rules .map((rule) => rule.toString()) @@ -131,7 +132,7 @@ void main() async { } test('defaults', () { - final sut = SentryReplayOptions(); + final sut = SentryRedactingOptions(); expect(rulesAsStrings(sut), [ ...alwaysEnabledRules, '$SentryMaskingCustomRule<$Image>(Closure: (Element, Widget) => SentryMaskingDecision)', @@ -141,7 +142,7 @@ void main() async { }); test('maskAllImages=true & maskAssetImages=true', () { - final sut = SentryReplayOptions() + final sut = SentryRedactingOptions() ..maskAllText = false ..maskAllImages = true ..maskAssetImages = true; @@ -152,7 +153,7 @@ void main() async { }); test('maskAllImages=true & maskAssetImages=false', () { - final sut = SentryReplayOptions() + final sut = SentryRedactingOptions() ..maskAllText = false ..maskAllImages = true ..maskAssetImages = false; @@ -163,7 +164,7 @@ void main() async { }); test('maskAllText=true', () { - final sut = SentryReplayOptions() + final sut = SentryRedactingOptions() ..maskAllText = true ..maskAllImages = false ..maskAssetImages = false; @@ -175,7 +176,7 @@ void main() async { }); test('maskAllText=false', () { - final sut = SentryReplayOptions() + final sut = SentryRedactingOptions() ..maskAllText = false ..maskAllImages = false ..maskAssetImages = false; @@ -190,19 +191,19 @@ void main() async { '$SentryMaskingConstantRule<$EditableText>(mask)' ]; test('mask() takes precedence', () { - final sut = SentryReplayOptions(); + final sut = SentryRedactingOptions(); sut.mask(); expect(rulesAsStrings(sut), ['$SentryMaskingConstantRule<$Image>(mask)', ...defaultRules]); }); test('unmask() takes precedence', () { - final sut = SentryReplayOptions(); + final sut = SentryRedactingOptions(); sut.unmask(); expect(rulesAsStrings(sut), ['$SentryMaskingConstantRule<$Image>(unmask)', ...defaultRules]); }); test('are ordered in the call order', () { - var sut = SentryReplayOptions(); + var sut = SentryRedactingOptions(); sut.mask(); sut.unmask(); expect(rulesAsStrings(sut), [ @@ -210,7 +211,7 @@ void main() async { '$SentryMaskingConstantRule<$Image>(unmask)', ...defaultRules ]); - sut = SentryReplayOptions(); + sut = SentryRedactingOptions(); sut.unmask(); sut.mask(); expect(rulesAsStrings(sut), [ @@ -218,7 +219,7 @@ void main() async { '$SentryMaskingConstantRule<$Image>(mask)', ...defaultRules ]); - sut = SentryReplayOptions(); + sut = SentryRedactingOptions(); sut.unmask(); sut.maskCallback( (Element element, Image widget) => SentryMaskingDecision.mask); @@ -231,7 +232,7 @@ void main() async { ]); }); test('maskCallback() takes precedence', () { - final sut = SentryReplayOptions(); + final sut = SentryRedactingOptions(); sut.maskCallback( (Element element, Image widget) => SentryMaskingDecision.mask); expect(rulesAsStrings(sut), [ @@ -240,7 +241,7 @@ void main() async { ]); }); test('User cannot add $SentryMask and $SentryUnmask rules', () { - final sut = SentryReplayOptions(); + final sut = SentryRedactingOptions(); expect(sut.mask, throwsA(isA())); expect(sut.mask, throwsA(isA())); expect(sut.unmask, throwsA(isA())); diff --git a/flutter/test/replay/scheduled_recorder_test.dart b/flutter/test/replay/scheduled_recorder_test.dart index 59cf991fc6..b6cb228f57 100644 --- a/flutter/test/replay/scheduled_recorder_test.dart +++ b/flutter/test/replay/scheduled_recorder_test.dart @@ -16,6 +16,7 @@ void main() async { TestWidgetsFlutterBinding.ensureInitialized(); testWidgets('captures images', (tester) async { + await tester.binding.setSurfaceSize(Size(1000, 750)); final fixture = await _Fixture.create(tester); expect(fixture.capturedImages, isEmpty); await fixture.nextFrame(); diff --git a/flutter/test/replay/widget_filter_test.dart b/flutter/test/replay/widget_filter_test.dart index 3d72c5a3e2..56ca79d63b 100644 --- a/flutter/test/replay/widget_filter_test.dart +++ b/flutter/test/replay/widget_filter_test.dart @@ -3,6 +3,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/screenshot/widget_filter.dart'; +import 'package:sentry_flutter/src/sentry_redaction_options.dart'; import 'test_widget.dart'; @@ -16,9 +17,9 @@ void main() async { final otherBundle = TestAssetBundle(); final createSut = ({bool redactImages = false, bool redactText = false}) { - final replayOptions = SentryReplayOptions(); - replayOptions.redactAllImages = redactImages; - replayOptions.redactAllText = redactText; + final replayOptions = SentryRedactingOptions(); + replayOptions.maskAllImages = redactImages; + replayOptions.maskAllText = redactText; return WidgetFilter(replayOptions.buildMaskingConfig(), (level, message, {exception, logger, stackTrace}) {}); }; From 0969b7b9f65e0bcfab250da21f33933cd07a53ba Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Thu, 24 Oct 2024 17:18:29 +0200 Subject: [PATCH 09/51] fix comments --- flutter/lib/src/sentry_redaction_options.dart | 2 +- flutter/lib/src/sentry_screenshot_options.dart | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/flutter/lib/src/sentry_redaction_options.dart b/flutter/lib/src/sentry_redaction_options.dart index 0438cf3d85..cd33a0a144 100644 --- a/flutter/lib/src/sentry_redaction_options.dart +++ b/flutter/lib/src/sentry_redaction_options.dart @@ -6,7 +6,7 @@ import '../sentry_flutter.dart'; import 'screenshot/masking_config.dart'; import 'screenshot/widget_filter.dart'; -/// Configuration of the experimental screenshot feature. +/// Configuration of the experimental redaction feature. class SentryRedactingOptions { /// Mask all text content. Draws a rectangle of text bounds with text color /// on top. Currently, only [Text] and [EditableText] Widgets are redacted. diff --git a/flutter/lib/src/sentry_screenshot_options.dart b/flutter/lib/src/sentry_screenshot_options.dart index 30eaf7122b..66aae00b11 100644 --- a/flutter/lib/src/sentry_screenshot_options.dart +++ b/flutter/lib/src/sentry_screenshot_options.dart @@ -1,6 +1,5 @@ import 'dart:async'; - import '../sentry_flutter.dart'; /// Configuration of the screenshot feature. From c2daf00485c7d8e934e952c2695bad4a7d06b297 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Fri, 25 Oct 2024 11:25:10 +0200 Subject: [PATCH 10/51] export redaction options and remove unused dependencies --- flutter/lib/sentry_flutter.dart | 1 + flutter/lib/src/screenshot/recorder.dart | 1 - flutter/lib/src/sentry_flutter.dart | 5 ++--- flutter/lib/src/sentry_replay_options.dart | 1 - flutter/test/replay/masking_config_test.dart | 1 - flutter/test/replay/widget_filter_test.dart | 1 - 6 files changed, 3 insertions(+), 7 deletions(-) diff --git a/flutter/lib/sentry_flutter.dart b/flutter/lib/sentry_flutter.dart index e902455ce6..20080c0c91 100644 --- a/flutter/lib/sentry_flutter.dart +++ b/flutter/lib/sentry_flutter.dart @@ -9,6 +9,7 @@ export 'src/navigation/sentry_navigator_observer.dart'; export 'src/sentry_flutter.dart'; export 'src/sentry_flutter_options.dart'; export 'src/sentry_replay_options.dart'; +export 'src/sentry_redaction_options.dart'; export 'src/flutter_sentry_attachment.dart'; export 'src/sentry_asset_bundle.dart' show SentryAssetBundle; export 'src/integrations/on_error_integration.dart'; diff --git a/flutter/lib/src/screenshot/recorder.dart b/flutter/lib/src/screenshot/recorder.dart index e8eb3f0038..5a3fdbac7e 100644 --- a/flutter/lib/src/screenshot/recorder.dart +++ b/flutter/lib/src/screenshot/recorder.dart @@ -3,7 +3,6 @@ import 'dart:ui'; import 'package:flutter/rendering.dart'; import 'package:meta/meta.dart'; -import '../sentry_redaction_options.dart'; import '../../sentry_flutter.dart'; import 'masking_config.dart'; diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 33b7cbf289..4f796310a6 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -23,7 +23,6 @@ import 'native/native_scope_observer.dart'; import 'native/sentry_native_binding.dart'; import 'profiling.dart'; import 'renderer/renderer.dart'; -import 'sentry_redaction_options.dart'; import 'span_frame_metrics_collector.dart'; import 'version.dart'; import 'view_hierarchy/view_hierarchy_integration.dart'; @@ -100,6 +99,8 @@ mixin SentryFlutter { // ignore: invalid_use_of_internal_member runZonedGuardedOnError: runZonedGuardedOnError, ); + // TODO: Remove when we synced SS and SR configurations and have a single default configuration + _setRedactionOptions(options); if (_native != null) { // ignore: invalid_use_of_internal_member @@ -150,8 +151,6 @@ mixin SentryFlutter { SentryFlutterOptions options, bool isOnErrorSupported, ) { - _setRedactionOptions(options); - final integrations = []; final platformChecker = options.platformChecker; diff --git a/flutter/lib/src/sentry_replay_options.dart b/flutter/lib/src/sentry_replay_options.dart index 2631f78554..72d758c46d 100644 --- a/flutter/lib/src/sentry_replay_options.dart +++ b/flutter/lib/src/sentry_replay_options.dart @@ -25,7 +25,6 @@ class SentryReplayOptions { _onErrorSampleRate = value; } - @internal bool get isEnabled => ((sessionSampleRate ?? 0) > 0) || ((onErrorSampleRate ?? 0) > 0); diff --git a/flutter/test/replay/masking_config_test.dart b/flutter/test/replay/masking_config_test.dart index 1f1a02120e..ed840cadfe 100644 --- a/flutter/test/replay/masking_config_test.dart +++ b/flutter/test/replay/masking_config_test.dart @@ -2,7 +2,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/screenshot/masking_config.dart'; -import 'package:sentry_flutter/src/sentry_redaction_options.dart'; import 'test_widget.dart'; diff --git a/flutter/test/replay/widget_filter_test.dart b/flutter/test/replay/widget_filter_test.dart index 56ca79d63b..00a1683a4a 100644 --- a/flutter/test/replay/widget_filter_test.dart +++ b/flutter/test/replay/widget_filter_test.dart @@ -3,7 +3,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/screenshot/widget_filter.dart'; -import 'package:sentry_flutter/src/sentry_redaction_options.dart'; import 'test_widget.dart'; From 3606202d1de69ea75ce13f3143918ef57928dd80 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Fri, 25 Oct 2024 11:33:53 +0200 Subject: [PATCH 11/51] renaming to SentryPrivacyOptions --- flutter/lib/sentry_flutter.dart | 2 +- flutter/lib/src/screenshot/recorder.dart | 3 +-- flutter/lib/src/sentry_flutter.dart | 6 ++--- flutter/lib/src/sentry_flutter_options.dart | 6 +++-- ...tions.dart => sentry_privacy_options.dart} | 4 +-- flutter/test/replay/masking_config_test.dart | 26 +++++++++---------- flutter/test/replay/widget_filter_test.dart | 2 +- 7 files changed, 25 insertions(+), 24 deletions(-) rename flutter/lib/src/{sentry_redaction_options.dart => sentry_privacy_options.dart} (97%) diff --git a/flutter/lib/sentry_flutter.dart b/flutter/lib/sentry_flutter.dart index 20080c0c91..8c7ca8785a 100644 --- a/flutter/lib/sentry_flutter.dart +++ b/flutter/lib/sentry_flutter.dart @@ -9,7 +9,7 @@ export 'src/navigation/sentry_navigator_observer.dart'; export 'src/sentry_flutter.dart'; export 'src/sentry_flutter_options.dart'; export 'src/sentry_replay_options.dart'; -export 'src/sentry_redaction_options.dart'; +export 'src/sentry_privacy_options.dart'; export 'src/flutter_sentry_attachment.dart'; export 'src/sentry_asset_bundle.dart' show SentryAssetBundle; export 'src/integrations/on_error_integration.dart'; diff --git a/flutter/lib/src/screenshot/recorder.dart b/flutter/lib/src/screenshot/recorder.dart index 5a3fdbac7e..93e8313b1f 100644 --- a/flutter/lib/src/screenshot/recorder.dart +++ b/flutter/lib/src/screenshot/recorder.dart @@ -24,8 +24,7 @@ class ScreenshotRecorder { ScreenshotRecorder(this.config, this.options) { /// TODO: Rewrite when default redaction value are synced with SS & SR final SentryMaskingConfig maskingConfig = - (options.experimental.sentryRedactingOptions ?? - SentryRedactingOptions()) + (options.experimental.privacy ?? SentryPrivacyOptions()) .buildMaskingConfig(); if (maskingConfig.length > 0) { diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 4f796310a6..6f58245b0b 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -246,15 +246,15 @@ mixin SentryFlutter { } static void _setRedactionOptions(SentryFlutterOptions options) { - if (options.experimental.sentryRedactingOptions != null) { + if (options.experimental.privacy != null) { return; } else if (options.screenshot.attachScreenshot == true && !options.experimental.replay.isEnabled) { - options.experimental.sentryRedactingOptions = SentryRedactingOptions() + options.experimental.privacy = SentryPrivacyOptions() ..maskAllText = false ..maskAllImages = false; } else { - options.experimental.sentryRedactingOptions = SentryRedactingOptions(); + options.experimental.privacy = SentryPrivacyOptions(); } } diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index e9c9f034ce..4e18de821f 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:meta/meta.dart' as meta; import 'package:sentry/sentry.dart'; import 'package:flutter/widgets.dart'; +import 'sentry_privacy_options.dart'; import 'binding_wrapper.dart'; import 'navigation/time_to_display_tracker.dart'; @@ -11,7 +12,6 @@ import 'renderer/renderer.dart'; import 'screenshot/sentry_screenshot_quality.dart'; import 'screenshot/sentry_screenshot_widget.dart'; import 'sentry_flutter.dart'; -import 'sentry_redaction_options.dart'; import 'sentry_replay_options.dart'; import 'sentry_screenshot_options.dart'; import 'user_interaction/sentry_user_interaction_widget.dart'; @@ -393,5 +393,7 @@ class SentryFlutterOptions extends SentryOptions { class _SentryFlutterExperimentalOptions { /// Replay recording configuration. final replay = SentryReplayOptions(); - SentryRedactingOptions? sentryRedactingOptions; + + /// Privacy configuration for masking sensitive data in the Screenshot and Session Replay. + SentryPrivacyOptions? privacy; } diff --git a/flutter/lib/src/sentry_redaction_options.dart b/flutter/lib/src/sentry_privacy_options.dart similarity index 97% rename from flutter/lib/src/sentry_redaction_options.dart rename to flutter/lib/src/sentry_privacy_options.dart index cd33a0a144..74fd99468f 100644 --- a/flutter/lib/src/sentry_redaction_options.dart +++ b/flutter/lib/src/sentry_privacy_options.dart @@ -6,8 +6,8 @@ import '../sentry_flutter.dart'; import 'screenshot/masking_config.dart'; import 'screenshot/widget_filter.dart'; -/// Configuration of the experimental redaction feature. -class SentryRedactingOptions { +/// Configuration of the experimental privacy feature. +class SentryPrivacyOptions { /// 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. diff --git a/flutter/test/replay/masking_config_test.dart b/flutter/test/replay/masking_config_test.dart index ed840cadfe..90333c5fe0 100644 --- a/flutter/test/replay/masking_config_test.dart +++ b/flutter/test/replay/masking_config_test.dart @@ -114,7 +114,7 @@ void main() async { }); group('$SentryReplayOptions.buildMaskingConfig()', () { - List rulesAsStrings(SentryRedactingOptions options) { + List rulesAsStrings(SentryPrivacyOptions options) { final config = options.buildMaskingConfig(); return config.rules .map((rule) => rule.toString()) @@ -131,7 +131,7 @@ void main() async { } test('defaults', () { - final sut = SentryRedactingOptions(); + final sut = SentryPrivacyOptions(); expect(rulesAsStrings(sut), [ ...alwaysEnabledRules, '$SentryMaskingCustomRule<$Image>(Closure: (Element, Widget) => SentryMaskingDecision)', @@ -141,7 +141,7 @@ void main() async { }); test('maskAllImages=true & maskAssetImages=true', () { - final sut = SentryRedactingOptions() + final sut = SentryPrivacyOptions() ..maskAllText = false ..maskAllImages = true ..maskAssetImages = true; @@ -152,7 +152,7 @@ void main() async { }); test('maskAllImages=true & maskAssetImages=false', () { - final sut = SentryRedactingOptions() + final sut = SentryPrivacyOptions() ..maskAllText = false ..maskAllImages = true ..maskAssetImages = false; @@ -163,7 +163,7 @@ void main() async { }); test('maskAllText=true', () { - final sut = SentryRedactingOptions() + final sut = SentryPrivacyOptions() ..maskAllText = true ..maskAllImages = false ..maskAssetImages = false; @@ -175,7 +175,7 @@ void main() async { }); test('maskAllText=false', () { - final sut = SentryRedactingOptions() + final sut = SentryPrivacyOptions() ..maskAllText = false ..maskAllImages = false ..maskAssetImages = false; @@ -190,19 +190,19 @@ void main() async { '$SentryMaskingConstantRule<$EditableText>(mask)' ]; test('mask() takes precedence', () { - final sut = SentryRedactingOptions(); + final sut = SentryPrivacyOptions(); sut.mask(); expect(rulesAsStrings(sut), ['$SentryMaskingConstantRule<$Image>(mask)', ...defaultRules]); }); test('unmask() takes precedence', () { - final sut = SentryRedactingOptions(); + final sut = SentryPrivacyOptions(); sut.unmask(); expect(rulesAsStrings(sut), ['$SentryMaskingConstantRule<$Image>(unmask)', ...defaultRules]); }); test('are ordered in the call order', () { - var sut = SentryRedactingOptions(); + var sut = SentryPrivacyOptions(); sut.mask(); sut.unmask(); expect(rulesAsStrings(sut), [ @@ -210,7 +210,7 @@ void main() async { '$SentryMaskingConstantRule<$Image>(unmask)', ...defaultRules ]); - sut = SentryRedactingOptions(); + sut = SentryPrivacyOptions(); sut.unmask(); sut.mask(); expect(rulesAsStrings(sut), [ @@ -218,7 +218,7 @@ void main() async { '$SentryMaskingConstantRule<$Image>(mask)', ...defaultRules ]); - sut = SentryRedactingOptions(); + sut = SentryPrivacyOptions(); sut.unmask(); sut.maskCallback( (Element element, Image widget) => SentryMaskingDecision.mask); @@ -231,7 +231,7 @@ void main() async { ]); }); test('maskCallback() takes precedence', () { - final sut = SentryRedactingOptions(); + final sut = SentryPrivacyOptions(); sut.maskCallback( (Element element, Image widget) => SentryMaskingDecision.mask); expect(rulesAsStrings(sut), [ @@ -240,7 +240,7 @@ void main() async { ]); }); test('User cannot add $SentryMask and $SentryUnmask rules', () { - final sut = SentryRedactingOptions(); + final sut = SentryPrivacyOptions(); expect(sut.mask, throwsA(isA())); expect(sut.mask, throwsA(isA())); expect(sut.unmask, throwsA(isA())); diff --git a/flutter/test/replay/widget_filter_test.dart b/flutter/test/replay/widget_filter_test.dart index 00a1683a4a..a5bab11671 100644 --- a/flutter/test/replay/widget_filter_test.dart +++ b/flutter/test/replay/widget_filter_test.dart @@ -16,7 +16,7 @@ void main() async { final otherBundle = TestAssetBundle(); final createSut = ({bool redactImages = false, bool redactText = false}) { - final replayOptions = SentryRedactingOptions(); + final replayOptions = SentryPrivacyOptions(); replayOptions.maskAllImages = redactImages; replayOptions.maskAllText = redactText; return WidgetFilter(replayOptions.buildMaskingConfig(), From 35ebb0bd63ef512afe408baf38f766a0cc800967 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Tue, 29 Oct 2024 09:57:00 +0100 Subject: [PATCH 12/51] add explanation for setRedactionOptions --- flutter/lib/src/sentry_flutter.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 6f58245b0b..79b0856da2 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -245,6 +245,14 @@ mixin SentryFlutter { options.sdk = sdk; } + /// Screen redaction was previously introduced with the SessionReplay feature. + /// Screen redaction is enabled by default for SessionReplay. + /// As we also to use this feature for Screenshot, which previously was not + /// capable of redacting the screenshot, we need to disable redaction for Screenshot by default + /// so we don`t break the existing behavior. + /// As we have only one central place to configure the redaction, + /// we need to set the redaction options to full fill the above default settings. + /// The plan is to unify this behaviour with the next major release. static void _setRedactionOptions(SentryFlutterOptions options) { if (options.experimental.privacy != null) { return; From 57a0823d3c7d252a1085cece3c53afa03d981c34 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Tue, 29 Oct 2024 10:30:11 +0100 Subject: [PATCH 13/51] fix deprecation warnings --- .../src/event_processor/screenshot_event_processor.dart | 7 ++++--- flutter/lib/src/integrations/screenshot_integration.dart | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index 8b4140f75d..3139b1f8bf 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -30,7 +30,7 @@ class ScreenshotEventProcessor implements EventProcessor { _hasSentryScreenshotWidget) { return event; } - final beforeScreenshot = _options.beforeScreenshot; + final beforeScreenshot = _options.screenshot.beforeScreenshot; if (beforeScreenshot != null) { try { final result = beforeScreenshot(event, hint: hint); @@ -67,7 +67,7 @@ class ScreenshotEventProcessor implements EventProcessor { return event; } - if (_options.attachScreenshotOnlyWhenResumed && + if (_options.screenshot.attachScreenshotOnlyWhenResumed && widget.WidgetsBinding.instance.lifecycleState != AppLifecycleState.resumed) { _options.logger(SentryLevel.debug, @@ -77,7 +77,8 @@ class ScreenshotEventProcessor implements EventProcessor { // ignore: deprecated_member_use var recorder = ScreenshotRecorder( - ScreenshotRecorderConfig(quality: _options.screenshotQuality), + ScreenshotRecorderConfig( + quality: _options.screenshot.screenshotQuality), _options); Uint8List? _screenshotData; diff --git a/flutter/lib/src/integrations/screenshot_integration.dart b/flutter/lib/src/integrations/screenshot_integration.dart index 10cf60228a..bafe4b696e 100644 --- a/flutter/lib/src/integrations/screenshot_integration.dart +++ b/flutter/lib/src/integrations/screenshot_integration.dart @@ -3,14 +3,14 @@ import '../event_processor/screenshot_event_processor.dart'; import '../sentry_flutter_options.dart'; /// Adds [ScreenshotEventProcessor] to options event processors if -/// [SentryFlutterOptions.attachScreenshot] is true +/// [SentryFlutterOptions.screenshot.attachScreenshot] is true class ScreenshotIntegration implements Integration { SentryFlutterOptions? _options; ScreenshotEventProcessor? _screenshotEventProcessor; @override void call(Hub hub, SentryFlutterOptions options) { - if (options.attachScreenshot) { + if (options.screenshot.attachScreenshot) { _options = options; final screenshotEventProcessor = ScreenshotEventProcessor(options); options.addEventProcessor(screenshotEventProcessor); From 5e45d77619f957357135264d44efbecdd99df4a2 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Mon, 11 Nov 2024 11:52:39 +0100 Subject: [PATCH 14/51] Update flutter/lib/src/sentry_flutter.dart Co-authored-by: Giancarlo Buenaflor --- flutter/lib/src/sentry_flutter.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 79b0856da2..65debf1197 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -253,7 +253,7 @@ mixin SentryFlutter { /// As we have only one central place to configure the redaction, /// we need to set the redaction options to full fill the above default settings. /// The plan is to unify this behaviour with the next major release. - static void _setRedactionOptions(SentryFlutterOptions options) { + static void _setMaskingOptions(SentryFlutterOptions options) { if (options.experimental.privacy != null) { return; } else if (options.screenshot.attachScreenshot == true && From 487fd2730f12dc8df6b746d13e38e4e457c319cc Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Mon, 11 Nov 2024 11:53:16 +0100 Subject: [PATCH 15/51] Update flutter/lib/src/sentry_flutter.dart Co-authored-by: Giancarlo Buenaflor --- flutter/lib/src/sentry_flutter.dart | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 65debf1197..25aa65370b 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -245,14 +245,12 @@ mixin SentryFlutter { options.sdk = sdk; } - /// Screen redaction was previously introduced with the SessionReplay feature. - /// Screen redaction is enabled by default for SessionReplay. - /// As we also to use this feature for Screenshot, which previously was not - /// capable of redacting the screenshot, we need to disable redaction for Screenshot by default - /// so we don`t break the existing behavior. - /// As we have only one central place to configure the redaction, - /// we need to set the redaction options to full fill the above default settings. - /// The plan is to unify this behaviour with the next major release. + /// Masking behaviour + /// - If only Screenshot is enabled: masking is disabled by default. + /// - If both Screenshot and Session Replay are enabled: masking is enabled for both by default. + /// - If the user explicitly sets masking to false: masking is disabled for both features. + /// We don't want to break the existing screenshot integration which is not masked by default. + /// The plan is to unify screenshot and replay masking with the next major release. static void _setMaskingOptions(SentryFlutterOptions options) { if (options.experimental.privacy != null) { return; From 4e062e83c9b7927d5f44645bef2955fecce5561c Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Mon, 11 Nov 2024 11:53:25 +0100 Subject: [PATCH 16/51] Update flutter/lib/src/sentry_privacy_options.dart Co-authored-by: Giancarlo Buenaflor --- flutter/lib/src/sentry_privacy_options.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/src/sentry_privacy_options.dart b/flutter/lib/src/sentry_privacy_options.dart index 74fd99468f..cdc5c8aadd 100644 --- a/flutter/lib/src/sentry_privacy_options.dart +++ b/flutter/lib/src/sentry_privacy_options.dart @@ -9,7 +9,7 @@ import 'screenshot/widget_filter.dart'; /// Configuration of the experimental privacy feature. class SentryPrivacyOptions { /// Mask all text content. Draws a rectangle of text bounds with text color - /// on top. Currently, only [Text] and [EditableText] Widgets are redacted. + /// on top. Currently, only [Text] and [EditableText] Widgets are masked. /// Default is enabled. @experimental var maskAllText = true; From 21851d95fb2a7454b4e04364d896997e441e6362 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos <6349682+vaind@users.noreply.github.com> Date: Mon, 11 Nov 2024 13:25:00 +0100 Subject: [PATCH 17/51] feat: non-nullable privacy setting (#2382) * refactor: make privacy setting non-nullable and automatically set screenshot redaction when user accesses it. * Apply suggestions from code review * formatting --- .../screenshot_event_processor.dart | 3 +- flutter/lib/src/screenshot/recorder.dart | 21 ++++++++------ flutter/lib/src/sentry_flutter.dart | 21 -------------- flutter/lib/src/sentry_flutter_options.dart | 28 ++++++++++++++++++- flutter/test/replay/recorder_test.dart | 27 ++++++++++++++++++ 5 files changed, 69 insertions(+), 31 deletions(-) diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index 3139b1f8bf..884a4a86d5 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -79,7 +79,8 @@ class ScreenshotEventProcessor implements EventProcessor { var recorder = ScreenshotRecorder( ScreenshotRecorderConfig( quality: _options.screenshot.screenshotQuality), - _options); + _options, + isReplayRecorder: false); Uint8List? _screenshotData; diff --git a/flutter/lib/src/screenshot/recorder.dart b/flutter/lib/src/screenshot/recorder.dart index 93e8313b1f..62029d5358 100644 --- a/flutter/lib/src/screenshot/recorder.dart +++ b/flutter/lib/src/screenshot/recorder.dart @@ -5,7 +5,6 @@ import 'package:flutter/rendering.dart'; import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; -import 'masking_config.dart'; import 'recorder_config.dart'; import 'widget_filter.dart'; @@ -21,13 +20,19 @@ class ScreenshotRecorder { WidgetFilter? _widgetFilter; bool warningLogged = false; - ScreenshotRecorder(this.config, this.options) { - /// TODO: Rewrite when default redaction value are synced with SS & SR - final SentryMaskingConfig maskingConfig = - (options.experimental.privacy ?? SentryPrivacyOptions()) - .buildMaskingConfig(); - - if (maskingConfig.length > 0) { + // TODO: remove in the next major release, see recorder_test.dart. + @visibleForTesting + bool get hasWidgetFilter => _widgetFilter != null; + + // TODO: remove [isReplayRecorder] parameter in the next major release, see _SentryFlutterExperimentalOptions. + ScreenshotRecorder(this.config, this.options, + {bool isReplayRecorder = true}) { + // see `options.experimental.privacy` docs for details + final privacyOptions = isReplayRecorder + ? options.experimental.privacyForReplay + : options.experimental.privacyForScreenshots; + final maskingConfig = privacyOptions?.buildMaskingConfig(); + if (maskingConfig != null && maskingConfig.length > 0) { _widgetFilter = WidgetFilter(maskingConfig, options.logger); } } diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 25aa65370b..e63366c3f7 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -99,8 +99,6 @@ mixin SentryFlutter { // ignore: invalid_use_of_internal_member runZonedGuardedOnError: runZonedGuardedOnError, ); - // TODO: Remove when we synced SS and SR configurations and have a single default configuration - _setRedactionOptions(options); if (_native != null) { // ignore: invalid_use_of_internal_member @@ -245,25 +243,6 @@ mixin SentryFlutter { options.sdk = sdk; } - /// Masking behaviour - /// - If only Screenshot is enabled: masking is disabled by default. - /// - If both Screenshot and Session Replay are enabled: masking is enabled for both by default. - /// - If the user explicitly sets masking to false: masking is disabled for both features. - /// We don't want to break the existing screenshot integration which is not masked by default. - /// The plan is to unify screenshot and replay masking with the next major release. - static void _setMaskingOptions(SentryFlutterOptions options) { - if (options.experimental.privacy != null) { - return; - } else if (options.screenshot.attachScreenshot == true && - !options.experimental.replay.isEnabled) { - options.experimental.privacy = SentryPrivacyOptions() - ..maskAllText = false - ..maskAllImages = false; - } else { - options.experimental.privacy = SentryPrivacyOptions(); - } - } - /// Reports the time it took for the screen to be fully displayed. /// This requires the [SentryFlutterOptions.enableTimeToFullDisplayTracing] option to be set to `true`. static Future reportFullyDisplayed() async { diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index 4e18de821f..d404578921 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -395,5 +395,31 @@ class _SentryFlutterExperimentalOptions { final replay = SentryReplayOptions(); /// Privacy configuration for masking sensitive data in the Screenshot and Session Replay. - SentryPrivacyOptions? privacy; + /// Screen content masking redaction is: + /// - enabled by default for SessionReplay + /// - disabled by default for screenshots captured with events. + /// In order to redact screenshots captured with events, access or change + /// this property in your application: `options.experimental.privacy`. + /// Doing so will indicate that you want to configure privacy settings and + /// will enable screenshot redaction alongside the default replay redaction. + /// Note: this will change in a future SDK major release to enable screenshot + /// redaction by default for all captures. + SentryPrivacyOptions get privacy { + // If the user explicitly sets the privacy setting, we use that. + // Otherwise, we use the default settings, which is no redaction for screenshots + // and full redaction for session replay. + // This property must only by accessed by user code otherwise it defeats the purpose. + _privacy ??= SentryPrivacyOptions(); + return _privacy!; + } + + /// TODO: remove when default redaction value are synced with SS & SR in the next major release + SentryPrivacyOptions? _privacy; + + @meta.internal + SentryPrivacyOptions? get privacyForScreenshots => _privacy; + + @meta.internal + SentryPrivacyOptions get privacyForReplay => + _privacy ?? SentryPrivacyOptions(); } diff --git a/flutter/test/replay/recorder_test.dart b/flutter/test/replay/recorder_test.dart index b01fd2872c..d633d4a771 100644 --- a/flutter/test/replay/recorder_test.dart +++ b/flutter/test/replay/recorder_test.dart @@ -55,6 +55,33 @@ void main() async { await _Fixture.create(tester, quality: SentryScreenshotQuality.low); expect(fixture.capture(), completion('427x854')); }); + + // TODO: remove in the next major release, see _SentryFlutterExperimentalOptions. + group('Widget filter is used based on config or application', () { + test('Uses widget filter by default for Replay', () { + final sut = ScreenshotRecorder( + ScreenshotRecorderConfig(), + defaultTestOptions(), + ); + expect(sut.hasWidgetFilter, isTrue); + }); + + test('Does not use widget filter by default for Screenshots', () { + final sut = ScreenshotRecorder( + ScreenshotRecorderConfig(), defaultTestOptions(), + isReplayRecorder: false); + expect(sut.hasWidgetFilter, isFalse); + }); + + test( + 'Uses widget filter for Screenshots when privacy configured explicitly', + () { + final sut = ScreenshotRecorder(ScreenshotRecorderConfig(), + defaultTestOptions()..experimental.privacy.maskAllText = false, + isReplayRecorder: false); + expect(sut.hasWidgetFilter, isTrue); + }); + }); } class _Fixture { From 6768268915463f4893e81e148b7c4edf0bccc245 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Mon, 11 Nov 2024 14:19:33 +0100 Subject: [PATCH 18/51] keep recorder instance in memory --- .../screenshot_event_processor.dart | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index 946acbf79b..4edcac41d6 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:typed_data'; import 'dart:ui'; -import 'package:flutter/rendering.dart'; import 'package:meta/meta.dart'; import 'package:sentry/sentry.dart'; import '../screenshot/recorder.dart'; @@ -15,7 +14,15 @@ import 'package:flutter/widgets.dart' as widget; class ScreenshotEventProcessor implements EventProcessor { final SentryFlutterOptions _options; - ScreenshotEventProcessor(this._options); + late final ScreenshotRecorder _recorder; + + ScreenshotEventProcessor(this._options) { + _recorder = ScreenshotRecorder( + ScreenshotRecorderConfig(quality: _options.screenshot.screenshotQuality), + _options, + isReplayRecorder: false, + ); + } @override Future apply(SentryEvent event, Hint hint) async { @@ -89,16 +96,9 @@ class ScreenshotEventProcessor implements EventProcessor { @internal Future createScreenshot() async { - // ignore: deprecated_member_use - var recorder = ScreenshotRecorder( - ScreenshotRecorderConfig( - quality: _options.screenshot.screenshotQuality), - _options, - isReplayRecorder: false); - Uint8List? screenshotData; - await recorder.capture((Image image) async { + await _recorder.capture((Image image) async { screenshotData = await _convertImageToUint8List(image); }); From eee9c23141c99c744e341a12fca73d8192562a51 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Mon, 11 Nov 2024 14:23:37 +0100 Subject: [PATCH 19/51] Update flutter/lib/src/sentry_screenshot_options.dart Co-authored-by: Ivan Dlugos <6349682+vaind@users.noreply.github.com> --- flutter/lib/src/sentry_screenshot_options.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/src/sentry_screenshot_options.dart b/flutter/lib/src/sentry_screenshot_options.dart index 66aae00b11..0ebb57ebc7 100644 --- a/flutter/lib/src/sentry_screenshot_options.dart +++ b/flutter/lib/src/sentry_screenshot_options.dart @@ -9,7 +9,7 @@ class SentryScreenshotOptions { /// Requires adding the [SentryScreenshotWidget] to the widget tree. /// Example: /// runApp(SentryScreenshotWidget(child: App())); - /// The [SentryScreenshotWidget] has to be the root widget of the app. + /// The [SentryWidget] has to be the root widget of the app. bool attachScreenshot = false; /// Sets a callback which is executed before capturing screenshots. Only From 2481c04cfce39b14e9905c90d19e86d78e81084a Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Mon, 11 Nov 2024 14:43:29 +0100 Subject: [PATCH 20/51] update comment for sentry screenshot options --- flutter/lib/src/sentry_screenshot_options.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/src/sentry_screenshot_options.dart b/flutter/lib/src/sentry_screenshot_options.dart index 0ebb57ebc7..5068c770a4 100644 --- a/flutter/lib/src/sentry_screenshot_options.dart +++ b/flutter/lib/src/sentry_screenshot_options.dart @@ -6,9 +6,9 @@ import '../sentry_flutter.dart'; class SentryScreenshotOptions { /// Automatically attaches a screenshot when capturing an error or exception. /// - /// Requires adding the [SentryScreenshotWidget] to the widget tree. + /// Requires adding the [SentryWidget] to the widget tree. /// Example: - /// runApp(SentryScreenshotWidget(child: App())); + /// runApp(SentryWidget(child: App())); /// The [SentryWidget] has to be the root widget of the app. bool attachScreenshot = false; From 3a82edf2187eb2d23b4882ebe53d159b9e10c6a8 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Tue, 12 Nov 2024 10:09:24 +0100 Subject: [PATCH 21/51] fixed screenshot size mismatch --- .../flutter/SentryFlutterReplayRecorder.kt | 4 ++-- .../src/native/java/sentry_native_java.dart | 4 ++-- .../src/replay/scheduled_recorder_config.dart | 6 ++--- flutter/lib/src/screenshot/recorder.dart | 22 +++++++++++-------- .../lib/src/screenshot/recorder_config.dart | 20 ++++++++--------- .../screenshot/sentry_screenshot_quality.dart | 8 +++++-- flutter/test/replay/recorder_config_test.dart | 8 +++---- .../test/replay/scheduled_recorder_test.dart | 4 ++-- 8 files changed, 42 insertions(+), 34 deletions(-) diff --git a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt index ba285a12a0..d1746d5775 100644 --- a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt +++ b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt @@ -24,8 +24,8 @@ internal class SentryFlutterReplayRecorder( "ReplayRecorder.start", mapOf( "directory" to cacheDirPath, - "width" to recorderConfig.recordingWidth, - "height" to recorderConfig.recordingHeight, + "srcWidth" to recorderConfig.recordingWidth, + "srcHeight" to recorderConfig.recordingHeight, "frameRate" to recorderConfig.frameRate, "replayId" to integration.getReplayId().toString(), ), diff --git a/flutter/lib/src/native/java/sentry_native_java.dart b/flutter/lib/src/native/java/sentry_native_java.dart index 91c127de4f..32f2415e17 100644 --- a/flutter/lib/src/native/java/sentry_native_java.dart +++ b/flutter/lib/src/native/java/sentry_native_java.dart @@ -40,8 +40,8 @@ class SentryNativeJava extends SentryNativeChannel { _startRecorder( call.arguments['directory'] as String, ScheduledScreenshotRecorderConfig( - width: call.arguments['width'] as int, - height: call.arguments['height'] as int, + srcWidth: call.arguments['srcWidth'] as int, + srcHeight: call.arguments['srcHeight'] as int, frameRate: call.arguments['frameRate'] as int, ), ); diff --git a/flutter/lib/src/replay/scheduled_recorder_config.dart b/flutter/lib/src/replay/scheduled_recorder_config.dart index 67c5d672ef..f4c9b3cac9 100644 --- a/flutter/lib/src/replay/scheduled_recorder_config.dart +++ b/flutter/lib/src/replay/scheduled_recorder_config.dart @@ -3,9 +3,9 @@ import '../screenshot/recorder_config.dart'; class ScheduledScreenshotRecorderConfig extends ScreenshotRecorderConfig { final int frameRate; - const ScheduledScreenshotRecorderConfig({ - super.width, - super.height, + ScheduledScreenshotRecorderConfig({ + super.srcWidth, + super.srcHeight, required this.frameRate, }); } diff --git a/flutter/lib/src/screenshot/recorder.dart b/flutter/lib/src/screenshot/recorder.dart index 62029d5358..90b2817fa1 100644 --- a/flutter/lib/src/screenshot/recorder.dart +++ b/flutter/lib/src/screenshot/recorder.dart @@ -19,6 +19,8 @@ class ScreenshotRecorder { final SentryFlutterOptions options; WidgetFilter? _widgetFilter; bool warningLogged = false; + @protected + final bool isReplayRecorder; // TODO: remove in the next major release, see recorder_test.dart. @visibleForTesting @@ -26,7 +28,7 @@ class ScreenshotRecorder { // TODO: remove [isReplayRecorder] parameter in the next major release, see _SentryFlutterExperimentalOptions. ScreenshotRecorder(this.config, this.options, - {bool isReplayRecorder = true}) { + {this.isReplayRecorder = true}) { // see `options.experimental.privacy` docs for details final privacyOptions = isReplayRecorder ? options.experimental.privacyForReplay @@ -57,9 +59,15 @@ class ScreenshotRecorder { // On Android, the desired resolution (coming from the configuration) // is rounded to next multitude of 16 . Therefore, we scale the image. // On iOS, the screenshot resolution is not adjusted. - final srcWidth = renderObject.size.width; - final srcHeight = renderObject.size.height; - final pixelRatio = config.getPixelRatio(srcWidth, srcHeight); + config.srcWidth = renderObject.size.width.toInt(); + config.srcHeight = renderObject.size.height.toInt(); + final targetHeight = + config.quality.calculateHeight(config.srcWidth!, config.srcHeight!); + final targetWidth = + config.quality.calculateWidth(config.srcWidth!, config.srcHeight!); + + final pixelRatio = + config.getPixelRatio(targetWidth.toDouble(), targetHeight.toDouble()); // First, we synchronously capture the image and enumerate widgets on the main UI loop. final futureImage = renderObject.toImage(pixelRatio: pixelRatio); @@ -69,7 +77,7 @@ class ScreenshotRecorder { filter.obscure( context, pixelRatio, - Rect.fromLTWH(0, 0, srcWidth * pixelRatio, srcHeight * pixelRatio), + Rect.fromLTWH(0, 0, targetWidth.toDouble(), targetHeight.toDouble()), ); } @@ -93,10 +101,6 @@ class ScreenshotRecorder { try { Image finalImage; - final targetHeight = - config.quality.calculateHeight(srcWidth.toInt(), srcHeight.toInt()); - final targetWidth = - config.quality.calculateWidth(srcWidth.toInt(), srcHeight.toInt()); finalImage = await picture.toImage(targetWidth, targetHeight); try { await callback(finalImage); diff --git a/flutter/lib/src/screenshot/recorder_config.dart b/flutter/lib/src/screenshot/recorder_config.dart index 64a1d7d183..146ebefa12 100644 --- a/flutter/lib/src/screenshot/recorder_config.dart +++ b/flutter/lib/src/screenshot/recorder_config.dart @@ -6,21 +6,21 @@ import '../../sentry_flutter.dart'; @internal class ScreenshotRecorderConfig { - final int? width; - final int? height; + int? srcWidth; + int? srcHeight; final SentryScreenshotQuality quality; - const ScreenshotRecorderConfig({ - this.width, - this.height, - this.quality = SentryScreenshotQuality.full, + ScreenshotRecorderConfig({ + this.srcWidth, + this.srcHeight, + this.quality = SentryScreenshotQuality.low, }); - double getPixelRatio(double srcWidth, double srcHeight) { - assert((width == null) == (height == null)); - if (width == null || height == null) { + double getPixelRatio(double targetWidth, double targetHeight) { + assert((srcWidth == null) == (srcHeight == null)); + if (srcWidth == null || srcHeight == null) { return 1.0; } - return min(width! / srcWidth, height! / srcHeight); + return min(targetWidth / srcWidth!, targetHeight / srcHeight!); } } diff --git a/flutter/lib/src/screenshot/sentry_screenshot_quality.dart b/flutter/lib/src/screenshot/sentry_screenshot_quality.dart index 2110a1ec49..a2247b2440 100644 --- a/flutter/lib/src/screenshot/sentry_screenshot_quality.dart +++ b/flutter/lib/src/screenshot/sentry_screenshot_quality.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:meta/meta.dart'; /// The quality of the attached screenshot @@ -23,7 +25,8 @@ enum SentryScreenshotQuality { @internal int calculateHeight(int width, int height) { if (this == SentryScreenshotQuality.full) { - return height; + // ignore: deprecated_member_use + return window.physicalSize.height.round(); } else { if (height > width) { return targetResolution()!; @@ -37,7 +40,8 @@ enum SentryScreenshotQuality { @internal int calculateWidth(int width, int height) { if (this == SentryScreenshotQuality.full) { - return width; + // ignore: deprecated_member_use + return window.physicalSize.width.round(); } else { if (width > height) { return targetResolution()!; diff --git a/flutter/test/replay/recorder_config_test.dart b/flutter/test/replay/recorder_config_test.dart index 7aa4612f7e..e6a5d60484 100644 --- a/flutter/test/replay/recorder_config_test.dart +++ b/flutter/test/replay/recorder_config_test.dart @@ -5,18 +5,18 @@ void main() async { group('$ScreenshotRecorderConfig', () { test('defaults', () { var sut = ScreenshotRecorderConfig(); - expect(sut.height, isNull); - expect(sut.width, isNull); + expect(sut.srcHeight, isNull); + expect(sut.srcWidth, isNull); }); test('pixel ratio calculation', () { expect(ScreenshotRecorderConfig().getPixelRatio(100, 100), 1.0); expect( - ScreenshotRecorderConfig(width: 5, height: 10) + ScreenshotRecorderConfig(srcWidth: 5, srcHeight: 10) .getPixelRatio(100, 100), 0.05); expect( - ScreenshotRecorderConfig(width: 20, height: 10) + ScreenshotRecorderConfig(srcWidth: 20, srcHeight: 10) .getPixelRatio(100, 100), 0.1); }); diff --git a/flutter/test/replay/scheduled_recorder_test.dart b/flutter/test/replay/scheduled_recorder_test.dart index b6cb228f57..ecaf2d3dd3 100644 --- a/flutter/test/replay/scheduled_recorder_test.dart +++ b/flutter/test/replay/scheduled_recorder_test.dart @@ -38,8 +38,8 @@ class _Fixture { _Fixture._(this._tester) { sut = ScheduledScreenshotRecorder( ScheduledScreenshotRecorderConfig( - width: 1000, - height: 1000, + srcWidth: 1000, + srcHeight: 1000, frameRate: 1000, ), (Image image) async { From b565f3a4990b528f092260cd8a6c634799eb2dd3 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Tue, 12 Nov 2024 12:30:55 +0100 Subject: [PATCH 22/51] renamed some variables to make it more readable --- .../flutter/SentryFlutterReplayRecorder.kt | 4 +-- .../src/native/java/sentry_native_java.dart | 4 +-- .../src/replay/scheduled_recorder_config.dart | 4 +-- flutter/lib/src/screenshot/recorder.dart | 28 ++++++++++++------- .../lib/src/screenshot/recorder_config.dart | 16 +++++------ flutter/test/replay/recorder_config_test.dart | 8 +++--- 6 files changed, 36 insertions(+), 28 deletions(-) diff --git a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt index d1746d5775..ce296307f1 100644 --- a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt +++ b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt @@ -24,8 +24,8 @@ internal class SentryFlutterReplayRecorder( "ReplayRecorder.start", mapOf( "directory" to cacheDirPath, - "srcWidth" to recorderConfig.recordingWidth, - "srcHeight" to recorderConfig.recordingHeight, + "targetWidth" to recorderConfig.recordingWidth, + "targetHeight" to recorderConfig.recordingHeight, "frameRate" to recorderConfig.frameRate, "replayId" to integration.getReplayId().toString(), ), diff --git a/flutter/lib/src/native/java/sentry_native_java.dart b/flutter/lib/src/native/java/sentry_native_java.dart index 32f2415e17..32276b1e19 100644 --- a/flutter/lib/src/native/java/sentry_native_java.dart +++ b/flutter/lib/src/native/java/sentry_native_java.dart @@ -40,8 +40,8 @@ class SentryNativeJava extends SentryNativeChannel { _startRecorder( call.arguments['directory'] as String, ScheduledScreenshotRecorderConfig( - srcWidth: call.arguments['srcWidth'] as int, - srcHeight: call.arguments['srcHeight'] as int, + targetWidth: call.arguments['targetWidth'] as int, + targetHeight: call.arguments['targetHeight'] as int, frameRate: call.arguments['frameRate'] as int, ), ); diff --git a/flutter/lib/src/replay/scheduled_recorder_config.dart b/flutter/lib/src/replay/scheduled_recorder_config.dart index f4c9b3cac9..aa754f9e79 100644 --- a/flutter/lib/src/replay/scheduled_recorder_config.dart +++ b/flutter/lib/src/replay/scheduled_recorder_config.dart @@ -4,8 +4,8 @@ class ScheduledScreenshotRecorderConfig extends ScreenshotRecorderConfig { final int frameRate; ScheduledScreenshotRecorderConfig({ - super.srcWidth, - super.srcHeight, + super.targetWidth, + super.targetHeight, required this.frameRate, }); } diff --git a/flutter/lib/src/screenshot/recorder.dart b/flutter/lib/src/screenshot/recorder.dart index 90b2817fa1..f53ce70a96 100644 --- a/flutter/lib/src/screenshot/recorder.dart +++ b/flutter/lib/src/screenshot/recorder.dart @@ -19,6 +19,8 @@ class ScreenshotRecorder { final SentryFlutterOptions options; WidgetFilter? _widgetFilter; bool warningLogged = false; + + // TODO: remove [isReplayRecorder] parameter in the next major release, see _SentryFlutterExperimentalOptions. @protected final bool isReplayRecorder; @@ -59,15 +61,19 @@ class ScreenshotRecorder { // On Android, the desired resolution (coming from the configuration) // is rounded to next multitude of 16 . Therefore, we scale the image. // On iOS, the screenshot resolution is not adjusted. - config.srcWidth = renderObject.size.width.toInt(); - config.srcHeight = renderObject.size.height.toInt(); - final targetHeight = - config.quality.calculateHeight(config.srcWidth!, config.srcHeight!); - final targetWidth = - config.quality.calculateWidth(config.srcWidth!, config.srcHeight!); + final srcWidth = renderObject.size.width.toInt(); + final srcHeight = renderObject.size.height.toInt(); + + // In Session Replay the target size is already set and should not be changed. + // For Screenshots, we need to calculate the target size based on the quality setting. + if (!isReplayRecorder) { + config.targetHeight = + config.quality.calculateHeight(srcWidth, srcHeight); + config.targetWidth = config.quality.calculateWidth(srcWidth, srcHeight); + } - final pixelRatio = - config.getPixelRatio(targetWidth.toDouble(), targetHeight.toDouble()); + var pixelRatio = + config.getPixelRatio(srcWidth.toDouble(), srcHeight.toDouble()); // First, we synchronously capture the image and enumerate widgets on the main UI loop. final futureImage = renderObject.toImage(pixelRatio: pixelRatio); @@ -77,7 +83,8 @@ class ScreenshotRecorder { filter.obscure( context, pixelRatio, - Rect.fromLTWH(0, 0, targetWidth.toDouble(), targetHeight.toDouble()), + Rect.fromLTWH(0, 0, config.targetWidth!.toDouble(), + config.targetHeight!.toDouble()), ); } @@ -101,7 +108,8 @@ class ScreenshotRecorder { try { Image finalImage; - finalImage = await picture.toImage(targetWidth, targetHeight); + finalImage = + await picture.toImage(config.targetWidth!, config.targetHeight!); try { await callback(finalImage); } finally { diff --git a/flutter/lib/src/screenshot/recorder_config.dart b/flutter/lib/src/screenshot/recorder_config.dart index 146ebefa12..e6221cfec5 100644 --- a/flutter/lib/src/screenshot/recorder_config.dart +++ b/flutter/lib/src/screenshot/recorder_config.dart @@ -6,21 +6,21 @@ import '../../sentry_flutter.dart'; @internal class ScreenshotRecorderConfig { - int? srcWidth; - int? srcHeight; + int? targetWidth; + int? targetHeight; final SentryScreenshotQuality quality; ScreenshotRecorderConfig({ - this.srcWidth, - this.srcHeight, + this.targetWidth, + this.targetHeight, this.quality = SentryScreenshotQuality.low, }); - double getPixelRatio(double targetWidth, double targetHeight) { - assert((srcWidth == null) == (srcHeight == null)); - if (srcWidth == null || srcHeight == null) { + double getPixelRatio(double srcWidth, double srcHeight) { + assert((targetWidth == null) == (targetHeight == null)); + if (targetWidth == null || targetHeight == null) { return 1.0; } - return min(targetWidth / srcWidth!, targetHeight / srcHeight!); + return min(targetWidth! / srcWidth, targetHeight! / srcHeight); } } diff --git a/flutter/test/replay/recorder_config_test.dart b/flutter/test/replay/recorder_config_test.dart index e6a5d60484..ded992b2ca 100644 --- a/flutter/test/replay/recorder_config_test.dart +++ b/flutter/test/replay/recorder_config_test.dart @@ -5,18 +5,18 @@ void main() async { group('$ScreenshotRecorderConfig', () { test('defaults', () { var sut = ScreenshotRecorderConfig(); - expect(sut.srcHeight, isNull); - expect(sut.srcWidth, isNull); + expect(sut.targetHeight, isNull); + expect(sut.targetWidth, isNull); }); test('pixel ratio calculation', () { expect(ScreenshotRecorderConfig().getPixelRatio(100, 100), 1.0); expect( - ScreenshotRecorderConfig(srcWidth: 5, srcHeight: 10) + ScreenshotRecorderConfig(targetWidth: 5, targetHeight: 10) .getPixelRatio(100, 100), 0.05); expect( - ScreenshotRecorderConfig(srcWidth: 20, srcHeight: 10) + ScreenshotRecorderConfig(targetWidth: 20, targetHeight: 10) .getPixelRatio(100, 100), 0.1); }); From 3febb1f1582161648ed18cde53bcf2b1d5739ce0 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Tue, 12 Nov 2024 14:57:47 +0100 Subject: [PATCH 23/51] fix unit tests --- flutter/lib/src/screenshot/recorder.dart | 8 +++----- .../screenshot/sentry_screenshot_quality.dart | 18 +++++++++--------- .../test/replay/scheduled_recorder_test.dart | 6 +++--- .../sentry_screenshot_quality_test.dart | 12 ++++++++---- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/flutter/lib/src/screenshot/recorder.dart b/flutter/lib/src/screenshot/recorder.dart index f53ce70a96..d6a0df842d 100644 --- a/flutter/lib/src/screenshot/recorder.dart +++ b/flutter/lib/src/screenshot/recorder.dart @@ -66,11 +66,9 @@ class ScreenshotRecorder { // In Session Replay the target size is already set and should not be changed. // For Screenshots, we need to calculate the target size based on the quality setting. - if (!isReplayRecorder) { - config.targetHeight = - config.quality.calculateHeight(srcWidth, srcHeight); - config.targetWidth = config.quality.calculateWidth(srcWidth, srcHeight); - } + config.targetHeight ??= + config.quality.calculateHeight(srcWidth, srcHeight); + config.targetWidth ??= config.quality.calculateWidth(srcWidth, srcHeight); var pixelRatio = config.getPixelRatio(srcWidth.toDouble(), srcHeight.toDouble()); diff --git a/flutter/lib/src/screenshot/sentry_screenshot_quality.dart b/flutter/lib/src/screenshot/sentry_screenshot_quality.dart index a2247b2440..f7d5930e95 100644 --- a/flutter/lib/src/screenshot/sentry_screenshot_quality.dart +++ b/flutter/lib/src/screenshot/sentry_screenshot_quality.dart @@ -12,7 +12,7 @@ enum SentryScreenshotQuality { int? targetResolution() { switch (this) { case SentryScreenshotQuality.full: - return null; // Keep current scale + return null; // Use device resolution case SentryScreenshotQuality.high: return 1920; case SentryScreenshotQuality.medium: @@ -23,31 +23,31 @@ enum SentryScreenshotQuality { } @internal - int calculateHeight(int width, int height) { + int calculateHeight(int deviceWidth, int deviceHeight) { if (this == SentryScreenshotQuality.full) { // ignore: deprecated_member_use return window.physicalSize.height.round(); } else { - if (height > width) { + if (deviceHeight > deviceWidth) { return targetResolution()!; } else { - var ratio = targetResolution()! / width; - return (height * ratio).round(); + var ratio = targetResolution()! / deviceWidth; + return (deviceHeight * ratio).round(); } } } @internal - int calculateWidth(int width, int height) { + int calculateWidth(int deviceWidth, int deviceHeight) { if (this == SentryScreenshotQuality.full) { // ignore: deprecated_member_use return window.physicalSize.width.round(); } else { - if (width > height) { + if (deviceWidth > deviceHeight) { return targetResolution()!; } else { - var ratio = targetResolution()! / height; - return (width * ratio).round(); + var ratio = targetResolution()! / deviceHeight; + return (deviceWidth * ratio).round(); } } } diff --git a/flutter/test/replay/scheduled_recorder_test.dart b/flutter/test/replay/scheduled_recorder_test.dart index ecaf2d3dd3..d747296c1e 100644 --- a/flutter/test/replay/scheduled_recorder_test.dart +++ b/flutter/test/replay/scheduled_recorder_test.dart @@ -16,7 +16,7 @@ void main() async { TestWidgetsFlutterBinding.ensureInitialized(); testWidgets('captures images', (tester) async { - await tester.binding.setSurfaceSize(Size(1000, 750)); + await tester.binding.setSurfaceSize(Size(1000, 1000)); final fixture = await _Fixture.create(tester); expect(fixture.capturedImages, isEmpty); await fixture.nextFrame(); @@ -38,8 +38,8 @@ class _Fixture { _Fixture._(this._tester) { sut = ScheduledScreenshotRecorder( ScheduledScreenshotRecorderConfig( - srcWidth: 1000, - srcHeight: 1000, + targetWidth: 1000, + targetHeight: 750, frameRate: 1000, ), (Image image) async { diff --git a/flutter/test/screenshot/sentry_screenshot_quality_test.dart b/flutter/test/screenshot/sentry_screenshot_quality_test.dart index a4663bdf0f..a0c721d7eb 100644 --- a/flutter/test/screenshot/sentry_screenshot_quality_test.dart +++ b/flutter/test/screenshot/sentry_screenshot_quality_test.dart @@ -1,15 +1,19 @@ +import 'dart:ui'; + import 'package:flutter_test/flutter_test.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +// ignore_for_file: deprecated_member_use + void main() async { group('$SentryScreenshotQuality', () { test('test quality: full', () { final sut = SentryScreenshotQuality.full; expect(sut.targetResolution(), isNull); - expect(sut.calculateHeight(2000, 4000), 4000); - expect(sut.calculateWidth(2000, 4000), 2000); - expect(sut.calculateHeight(4000, 2000), 2000); - expect(sut.calculateWidth(4000, 2000), 4000); + expect(sut.calculateHeight(2000, 4000), window.physicalSize.height); + expect(sut.calculateWidth(2000, 4000), window.physicalSize.width); + expect(sut.calculateHeight(4000, 2000), window.physicalSize.height); + expect(sut.calculateWidth(4000, 2000), window.physicalSize.width); }); test('test quality: high', () { From 1d99b52ab15f69c85c5d44d62a2a5b6aa731a6e3 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Tue, 12 Nov 2024 16:31:38 +0100 Subject: [PATCH 24/51] reorganized tests --- flutter/test/replay/replay_native_test.dart | 2 +- flutter/test/replay/scheduled_recorder_test.dart | 2 +- .../test/{replay => screenshot}/masking_config_test.dart | 0 .../{replay => screenshot}/recorder_config_test.dart | 0 flutter/test/{replay => screenshot}/recorder_test.dart | 9 +-------- flutter/test/{replay => screenshot}/test_widget.dart | 0 .../test/{replay => screenshot}/widget_filter_test.dart | 0 7 files changed, 3 insertions(+), 10 deletions(-) rename flutter/test/{replay => screenshot}/masking_config_test.dart (100%) rename flutter/test/{replay => screenshot}/recorder_config_test.dart (100%) rename flutter/test/{replay => screenshot}/recorder_test.dart (90%) rename flutter/test/{replay => screenshot}/test_widget.dart (100%) rename flutter/test/{replay => screenshot}/widget_filter_test.dart (100%) diff --git a/flutter/test/replay/replay_native_test.dart b/flutter/test/replay/replay_native_test.dart index aad2f3a037..3b35f53c2e 100644 --- a/flutter/test/replay/replay_native_test.dart +++ b/flutter/test/replay/replay_native_test.dart @@ -17,7 +17,7 @@ import 'package:sentry_flutter/src/replay/integration.dart'; import '../mocks.dart'; import '../mocks.mocks.dart'; -import 'test_widget.dart'; +import '../screenshot/test_widget.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); diff --git a/flutter/test/replay/scheduled_recorder_test.dart b/flutter/test/replay/scheduled_recorder_test.dart index d747296c1e..06716bceb8 100644 --- a/flutter/test/replay/scheduled_recorder_test.dart +++ b/flutter/test/replay/scheduled_recorder_test.dart @@ -10,7 +10,7 @@ import 'package:sentry_flutter/src/replay/scheduled_recorder.dart'; import 'package:sentry_flutter/src/replay/scheduled_recorder_config.dart'; import '../mocks.dart'; -import 'test_widget.dart'; +import '../screenshot/test_widget.dart'; void main() async { TestWidgetsFlutterBinding.ensureInitialized(); diff --git a/flutter/test/replay/masking_config_test.dart b/flutter/test/screenshot/masking_config_test.dart similarity index 100% rename from flutter/test/replay/masking_config_test.dart rename to flutter/test/screenshot/masking_config_test.dart diff --git a/flutter/test/replay/recorder_config_test.dart b/flutter/test/screenshot/recorder_config_test.dart similarity index 100% rename from flutter/test/replay/recorder_config_test.dart rename to flutter/test/screenshot/recorder_config_test.dart diff --git a/flutter/test/replay/recorder_test.dart b/flutter/test/screenshot/recorder_test.dart similarity index 90% rename from flutter/test/replay/recorder_test.dart rename to flutter/test/screenshot/recorder_test.dart index d633d4a771..3c6f1f8018 100644 --- a/flutter/test/replay/recorder_test.dart +++ b/flutter/test/screenshot/recorder_test.dart @@ -16,16 +16,9 @@ import 'test_widget.dart'; void main() async { TestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('captures full resolution images - portrait', (tester) async { - await tester.binding.setSurfaceSize(Size(2000, 4000)); - final fixture = await _Fixture.create(tester); - expect(fixture.capture(), completion('2000x4000')); - }); - testWidgets('captures full resolution images - landscape', (tester) async { - await tester.binding.setSurfaceSize(Size(4000, 2000)); final fixture = await _Fixture.create(tester); - expect(fixture.capture(), completion('4000x2000')); + expect(fixture.capture(), completion('2400x1800')); }); testWidgets('captures high resolution images - portrait', (tester) async { diff --git a/flutter/test/replay/test_widget.dart b/flutter/test/screenshot/test_widget.dart similarity index 100% rename from flutter/test/replay/test_widget.dart rename to flutter/test/screenshot/test_widget.dart diff --git a/flutter/test/replay/widget_filter_test.dart b/flutter/test/screenshot/widget_filter_test.dart similarity index 100% rename from flutter/test/replay/widget_filter_test.dart rename to flutter/test/screenshot/widget_filter_test.dart From a22320028669312c294839e582a264811f903403 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Tue, 12 Nov 2024 16:49:54 +0100 Subject: [PATCH 25/51] update changelog --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae8e7dc0cc..a488c75b2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## Unreleased + +### Features + +- Exchange of the internal screenshot generation, which now enables masking (redaction) of Screenshots for privacy reasons ([#2361](https://github.com/getsentry/sentry-dart/pull/2361)) + Masking behaviour: + - enabled by default for SessionReplay + - disabled by default for screenshots captured with events. + ```dart + await SentryFlutter.init( + (options) { + ... + options.experimental.privacy.maskAllText = true; + options.experimental.privacy.maskAllImages = true; + options.experimental.privacy.maskAssetImages = true; + }, + appRunner: () => runApp(MyApp()), + ); + ``` + + ## 8.10.1 ### Fixes From 2821d780294ee3ed9ab3ef51c1c20ca770ab237f Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Tue, 12 Nov 2024 17:30:09 +0100 Subject: [PATCH 26/51] screenshots now also working with canvas kit --- .../event_processor/screenshot_event_processor.dart | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index 4edcac41d6..cf6eca8777 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -8,7 +8,6 @@ import '../screenshot/recorder.dart'; import '../screenshot/recorder_config.dart'; import '../screenshot/sentry_screenshot_widget.dart'; import '../sentry_flutter_options.dart'; -import '../renderer/renderer.dart'; import 'package:flutter/widgets.dart' as widget; class ScreenshotEventProcessor implements EventProcessor { @@ -66,17 +65,6 @@ class ScreenshotEventProcessor implements EventProcessor { } } - final renderer = _options.rendererWrapper.getRenderer(); - - if (_options.platformChecker.isWeb && - renderer != FlutterRenderer.canvasKit) { - _options.logger( - SentryLevel.debug, - 'Cannot take screenshot with ${renderer?.name} renderer.', - ); - return event; - } - if (_options.screenshot.attachScreenshotOnlyWhenResumed && widget.WidgetsBinding.instance.lifecycleState != AppLifecycleState.resumed) { From ec80659a70e6d98d728449b3b6215f26aa4e1074 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Wed, 13 Nov 2024 14:35:50 +0100 Subject: [PATCH 27/51] Update CHANGELOG.md Co-authored-by: Ivan Dlugos <6349682+vaind@users.noreply.github.com> --- CHANGELOG.md | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cd22f05bc..ada37b4b3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,17 +13,29 @@ ### Features -- Exchange of the internal screenshot generation, which now enables masking (redaction) of Screenshots for privacy reasons ([#2361](https://github.com/getsentry/sentry-dart/pull/2361)) - Masking behaviour: - - enabled by default for SessionReplay - - disabled by default for screenshots captured with events. - ```dart +- Support for screenshot PII content redaction (masking) ([#2361](https://github.com/getsentry/sentry-dart/pull/2361)) + By default, masking is enabled for SessionReplay. To also enable it for screenshots captured with events, you just can specify `options.experimental.privacy`: + ```dart await SentryFlutter.init( (options) { ... + // the defaults are: options.experimental.privacy.maskAllText = true; options.experimental.privacy.maskAllImages = true; - options.experimental.privacy.maskAssetImages = true; + options.experimental.privacy.maskAssetImages = false; + // you cal also set up custom masking, for example: + options.experimental.privacy.mask(); + }, + appRunner: () => runApp(MyApp()), + ); + ``` + Actually, just accessing this field will cause it to be initialized with the default settings to mask all text and images: + ```dart + await SentryFlutter.init( + (options) { + ... + // this has a side-effect of creating the default privacy configuration, thus enabling Screenshot redaction: + options.experimental.privacy; }, appRunner: () => runApp(MyApp()), ); From e3c45f4baf6c7bcb0b3bd9567470e9ab9064225c Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Wed, 13 Nov 2024 14:39:26 +0100 Subject: [PATCH 28/51] Update flutter/lib/src/sentry_flutter_options.dart Co-authored-by: Ivan Dlugos <6349682+vaind@users.noreply.github.com> --- flutter/lib/src/sentry_flutter_options.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index 5fe1018fba..d69454dd78 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -399,7 +399,7 @@ class _SentryFlutterExperimentalOptions { /// Replay recording configuration. final replay = SentryReplayOptions(); - /// Privacy configuration for masking sensitive data in the Screenshot and Session Replay. + /// Privacy configuration for masking sensitive data in screenshots and Session Replay. /// Screen content masking redaction is: /// - enabled by default for SessionReplay /// - disabled by default for screenshots captured with events. From 23cd66c43e318dc5e892e8eb41b56a2ceaee9e89 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Wed, 13 Nov 2024 14:40:31 +0100 Subject: [PATCH 29/51] Update flutter/lib/src/sentry_screenshot_options.dart Co-authored-by: Ivan Dlugos <6349682+vaind@users.noreply.github.com> --- flutter/lib/src/sentry_screenshot_options.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/lib/src/sentry_screenshot_options.dart b/flutter/lib/src/sentry_screenshot_options.dart index 5068c770a4..a7ee0874bd 100644 --- a/flutter/lib/src/sentry_screenshot_options.dart +++ b/flutter/lib/src/sentry_screenshot_options.dart @@ -10,18 +10,18 @@ class SentryScreenshotOptions { /// Example: /// runApp(SentryWidget(child: App())); /// The [SentryWidget] has to be the root widget of the app. - bool attachScreenshot = false; + bool attach = false; /// Sets a callback which is executed before capturing screenshots. Only /// relevant if `attachScreenshot` is set to true. When false is returned /// from the function, no screenshot will be attached. - BeforeScreenshotCallback? beforeScreenshot; + BeforeScreenshotCallback? beforeCapture; /// Only attach a screenshot when the app is resumed. - bool attachScreenshotOnlyWhenResumed = false; + bool attachOnlyWhenResumed = false; /// The quality of the attached screenshot - SentryScreenshotQuality screenshotQuality = SentryScreenshotQuality.high; + SentryScreenshotQuality quality = SentryScreenshotQuality.high; } /// Callback being executed in [ScreenshotEventProcessor], deciding if a From 673ef0bc2afe7d701a94ab83e6edf671ed612cb6 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Wed, 13 Nov 2024 15:03:50 +0100 Subject: [PATCH 30/51] renamed screenshot settings --- .../screenshot_event_processor.dart | 6 ++-- .../integrations/screenshot_integration.dart | 2 +- flutter/lib/src/sentry_flutter_options.dart | 31 +++++++++---------- .../lib/src/sentry_screenshot_options.dart | 2 +- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index cf6eca8777..a6dfd7f3f3 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -17,7 +17,7 @@ class ScreenshotEventProcessor implements EventProcessor { ScreenshotEventProcessor(this._options) { _recorder = ScreenshotRecorder( - ScreenshotRecorderConfig(quality: _options.screenshot.screenshotQuality), + ScreenshotRecorderConfig(quality: _options.screenshot.quality), _options, isReplayRecorder: false, ); @@ -39,7 +39,7 @@ class ScreenshotEventProcessor implements EventProcessor { return event; // No need to attach screenshot of feedback form. } - final beforeScreenshot = _options.screenshot.beforeScreenshot; + final beforeScreenshot = _options.screenshot.beforeCapture; if (beforeScreenshot != null) { try { final result = beforeScreenshot(event, hint: hint); @@ -65,7 +65,7 @@ class ScreenshotEventProcessor implements EventProcessor { } } - if (_options.screenshot.attachScreenshotOnlyWhenResumed && + if (_options.screenshot.attachOnlyWhenResumed && widget.WidgetsBinding.instance.lifecycleState != AppLifecycleState.resumed) { _options.logger(SentryLevel.debug, diff --git a/flutter/lib/src/integrations/screenshot_integration.dart b/flutter/lib/src/integrations/screenshot_integration.dart index bafe4b696e..37fa4ab12d 100644 --- a/flutter/lib/src/integrations/screenshot_integration.dart +++ b/flutter/lib/src/integrations/screenshot_integration.dart @@ -10,7 +10,7 @@ class ScreenshotIntegration implements Integration { @override void call(Hub hub, SentryFlutterOptions options) { - if (options.screenshot.attachScreenshot) { + if (options.screenshot.attach) { _options = options; final screenshotEventProcessor = ScreenshotEventProcessor(options); options.addEventProcessor(screenshotEventProcessor); diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index d69454dd78..35679ffcad 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -182,34 +182,33 @@ class SentryFlutterOptions extends SentryOptions { /// Automatically attaches a screenshot when capturing an error or exception. /// - /// Requires adding the [SentryScreenshotWidget] to the widget tree. + /// Requires adding the [SentryWidget] to the widget tree. /// Example: - /// runApp(SentryScreenshotWidget(child: App())); - /// The [SentryScreenshotWidget] has to be the root widget of the app. - @Deprecated('Use `screenshot.attachScreenshot` instead') - bool get attachScreenshot => screenshot.attachScreenshot; - set attachScreenshot(bool value) => screenshot.attachScreenshot = value; + /// runApp(SentryWidget(child: App())); + /// The [SentryWidget] has to be the root widget of the app. + @Deprecated('Use `screenshot.attach` instead') + bool get attachScreenshot => screenshot.attach; + set attachScreenshot(bool value) => screenshot.attach = value; /// The quality of the attached screenshot - @Deprecated('Use `screenshot.screenshotQuality` instead') - SentryScreenshotQuality get screenshotQuality => screenshot.screenshotQuality; + @Deprecated('Use `screenshot.quality` instead') + SentryScreenshotQuality get screenshotQuality => screenshot.quality; set screenshotQuality(SentryScreenshotQuality value) => - screenshot.screenshotQuality = value; + screenshot.quality = value; /// Only attach a screenshot when the app is resumed. - @Deprecated('Use `screenshot.attachScreenshotOnlyWhenResumed` instead') - bool get attachScreenshotOnlyWhenResumed => - screenshot.attachScreenshotOnlyWhenResumed; + @Deprecated('Use `screenshot.attachOnlyWhenResumed` instead') + bool get attachScreenshotOnlyWhenResumed => screenshot.attachOnlyWhenResumed; set attachScreenshotOnlyWhenResumed(bool value) => - screenshot.attachScreenshotOnlyWhenResumed = value; + screenshot.attachOnlyWhenResumed = value; /// Sets a callback which is executed before capturing screenshots. Only /// relevant if `attachScreenshot` is set to true. When false is returned /// from the function, no screenshot will be attached. - @Deprecated('Use `screenshot.beforeScreenshot` instead') - BeforeScreenshotCallback? get beforeScreenshot => screenshot.beforeScreenshot; + @Deprecated('Use `screenshot.beforeCapture` instead') + BeforeScreenshotCallback? get beforeScreenshot => screenshot.beforeCapture; set beforeScreenshot(BeforeScreenshotCallback? value) => - screenshot.beforeScreenshot = value; + screenshot.beforeCapture = value; final screenshot = SentryScreenshotOptions(); diff --git a/flutter/lib/src/sentry_screenshot_options.dart b/flutter/lib/src/sentry_screenshot_options.dart index a7ee0874bd..365959d311 100644 --- a/flutter/lib/src/sentry_screenshot_options.dart +++ b/flutter/lib/src/sentry_screenshot_options.dart @@ -13,7 +13,7 @@ class SentryScreenshotOptions { bool attach = false; /// Sets a callback which is executed before capturing screenshots. Only - /// relevant if `attachScreenshot` is set to true. When false is returned + /// relevant if `attach` is set to true. When false is returned /// from the function, no screenshot will be attached. BeforeScreenshotCallback? beforeCapture; From b9772aa40158fe778eb3c3288e633d0eaa772faf Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Wed, 13 Nov 2024 16:52:33 +0100 Subject: [PATCH 31/51] removed quality from recorder config. --- .../flutter/SentryFlutterReplayRecorder.kt | 4 +- .../screenshot_event_processor.dart | 11 +++-- .../src/native/java/sentry_native_java.dart | 4 +- .../src/replay/scheduled_recorder_config.dart | 4 +- flutter/lib/src/screenshot/recorder.dart | 41 +++++++++++-------- .../lib/src/screenshot/recorder_config.dart | 22 ++-------- .../screenshot/sentry_screenshot_quality.dart | 34 --------------- flutter/lib/src/sentry_flutter_options.dart | 1 - .../screenshot_event_processor_test.dart | 2 +- .../test/replay/scheduled_recorder_test.dart | 5 +-- .../test/screenshot/recorder_config_test.dart | 16 +------- flutter/test/screenshot/recorder_test.dart | 33 +++++++++------ .../sentry_screenshot_quality_test.dart | 16 -------- 13 files changed, 68 insertions(+), 125 deletions(-) diff --git a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt index ce296307f1..ba285a12a0 100644 --- a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt +++ b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt @@ -24,8 +24,8 @@ internal class SentryFlutterReplayRecorder( "ReplayRecorder.start", mapOf( "directory" to cacheDirPath, - "targetWidth" to recorderConfig.recordingWidth, - "targetHeight" to recorderConfig.recordingHeight, + "width" to recorderConfig.recordingWidth, + "height" to recorderConfig.recordingHeight, "frameRate" to recorderConfig.frameRate, "replayId" to integration.getReplayId().toString(), ), diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index a6dfd7f3f3..9f7ee6fb65 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -3,11 +3,9 @@ import 'dart:typed_data'; import 'dart:ui'; import 'package:meta/meta.dart'; -import 'package:sentry/sentry.dart'; +import '../../sentry_flutter.dart'; import '../screenshot/recorder.dart'; import '../screenshot/recorder_config.dart'; -import '../screenshot/sentry_screenshot_widget.dart'; -import '../sentry_flutter_options.dart'; import 'package:flutter/widgets.dart' as widget; class ScreenshotEventProcessor implements EventProcessor { @@ -16,8 +14,13 @@ class ScreenshotEventProcessor implements EventProcessor { late final ScreenshotRecorder _recorder; ScreenshotEventProcessor(this._options) { + final targetResolution = _options.screenshot.quality.targetResolution(); + _recorder = ScreenshotRecorder( - ScreenshotRecorderConfig(quality: _options.screenshot.quality), + ScreenshotRecorderConfig( + width: targetResolution?.toInt(), + height: targetResolution?.toInt(), + ), _options, isReplayRecorder: false, ); diff --git a/flutter/lib/src/native/java/sentry_native_java.dart b/flutter/lib/src/native/java/sentry_native_java.dart index 32276b1e19..91c127de4f 100644 --- a/flutter/lib/src/native/java/sentry_native_java.dart +++ b/flutter/lib/src/native/java/sentry_native_java.dart @@ -40,8 +40,8 @@ class SentryNativeJava extends SentryNativeChannel { _startRecorder( call.arguments['directory'] as String, ScheduledScreenshotRecorderConfig( - targetWidth: call.arguments['targetWidth'] as int, - targetHeight: call.arguments['targetHeight'] as int, + width: call.arguments['width'] as int, + height: call.arguments['height'] as int, frameRate: call.arguments['frameRate'] as int, ), ); diff --git a/flutter/lib/src/replay/scheduled_recorder_config.dart b/flutter/lib/src/replay/scheduled_recorder_config.dart index aa754f9e79..8f3c3addb2 100644 --- a/flutter/lib/src/replay/scheduled_recorder_config.dart +++ b/flutter/lib/src/replay/scheduled_recorder_config.dart @@ -4,8 +4,8 @@ class ScheduledScreenshotRecorderConfig extends ScreenshotRecorderConfig { final int frameRate; ScheduledScreenshotRecorderConfig({ - super.targetWidth, - super.targetHeight, + super.width, + super.height, required this.frameRate, }); } diff --git a/flutter/lib/src/screenshot/recorder.dart b/flutter/lib/src/screenshot/recorder.dart index d6a0df842d..8a563dbc81 100644 --- a/flutter/lib/src/screenshot/recorder.dart +++ b/flutter/lib/src/screenshot/recorder.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'dart:ui'; import 'package:flutter/rendering.dart'; @@ -20,17 +21,13 @@ class ScreenshotRecorder { WidgetFilter? _widgetFilter; bool warningLogged = false; - // TODO: remove [isReplayRecorder] parameter in the next major release, see _SentryFlutterExperimentalOptions. - @protected - final bool isReplayRecorder; - // TODO: remove in the next major release, see recorder_test.dart. @visibleForTesting bool get hasWidgetFilter => _widgetFilter != null; // TODO: remove [isReplayRecorder] parameter in the next major release, see _SentryFlutterExperimentalOptions. ScreenshotRecorder(this.config, this.options, - {this.isReplayRecorder = true}) { + {bool isReplayRecorder = true}) { // see `options.experimental.privacy` docs for details final privacyOptions = isReplayRecorder ? options.experimental.privacyForReplay @@ -64,14 +61,28 @@ class ScreenshotRecorder { final srcWidth = renderObject.size.width.toInt(); final srcHeight = renderObject.size.height.toInt(); - // In Session Replay the target size is already set and should not be changed. - // For Screenshots, we need to calculate the target size based on the quality setting. - config.targetHeight ??= - config.quality.calculateHeight(srcWidth, srcHeight); - config.targetWidth ??= config.quality.calculateWidth(srcWidth, srcHeight); + final int targetWidth; + final int targetHeight; + + final double pixelRatio; - var pixelRatio = - config.getPixelRatio(srcWidth.toDouble(), srcHeight.toDouble()); + // If width or height is not set, we use the device resolution. + if (config.width == null || config.height == null) { + final binding = options.bindingUtils.instance!; + + // ignore: deprecated_member_use + targetHeight = binding.window.physicalSize.height.toInt(); + // ignore: deprecated_member_use + targetWidth = binding.window.physicalSize.width.toInt(); + + pixelRatio = max(targetWidth, targetHeight) / max(srcWidth, srcHeight); + } else { + pixelRatio = + max(config.width!, config.height!) / max(srcWidth, srcHeight); + + targetWidth = (srcWidth * pixelRatio).toInt(); + targetHeight = (srcHeight * pixelRatio).toInt(); + } // First, we synchronously capture the image and enumerate widgets on the main UI loop. final futureImage = renderObject.toImage(pixelRatio: pixelRatio); @@ -81,8 +92,7 @@ class ScreenshotRecorder { filter.obscure( context, pixelRatio, - Rect.fromLTWH(0, 0, config.targetWidth!.toDouble(), - config.targetHeight!.toDouble()), + Rect.fromLTWH(0, 0, targetWidth.toDouble(), targetHeight.toDouble()), ); } @@ -106,8 +116,7 @@ class ScreenshotRecorder { try { Image finalImage; - finalImage = - await picture.toImage(config.targetWidth!, config.targetHeight!); + finalImage = await picture.toImage(targetWidth, targetHeight); try { await callback(finalImage); } finally { diff --git a/flutter/lib/src/screenshot/recorder_config.dart b/flutter/lib/src/screenshot/recorder_config.dart index e6221cfec5..a1ecd56e3b 100644 --- a/flutter/lib/src/screenshot/recorder_config.dart +++ b/flutter/lib/src/screenshot/recorder_config.dart @@ -1,26 +1,12 @@ -import 'dart:math'; - import 'package:meta/meta.dart'; -import '../../sentry_flutter.dart'; - @internal class ScreenshotRecorderConfig { - int? targetWidth; - int? targetHeight; - final SentryScreenshotQuality quality; + final int? width; + final int? height; ScreenshotRecorderConfig({ - this.targetWidth, - this.targetHeight, - this.quality = SentryScreenshotQuality.low, + this.width, + this.height, }); - - double getPixelRatio(double srcWidth, double srcHeight) { - assert((targetWidth == null) == (targetHeight == null)); - if (targetWidth == null || targetHeight == null) { - return 1.0; - } - return min(targetWidth! / srcWidth, targetHeight! / srcHeight); - } } diff --git a/flutter/lib/src/screenshot/sentry_screenshot_quality.dart b/flutter/lib/src/screenshot/sentry_screenshot_quality.dart index f7d5930e95..d5e04ee0eb 100644 --- a/flutter/lib/src/screenshot/sentry_screenshot_quality.dart +++ b/flutter/lib/src/screenshot/sentry_screenshot_quality.dart @@ -1,7 +1,3 @@ -import 'dart:ui'; - -import 'package:meta/meta.dart'; - /// The quality of the attached screenshot enum SentryScreenshotQuality { full, @@ -21,34 +17,4 @@ enum SentryScreenshotQuality { return 854; } } - - @internal - int calculateHeight(int deviceWidth, int deviceHeight) { - if (this == SentryScreenshotQuality.full) { - // ignore: deprecated_member_use - return window.physicalSize.height.round(); - } else { - if (deviceHeight > deviceWidth) { - return targetResolution()!; - } else { - var ratio = targetResolution()! / deviceWidth; - return (deviceHeight * ratio).round(); - } - } - } - - @internal - int calculateWidth(int deviceWidth, int deviceHeight) { - if (this == SentryScreenshotQuality.full) { - // ignore: deprecated_member_use - return window.physicalSize.width.round(); - } else { - if (deviceWidth > deviceHeight) { - return targetResolution()!; - } else { - var ratio = targetResolution()! / deviceHeight; - return (deviceWidth * ratio).round(); - } - } - } } diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index 35679ffcad..4e95d0bebe 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -10,7 +10,6 @@ import 'binding_wrapper.dart'; import 'navigation/time_to_display_tracker.dart'; import 'renderer/renderer.dart'; import 'screenshot/sentry_screenshot_quality.dart'; -import 'screenshot/sentry_screenshot_widget.dart'; import 'sentry_flutter.dart'; import 'sentry_replay_options.dart'; import 'sentry_screenshot_options.dart'; diff --git a/flutter/test/event_processor/screenshot_event_processor_test.dart b/flutter/test/event_processor/screenshot_event_processor_test.dart index 635d2a5770..150a486c6f 100644 --- a/flutter/test/event_processor/screenshot_event_processor_test.dart +++ b/flutter/test/event_processor/screenshot_event_processor_test.dart @@ -69,7 +69,7 @@ void main() { testWidgets('does not add screenshot attachment with html renderer', (tester) async { await _addScreenshotAttachment(tester, FlutterRenderer.html, - added: false, isWeb: true); + added: true, isWeb: true); }); testWidgets('does add screenshot in correct resolution for low', diff --git a/flutter/test/replay/scheduled_recorder_test.dart b/flutter/test/replay/scheduled_recorder_test.dart index 06716bceb8..ba7a45e21b 100644 --- a/flutter/test/replay/scheduled_recorder_test.dart +++ b/flutter/test/replay/scheduled_recorder_test.dart @@ -16,7 +16,6 @@ void main() async { TestWidgetsFlutterBinding.ensureInitialized(); testWidgets('captures images', (tester) async { - await tester.binding.setSurfaceSize(Size(1000, 1000)); final fixture = await _Fixture.create(tester); expect(fixture.capturedImages, isEmpty); await fixture.nextFrame(); @@ -38,8 +37,8 @@ class _Fixture { _Fixture._(this._tester) { sut = ScheduledScreenshotRecorder( ScheduledScreenshotRecorderConfig( - targetWidth: 1000, - targetHeight: 750, + width: 1000, + height: 750, frameRate: 1000, ), (Image image) async { diff --git a/flutter/test/screenshot/recorder_config_test.dart b/flutter/test/screenshot/recorder_config_test.dart index ded992b2ca..ea6cc75302 100644 --- a/flutter/test/screenshot/recorder_config_test.dart +++ b/flutter/test/screenshot/recorder_config_test.dart @@ -5,20 +5,8 @@ void main() async { group('$ScreenshotRecorderConfig', () { test('defaults', () { var sut = ScreenshotRecorderConfig(); - expect(sut.targetHeight, isNull); - expect(sut.targetWidth, isNull); - }); - - test('pixel ratio calculation', () { - expect(ScreenshotRecorderConfig().getPixelRatio(100, 100), 1.0); - expect( - ScreenshotRecorderConfig(targetWidth: 5, targetHeight: 10) - .getPixelRatio(100, 100), - 0.05); - expect( - ScreenshotRecorderConfig(targetWidth: 20, targetHeight: 10) - .getPixelRatio(100, 100), - 0.1); + expect(sut.height, isNull); + expect(sut.width, isNull); }); }); } diff --git a/flutter/test/screenshot/recorder_test.dart b/flutter/test/screenshot/recorder_test.dart index 3c6f1f8018..0da324f96a 100644 --- a/flutter/test/screenshot/recorder_test.dart +++ b/flutter/test/screenshot/recorder_test.dart @@ -18,34 +18,43 @@ void main() async { testWidgets('captures full resolution images - landscape', (tester) async { final fixture = await _Fixture.create(tester); + expect(fixture.capture(), completion('2400x1800')); }); testWidgets('captures high resolution images - portrait', (tester) async { await tester.binding.setSurfaceSize(Size(2000, 4000)); - final fixture = - await _Fixture.create(tester, quality: SentryScreenshotQuality.high); + final targetResolution = SentryScreenshotQuality.high.targetResolution(); + final fixture = await _Fixture.create(tester, + width: targetResolution, height: targetResolution); + expect(fixture.capture(), completion('960x1920')); }); testWidgets('captures high resolution images - landscape', (tester) async { await tester.binding.setSurfaceSize(Size(4000, 2000)); - final fixture = - await _Fixture.create(tester, quality: SentryScreenshotQuality.high); + final targetResolution = SentryScreenshotQuality.high.targetResolution(); + final fixture = await _Fixture.create(tester, + width: targetResolution, height: targetResolution); + expect(fixture.capture(), completion('1920x960')); }); testWidgets('captures medium resolution images', (tester) async { await tester.binding.setSurfaceSize(Size(2000, 4000)); - final fixture = - await _Fixture.create(tester, quality: SentryScreenshotQuality.medium); + final targetResolution = SentryScreenshotQuality.medium.targetResolution(); + final fixture = await _Fixture.create(tester, + width: targetResolution, height: targetResolution); + expect(fixture.capture(), completion('640x1280')); }); testWidgets('captures low resolution images', (tester) async { await tester.binding.setSurfaceSize(Size(2000, 4000)); - final fixture = - await _Fixture.create(tester, quality: SentryScreenshotQuality.low); + final targetResolution = SentryScreenshotQuality.low.targetResolution(); + final fixture = await _Fixture.create(tester, + width: targetResolution, height: targetResolution); + expect(fixture.capture(), completion('427x854')); }); @@ -80,16 +89,16 @@ void main() async { class _Fixture { late final ScreenshotRecorder sut; - _Fixture({SentryScreenshotQuality quality = SentryScreenshotQuality.full}) { + _Fixture({int? width, int? height}) { sut = ScreenshotRecorder( - ScreenshotRecorderConfig(quality: quality), + ScreenshotRecorderConfig(width: width, height: height), defaultTestOptions()..bindingUtils = TestBindingWrapper(), ); } static Future<_Fixture> create(WidgetTester tester, - {SentryScreenshotQuality quality = SentryScreenshotQuality.full}) async { - final fixture = _Fixture(quality: quality); + {int? width, int? height}) async { + final fixture = _Fixture(width: width, height: height); await pumpTestElement(tester); return fixture; } diff --git a/flutter/test/screenshot/sentry_screenshot_quality_test.dart b/flutter/test/screenshot/sentry_screenshot_quality_test.dart index a0c721d7eb..246ebd61ba 100644 --- a/flutter/test/screenshot/sentry_screenshot_quality_test.dart +++ b/flutter/test/screenshot/sentry_screenshot_quality_test.dart @@ -10,40 +10,24 @@ void main() async { test('test quality: full', () { final sut = SentryScreenshotQuality.full; expect(sut.targetResolution(), isNull); - expect(sut.calculateHeight(2000, 4000), window.physicalSize.height); - expect(sut.calculateWidth(2000, 4000), window.physicalSize.width); - expect(sut.calculateHeight(4000, 2000), window.physicalSize.height); - expect(sut.calculateWidth(4000, 2000), window.physicalSize.width); }); test('test quality: high', () { final sut = SentryScreenshotQuality.high; final res = sut.targetResolution()!; expect(res, 1920); - expect(sut.calculateHeight(2000, 4000), res); - expect(sut.calculateWidth(2000, 4000), res / 2); - expect(sut.calculateHeight(4000, 2000), res / 2); - expect(sut.calculateWidth(4000, 2000), res); }); test('test quality: medium', () { final sut = SentryScreenshotQuality.medium; final res = sut.targetResolution()!; expect(res, 1280); - expect(sut.calculateHeight(2000, 4000), res); - expect(sut.calculateWidth(2000, 4000), res / 2); - expect(sut.calculateHeight(4000, 2000), res / 2); - expect(sut.calculateWidth(4000, 2000), res); }); test('test quality: low', () { final sut = SentryScreenshotQuality.low; final res = sut.targetResolution()!; expect(res, 854); - expect(sut.calculateHeight(2000, 4000), res); - expect(sut.calculateWidth(2000, 4000), res / 2); - expect(sut.calculateHeight(4000, 2000), res / 2); - expect(sut.calculateWidth(4000, 2000), res); }); }); } From feb579dc34f688608ea19e2a6a1e1b6f2358a5b9 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Thu, 14 Nov 2024 10:21:59 +0100 Subject: [PATCH 32/51] Update CHANGELOG.md Co-authored-by: Ivan Dlugos <6349682+vaind@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ada37b4b3d..120ffb37ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ ### Features - Support for screenshot PII content redaction (masking) ([#2361](https://github.com/getsentry/sentry-dart/pull/2361)) - By default, masking is enabled for SessionReplay. To also enable it for screenshots captured with events, you just can specify `options.experimental.privacy`: + By default, masking is enabled for SessionReplay. To also enable it for screenshots captured with events, you can specify `options.experimental.privacy`: ```dart await SentryFlutter.init( (options) { From d14704be80b3c27cfcfd8ea1a2de550fc4e3911b Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Thu, 14 Nov 2024 10:22:22 +0100 Subject: [PATCH 33/51] Update CHANGELOG.md Co-authored-by: Ivan Dlugos <6349682+vaind@users.noreply.github.com> --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 120ffb37ea..f5336099e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -141,7 +141,6 @@ - Avoid sending too many empty client reports when Http Transport is used ([#2380](https://github.com/getsentry/sentry-dart/pull/2380)) - Cache parsed DSN ([#2365](https://github.com/getsentry/sentry-dart/pull/2365)) -- Switching from traditional screenshot to view hierarchy for screenshots which allows redacting ([#2361](https://github.com/getsentry/sentry-dart/pull/2361)) - Handle backpressure earlier in pipeline ([#2371](https://github.com/getsentry/sentry-dart/pull/2371)) - Drops max un-awaited parallel tasks earlier, so event processors & callbacks are not executed for them. - Change by setting `SentryOptions.maxQueueSize`. Default is 30. From 9c4966c7931e79c6f3784b4064d97b41e542ccde Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Thu, 14 Nov 2024 10:28:44 +0100 Subject: [PATCH 34/51] Update flutter/lib/src/event_processor/screenshot_event_processor.dart Co-authored-by: Ivan Dlugos <6349682+vaind@users.noreply.github.com> --- .../lib/src/event_processor/screenshot_event_processor.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index 9f7ee6fb65..f9e97a20f0 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -18,8 +18,8 @@ class ScreenshotEventProcessor implements EventProcessor { _recorder = ScreenshotRecorder( ScreenshotRecorderConfig( - width: targetResolution?.toInt(), - height: targetResolution?.toInt(), + width: targetResolution, + height: targetResolution, ), _options, isReplayRecorder: false, From ef8d6b33d3c4ea6d6b716e9841728bff5040eb84 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Thu, 14 Nov 2024 10:48:52 +0100 Subject: [PATCH 35/51] add test for screenshots with flutter html renderer --- .../screenshot_event_processor_test.dart | 103 ++++++++++++++++-- 1 file changed, 96 insertions(+), 7 deletions(-) diff --git a/flutter/test/event_processor/screenshot_event_processor_test.dart b/flutter/test/event_processor/screenshot_event_processor_test.dart index 150a486c6f..41e9cbce10 100644 --- a/flutter/test/event_processor/screenshot_event_processor_test.dart +++ b/flutter/test/event_processor/screenshot_event_processor_test.dart @@ -126,8 +126,9 @@ void main() { }); }); - group('beforeScreenshot', () { - testWidgets('does add screenshot if beforeScreenshot returns true', + group('beforeScreenshot - canvasKit', () { + testWidgets( + '(canvasKit) does add screenshot if beforeScreenshot returns true', (tester) async { fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) { return true; @@ -136,7 +137,8 @@ void main() { added: true, isWeb: false); }); - testWidgets('does add screenshot if async beforeScreenshot returns true', + testWidgets( + '(canvasKit) does add screenshot if async beforeScreenshot returns true', (tester) async { fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) async { @@ -147,7 +149,8 @@ void main() { added: true, isWeb: false); }); - testWidgets('does not add screenshot if beforeScreenshot returns false', + testWidgets( + '(canvasKit) does not add screenshot if beforeScreenshot returns false', (tester) async { fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) { return false; @@ -157,7 +160,7 @@ void main() { }); testWidgets( - 'does not add screenshot if async beforeScreenshot returns false', + '(canvasKit) does not add screenshot if async beforeScreenshot returns false', (tester) async { fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) async { @@ -168,7 +171,7 @@ void main() { added: false, isWeb: false); }); - testWidgets('does add screenshot if beforeScreenshot throws', + testWidgets('(canvasKit) does add screenshot if beforeScreenshot throws', (tester) async { fixture.options.automatedTestMode = false; fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) { @@ -178,7 +181,8 @@ void main() { added: true, isWeb: false); }); - testWidgets('does add screenshot if async beforeScreenshot throws', + testWidgets( + '(canvasKit) does add screenshot if async beforeScreenshot throws', (tester) async { fixture.options.automatedTestMode = false; fixture.options.beforeScreenshot = @@ -190,6 +194,91 @@ void main() { added: true, isWeb: false); }); + testWidgets('(canvasKit) passes event & hint to beforeScreenshot callback', + (tester) async { + SentryEvent? beforeScreenshotEvent; + Hint? beforeScreenshotHint; + + fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) { + beforeScreenshotEvent = event; + beforeScreenshotHint = hint; + return true; + }; + + await _addScreenshotAttachment(tester, FlutterRenderer.canvasKit, + added: true, isWeb: false); + + expect(beforeScreenshotEvent, event); + expect(beforeScreenshotHint, hint); + }); + }); + + group('beforeScreenshot - html', () { + testWidgets('(html) does add screenshot if beforeScreenshot returns true', + (tester) async { + fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) { + return true; + }; + await _addScreenshotAttachment(tester, FlutterRenderer.html, + added: true, isWeb: true); + }); + + testWidgets( + '(html) does add screenshot if async beforeScreenshot returns true', + (tester) async { + fixture.options.beforeScreenshot = + (SentryEvent event, {Hint? hint}) async { + await Future.delayed(Duration(milliseconds: 1)); + return true; + }; + await _addScreenshotAttachment(tester, FlutterRenderer.html, + added: true, isWeb: true); + }); + + testWidgets( + '(html) does not add screenshot if beforeScreenshot returns false', + (tester) async { + fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) { + return false; + }; + await _addScreenshotAttachment(tester, FlutterRenderer.html, + added: false, isWeb: true); + }); + + testWidgets( + '(html) does not add screenshot if async beforeScreenshot returns false', + (tester) async { + fixture.options.beforeScreenshot = + (SentryEvent event, {Hint? hint}) async { + await Future.delayed(Duration(milliseconds: 1)); + return false; + }; + await _addScreenshotAttachment(tester, FlutterRenderer.html, + added: false, isWeb: true); + }); + + testWidgets('(html) does add screenshot if beforeScreenshot throws', + (tester) async { + fixture.options.automatedTestMode = false; + fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) { + throw Error(); + }; + await _addScreenshotAttachment(tester, FlutterRenderer.html, + added: true, isWeb: true); + }); + + testWidgets('(html) does add screenshot if async beforeScreenshot throws', + (tester) async { + fixture.options.automatedTestMode = false; + fixture.options.beforeScreenshot = + (SentryEvent event, {Hint? hint}) async { + await Future.delayed(Duration(milliseconds: 1)); + throw Error(); + }; + await _addScreenshotAttachment(tester, FlutterRenderer.html, + added: true, isWeb: true); + }); + testWidgets('passes event & hint to beforeScreenshot callback', (tester) async { SentryEvent? beforeScreenshotEvent; From 879d445cbba312079d73729cd4fb4474f13cee62 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Thu, 14 Nov 2024 10:49:30 +0100 Subject: [PATCH 36/51] Update flutter/lib/src/sentry_screenshot_options.dart Co-authored-by: Ivan Dlugos <6349682+vaind@users.noreply.github.com> --- flutter/lib/src/sentry_screenshot_options.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/lib/src/sentry_screenshot_options.dart b/flutter/lib/src/sentry_screenshot_options.dart index 365959d311..05501f1061 100644 --- a/flutter/lib/src/sentry_screenshot_options.dart +++ b/flutter/lib/src/sentry_screenshot_options.dart @@ -18,6 +18,7 @@ class SentryScreenshotOptions { BeforeScreenshotCallback? beforeCapture; /// Only attach a screenshot when the app is resumed. + /// See https://docs.sentry.io/platforms/flutter/troubleshooting/#screenshot-integration-background-crash bool attachOnlyWhenResumed = false; /// The quality of the attached screenshot From 93a8a7f67f976d12110f9a7e5523115e633d0c71 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Thu, 14 Nov 2024 14:08:21 +0100 Subject: [PATCH 37/51] add changelog for supported html renderer --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5336099e8..6115b2bb5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ### Features +- Screenshots are now supported for Flutter Web via HTML Renderer ([#2361](https://github.com/getsentry/sentry-dart/pull/2361)) - Support for screenshot PII content redaction (masking) ([#2361](https://github.com/getsentry/sentry-dart/pull/2361)) By default, masking is enabled for SessionReplay. To also enable it for screenshots captured with events, you can specify `options.experimental.privacy`: ```dart From 606454f73f1277f550614462279d783afbcd4a9b Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Thu, 14 Nov 2024 14:17:44 +0100 Subject: [PATCH 38/51] removed unused import --- flutter/test/screenshot/sentry_screenshot_quality_test.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/flutter/test/screenshot/sentry_screenshot_quality_test.dart b/flutter/test/screenshot/sentry_screenshot_quality_test.dart index 246ebd61ba..3f406fc007 100644 --- a/flutter/test/screenshot/sentry_screenshot_quality_test.dart +++ b/flutter/test/screenshot/sentry_screenshot_quality_test.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:flutter_test/flutter_test.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; From b8e14bed901b41fbfef24cce5a396bae53400999 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos <6349682+vaind@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:26:00 +0100 Subject: [PATCH 39/51] revert: screenshot recorder resolution handling (#2409) * add high-risk-code file filters * revert: screenshot recorder resolution handling * analyzer issues * chore: update example * improve screenshot recorder logs * fixup variable visibility * dart format --------- Co-authored-by: Martin Haintz --- .github/file-filters.yml | 3 +- flutter/example/lib/main.dart | 7 ++- flutter/lib/src/screenshot/recorder.dart | 57 ++++++------------- .../lib/src/screenshot/recorder_config.dart | 10 ++++ .../test/screenshot/recorder_config_test.dart | 10 ++++ flutter/test/screenshot/recorder_test.dart | 3 +- 6 files changed, 46 insertions(+), 44 deletions(-) diff --git a/.github/file-filters.yml b/.github/file-filters.yml index f86f14edad..4da3356a47 100644 --- a/.github/file-filters.yml +++ b/.github/file-filters.yml @@ -5,4 +5,5 @@ high_risk_code: &high_risk_code - "flutter/lib/src/integrations/native_app_start_integration.dart" - "flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt" - "flutter/ios/Classes/SentryFlutterPluginApple.swift" - + - "flutter/lib/src/screenshot/recorder.dart" + - "flutter/lib/src/screenshot/widget_filter.dart" diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index df44ef46fb..eb91d9f9fd 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -74,8 +74,7 @@ Future setupSentry( options.addIntegration(LoggingIntegration(minEventLevel: Level.INFO)); options.sendDefaultPii = true; options.reportSilentFlutterErrors = true; - options.attachScreenshot = true; - options.screenshotQuality = SentryScreenshotQuality.low; + options.screenshot.attach = true; options.attachViewHierarchy = true; // We can enable Sentry debug logging during development. This is likely // going to log too much for your app, but can be useful when figuring out @@ -92,6 +91,10 @@ Future setupSentry( options.experimental.replay.sessionSampleRate = 1.0; options.experimental.replay.onErrorSampleRate = 1.0; + // This has a side-effect of creating the default privacy configuration, + // thus enabling Screenshot redaction. No need to actually change it. + options.experimental.privacy; + _isIntegrationTest = isIntegrationTest; if (_isIntegrationTest) { options.dist = '1'; diff --git a/flutter/lib/src/screenshot/recorder.dart b/flutter/lib/src/screenshot/recorder.dart index 8a563dbc81..cd69876c03 100644 --- a/flutter/lib/src/screenshot/recorder.dart +++ b/flutter/lib/src/screenshot/recorder.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:math'; import 'dart:ui'; import 'package:flutter/rendering.dart'; @@ -18,16 +17,17 @@ class ScreenshotRecorder { final ScreenshotRecorderConfig config; @protected final SentryFlutterOptions options; + final String _logName; WidgetFilter? _widgetFilter; - bool warningLogged = false; + bool _warningLogged = false; // TODO: remove in the next major release, see recorder_test.dart. @visibleForTesting bool get hasWidgetFilter => _widgetFilter != null; // TODO: remove [isReplayRecorder] parameter in the next major release, see _SentryFlutterExperimentalOptions. - ScreenshotRecorder(this.config, this.options, - {bool isReplayRecorder = true}) { + ScreenshotRecorder(this.config, this.options, {bool isReplayRecorder = true}) + : _logName = isReplayRecorder ? 'ReplayRecorder' : 'ScreenshotyRecorder' { // see `options.experimental.privacy` docs for details final privacyOptions = isReplayRecorder ? options.experimental.privacyForReplay @@ -42,12 +42,10 @@ class ScreenshotRecorder { final context = sentryScreenshotWidgetGlobalKey.currentContext; final renderObject = context?.findRenderObject() as RenderRepaintBoundary?; if (context == null || renderObject == null) { - if (!warningLogged) { - options.logger( - SentryLevel.warning, - "Replay: SentryScreenshotWidget is not attached. " - "Skipping replay capture."); - warningLogged = true; + if (!_warningLogged) { + options.logger(SentryLevel.warning, + "$_logName: SentryScreenshotWidget is not attached, skipping capture."); + _warningLogged = true; } return; } @@ -58,31 +56,9 @@ class ScreenshotRecorder { // On Android, the desired resolution (coming from the configuration) // is rounded to next multitude of 16 . Therefore, we scale the image. // On iOS, the screenshot resolution is not adjusted. - final srcWidth = renderObject.size.width.toInt(); - final srcHeight = renderObject.size.height.toInt(); - - final int targetWidth; - final int targetHeight; - - final double pixelRatio; - - // If width or height is not set, we use the device resolution. - if (config.width == null || config.height == null) { - final binding = options.bindingUtils.instance!; - - // ignore: deprecated_member_use - targetHeight = binding.window.physicalSize.height.toInt(); - // ignore: deprecated_member_use - targetWidth = binding.window.physicalSize.width.toInt(); - - pixelRatio = max(targetWidth, targetHeight) / max(srcWidth, srcHeight); - } else { - pixelRatio = - max(config.width!, config.height!) / max(srcWidth, srcHeight); - - targetWidth = (srcWidth * pixelRatio).toInt(); - targetHeight = (srcHeight * pixelRatio).toInt(); - } + final srcWidth = renderObject.size.width; + final srcHeight = renderObject.size.height; + final pixelRatio = config.getPixelRatio(srcWidth, srcHeight); // First, we synchronously capture the image and enumerate widgets on the main UI loop. final futureImage = renderObject.toImage(pixelRatio: pixelRatio); @@ -92,7 +68,7 @@ class ScreenshotRecorder { filter.obscure( context, pixelRatio, - Rect.fromLTWH(0, 0, targetWidth.toDouble(), targetHeight.toDouble()), + Rect.fromLTWH(0, 0, srcWidth * pixelRatio, srcHeight * pixelRatio), ); } @@ -115,8 +91,8 @@ class ScreenshotRecorder { final picture = recorder.endRecording(); try { - Image finalImage; - finalImage = await picture.toImage(targetWidth, targetHeight); + final finalImage = await picture.toImage( + (srcWidth * pixelRatio).round(), (srcHeight * pixelRatio).round()); try { await callback(finalImage); } finally { @@ -128,10 +104,11 @@ class ScreenshotRecorder { options.logger( SentryLevel.debug, - "Replay: captured a screenshot in ${watch.elapsedMilliseconds}" + "$_logName: captured a screenshot in ${watch.elapsedMilliseconds}" " ms ($blockingTime ms blocking)."); } catch (e, stackTrace) { - options.logger(SentryLevel.error, "Replay: failed to capture screenshot.", + options.logger( + SentryLevel.error, "$_logName: failed to capture screenshot.", exception: e, stackTrace: stackTrace); if (options.automatedTestMode) { rethrow; diff --git a/flutter/lib/src/screenshot/recorder_config.dart b/flutter/lib/src/screenshot/recorder_config.dart index a1ecd56e3b..7da7a37e4c 100644 --- a/flutter/lib/src/screenshot/recorder_config.dart +++ b/flutter/lib/src/screenshot/recorder_config.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:meta/meta.dart'; @internal @@ -9,4 +11,12 @@ class ScreenshotRecorderConfig { this.width, this.height, }); + + double getPixelRatio(double srcWidth, double srcHeight) { + assert((width == null) == (height == null)); + if (width == null || height == null) { + return 1.0; + } + return min(width! / srcWidth, height! / srcHeight); + } } diff --git a/flutter/test/screenshot/recorder_config_test.dart b/flutter/test/screenshot/recorder_config_test.dart index ea6cc75302..fcad3d1a61 100644 --- a/flutter/test/screenshot/recorder_config_test.dart +++ b/flutter/test/screenshot/recorder_config_test.dart @@ -9,4 +9,14 @@ void main() async { expect(sut.width, isNull); }); }); + + test('pixel ratio calculation', () { + expect(ScreenshotRecorderConfig().getPixelRatio(100, 100), 1.0); + expect( + ScreenshotRecorderConfig(width: 5, height: 10).getPixelRatio(100, 100), + 0.05); + expect( + ScreenshotRecorderConfig(width: 20, height: 10).getPixelRatio(100, 100), + 0.1); + }); } diff --git a/flutter/test/screenshot/recorder_test.dart b/flutter/test/screenshot/recorder_test.dart index 0da324f96a..8d8e582260 100644 --- a/flutter/test/screenshot/recorder_test.dart +++ b/flutter/test/screenshot/recorder_test.dart @@ -17,9 +17,10 @@ void main() async { TestWidgetsFlutterBinding.ensureInitialized(); testWidgets('captures full resolution images - landscape', (tester) async { + await tester.binding.setSurfaceSize(Size(2000, 4000)); final fixture = await _Fixture.create(tester); - expect(fixture.capture(), completion('2400x1800')); + expect(fixture.capture(), completion('2000x4000')); }); testWidgets('captures high resolution images - portrait', (tester) async { From 2332d9de8345c9517a841b7ab3c6f83785096195 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Thu, 14 Nov 2024 15:35:06 +0100 Subject: [PATCH 40/51] fix pixelRatio --- flutter/lib/src/screenshot/recorder.dart | 5 ++++- flutter/lib/src/screenshot/recorder_config.dart | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/flutter/lib/src/screenshot/recorder.dart b/flutter/lib/src/screenshot/recorder.dart index cd69876c03..fe58054ef4 100644 --- a/flutter/lib/src/screenshot/recorder.dart +++ b/flutter/lib/src/screenshot/recorder.dart @@ -58,7 +58,10 @@ class ScreenshotRecorder { // On iOS, the screenshot resolution is not adjusted. final srcWidth = renderObject.size.width; final srcHeight = renderObject.size.height; - final pixelRatio = config.getPixelRatio(srcWidth, srcHeight); + + final pixelRatio = config.getPixelRatio(srcWidth, srcHeight) ?? + // ignore: deprecated_member_use + options.bindingUtils.instance!.window.devicePixelRatio; // First, we synchronously capture the image and enumerate widgets on the main UI loop. final futureImage = renderObject.toImage(pixelRatio: pixelRatio); diff --git a/flutter/lib/src/screenshot/recorder_config.dart b/flutter/lib/src/screenshot/recorder_config.dart index 7da7a37e4c..922a7cd62a 100644 --- a/flutter/lib/src/screenshot/recorder_config.dart +++ b/flutter/lib/src/screenshot/recorder_config.dart @@ -12,10 +12,10 @@ class ScreenshotRecorderConfig { this.height, }); - double getPixelRatio(double srcWidth, double srcHeight) { + double? getPixelRatio(double srcWidth, double srcHeight) { assert((width == null) == (height == null)); if (width == null || height == null) { - return 1.0; + return null; } return min(width! / srcWidth, height! / srcHeight); } From 080989d486de6c2705632a987074c68c1bb33f40 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Thu, 14 Nov 2024 15:49:28 +0100 Subject: [PATCH 41/51] Update flutter/test/replay/scheduled_recorder_test.dart Co-authored-by: Ivan Dlugos <6349682+vaind@users.noreply.github.com> --- flutter/test/replay/scheduled_recorder_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/test/replay/scheduled_recorder_test.dart b/flutter/test/replay/scheduled_recorder_test.dart index ba7a45e21b..603c2b24b2 100644 --- a/flutter/test/replay/scheduled_recorder_test.dart +++ b/flutter/test/replay/scheduled_recorder_test.dart @@ -38,7 +38,7 @@ class _Fixture { sut = ScheduledScreenshotRecorder( ScheduledScreenshotRecorderConfig( width: 1000, - height: 750, + height: 1000, frameRate: 1000, ), (Image image) async { From ca18328e9582725aed72d9b9b519d47a446d32cd Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Thu, 14 Nov 2024 16:00:53 +0100 Subject: [PATCH 42/51] fix unit tests --- .../test/screenshot/recorder_config_test.dart | 2 +- flutter/test/screenshot/recorder_test.dart | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/flutter/test/screenshot/recorder_config_test.dart b/flutter/test/screenshot/recorder_config_test.dart index fcad3d1a61..d640a7ddb3 100644 --- a/flutter/test/screenshot/recorder_config_test.dart +++ b/flutter/test/screenshot/recorder_config_test.dart @@ -11,7 +11,7 @@ void main() async { }); test('pixel ratio calculation', () { - expect(ScreenshotRecorderConfig().getPixelRatio(100, 100), 1.0); + expect(ScreenshotRecorderConfig().getPixelRatio(100, 100), null); expect( ScreenshotRecorderConfig(width: 5, height: 10).getPixelRatio(100, 100), 0.05); diff --git a/flutter/test/screenshot/recorder_test.dart b/flutter/test/screenshot/recorder_test.dart index 8d8e582260..4ecb78cff6 100644 --- a/flutter/test/screenshot/recorder_test.dart +++ b/flutter/test/screenshot/recorder_test.dart @@ -16,11 +16,24 @@ import 'test_widget.dart'; void main() async { TestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('captures full resolution images - landscape', (tester) async { + // with `tester.binding.setSurfaceSize` you are setting the `logical resolution` + // not the `device screen resolution`. + // The `device screen resolution = logical resolution * devicePixelRatio` + + testWidgets('captures full resolution images - portrait', (tester) async { await tester.binding.setSurfaceSize(Size(2000, 4000)); final fixture = await _Fixture.create(tester); - expect(fixture.capture(), completion('2000x4000')); + //devicePixelRatio is 3.0 therefore the resolution multiplied by 3 + expect(fixture.capture(), completion('6000x12000')); + }); + + testWidgets('captures full resolution images - landscape', (tester) async { + await tester.binding.setSurfaceSize(Size(4000, 2000)); + final fixture = await _Fixture.create(tester); + + //devicePixelRatio is 3.0 therefore the resolution multiplied by 3 + expect(fixture.capture(), completion('12000x6000')); }); testWidgets('captures high resolution images - portrait', (tester) async { From efd66c8d7384735c358d24144afe79ff52bcf251 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Thu, 14 Nov 2024 17:00:04 +0100 Subject: [PATCH 43/51] Update flutter/lib/src/screenshot/recorder.dart Co-authored-by: Ivan Dlugos <6349682+vaind@users.noreply.github.com> --- flutter/lib/src/screenshot/recorder.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/flutter/lib/src/screenshot/recorder.dart b/flutter/lib/src/screenshot/recorder.dart index fe58054ef4..0f49cb7ede 100644 --- a/flutter/lib/src/screenshot/recorder.dart +++ b/flutter/lib/src/screenshot/recorder.dart @@ -60,8 +60,7 @@ class ScreenshotRecorder { final srcHeight = renderObject.size.height; final pixelRatio = config.getPixelRatio(srcWidth, srcHeight) ?? - // ignore: deprecated_member_use - options.bindingUtils.instance!.window.devicePixelRatio; + MediaQuery.of(context).devicePixelRatio; // First, we synchronously capture the image and enumerate widgets on the main UI loop. final futureImage = renderObject.toImage(pixelRatio: pixelRatio); From 0671009bb9a6df1e586399bdcc68a798689ca961 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Fri, 15 Nov 2024 10:13:40 +0100 Subject: [PATCH 44/51] reverted changes for html renderer as it is not working --- CHANGELOG.md | 1 - .../screenshot_event_processor_test.dart | 105 ++---------------- 2 files changed, 8 insertions(+), 98 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d905922c29..1df220210c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,6 @@ ### Features -- Screenshots are now supported for Flutter Web via HTML Renderer ([#2361](https://github.com/getsentry/sentry-dart/pull/2361)) - Support for screenshot PII content redaction (masking) ([#2361](https://github.com/getsentry/sentry-dart/pull/2361)) By default, masking is enabled for SessionReplay. To also enable it for screenshots captured with events, you can specify `options.experimental.privacy`: ```dart diff --git a/flutter/test/event_processor/screenshot_event_processor_test.dart b/flutter/test/event_processor/screenshot_event_processor_test.dart index 41e9cbce10..635d2a5770 100644 --- a/flutter/test/event_processor/screenshot_event_processor_test.dart +++ b/flutter/test/event_processor/screenshot_event_processor_test.dart @@ -69,7 +69,7 @@ void main() { testWidgets('does not add screenshot attachment with html renderer', (tester) async { await _addScreenshotAttachment(tester, FlutterRenderer.html, - added: true, isWeb: true); + added: false, isWeb: true); }); testWidgets('does add screenshot in correct resolution for low', @@ -126,9 +126,8 @@ void main() { }); }); - group('beforeScreenshot - canvasKit', () { - testWidgets( - '(canvasKit) does add screenshot if beforeScreenshot returns true', + group('beforeScreenshot', () { + testWidgets('does add screenshot if beforeScreenshot returns true', (tester) async { fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) { return true; @@ -137,8 +136,7 @@ void main() { added: true, isWeb: false); }); - testWidgets( - '(canvasKit) does add screenshot if async beforeScreenshot returns true', + testWidgets('does add screenshot if async beforeScreenshot returns true', (tester) async { fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) async { @@ -149,8 +147,7 @@ void main() { added: true, isWeb: false); }); - testWidgets( - '(canvasKit) does not add screenshot if beforeScreenshot returns false', + testWidgets('does not add screenshot if beforeScreenshot returns false', (tester) async { fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) { return false; @@ -160,7 +157,7 @@ void main() { }); testWidgets( - '(canvasKit) does not add screenshot if async beforeScreenshot returns false', + 'does not add screenshot if async beforeScreenshot returns false', (tester) async { fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) async { @@ -171,7 +168,7 @@ void main() { added: false, isWeb: false); }); - testWidgets('(canvasKit) does add screenshot if beforeScreenshot throws', + testWidgets('does add screenshot if beforeScreenshot throws', (tester) async { fixture.options.automatedTestMode = false; fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) { @@ -181,8 +178,7 @@ void main() { added: true, isWeb: false); }); - testWidgets( - '(canvasKit) does add screenshot if async beforeScreenshot throws', + testWidgets('does add screenshot if async beforeScreenshot throws', (tester) async { fixture.options.automatedTestMode = false; fixture.options.beforeScreenshot = @@ -194,91 +190,6 @@ void main() { added: true, isWeb: false); }); - testWidgets('(canvasKit) passes event & hint to beforeScreenshot callback', - (tester) async { - SentryEvent? beforeScreenshotEvent; - Hint? beforeScreenshotHint; - - fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) { - beforeScreenshotEvent = event; - beforeScreenshotHint = hint; - return true; - }; - - await _addScreenshotAttachment(tester, FlutterRenderer.canvasKit, - added: true, isWeb: false); - - expect(beforeScreenshotEvent, event); - expect(beforeScreenshotHint, hint); - }); - }); - - group('beforeScreenshot - html', () { - testWidgets('(html) does add screenshot if beforeScreenshot returns true', - (tester) async { - fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) { - return true; - }; - await _addScreenshotAttachment(tester, FlutterRenderer.html, - added: true, isWeb: true); - }); - - testWidgets( - '(html) does add screenshot if async beforeScreenshot returns true', - (tester) async { - fixture.options.beforeScreenshot = - (SentryEvent event, {Hint? hint}) async { - await Future.delayed(Duration(milliseconds: 1)); - return true; - }; - await _addScreenshotAttachment(tester, FlutterRenderer.html, - added: true, isWeb: true); - }); - - testWidgets( - '(html) does not add screenshot if beforeScreenshot returns false', - (tester) async { - fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) { - return false; - }; - await _addScreenshotAttachment(tester, FlutterRenderer.html, - added: false, isWeb: true); - }); - - testWidgets( - '(html) does not add screenshot if async beforeScreenshot returns false', - (tester) async { - fixture.options.beforeScreenshot = - (SentryEvent event, {Hint? hint}) async { - await Future.delayed(Duration(milliseconds: 1)); - return false; - }; - await _addScreenshotAttachment(tester, FlutterRenderer.html, - added: false, isWeb: true); - }); - - testWidgets('(html) does add screenshot if beforeScreenshot throws', - (tester) async { - fixture.options.automatedTestMode = false; - fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) { - throw Error(); - }; - await _addScreenshotAttachment(tester, FlutterRenderer.html, - added: true, isWeb: true); - }); - - testWidgets('(html) does add screenshot if async beforeScreenshot throws', - (tester) async { - fixture.options.automatedTestMode = false; - fixture.options.beforeScreenshot = - (SentryEvent event, {Hint? hint}) async { - await Future.delayed(Duration(milliseconds: 1)); - throw Error(); - }; - await _addScreenshotAttachment(tester, FlutterRenderer.html, - added: true, isWeb: true); - }); - testWidgets('passes event & hint to beforeScreenshot callback', (tester) async { SentryEvent? beforeScreenshotEvent; From de3550c85fc541e3a1d8d98dde34ecf9476c2c4d Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Fri, 15 Nov 2024 10:28:12 +0100 Subject: [PATCH 45/51] fix unit tests and errors --- .../event_processor/screenshot_event_processor.dart | 12 ++++++++++++ flutter/lib/src/screenshot/recorder.dart | 5 +++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index f9e97a20f0..1899255585 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -4,6 +4,7 @@ import 'dart:ui'; import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; +import '../renderer/renderer.dart'; import '../screenshot/recorder.dart'; import '../screenshot/recorder_config.dart'; import 'package:flutter/widgets.dart' as widget; @@ -42,6 +43,17 @@ class ScreenshotEventProcessor implements EventProcessor { return event; // No need to attach screenshot of feedback form. } + final renderer = _options.rendererWrapper.getRenderer(); + + if (_options.platformChecker.isWeb && + renderer != FlutterRenderer.canvasKit) { + _options.logger( + SentryLevel.debug, + 'Cannot take screenshot with ${renderer?.name} renderer.', + ); + return event; + } + final beforeScreenshot = _options.screenshot.beforeCapture; if (beforeScreenshot != null) { try { diff --git a/flutter/lib/src/screenshot/recorder.dart b/flutter/lib/src/screenshot/recorder.dart index 0f49cb7ede..2d8dfffa67 100644 --- a/flutter/lib/src/screenshot/recorder.dart +++ b/flutter/lib/src/screenshot/recorder.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:ui'; import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart' as widgets; import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; @@ -27,7 +28,7 @@ class ScreenshotRecorder { // TODO: remove [isReplayRecorder] parameter in the next major release, see _SentryFlutterExperimentalOptions. ScreenshotRecorder(this.config, this.options, {bool isReplayRecorder = true}) - : _logName = isReplayRecorder ? 'ReplayRecorder' : 'ScreenshotyRecorder' { + : _logName = isReplayRecorder ? 'ReplayRecorder' : 'ScreenshotRecorder' { // see `options.experimental.privacy` docs for details final privacyOptions = isReplayRecorder ? options.experimental.privacyForReplay @@ -60,7 +61,7 @@ class ScreenshotRecorder { final srcHeight = renderObject.size.height; final pixelRatio = config.getPixelRatio(srcWidth, srcHeight) ?? - MediaQuery.of(context).devicePixelRatio; + widgets.MediaQuery.of(context).devicePixelRatio; // First, we synchronously capture the image and enumerate widgets on the main UI loop. final futureImage = renderObject.toImage(pixelRatio: pixelRatio); From cc1d8361170bbc98f531106a46e9645856220245 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Fri, 15 Nov 2024 10:40:19 +0100 Subject: [PATCH 46/51] add comment for quality --- flutter/lib/src/screenshot/sentry_screenshot_quality.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/src/screenshot/sentry_screenshot_quality.dart b/flutter/lib/src/screenshot/sentry_screenshot_quality.dart index d5e04ee0eb..9b36add9d6 100644 --- a/flutter/lib/src/screenshot/sentry_screenshot_quality.dart +++ b/flutter/lib/src/screenshot/sentry_screenshot_quality.dart @@ -8,7 +8,7 @@ enum SentryScreenshotQuality { int? targetResolution() { switch (this) { case SentryScreenshotQuality.full: - return null; // Use device resolution + return null; // Uses the device pixel ratio to scale the screenshot case SentryScreenshotQuality.high: return 1920; case SentryScreenshotQuality.medium: From 820200463b52cb5a04982f136dc3f57c0ab69883 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Fri, 15 Nov 2024 10:52:40 +0100 Subject: [PATCH 47/51] moved code back to previous position --- .../screenshot_event_processor.dart | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index 1899255585..e2ac17651e 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -43,17 +43,6 @@ class ScreenshotEventProcessor implements EventProcessor { return event; // No need to attach screenshot of feedback form. } - final renderer = _options.rendererWrapper.getRenderer(); - - if (_options.platformChecker.isWeb && - renderer != FlutterRenderer.canvasKit) { - _options.logger( - SentryLevel.debug, - 'Cannot take screenshot with ${renderer?.name} renderer.', - ); - return event; - } - final beforeScreenshot = _options.screenshot.beforeCapture; if (beforeScreenshot != null) { try { @@ -80,6 +69,17 @@ class ScreenshotEventProcessor implements EventProcessor { } } + final renderer = _options.rendererWrapper.getRenderer(); + + if (_options.platformChecker.isWeb && + renderer != FlutterRenderer.canvasKit) { + _options.logger( + SentryLevel.debug, + 'Cannot take screenshot with ${renderer?.name} renderer.', + ); + return event; + } + if (_options.screenshot.attachOnlyWhenResumed && widget.WidgetsBinding.instance.lifecycleState != AppLifecycleState.resumed) { From 823598ac7b14830bace5f6700006663d8c497035 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Fri, 15 Nov 2024 11:24:52 +0100 Subject: [PATCH 48/51] rearrange imports --- flutter/lib/src/integrations/screenshot_integration.dart | 2 +- flutter/lib/src/replay/scheduled_recorder.dart | 2 +- flutter/lib/src/sentry_flutter_options.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/lib/src/integrations/screenshot_integration.dart b/flutter/lib/src/integrations/screenshot_integration.dart index 37fa4ab12d..da356eac65 100644 --- a/flutter/lib/src/integrations/screenshot_integration.dart +++ b/flutter/lib/src/integrations/screenshot_integration.dart @@ -3,7 +3,7 @@ import '../event_processor/screenshot_event_processor.dart'; import '../sentry_flutter_options.dart'; /// Adds [ScreenshotEventProcessor] to options event processors if -/// [SentryFlutterOptions.screenshot.attachScreenshot] is true +/// [SentryFlutterOptions.screenshot.attach] is true class ScreenshotIntegration implements Integration { SentryFlutterOptions? _options; ScreenshotEventProcessor? _screenshotEventProcessor; diff --git a/flutter/lib/src/replay/scheduled_recorder.dart b/flutter/lib/src/replay/scheduled_recorder.dart index a8a29a276f..70d8ff390c 100644 --- a/flutter/lib/src/replay/scheduled_recorder.dart +++ b/flutter/lib/src/replay/scheduled_recorder.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'dart:ui'; import 'package:meta/meta.dart'; -import 'scheduled_recorder_config.dart'; import '../../sentry_flutter.dart'; import '../screenshot/recorder.dart'; +import 'scheduled_recorder_config.dart'; import 'scheduler.dart'; @internal diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index 4e95d0bebe..395e4cf64f 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -4,13 +4,13 @@ import 'package:flutter/services.dart'; import 'package:meta/meta.dart' as meta; import 'package:sentry/sentry.dart'; import 'package:flutter/widgets.dart'; -import 'sentry_privacy_options.dart'; import 'binding_wrapper.dart'; import 'navigation/time_to_display_tracker.dart'; import 'renderer/renderer.dart'; import 'screenshot/sentry_screenshot_quality.dart'; import 'sentry_flutter.dart'; +import 'sentry_privacy_options.dart'; import 'sentry_replay_options.dart'; import 'sentry_screenshot_options.dart'; import 'user_interaction/sentry_user_interaction_widget.dart'; From d25e4cfc17e5bba79d6d7efd4f1a90371cde228f Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Wed, 20 Nov 2024 11:44:15 +0100 Subject: [PATCH 49/51] move screenshot options into global options --- flutter/example/lib/main.dart | 2 +- .../screenshot_event_processor.dart | 6 ++-- .../integrations/screenshot_integration.dart | 2 +- flutter/lib/src/sentry_flutter_options.dart | 32 ++++++++---------- .../lib/src/sentry_screenshot_options.dart | 33 ------------------- 5 files changed, 19 insertions(+), 56 deletions(-) delete mode 100644 flutter/lib/src/sentry_screenshot_options.dart diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index eb91d9f9fd..9e7f17a821 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -74,7 +74,7 @@ Future setupSentry( options.addIntegration(LoggingIntegration(minEventLevel: Level.INFO)); options.sendDefaultPii = true; options.reportSilentFlutterErrors = true; - options.screenshot.attach = true; + options.attachScreenshot = true; options.attachViewHierarchy = true; // We can enable Sentry debug logging during development. This is likely // going to log too much for your app, but can be useful when figuring out diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index e2ac17651e..724d74e0d5 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -15,7 +15,7 @@ class ScreenshotEventProcessor implements EventProcessor { late final ScreenshotRecorder _recorder; ScreenshotEventProcessor(this._options) { - final targetResolution = _options.screenshot.quality.targetResolution(); + final targetResolution = _options.screenshotQuality.targetResolution(); _recorder = ScreenshotRecorder( ScreenshotRecorderConfig( @@ -43,7 +43,7 @@ class ScreenshotEventProcessor implements EventProcessor { return event; // No need to attach screenshot of feedback form. } - final beforeScreenshot = _options.screenshot.beforeCapture; + final beforeScreenshot = _options.beforeScreenshot; if (beforeScreenshot != null) { try { final result = beforeScreenshot(event, hint: hint); @@ -80,7 +80,7 @@ class ScreenshotEventProcessor implements EventProcessor { return event; } - if (_options.screenshot.attachOnlyWhenResumed && + if (_options.attachScreenshotOnlyWhenResumed && widget.WidgetsBinding.instance.lifecycleState != AppLifecycleState.resumed) { _options.logger(SentryLevel.debug, diff --git a/flutter/lib/src/integrations/screenshot_integration.dart b/flutter/lib/src/integrations/screenshot_integration.dart index da356eac65..b1d9db3dd3 100644 --- a/flutter/lib/src/integrations/screenshot_integration.dart +++ b/flutter/lib/src/integrations/screenshot_integration.dart @@ -10,7 +10,7 @@ class ScreenshotIntegration implements Integration { @override void call(Hub hub, SentryFlutterOptions options) { - if (options.screenshot.attach) { + if (options.attachScreenshot) { _options = options; final screenshotEventProcessor = ScreenshotEventProcessor(options); options.addEventProcessor(screenshotEventProcessor); diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index 395e4cf64f..3589989db6 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:file/file.dart'; import 'package:file/local.dart'; import 'package:flutter/services.dart'; @@ -12,7 +14,6 @@ import 'screenshot/sentry_screenshot_quality.dart'; import 'sentry_flutter.dart'; import 'sentry_privacy_options.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. @@ -185,31 +186,19 @@ class SentryFlutterOptions extends SentryOptions { /// Example: /// runApp(SentryWidget(child: App())); /// The [SentryWidget] has to be the root widget of the app. - @Deprecated('Use `screenshot.attach` instead') - bool get attachScreenshot => screenshot.attach; - set attachScreenshot(bool value) => screenshot.attach = value; + bool attachScreenshot = false; /// The quality of the attached screenshot - @Deprecated('Use `screenshot.quality` instead') - SentryScreenshotQuality get screenshotQuality => screenshot.quality; - set screenshotQuality(SentryScreenshotQuality value) => - screenshot.quality = value; + SentryScreenshotQuality screenshotQuality = SentryScreenshotQuality.high; /// Only attach a screenshot when the app is resumed. - @Deprecated('Use `screenshot.attachOnlyWhenResumed` instead') - bool get attachScreenshotOnlyWhenResumed => screenshot.attachOnlyWhenResumed; - set attachScreenshotOnlyWhenResumed(bool value) => - screenshot.attachOnlyWhenResumed = value; + /// See https://docs.sentry.io/platforms/flutter/troubleshooting/#screenshot-integration-background-crash + bool attachScreenshotOnlyWhenResumed = false; /// Sets a callback which is executed before capturing screenshots. Only /// relevant if `attachScreenshot` is set to true. When false is returned /// from the function, no screenshot will be attached. - @Deprecated('Use `screenshot.beforeCapture` instead') - BeforeScreenshotCallback? get beforeScreenshot => screenshot.beforeCapture; - set beforeScreenshot(BeforeScreenshotCallback? value) => - screenshot.beforeCapture = value; - - final screenshot = SentryScreenshotOptions(); + BeforeScreenshotCallback? beforeScreenshot; /// Enable or disable automatic breadcrumbs for User interactions Using [Listener] /// @@ -426,3 +415,10 @@ class _SentryFlutterExperimentalOptions { SentryPrivacyOptions get privacyForReplay => _privacy ?? SentryPrivacyOptions(); } + +/// Callback being executed in [ScreenshotEventProcessor], deciding if a +/// screenshot should be recorded and attached. +typedef BeforeScreenshotCallback = FutureOr Function( + SentryEvent event, { + Hint? hint, +}); diff --git a/flutter/lib/src/sentry_screenshot_options.dart b/flutter/lib/src/sentry_screenshot_options.dart deleted file mode 100644 index 05501f1061..0000000000 --- a/flutter/lib/src/sentry_screenshot_options.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'dart:async'; - -import '../sentry_flutter.dart'; - -/// Configuration of the screenshot feature. -class SentryScreenshotOptions { - /// Automatically attaches a screenshot when capturing an error or exception. - /// - /// Requires adding the [SentryWidget] to the widget tree. - /// Example: - /// runApp(SentryWidget(child: App())); - /// The [SentryWidget] has to be the root widget of the app. - bool attach = false; - - /// Sets a callback which is executed before capturing screenshots. Only - /// relevant if `attach` is set to true. When false is returned - /// from the function, no screenshot will be attached. - BeforeScreenshotCallback? beforeCapture; - - /// Only attach a screenshot when the app is resumed. - /// See https://docs.sentry.io/platforms/flutter/troubleshooting/#screenshot-integration-background-crash - bool attachOnlyWhenResumed = false; - - /// The quality of the attached screenshot - SentryScreenshotQuality quality = SentryScreenshotQuality.high; -} - -/// Callback being executed in [ScreenshotEventProcessor], deciding if a -/// screenshot should be recorded and attached. -typedef BeforeScreenshotCallback = FutureOr Function( - SentryEvent event, { - Hint? hint, -}); From d3a0a2a26f9f190053f083ab0014a2dbcbb1b2c9 Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Wed, 20 Nov 2024 11:54:10 +0100 Subject: [PATCH 50/51] change wording from redaction to masking and similar. --- CHANGELOG.md | 4 ++-- flutter/example/lib/main.dart | 2 +- flutter/lib/src/sentry_flutter_options.dart | 14 +++++++------- flutter/lib/src/sentry_privacy_options.dart | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1df220210c..91c0ca16ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ ### Features -- Support for screenshot PII content redaction (masking) ([#2361](https://github.com/getsentry/sentry-dart/pull/2361)) +- Support for screenshot PII content masking ([#2361](https://github.com/getsentry/sentry-dart/pull/2361)) By default, masking is enabled for SessionReplay. To also enable it for screenshots captured with events, you can specify `options.experimental.privacy`: ```dart await SentryFlutter.init( @@ -34,7 +34,7 @@ await SentryFlutter.init( (options) { ... - // this has a side-effect of creating the default privacy configuration, thus enabling Screenshot redaction: + // this has a side-effect of creating the default privacy configuration, thus enabling Screenshot masking: options.experimental.privacy; }, appRunner: () => runApp(MyApp()), diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 9e7f17a821..454be356f6 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -92,7 +92,7 @@ Future setupSentry( options.experimental.replay.onErrorSampleRate = 1.0; // This has a side-effect of creating the default privacy configuration, - // thus enabling Screenshot redaction. No need to actually change it. + // thus enabling Screenshot masking. No need to actually change it. options.experimental.privacy; _isIntegrationTest = isIntegrationTest; diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index 3589989db6..a7b5b396bb 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -387,25 +387,25 @@ class _SentryFlutterExperimentalOptions { final replay = SentryReplayOptions(); /// Privacy configuration for masking sensitive data in screenshots and Session Replay. - /// Screen content masking redaction is: + /// Screen content masking is: /// - enabled by default for SessionReplay /// - disabled by default for screenshots captured with events. - /// In order to redact screenshots captured with events, access or change + /// In order to mask screenshots captured with events, access or change /// this property in your application: `options.experimental.privacy`. /// Doing so will indicate that you want to configure privacy settings and - /// will enable screenshot redaction alongside the default replay redaction. + /// will enable screenshot masking alongside the default replay masking. /// Note: this will change in a future SDK major release to enable screenshot - /// redaction by default for all captures. + /// masking by default for all captures. SentryPrivacyOptions get privacy { // If the user explicitly sets the privacy setting, we use that. - // Otherwise, we use the default settings, which is no redaction for screenshots - // and full redaction for session replay. + // Otherwise, we use the default settings, which is no masking for screenshots + // and full masking for session replay. // This property must only by accessed by user code otherwise it defeats the purpose. _privacy ??= SentryPrivacyOptions(); return _privacy!; } - /// TODO: remove when default redaction value are synced with SS & SR in the next major release + /// TODO: remove when default masking value are synced with SS & SR in the next major release SentryPrivacyOptions? _privacy; @meta.internal diff --git a/flutter/lib/src/sentry_privacy_options.dart b/flutter/lib/src/sentry_privacy_options.dart index cdc5c8aadd..7873116b9b 100644 --- a/flutter/lib/src/sentry_privacy_options.dart +++ b/flutter/lib/src/sentry_privacy_options.dart @@ -15,7 +15,7 @@ class SentryPrivacyOptions { 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. + /// dominant color on top. Currently, only [Image] widgets are masked. /// Default is enabled (except for asset images, see [maskAssetImages]). @experimental var maskAllImages = true; From 3fbe92450c544b887f8f52c2c7a254b8b3cbf8bc Mon Sep 17 00:00:00 2001 From: Martin Haintz Date: Sun, 24 Nov 2024 18:31:50 +0100 Subject: [PATCH 51/51] add import for screenshot event processor --- flutter/lib/src/sentry_flutter_options.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index a7b5b396bb..6459eb55bc 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -11,6 +11,7 @@ import 'binding_wrapper.dart'; import 'navigation/time_to_display_tracker.dart'; import 'renderer/renderer.dart'; import 'screenshot/sentry_screenshot_quality.dart'; +import 'event_processor/screenshot_event_processor.dart'; import 'sentry_flutter.dart'; import 'sentry_privacy_options.dart'; import 'sentry_replay_options.dart';