Skip to content

Commit e2589f5

Browse files
committed
refactor: change redaction logic to custom filters
1 parent 8a111a9 commit e2589f5

File tree

4 files changed

+140
-49
lines changed

4 files changed

+140
-49
lines changed

flutter/lib/src/replay/recorder.dart

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,8 @@ class ScreenshotRecorder {
2222

2323
ScreenshotRecorder(this.config, this.options) {
2424
final replayOptions = options.experimental.replay;
25-
if (replayOptions.redactAllText || replayOptions.redactAllImages) {
26-
_widgetFilter = WidgetFilter(
27-
redactText: replayOptions.redactAllText,
28-
redactImages: replayOptions.redactAllImages,
29-
logger: options.logger);
25+
if (replayOptions.maskingConfig.isNotEmpty) {
26+
_widgetFilter = WidgetFilter(replayOptions.maskingConfig, options.logger);
3027
}
3128
}
3229

flutter/lib/src/replay/widget_filter.dart

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import 'package:flutter/rendering.dart';
2-
import 'package:flutter/services.dart';
32
import 'package:flutter/widgets.dart';
43
import 'package:meta/meta.dart';
54

@@ -10,20 +9,13 @@ import '../sentry_asset_bundle.dart';
109
class WidgetFilter {
1110
final items = <WidgetFilterItem>[];
1211
final SentryLogger logger;
13-
final bool redactText;
14-
final bool redactImages;
12+
final Map<Type, WidgetFilterMaskingConfig> config;
1513
static const _defaultColor = Color.fromARGB(255, 0, 0, 0);
1614
late double _pixelRatio;
1715
late Rect _bounds;
1816
final _warnedWidgets = <int>{};
19-
final AssetBundle _rootAssetBundle;
2017

21-
WidgetFilter(
22-
{required this.redactText,
23-
required this.redactImages,
24-
required this.logger,
25-
@visibleForTesting AssetBundle? rootAssetBundle})
26-
: _rootAssetBundle = rootAssetBundle ?? rootBundle;
18+
WidgetFilter(this.config, this.logger);
2719

2820
void obscure(BuildContext context, double pixelRatio, Rect bounds) {
2921
_pixelRatio = pixelRatio;
@@ -55,21 +47,20 @@ class WidgetFilter {
5547

5648
@pragma('vm:prefer-inline')
5749
bool _obscureIfNeeded(Element element, Widget widget) {
58-
Color? color;
50+
final maskingConfig = config[widget.runtimeType];
51+
if (maskingConfig == null) {
52+
return false;
53+
} else if (!maskingConfig.shouldMask(element, widget)) {
54+
logger(SentryLevel.debug, "WidgetFilter skipping: $widget");
55+
return false;
56+
}
5957

60-
if (redactText && widget is Text) {
58+
Color? color;
59+
if (widget is Text) {
6160
color = widget.style?.color;
62-
} else if (redactText && widget is EditableText) {
61+
} else if (widget is EditableText) {
6362
color = widget.style.color;
64-
} else if (redactImages && widget is Image) {
65-
if (widget.image is AssetBundleImageProvider) {
66-
final image = widget.image as AssetBundleImageProvider;
67-
if (isBuiltInAssetImage(image)) {
68-
logger(SentryLevel.debug,
69-
"WidgetFilter skipping asset: $widget ($image).");
70-
return false;
71-
}
72-
}
63+
} else if (widget is Image) {
7364
color = widget.color;
7465
} else {
7566
// No other type is currently obscured.
@@ -128,9 +119,10 @@ class WidgetFilter {
128119
return true;
129120
}
130121

131-
@visibleForTesting
122+
@internal
132123
@pragma('vm:prefer-inline')
133-
bool isBuiltInAssetImage(AssetBundleImageProvider image) {
124+
static bool isBuiltInAssetImage(
125+
AssetBundleImageProvider image, AssetBundle rootAssetBundle) {
134126
late final AssetBundle? bundle;
135127
if (image is AssetImage) {
136128
bundle = image.bundle;
@@ -140,8 +132,8 @@ class WidgetFilter {
140132
return false;
141133
}
142134
return (bundle == null ||
143-
bundle == _rootAssetBundle ||
144-
(bundle is SentryAssetBundle && bundle.bundle == _rootAssetBundle));
135+
bundle == rootAssetBundle ||
136+
(bundle is SentryAssetBundle && bundle.bundle == rootAssetBundle));
145137
}
146138

147139
@pragma('vm:prefer-inline')
@@ -165,9 +157,40 @@ class WidgetFilter {
165157
}
166158
}
167159

160+
@internal
168161
class WidgetFilterItem {
169162
final Color color;
170163
final Rect bounds;
171164

172165
const WidgetFilterItem(this.color, this.bounds);
173166
}
167+
168+
@internal
169+
class WidgetFilterMaskingConfig {
170+
static const mask = WidgetFilterMaskingConfig._(1, 'mask');
171+
static const show = WidgetFilterMaskingConfig._(2, 'mask');
172+
173+
final int index;
174+
final String _name;
175+
final bool Function(Element, Widget)? _shouldMask;
176+
177+
const WidgetFilterMaskingConfig._(this.index, this._name)
178+
: _shouldMask = null;
179+
const WidgetFilterMaskingConfig.custom(this._shouldMask)
180+
: index = 3,
181+
_name = 'custom';
182+
183+
@override
184+
String toString() => "$WidgetFilterMaskingConfig.$_name";
185+
186+
bool shouldMask(Element element, Widget widget) {
187+
switch (this) {
188+
case mask:
189+
return true;
190+
case show:
191+
return false;
192+
default:
193+
return _shouldMask!(element, widget);
194+
}
195+
}
196+
}

flutter/lib/src/sentry_replay_options.dart

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1+
import 'package:flutter/services.dart';
2+
import 'package:flutter/widgets.dart';
13
import 'package:meta/meta.dart';
24

5+
import 'replay/widget_filter.dart';
6+
37
/// Configuration of the experimental replay feature.
48
class SentryReplayOptions {
9+
SentryReplayOptions() {
10+
redactAllText = true;
11+
redactAllImages = true;
12+
}
13+
514
double? _sessionSampleRate;
615

716
/// A percentage of sessions in which a replay will be created.
@@ -27,12 +36,67 @@ class SentryReplayOptions {
2736
/// Redact all text content. Draws a rectangle of text bounds with text color
2837
/// on top. Currently, only [Text] and [EditableText] Widgets are redacted.
2938
/// Default is enabled.
30-
var redactAllText = true;
39+
set redactAllText(bool value) {
40+
if (value) {
41+
mask(Text);
42+
mask(EditableText);
43+
} else {
44+
unmask(Text);
45+
unmask(EditableText);
46+
}
47+
}
3148

3249
/// Redact all image content. Draws a rectangle of image bounds with image's
3350
/// dominant color on top. Currently, only [Image] widgets are redacted.
3451
/// Default is enabled.
35-
var redactAllImages = true;
52+
set redactAllImages(bool value) {
53+
if (value) {
54+
maskIfTrue(Image, (element, widget) {
55+
widget as Image;
56+
if (widget.image is AssetBundleImageProvider) {
57+
final image = widget.image as AssetBundleImageProvider;
58+
if (WidgetFilter.isBuiltInAssetImage(image, rootBundle)) {
59+
return false;
60+
}
61+
}
62+
return true;
63+
});
64+
} else {
65+
unmask(Image);
66+
}
67+
}
68+
69+
Map<Type, WidgetFilterMaskingConfig> _maskingConfig = {};
70+
bool _finished = false;
71+
72+
/// Once accessed, masking confing cannot change anymore.
73+
@internal
74+
Map<Type, WidgetFilterMaskingConfig> get maskingConfig {
75+
if (_finished) {
76+
return _maskingConfig;
77+
}
78+
_finished = true;
79+
final result =
80+
Map<Type, WidgetFilterMaskingConfig>.unmodifiable(_maskingConfig);
81+
_maskingConfig = result;
82+
return result;
83+
}
84+
85+
/// Mask given widget type in the replay.
86+
void mask(Type type) {
87+
_maskingConfig[type] = WidgetFilterMaskingConfig.mask;
88+
}
89+
90+
/// Unmask given widget type in the replay.
91+
void unmask(Type type) {
92+
_maskingConfig.remove(type);
93+
}
94+
95+
/// Unmask given widget type in the replay if it's masked by default rules
96+
/// [redactAllText] or [redactAllImages].
97+
void maskIfTrue(Type type, bool Function(Element, Widget) shouldMask) {
98+
_maskingConfig[type] = WidgetFilterMaskingConfig.custom(shouldMask);
99+
}
36100

37101
@internal
38102
bool get isEnabled =>

flutter/test/replay/widget_filter_test.dart

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ void main() async {
1313
final rootBundle = TestAssetBundle();
1414
final otherBundle = TestAssetBundle();
1515

16-
final createSut =
17-
({bool redactImages = false, bool redactText = false}) => WidgetFilter(
18-
logger: (level, message, {exception, logger, stackTrace}) {},
19-
redactImages: redactImages,
20-
redactText: redactText,
21-
rootAssetBundle: rootBundle,
22-
);
16+
final createSut = ({bool redactImages = false, bool redactText = false}) {
17+
final replayOptions = SentryReplayOptions();
18+
replayOptions.redactAllImages = redactImages;
19+
replayOptions.redactAllText = redactText;
20+
return WidgetFilter(replayOptions.maskingConfig,
21+
(level, message, {exception, logger, stackTrace}) {});
22+
};
2323

2424
boundsRect(WidgetFilterItem item) =>
2525
'${item.bounds.width.floor()}x${item.bounds.height.floor()}';
@@ -75,20 +75,27 @@ void main() async {
7575
testWidgets(
7676
'recognizes ${newAssetImage('').runtimeType} from the root bundle',
7777
(tester) async {
78-
final sut = createSut(redactImages: true);
79-
80-
expect(sut.isBuiltInAssetImage(newAssetImage('')), isTrue);
81-
expect(sut.isBuiltInAssetImage(newAssetImage('', bundle: rootBundle)),
78+
expect(WidgetFilter.isBuiltInAssetImage(newAssetImage(''), rootBundle),
79+
isTrue);
80+
expect(
81+
WidgetFilter.isBuiltInAssetImage(
82+
newAssetImage('', bundle: rootBundle), rootBundle),
8283
isTrue);
83-
expect(sut.isBuiltInAssetImage(newAssetImage('', bundle: otherBundle)),
84+
expect(
85+
WidgetFilter.isBuiltInAssetImage(
86+
newAssetImage('', bundle: otherBundle), rootBundle),
8487
isFalse);
8588
expect(
86-
sut.isBuiltInAssetImage(newAssetImage('',
87-
bundle: SentryAssetBundle(bundle: rootBundle))),
89+
WidgetFilter.isBuiltInAssetImage(
90+
newAssetImage('',
91+
bundle: SentryAssetBundle(bundle: rootBundle)),
92+
rootBundle),
8893
isTrue);
8994
expect(
90-
sut.isBuiltInAssetImage(newAssetImage('',
91-
bundle: SentryAssetBundle(bundle: otherBundle))),
95+
WidgetFilter.isBuiltInAssetImage(
96+
newAssetImage('',
97+
bundle: SentryAssetBundle(bundle: otherBundle)),
98+
rootBundle),
9299
isFalse);
93100
});
94101
}

0 commit comments

Comments
 (0)