From 333f93a6e91068d83b7f7db556efc58cc654f05d Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 13 Jun 2025 11:06:14 +0100 Subject: [PATCH 01/18] Add sensitive content widget to default masking --- .../sentry_sensitive_content_widget.dart | 15 +++++++++++++++ flutter/lib/src/sentry_privacy_options.dart | 5 +++++ 2 files changed, 20 insertions(+) create mode 100644 flutter/lib/src/screenshot/sentry_sensitive_content_widget.dart diff --git a/flutter/lib/src/screenshot/sentry_sensitive_content_widget.dart b/flutter/lib/src/screenshot/sentry_sensitive_content_widget.dart new file mode 100644 index 0000000000..275b2a154b --- /dev/null +++ b/flutter/lib/src/screenshot/sentry_sensitive_content_widget.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +import 'package:meta/meta.dart'; + +/// Wrapping your widget in [SensitiveContent] will mark it as containing +/// sensitive content when capturing replays. This is used for backward +/// compatibility with older masking configurations. +@experimental +class SensitiveContent extends StatelessWidget { + final Widget child; + + const SensitiveContent(this.child, {super.key}); + + @override + Widget build(BuildContext context) => child; +} \ No newline at end of file diff --git a/flutter/lib/src/sentry_privacy_options.dart b/flutter/lib/src/sentry_privacy_options.dart index cbef72d951..fe70f83d2c 100644 --- a/flutter/lib/src/sentry_privacy_options.dart +++ b/flutter/lib/src/sentry_privacy_options.dart @@ -75,6 +75,11 @@ class SentryPrivacyOptions { )); } + rules.add(const SentryMaskingConstantRule( + mask: true, + name: 'SensitiveContent', + )); + // In Debug mode, check if users explicitly mask (or unmask) widgets that // look like they should be masked, e.g. Videos, WebViews, etc. if (runtimeChecker.isDebugMode()) { From a24067437920405f089f54e869285d19ad07678e Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 13 Jun 2025 11:07:46 +0100 Subject: [PATCH 02/18] Add sensitive content widget to default masking --- .../sentry_sensitive_content_widget.dart | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 flutter/lib/src/screenshot/sentry_sensitive_content_widget.dart diff --git a/flutter/lib/src/screenshot/sentry_sensitive_content_widget.dart b/flutter/lib/src/screenshot/sentry_sensitive_content_widget.dart deleted file mode 100644 index 275b2a154b..0000000000 --- a/flutter/lib/src/screenshot/sentry_sensitive_content_widget.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:meta/meta.dart'; - -/// Wrapping your widget in [SensitiveContent] will mark it as containing -/// sensitive content when capturing replays. This is used for backward -/// compatibility with older masking configurations. -@experimental -class SensitiveContent extends StatelessWidget { - final Widget child; - - const SensitiveContent(this.child, {super.key}); - - @override - Widget build(BuildContext context) => child; -} \ No newline at end of file From 74eb6941d3ca7d448cf5c2eff64f599e244f04f8 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 17 Jun 2025 14:29:37 +0200 Subject: [PATCH 03/18] Update impl --- flutter/lib/src/sentry_privacy_options.dart | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/flutter/lib/src/sentry_privacy_options.dart b/flutter/lib/src/sentry_privacy_options.dart index fe70f83d2c..16d78925ad 100644 --- a/flutter/lib/src/sentry_privacy_options.dart +++ b/flutter/lib/src/sentry_privacy_options.dart @@ -41,6 +41,20 @@ class SentryPrivacyOptions { mask: false, name: 'SentryUnmask', )); + // TODO: check for Flutter version and make this testable + rules.add(SentryMaskingCustomRule( + callback: (Element element, Widget widget) { + dynamic dynWidget = widget; + try { + final sensitivity = dynWidget.sensitivity; + assert(sensitivity is Enum); + return SentryMaskingDecision.mask; + } catch (e) { + return SentryMaskingDecision.continueProcessing; + } + }, + name: 'SensitiveContent', + description: 'Mask SensitiveContent')); // Then, we apply apply rules based on the configuration. if (maskAllImages) { @@ -75,11 +89,6 @@ class SentryPrivacyOptions { )); } - rules.add(const SentryMaskingConstantRule( - mask: true, - name: 'SensitiveContent', - )); - // In Debug mode, check if users explicitly mask (or unmask) widgets that // look like they should be masked, e.g. Videos, WebViews, etc. if (runtimeChecker.isDebugMode()) { From 132f050d579b7f175d3e4bb1250079a99ff3ff87 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 11 Jul 2025 13:21:14 +0200 Subject: [PATCH 04/18] Update rule --- flutter/lib/src/sentry_privacy_options.dart | 75 +++++++++++++++++---- 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/flutter/lib/src/sentry_privacy_options.dart b/flutter/lib/src/sentry_privacy_options.dart index 16d78925ad..0d89db298e 100644 --- a/flutter/lib/src/sentry_privacy_options.dart +++ b/flutter/lib/src/sentry_privacy_options.dart @@ -41,20 +41,6 @@ class SentryPrivacyOptions { mask: false, name: 'SentryUnmask', )); - // TODO: check for Flutter version and make this testable - rules.add(SentryMaskingCustomRule( - callback: (Element element, Widget widget) { - dynamic dynWidget = widget; - try { - final sensitivity = dynWidget.sensitivity; - assert(sensitivity is Enum); - return SentryMaskingDecision.mask; - } catch (e) { - return SentryMaskingDecision.continueProcessing; - } - }, - name: 'SensitiveContent', - description: 'Mask SensitiveContent')); // Then, we apply apply rules based on the configuration. if (maskAllImages) { @@ -89,6 +75,8 @@ class SentryPrivacyOptions { )); } + _maybeAddSensitiveContentRule(rules); + // In Debug mode, check if users explicitly mask (or unmask) widgets that // look like they should be masked, e.g. Videos, WebViews, etc. if (runtimeChecker.isDebugMode()) { @@ -173,6 +161,65 @@ class SentryPrivacyOptions { } } +/// Adds a masking rule for the [SensitiveContent] widget if we're running on a +/// Flutter SDK that doesn't natively include it yet (prior to 3.33.0). +/// +/// The rule masks any widget that exposes a `sensitivity` property which is an +/// [Enum]. This is how the future [SensitiveContent] widget can be detected +/// without depending on its type directly (which would fail to compile on +/// older Flutter versions). +void _maybeAddSensitiveContentRule(List rules) { + const requiredMajor = 3; + const requiredMinor = 33; + final flutterVersion = FlutterVersion.version; + + // Only add the rule if we can statically determine that the running + // Flutter SDK is at least 3.33 – that is the first version that contains + // the SensitiveContent widget. For older SDKs we skip the rule entirely so + // that the dynamic `sensitivity` lookup is tree-shaken away. + if (flutterVersion == null) { + return; + } + + final parts = flutterVersion.split('.'); + if (parts.length < 2) { + // Malformed version string – be safe and skip. + return; + } + + final major = int.tryParse(parts[0]) ?? 0; + final minor = int.tryParse(parts[1]) ?? 0; + + final isNewEnough = major > requiredMajor || + (major == requiredMajor && minor >= requiredMinor); + + if (!isNewEnough) { + // Older than 3.33 – skip. + return; + } + + rules.add(SentryMaskingCustomRule( + callback: _maskSensitiveContent, + name: 'SensitiveContent', + description: 'Mask SensitiveContent widget.', + )); +} + +/// Callback that detects the future `SensitiveContent` widget by checking for +/// the presence of a `sensitivity` property at runtime. +SentryMaskingDecision _maskSensitiveContent(Element element, Widget widget) { + try { + final dynamic dynWidget = widget; + final sensitivity = dynWidget.sensitivity; + // If the property exists, we assume this is the SensitiveContent widget. + assert(sensitivity is Enum); + return SentryMaskingDecision.mask; + } catch (_) { + // Property not found – continue processing other rules. + return SentryMaskingDecision.continueProcessing; + } +} + SentryMaskingDecision _maskImagesExceptAssets(Element element, Image widget) { final image = widget.image; if (image is AssetBundleImageProvider) { From 591b9c3ef17e3c1346f211ec4ab2a2d5b3ede9ea Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 11 Jul 2025 13:21:54 +0200 Subject: [PATCH 05/18] Update doc --- flutter/lib/src/sentry_privacy_options.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/flutter/lib/src/sentry_privacy_options.dart b/flutter/lib/src/sentry_privacy_options.dart index 0d89db298e..8a51e7d6a9 100644 --- a/flutter/lib/src/sentry_privacy_options.dart +++ b/flutter/lib/src/sentry_privacy_options.dart @@ -161,8 +161,7 @@ class SentryPrivacyOptions { } } -/// Adds a masking rule for the [SensitiveContent] widget if we're running on a -/// Flutter SDK that doesn't natively include it yet (prior to 3.33.0). +/// Adds a masking rule for the [SensitiveContent] widget. /// /// The rule masks any widget that exposes a `sensitivity` property which is an /// [Enum]. This is how the future [SensitiveContent] widget can be detected From 5ce2839b08f147ef91af3745bf70e61c73bf5b89 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 11 Jul 2025 13:22:29 +0200 Subject: [PATCH 06/18] Update --- flutter/lib/src/sentry_privacy_options.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/lib/src/sentry_privacy_options.dart b/flutter/lib/src/sentry_privacy_options.dart index 8a51e7d6a9..cef9b2d948 100644 --- a/flutter/lib/src/sentry_privacy_options.dart +++ b/flutter/lib/src/sentry_privacy_options.dart @@ -168,8 +168,6 @@ class SentryPrivacyOptions { /// without depending on its type directly (which would fail to compile on /// older Flutter versions). void _maybeAddSensitiveContentRule(List rules) { - const requiredMajor = 3; - const requiredMinor = 33; final flutterVersion = FlutterVersion.version; // Only add the rule if we can statically determine that the running @@ -186,6 +184,8 @@ void _maybeAddSensitiveContentRule(List rules) { return; } + const requiredMajor = 3; + const requiredMinor = 33; final major = int.tryParse(parts[0]) ?? 0; final minor = int.tryParse(parts[1]) ?? 0; From dfce0e8a0964fb5963d1f51fba2159c33dc9716d Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 17 Jul 2025 12:21:07 +0200 Subject: [PATCH 07/18] Update sentry_privacy_options.dart --- flutter/lib/src/sentry_privacy_options.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/flutter/lib/src/sentry_privacy_options.dart b/flutter/lib/src/sentry_privacy_options.dart index cef9b2d948..265976c867 100644 --- a/flutter/lib/src/sentry_privacy_options.dart +++ b/flutter/lib/src/sentry_privacy_options.dart @@ -164,7 +164,7 @@ class SentryPrivacyOptions { /// Adds a masking rule for the [SensitiveContent] widget. /// /// The rule masks any widget that exposes a `sensitivity` property which is an -/// [Enum]. This is how the future [SensitiveContent] widget can be detected +/// [Enum]. This is how the [SensitiveContent] widget can be detected /// without depending on its type directly (which would fail to compile on /// older Flutter versions). void _maybeAddSensitiveContentRule(List rules) { @@ -172,8 +172,7 @@ void _maybeAddSensitiveContentRule(List rules) { // Only add the rule if we can statically determine that the running // Flutter SDK is at least 3.33 – that is the first version that contains - // the SensitiveContent widget. For older SDKs we skip the rule entirely so - // that the dynamic `sensitivity` lookup is tree-shaken away. + // the SensitiveContent widget. For older SDKs we skip the rule entirely. if (flutterVersion == null) { return; } From f76a31df1ebe4e17eecbe5afb2a5641ad382360c Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 17 Jul 2025 13:56:08 +0200 Subject: [PATCH 08/18] Update --- flutter/lib/src/screenshot/recorder.dart | 5 +- flutter/lib/src/sentry_privacy_options.dart | 82 +++++++++---------- .../test/screenshot/masking_config_test.dart | 33 +++++++- .../test/screenshot/widget_filter_test.dart | 4 +- 4 files changed, 76 insertions(+), 48 deletions(-) diff --git a/flutter/lib/src/screenshot/recorder.dart b/flutter/lib/src/screenshot/recorder.dart index 2ae1ae28aa..90f82f852d 100644 --- a/flutter/lib/src/screenshot/recorder.dart +++ b/flutter/lib/src/screenshot/recorder.dart @@ -5,6 +5,7 @@ import 'dart:ui'; import 'package:flutter/cupertino.dart' as cupertino; import 'package:flutter/material.dart' as material; import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart' as widgets; import 'package:meta/meta.dart'; @@ -34,8 +35,8 @@ class ScreenshotRecorder { }) { privacyOptions ??= options.privacy; - final maskingConfig = - privacyOptions.buildMaskingConfig(_log, options.runtimeChecker); + final maskingConfig = privacyOptions.buildMaskingConfig( + _log, options.runtimeChecker, FlutterVersion.version); _maskingConfig = maskingConfig.length > 0 ? maskingConfig : null; } diff --git a/flutter/lib/src/sentry_privacy_options.dart b/flutter/lib/src/sentry_privacy_options.dart index 265976c867..4f08521c87 100644 --- a/flutter/lib/src/sentry_privacy_options.dart +++ b/flutter/lib/src/sentry_privacy_options.dart @@ -27,8 +27,8 @@ class SentryPrivacyOptions { Iterable get userMaskingRules => _userMaskingRules; @internal - SentryMaskingConfig buildMaskingConfig( - SdkLogCallback logger, RuntimeChecker runtimeChecker) { + SentryMaskingConfig buildMaskingConfig(SdkLogCallback logger, + RuntimeChecker runtimeChecker, String? flutterVersion) { // First, we collect rules defined by the user (so they're applied first). final rules = _userMaskingRules.toList(); @@ -75,7 +75,7 @@ class SentryPrivacyOptions { )); } - _maybeAddSensitiveContentRule(rules); + maybeAddSensitiveContentRule(rules, flutterVersion); // In Debug mode, check if users explicitly mask (or unmask) widgets that // look like they should be masked, e.g. Videos, WebViews, etc. @@ -161,63 +161,63 @@ class SentryPrivacyOptions { } } -/// Adds a masking rule for the [SensitiveContent] widget. -/// -/// The rule masks any widget that exposes a `sensitivity` property which is an -/// [Enum]. This is how the [SensitiveContent] widget can be detected -/// without depending on its type directly (which would fail to compile on -/// older Flutter versions). -void _maybeAddSensitiveContentRule(List rules) { - final flutterVersion = FlutterVersion.version; - - // Only add the rule if we can statically determine that the running - // Flutter SDK is at least 3.33 – that is the first version that contains - // the SensitiveContent widget. For older SDKs we skip the rule entirely. - if (flutterVersion == null) { - return; - } +/// Returns `true` if a SensitiveContent masking rule _should_ be added for a +/// given [flutterVersion] string. The SensitiveContent widget was introduced +/// in Flutter 3.33, therefore we only add the masking rule when the detected +/// version is >= 3.33. +bool _shouldAddSensitiveContentRule(String? flutterVersion) { + if (flutterVersion == null) return false; final parts = flutterVersion.split('.'); if (parts.length < 2) { // Malformed version string – be safe and skip. - return; + return false; } const requiredMajor = 3; const requiredMinor = 33; - final major = int.tryParse(parts[0]) ?? 0; - final minor = int.tryParse(parts[1]) ?? 0; + final major = int.tryParse(parts[0]); + final minor = int.tryParse(parts[1]); + if (major == null || minor == null) { + // Not numeric – treat as unknown. + return false; + } - final isNewEnough = major > requiredMajor || + return major > requiredMajor || (major == requiredMajor && minor >= requiredMinor); +} - if (!isNewEnough) { - // Older than 3.33 – skip. - return; +/// Adds a masking rule for the [SensitiveContent] widget. +/// +/// The rule masks any widget that exposes a `sensitivity` property which is an +/// [Enum]. This is how the [SensitiveContent] widget can be detected +/// without depending on its type directly (which would fail to compile on +/// older Flutter versions). +@visibleForTesting +void maybeAddSensitiveContentRule( + List rules, String? flutterVersion) { + if (!_shouldAddSensitiveContentRule(flutterVersion)) return; + + SentryMaskingDecision maskSensitiveContent(Element element, Widget widget) { + try { + final dynamic dynWidget = widget; + final sensitivity = dynWidget.sensitivity; + // If the property exists, we assume this is the SensitiveContent widget. + assert(sensitivity is Enum); + return SentryMaskingDecision.mask; + } catch (_) { + // Property not found – continue processing other rules. + return SentryMaskingDecision.continueProcessing; + } } rules.add(SentryMaskingCustomRule( - callback: _maskSensitiveContent, + callback: maskSensitiveContent, name: 'SensitiveContent', description: 'Mask SensitiveContent widget.', )); } -/// Callback that detects the future `SensitiveContent` widget by checking for -/// the presence of a `sensitivity` property at runtime. -SentryMaskingDecision _maskSensitiveContent(Element element, Widget widget) { - try { - final dynamic dynWidget = widget; - final sensitivity = dynWidget.sensitivity; - // If the property exists, we assume this is the SensitiveContent widget. - assert(sensitivity is Enum); - return SentryMaskingDecision.mask; - } catch (_) { - // Property not found – continue processing other rules. - return SentryMaskingDecision.continueProcessing; - } -} - SentryMaskingDecision _maskImagesExceptAssets(Element element, Image widget) { final image = widget.image; if (image is AssetBundleImageProvider) { diff --git a/flutter/test/screenshot/masking_config_test.dart b/flutter/test/screenshot/masking_config_test.dart index 992ef67cc3..55f3257842 100644 --- a/flutter/test/screenshot/masking_config_test.dart +++ b/flutter/test/screenshot/masking_config_test.dart @@ -1,3 +1,5 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -146,9 +148,10 @@ void main() async { }); group('$SentryReplayOptions.buildMaskingConfig()', () { - List rulesAsStrings(SentryPrivacyOptions options) { - final config = - options.buildMaskingConfig(MockLogger().call, RuntimeChecker()); + List rulesAsStrings(SentryPrivacyOptions options, + {String? flutterVersion}) { + final config = options.buildMaskingConfig( + MockLogger().call, RuntimeChecker(), flutterVersion); return config.rules .map((rule) => rule.toString()) // These normalize the string on VM & js & wasm: @@ -222,6 +225,30 @@ void main() async { ]); }); + test( + 'SensitiveContent rule is automatically added when current Flutter version is equal or newer than 3.33', + () { + final testCases = { + null: false, + '1.0.0': false, + '3.32.5': false, + '3.33.0': true, + '3.40.0': true, + '4.0.0': true, + '3.a.b': false, + 'invalid': false, + }; + + testCases.forEach((version, shouldAdd) { + final sut = SentryPrivacyOptions(); + expect( + rulesAsStrings(sut, flutterVersion: version).contains( + 'SentryMaskingCustomRule(Mask SensitiveContent widget.)'), + shouldAdd, + reason: 'Test failed with version: $version'); + }); + }); + group('user rules', () { final defaultRules = [ ...alwaysEnabledRules, diff --git a/flutter/test/screenshot/widget_filter_test.dart b/flutter/test/screenshot/widget_filter_test.dart index 315eede3a1..c4be44694c 100644 --- a/flutter/test/screenshot/widget_filter_test.dart +++ b/flutter/test/screenshot/widget_filter_test.dart @@ -29,8 +29,8 @@ void main() async { ..maskAllImages = redactImages ..maskAllText = redactText; logger.clear(); - final maskingConfig = privacyOptions.buildMaskingConfig( - logger.call, runtimeChecker ?? RuntimeChecker()); + final maskingConfig = privacyOptions.buildMaskingConfig(logger.call, + runtimeChecker ?? RuntimeChecker(), FlutterVersion.version); return WidgetFilter(maskingConfig, logger.call); }; From f4c8e581d48e96334cf5266755104d5bbdba36e6 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 24 Jul 2025 21:42:52 +0200 Subject: [PATCH 09/18] Update --- flutter/lib/src/sentry_privacy_options.dart | 42 +++++++++------------ 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/flutter/lib/src/sentry_privacy_options.dart b/flutter/lib/src/sentry_privacy_options.dart index 4f08521c87..ab3f2703bb 100644 --- a/flutter/lib/src/sentry_privacy_options.dart +++ b/flutter/lib/src/sentry_privacy_options.dart @@ -75,7 +75,11 @@ class SentryPrivacyOptions { )); } - maybeAddSensitiveContentRule(rules, flutterVersion); + const flutterVersion = FlutterVersion.version; + if (flutterVersion != null && + _shouldAddSensitiveContentRule(flutterVersion)) { + addSensitiveContentRule(rules, flutterVersion); + } // In Debug mode, check if users explicitly mask (or unmask) widgets that // look like they should be masked, e.g. Videos, WebViews, etc. @@ -165,26 +169,18 @@ class SentryPrivacyOptions { /// given [flutterVersion] string. The SensitiveContent widget was introduced /// in Flutter 3.33, therefore we only add the masking rule when the detected /// version is >= 3.33. -bool _shouldAddSensitiveContentRule(String? flutterVersion) { - if (flutterVersion == null) return false; - - final parts = flutterVersion.split('.'); - if (parts.length < 2) { - // Malformed version string – be safe and skip. - return false; - } - - const requiredMajor = 3; - const requiredMinor = 33; - final major = int.tryParse(parts[0]); - final minor = int.tryParse(parts[1]); - if (major == null || minor == null) { - // Not numeric – treat as unknown. - return false; - } - - return major > requiredMajor || - (major == requiredMajor && minor >= requiredMinor); +bool _shouldAddSensitiveContentRule(String version) { + final dot = version.indexOf('.'); + if (dot == -1) return false; + + final major = int.tryParse(version.substring(0, dot)); + final nextDot = version.indexOf('.', dot + 1); + final minor = int.tryParse( + version.substring(dot + 1, nextDot == -1 ? version.length : nextDot)); + + return major != null && + minor != null && + (major > 3 || (major == 3 && minor >= 33)); } /// Adds a masking rule for the [SensitiveContent] widget. @@ -194,10 +190,8 @@ bool _shouldAddSensitiveContentRule(String? flutterVersion) { /// without depending on its type directly (which would fail to compile on /// older Flutter versions). @visibleForTesting -void maybeAddSensitiveContentRule( +void addSensitiveContentRule( List rules, String? flutterVersion) { - if (!_shouldAddSensitiveContentRule(flutterVersion)) return; - SentryMaskingDecision maskSensitiveContent(Element element, Widget widget) { try { final dynamic dynWidget = widget; From 151a70d4ad6f58d4980f36ed7cf2328fc2b795f1 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 24 Jul 2025 22:40:58 +0200 Subject: [PATCH 10/18] Update impl --- flutter/lib/src/sentry_privacy_options.dart | 4 ++-- flutter/test/screenshot/masking_config_test.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/lib/src/sentry_privacy_options.dart b/flutter/lib/src/sentry_privacy_options.dart index ab3f2703bb..43f83ac9d0 100644 --- a/flutter/lib/src/sentry_privacy_options.dart +++ b/flutter/lib/src/sentry_privacy_options.dart @@ -27,8 +27,8 @@ class SentryPrivacyOptions { Iterable get userMaskingRules => _userMaskingRules; @internal - SentryMaskingConfig buildMaskingConfig(SdkLogCallback logger, - RuntimeChecker runtimeChecker, String? flutterVersion) { + SentryMaskingConfig buildMaskingConfig( + SdkLogCallback logger, RuntimeChecker runtimeChecker) { // First, we collect rules defined by the user (so they're applied first). final rules = _userMaskingRules.toList(); diff --git a/flutter/test/screenshot/masking_config_test.dart b/flutter/test/screenshot/masking_config_test.dart index 55f3257842..0e0383b2d3 100644 --- a/flutter/test/screenshot/masking_config_test.dart +++ b/flutter/test/screenshot/masking_config_test.dart @@ -150,8 +150,8 @@ void main() async { group('$SentryReplayOptions.buildMaskingConfig()', () { List rulesAsStrings(SentryPrivacyOptions options, {String? flutterVersion}) { - final config = options.buildMaskingConfig( - MockLogger().call, RuntimeChecker(), flutterVersion); + final config = + options.buildMaskingConfig(MockLogger().call, RuntimeChecker()); return config.rules .map((rule) => rule.toString()) // These normalize the string on VM & js & wasm: From c8596cdfdc99c08dc2181e1e1b5f2089505e6158 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 28 Jul 2025 17:31:40 +0200 Subject: [PATCH 11/18] Update --- flutter/lib/src/screenshot/recorder.dart | 4 +- flutter/lib/src/sentry_privacy_options.dart | 13 ++++--- .../test/screenshot/masking_config_test.dart | 39 ++++++++++--------- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/flutter/lib/src/screenshot/recorder.dart b/flutter/lib/src/screenshot/recorder.dart index 7d5c9f504a..3fbe795368 100644 --- a/flutter/lib/src/screenshot/recorder.dart +++ b/flutter/lib/src/screenshot/recorder.dart @@ -32,8 +32,8 @@ class ScreenshotRecorder { this.config}) { privacyOptions ??= options.privacy; - final maskingConfig = privacyOptions.buildMaskingConfig( - _log, options.runtimeChecker, FlutterVersion.version); + final maskingConfig = + privacyOptions.buildMaskingConfig(_log, options.runtimeChecker); _maskingConfig = maskingConfig.length > 0 ? maskingConfig : null; } diff --git a/flutter/lib/src/sentry_privacy_options.dart b/flutter/lib/src/sentry_privacy_options.dart index 43f83ac9d0..01c9ba45c1 100644 --- a/flutter/lib/src/sentry_privacy_options.dart +++ b/flutter/lib/src/sentry_privacy_options.dart @@ -76,9 +76,8 @@ class SentryPrivacyOptions { } const flutterVersion = FlutterVersion.version; - if (flutterVersion != null && - _shouldAddSensitiveContentRule(flutterVersion)) { - addSensitiveContentRule(rules, flutterVersion); + if (flutterVersion != null) { + maybeAddSensitiveContentRule(rules, flutterVersion); } // In Debug mode, check if users explicitly mask (or unmask) widgets that @@ -190,8 +189,12 @@ bool _shouldAddSensitiveContentRule(String version) { /// without depending on its type directly (which would fail to compile on /// older Flutter versions). @visibleForTesting -void addSensitiveContentRule( - List rules, String? flutterVersion) { +void maybeAddSensitiveContentRule( + List rules, String flutterVersion) { + if (!_shouldAddSensitiveContentRule(flutterVersion)) { + return; + } + SentryMaskingDecision maskSensitiveContent(Element element, Widget widget) { try { final dynamic dynWidget = widget; diff --git a/flutter/test/screenshot/masking_config_test.dart b/flutter/test/screenshot/masking_config_test.dart index 0e0383b2d3..ef3f44f0b3 100644 --- a/flutter/test/screenshot/masking_config_test.dart +++ b/flutter/test/screenshot/masking_config_test.dart @@ -148,8 +148,7 @@ void main() async { }); group('$SentryReplayOptions.buildMaskingConfig()', () { - List rulesAsStrings(SentryPrivacyOptions options, - {String? flutterVersion}) { + List rulesAsStrings(SentryPrivacyOptions options) { final config = options.buildMaskingConfig(MockLogger().call, RuntimeChecker()); return config.rules @@ -228,26 +227,28 @@ void main() async { test( 'SensitiveContent rule is automatically added when current Flutter version is equal or newer than 3.33', () { - final testCases = { - null: false, - '1.0.0': false, - '3.32.5': false, - '3.33.0': true, - '3.40.0': true, - '4.0.0': true, - '3.a.b': false, - 'invalid': false, - }; + final sut = SentryPrivacyOptions(); + final version = FlutterVersion.version!; + final dot = version.indexOf('.'); + final major = int.tryParse(version.substring(0, dot)); + final nextDot = version.indexOf('.', dot + 1); + final minor = int.tryParse( + version.substring(dot + 1, nextDot == -1 ? version.length : nextDot)); - testCases.forEach((version, shouldAdd) { - final sut = SentryPrivacyOptions(); + if (major! > 3 || (major == 3 && minor! >= 33)) { expect( - rulesAsStrings(sut, flutterVersion: version).contains( + rulesAsStrings(sut).contains( 'SentryMaskingCustomRule(Mask SensitiveContent widget.)'), - shouldAdd, - reason: 'Test failed with version: $version'); - }); - }); + isTrue, + reason: 'Test failed with version: ${FlutterVersion.version}'); + } else { + expect( + rulesAsStrings(sut).contains( + 'SentryMaskingCustomRule(Mask SensitiveContent widget.)'), + isFalse, + reason: 'Test failed with version: ${FlutterVersion.version}'); + } + }, skip: FlutterVersion.version == null); group('user rules', () { final defaultRules = [ From dde113248f860ccc9df24acf97d01cbe91e11138 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 28 Jul 2025 17:32:29 +0200 Subject: [PATCH 12/18] Update --- flutter/lib/src/sentry_privacy_options.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/flutter/lib/src/sentry_privacy_options.dart b/flutter/lib/src/sentry_privacy_options.dart index 01c9ba45c1..c32cc7dee4 100644 --- a/flutter/lib/src/sentry_privacy_options.dart +++ b/flutter/lib/src/sentry_privacy_options.dart @@ -77,7 +77,7 @@ class SentryPrivacyOptions { const flutterVersion = FlutterVersion.version; if (flutterVersion != null) { - maybeAddSensitiveContentRule(rules, flutterVersion); + _maybeAddSensitiveContentRule(rules, flutterVersion); } // In Debug mode, check if users explicitly mask (or unmask) widgets that @@ -188,8 +188,7 @@ bool _shouldAddSensitiveContentRule(String version) { /// [Enum]. This is how the [SensitiveContent] widget can be detected /// without depending on its type directly (which would fail to compile on /// older Flutter versions). -@visibleForTesting -void maybeAddSensitiveContentRule( +void _maybeAddSensitiveContentRule( List rules, String flutterVersion) { if (!_shouldAddSensitiveContentRule(flutterVersion)) { return; From 7c9f696d8bedc3054099bfb636601600f4c90dd2 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 28 Jul 2025 17:33:50 +0200 Subject: [PATCH 13/18] Update --- flutter/lib/src/screenshot/recorder.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/flutter/lib/src/screenshot/recorder.dart b/flutter/lib/src/screenshot/recorder.dart index 3fbe795368..4b0d466d24 100644 --- a/flutter/lib/src/screenshot/recorder.dart +++ b/flutter/lib/src/screenshot/recorder.dart @@ -5,7 +5,6 @@ import 'dart:ui'; import 'package:flutter/cupertino.dart' as cupertino; import 'package:flutter/material.dart' as material; import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart' as widgets; import 'package:meta/meta.dart'; From 088cf625597b70addfe7d607f11377017c9e5cab Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 28 Jul 2025 17:48:00 +0200 Subject: [PATCH 14/18] Update --- flutter/test/screenshot/masking_config_test.dart | 2 +- flutter/test/screenshot/widget_filter_test.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/test/screenshot/masking_config_test.dart b/flutter/test/screenshot/masking_config_test.dart index ef3f44f0b3..3402d023dd 100644 --- a/flutter/test/screenshot/masking_config_test.dart +++ b/flutter/test/screenshot/masking_config_test.dart @@ -127,7 +127,7 @@ void main() async { SentryMaskingDecision.unmask); }); - testWidgets('retuns false if no rule matches', (tester) async { + testWidgets('returns false if no rule matches', (tester) async { final sut = SentryMaskingConfig([ SentryMaskingCustomRule( callback: (e, w) => SentryMaskingDecision.continueProcessing, diff --git a/flutter/test/screenshot/widget_filter_test.dart b/flutter/test/screenshot/widget_filter_test.dart index c4be44694c..315eede3a1 100644 --- a/flutter/test/screenshot/widget_filter_test.dart +++ b/flutter/test/screenshot/widget_filter_test.dart @@ -29,8 +29,8 @@ void main() async { ..maskAllImages = redactImages ..maskAllText = redactText; logger.clear(); - final maskingConfig = privacyOptions.buildMaskingConfig(logger.call, - runtimeChecker ?? RuntimeChecker(), FlutterVersion.version); + final maskingConfig = privacyOptions.buildMaskingConfig( + logger.call, runtimeChecker ?? RuntimeChecker()); return WidgetFilter(maskingConfig, logger.call); }; From 4bbbded02bfe44ed3679af80c8c081d51c7e3beb Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 28 Jul 2025 19:42:56 +0200 Subject: [PATCH 15/18] Update --- .../test/screenshot/masking_config_test.dart | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/flutter/test/screenshot/masking_config_test.dart b/flutter/test/screenshot/masking_config_test.dart index 3402d023dd..1ff5e60c61 100644 --- a/flutter/test/screenshot/masking_config_test.dart +++ b/flutter/test/screenshot/masking_config_test.dart @@ -1,4 +1,3 @@ -import 'package:collection/collection.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -209,7 +208,8 @@ void main() async { 'SentryMaskingConstantRule(mask)', 'SentryMaskingConstantRule(mask)', 'SentryMaskingConstantRule(mask)', - 'SentryMaskingCustomRule(Debug-mode-only warning for potentially sensitive widgets.)' + 'SentryMaskingCustomRule(Debug-mode-only warning for potentially sensitive widgets.)', + ..._maybeWithSensitiveContent(), ]); }); @@ -259,18 +259,27 @@ void main() async { 'SentryMaskingConstantRule(mask)', 'SentryMaskingCustomRule(Debug-mode-only warning for potentially sensitive widgets.)' ]; + test('mask() takes precedence', () { final sut = SentryPrivacyOptions(); sut.mask(); - expect(rulesAsStrings(sut), - ['SentryMaskingConstantRule(mask)', ...defaultRules]); + expect(rulesAsStrings(sut), [ + 'SentryMaskingConstantRule(mask)', + ...defaultRules, + ..._maybeWithSensitiveContent(), + ]); }); + test('unmask() takes precedence', () { final sut = SentryPrivacyOptions(); sut.unmask(); - expect(rulesAsStrings(sut), - ['SentryMaskingConstantRule(unmask)', ...defaultRules]); + expect(rulesAsStrings(sut), [ + 'SentryMaskingConstantRule(unmask)', + ...defaultRules, + ..._maybeWithSensitiveContent(), + ]); }); + test('are ordered in the call order', () { var sut = SentryPrivacyOptions(); sut.mask(); @@ -300,13 +309,15 @@ void main() async { ...defaultRules ]); }); + test('maskCallback() takes precedence', () { final sut = SentryPrivacyOptions(); sut.maskCallback( (Element element, Image widget) => SentryMaskingDecision.mask); expect(rulesAsStrings(sut), [ 'SentryMaskingCustomRule(Custom callback-based rule (description unspecified))', - ...defaultRules + ...defaultRules, + ..._maybeWithSensitiveContent(), ]); }); test('User cannot add $SentryMask and $SentryUnmask rules', () { @@ -328,6 +339,25 @@ void main() async { }); } +List _maybeWithSensitiveContent() { + final version = FlutterVersion.version; + if (version == null) { + return []; + } + final dot = version.indexOf('.'); + final major = int.tryParse(version.substring(0, dot)); + final nextDot = version.indexOf('.', dot + 1); + final minor = int.tryParse( + version.substring(dot + 1, nextDot == -1 ? version.length : nextDot)); + if (major! > 3 || (major == 3 && minor! >= 33)) { + return [ + 'SentryMaskingCustomRule(Mask SensitiveContent widget.)' + ]; + } else { + return []; + } +} + extension on Element { Element findFirstOfType() { late Element result; From 20657ad9d3c36077f20608197cf491afe7d2005c Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 29 Jul 2025 02:33:17 +0200 Subject: [PATCH 16/18] Update --- flutter/test/screenshot/masking_config_test.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/flutter/test/screenshot/masking_config_test.dart b/flutter/test/screenshot/masking_config_test.dart index 1ff5e60c61..b298227cf1 100644 --- a/flutter/test/screenshot/masking_config_test.dart +++ b/flutter/test/screenshot/masking_config_test.dart @@ -170,6 +170,7 @@ void main() async { 'SentryMaskingConstantRule(mask)', 'SentryMaskingConstantRule(mask)', 'SentryMaskingConstantRule(mask)', + ..._maybeWithSensitiveContent(), 'SentryMaskingCustomRule(Debug-mode-only warning for potentially sensitive widgets.)' ]); }); @@ -182,6 +183,7 @@ void main() async { expect(rulesAsStrings(sut), [ ...alwaysEnabledRules, 'SentryMaskingConstantRule(mask)', + ..._maybeWithSensitiveContent(), 'SentryMaskingCustomRule(Debug-mode-only warning for potentially sensitive widgets.)' ]); }); @@ -194,6 +196,7 @@ void main() async { expect(rulesAsStrings(sut), [ ...alwaysEnabledRules, 'SentryMaskingCustomRule(Mask all images except asset images.)', + ..._maybeWithSensitiveContent(), 'SentryMaskingCustomRule(Debug-mode-only warning for potentially sensitive widgets.)' ]); }); @@ -208,8 +211,8 @@ void main() async { 'SentryMaskingConstantRule(mask)', 'SentryMaskingConstantRule(mask)', 'SentryMaskingConstantRule(mask)', - 'SentryMaskingCustomRule(Debug-mode-only warning for potentially sensitive widgets.)', ..._maybeWithSensitiveContent(), + 'SentryMaskingCustomRule(Debug-mode-only warning for potentially sensitive widgets.)' ]); }); @@ -220,6 +223,7 @@ void main() async { ..maskAssetImages = false; expect(rulesAsStrings(sut), [ ...alwaysEnabledRules, + ..._maybeWithSensitiveContent(), 'SentryMaskingCustomRule(Debug-mode-only warning for potentially sensitive widgets.)' ]); }); @@ -257,6 +261,7 @@ void main() async { 'SentryMaskingConstantRule(mask)', 'SentryMaskingConstantRule(mask)', 'SentryMaskingConstantRule(mask)', + ..._maybeWithSensitiveContent(), 'SentryMaskingCustomRule(Debug-mode-only warning for potentially sensitive widgets.)' ]; @@ -266,7 +271,6 @@ void main() async { expect(rulesAsStrings(sut), [ 'SentryMaskingConstantRule(mask)', ...defaultRules, - ..._maybeWithSensitiveContent(), ]); }); @@ -276,7 +280,6 @@ void main() async { expect(rulesAsStrings(sut), [ 'SentryMaskingConstantRule(unmask)', ...defaultRules, - ..._maybeWithSensitiveContent(), ]); }); @@ -317,7 +320,6 @@ void main() async { expect(rulesAsStrings(sut), [ 'SentryMaskingCustomRule(Custom callback-based rule (description unspecified))', ...defaultRules, - ..._maybeWithSensitiveContent(), ]); }); test('User cannot add $SentryMask and $SentryUnmask rules', () { From 32e3fc4538a72a589d086e0d6061681e3a2a8cbb Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 29 Jul 2025 12:29:29 +0200 Subject: [PATCH 17/18] Update --- flutter/lib/src/sentry_privacy_options.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flutter/lib/src/sentry_privacy_options.dart b/flutter/lib/src/sentry_privacy_options.dart index c32cc7dee4..70f1c8e10c 100644 --- a/flutter/lib/src/sentry_privacy_options.dart +++ b/flutter/lib/src/sentry_privacy_options.dart @@ -5,6 +5,9 @@ import 'package:meta/meta.dart'; import '../sentry_flutter.dart'; import 'screenshot/masking_config.dart'; import 'screenshot/widget_filter.dart'; +// ignore: implementation_imports +import 'package:sentry/src/event_processor/enricher/flutter_runtime.dart' + as flutter_runtime; /// Configuration of the experimental privacy feature. class SentryPrivacyOptions { @@ -75,7 +78,7 @@ class SentryPrivacyOptions { )); } - const flutterVersion = FlutterVersion.version; + const flutterVersion = flutter_runtime.FlutterVersion.version; if (flutterVersion != null) { _maybeAddSensitiveContentRule(rules, flutterVersion); } From 686750fc244c819c9a6f15964a48dc868347223e Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 29 Jul 2025 16:37:04 +0200 Subject: [PATCH 18/18] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63e9dfd32f..20d1541503 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Add [SensitiveContent](https://main-api.flutter.dev/flutter/widgets/SensitiveContent-class.html) widget to default masking ([#2989](https://github.com/getsentry/sentry-dart/pull/2989)) + ## 9.6.0-beta.1 ### Fixes