Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
333f93a
Add sensitive content widget to default masking
buenaflor Jun 13, 2025
a240674
Add sensitive content widget to default masking
buenaflor Jun 13, 2025
6d1e35e
Merge branch 'main' into replay/add-sensitive-content
buenaflor Jun 13, 2025
74eb694
Update impl
buenaflor Jun 17, 2025
2e01b33
Merge branch 'main' into replay/add-sensitive-content
buenaflor Jul 11, 2025
132f050
Update rule
buenaflor Jul 11, 2025
591b9c3
Update doc
buenaflor Jul 11, 2025
5ce2839
Update
buenaflor Jul 11, 2025
dfce0e8
Update sentry_privacy_options.dart
buenaflor Jul 17, 2025
efa9362
Merge branch 'main' into replay/add-sensitive-content
buenaflor Jul 17, 2025
f76a31d
Update
buenaflor Jul 17, 2025
b928028
Merge branch 'main' into replay/add-sensitive-content
buenaflor Jul 24, 2025
f4c8e58
Update
buenaflor Jul 24, 2025
151a70d
Update impl
buenaflor Jul 24, 2025
c8596cd
Update
buenaflor Jul 28, 2025
dde1132
Update
buenaflor Jul 28, 2025
7c9f696
Update
buenaflor Jul 28, 2025
088cf62
Update
buenaflor Jul 28, 2025
4bbbded
Update
buenaflor Jul 28, 2025
20657ad
Update
buenaflor Jul 29, 2025
32e3fc4
Update
buenaflor Jul 29, 2025
0c59077
Merge branch 'main' into replay/add-sensitive-content
buenaflor Jul 29, 2025
686750f
Update CHANGELOG
buenaflor Jul 29, 2025
1f8bf63
Merge branch 'main' into replay/add-sensitive-content
buenaflor Aug 6, 2025
e38ab52
Merge branch 'main' into replay/add-sensitive-content
buenaflor Oct 7, 2025
33b5751
Merge branch 'main' into replay/add-sensitive-content
buenaflor Nov 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@

### 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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 🚫 The changelog entry seems to be part of an already released section ## 9.8.0.
    Consider moving the entry to the ## Unreleased section, please.

- Tag all spans with thread info ([#3101](https://github.com/getsentry/sentry-dart/pull/3101))

### Enhancements
Expand Down
58 changes: 58 additions & 0 deletions packages/flutter/lib/src/sentry_privacy_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -75,6 +78,11 @@ class SentryPrivacyOptions {
));
}

const flutterVersion = flutter_runtime.FlutterVersion.version;
if (flutterVersion != null) {
_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.
if (runtimeChecker.isDebugMode()) {
Expand Down Expand Up @@ -162,6 +170,56 @@ class SentryPrivacyOptions {
}
}

/// 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 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 &&
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Could do early returns when either major/minor is null. Also, do we do version check anywhere else? Then we could extract the version reading to a common place and only do the version check here, as we do both now.

minor != null &&
(major > 3 || (major == 3 && minor >= 33));
}

/// 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<SentryMaskingRule> 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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth to double-check the cursor[bot] comment.

return SentryMaskingDecision.mask;
} catch (_) {
// Property not found – continue processing other rules.
return SentryMaskingDecision.continueProcessing;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Masking Behavior Varies Between Modes

The maskSensitiveContent function exhibits inconsistent widget masking behavior between debug and release modes. In debug mode, widgets are only masked if their sensitivity property exists and is an Enum (due to an assert statement; otherwise, the try/catch prevents masking). In release mode, the assert is disabled, causing any widget with a sensitivity property to be masked, regardless of its type. This leads to different masking behavior between development and production environments, making testing unreliable.

Fix in Cursor Fix in Web


rules.add(SentryMaskingCustomRule<Widget>(
callback: maskSensitiveContent,
name: 'SensitiveContent',
description: 'Mask SensitiveContent widget.',
));
}

SentryMaskingDecision _maskImagesExceptAssets(Element element, Image widget) {
final image = widget.image;
if (image is AssetBundleImageProvider) {
Expand Down
72 changes: 66 additions & 6 deletions packages/flutter/test/screenshot/masking_config_test.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
Expand Down Expand Up @@ -125,7 +126,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<Image>(
callback: (e, w) => SentryMaskingDecision.continueProcessing,
Expand Down Expand Up @@ -169,6 +170,7 @@ void main() async {
'SentryMaskingConstantRule<Text>(mask)',
'SentryMaskingConstantRule<EditableText>(mask)',
'SentryMaskingConstantRule<RichText>(mask)',
..._maybeWithSensitiveContent(),
'SentryMaskingCustomRule<Widget>(Debug-mode-only warning for potentially sensitive widgets.)'
]);
});
Expand All @@ -181,6 +183,7 @@ void main() async {
expect(rulesAsStrings(sut), [
...alwaysEnabledRules,
'SentryMaskingConstantRule<Image>(mask)',
..._maybeWithSensitiveContent(),
'SentryMaskingCustomRule<Widget>(Debug-mode-only warning for potentially sensitive widgets.)'
]);
});
Expand All @@ -193,6 +196,7 @@ void main() async {
expect(rulesAsStrings(sut), [
...alwaysEnabledRules,
'SentryMaskingCustomRule<Image>(Mask all images except asset images.)',
..._maybeWithSensitiveContent(),
'SentryMaskingCustomRule<Widget>(Debug-mode-only warning for potentially sensitive widgets.)'
]);
});
Expand All @@ -207,6 +211,7 @@ void main() async {
'SentryMaskingConstantRule<Text>(mask)',
'SentryMaskingConstantRule<EditableText>(mask)',
'SentryMaskingConstantRule<RichText>(mask)',
..._maybeWithSensitiveContent(),
'SentryMaskingCustomRule<Widget>(Debug-mode-only warning for potentially sensitive widgets.)'
]);
});
Expand All @@ -218,31 +223,66 @@ void main() async {
..maskAssetImages = false;
expect(rulesAsStrings(sut), [
...alwaysEnabledRules,
..._maybeWithSensitiveContent(),
'SentryMaskingCustomRule<Widget>(Debug-mode-only warning for potentially sensitive widgets.)'
]);
});

test(
'SensitiveContent rule is automatically added when current Flutter version is equal or newer than 3.33',
() {
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));

if (major! > 3 || (major == 3 && minor! >= 33)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Version Parsing Flaw in Test Code

The version parsing logic in masking_config_test.dart and its _maybeWithSensitiveContent helper function incorrectly uses non-null assertions (major!, minor!) on int.tryParse() results. This lack of null checking can lead to runtime exceptions for malformed version strings, a flaw not present in the main sentry_privacy_options.dart implementation.

Fix in Cursor Fix in Web

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From above, if we create a helepr and test it popertly we can use it here instead of duplicating version parsing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah sorry, I think those are leftovers of when I did the testing, I'll properly expose the function instead

expect(
rulesAsStrings(sut).contains(
'SentryMaskingCustomRule<SensitiveContent>(Mask SensitiveContent widget.)'),
isTrue,
reason: 'Test failed with version: ${FlutterVersion.version}');
} else {
expect(
rulesAsStrings(sut).contains(
'SentryMaskingCustomRule<SensitiveContent>(Mask SensitiveContent widget.)'),
isFalse,
reason: 'Test failed with version: ${FlutterVersion.version}');
}
}, skip: FlutterVersion.version == null);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Version Parsing Vulnerability in SensitiveContent Rule

The version parsing logic in the _maybeWithSensitiveContent() helper function and the SensitiveContent rule test case is unsafe. It can lead to a RangeError if the Flutter version string lacks a dot, and runtime exceptions if int.tryParse() returns null. This is due to missing boundary checks before substring() and reliance on null assertion operators (!) without prior null checks, contrasting with the main implementation's robust error handling.

Additional Locations (1)
Fix in Cursor Fix in Web


group('user rules', () {
final defaultRules = [
...alwaysEnabledRules,
'SentryMaskingCustomRule<Image>(Mask all images except asset images.)',
'SentryMaskingConstantRule<Text>(mask)',
'SentryMaskingConstantRule<EditableText>(mask)',
'SentryMaskingConstantRule<RichText>(mask)',
..._maybeWithSensitiveContent(),
'SentryMaskingCustomRule<Widget>(Debug-mode-only warning for potentially sensitive widgets.)'
];

test('mask() takes precedence', () {
final sut = SentryPrivacyOptions();
sut.mask<Image>();
expect(rulesAsStrings(sut),
['SentryMaskingConstantRule<Image>(mask)', ...defaultRules]);
expect(rulesAsStrings(sut), [
'SentryMaskingConstantRule<Image>(mask)',
...defaultRules,
]);
});

test('unmask() takes precedence', () {
final sut = SentryPrivacyOptions();
sut.unmask<Image>();
expect(rulesAsStrings(sut),
['SentryMaskingConstantRule<Image>(unmask)', ...defaultRules]);
expect(rulesAsStrings(sut), [
'SentryMaskingConstantRule<Image>(unmask)',
...defaultRules,
]);
});

test('are ordered in the call order', () {
var sut = SentryPrivacyOptions();
sut.mask<Image>();
Expand Down Expand Up @@ -272,13 +312,14 @@ void main() async {
...defaultRules
]);
});

test('maskCallback() takes precedence', () {
final sut = SentryPrivacyOptions();
sut.maskCallback(
(Element element, Image widget) => SentryMaskingDecision.mask);
expect(rulesAsStrings(sut), [
'SentryMaskingCustomRule<Image>(Custom callback-based rule (description unspecified))',
...defaultRules
...defaultRules,
]);
});
test('User cannot add $SentryMask and $SentryUnmask rules', () {
Expand Down Expand Up @@ -320,6 +361,25 @@ void main() async {
});
}

List<String> _maybeWithSensitiveContent() {
final version = FlutterVersion.version;
if (version == null) {
return [];
}
final dot = version.indexOf('.');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, duplicated logic for the third time.

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<SensitiveContent>(Mask SensitiveContent widget.)'
];
} else {
return [];
}
}

extension on Element {
Element findFirstOfType<T>() {
late Element result;
Expand Down
Loading