11import 'package:flutter/rendering.dart' ;
2- import 'package:flutter/services.dart' ;
32import 'package:flutter/widgets.dart' ;
43import 'package:meta/meta.dart' ;
54
65import '../../sentry_flutter.dart' ;
76import '../sentry_asset_bundle.dart' ;
7+ import 'masking_config.dart' ;
88
99@internal
1010class WidgetFilter {
1111 final items = < WidgetFilterItem > [];
1212 final SentryLogger logger;
13- final bool redactText;
14- final bool redactImages;
13+ final SentryMaskingConfig config;
1514 static const _defaultColor = Color .fromARGB (255 , 0 , 0 , 0 );
1615 late double _pixelRatio;
1716 late Rect _bounds;
1817 final _warnedWidgets = < int > {};
19- final AssetBundle _rootAssetBundle;
2018
21- WidgetFilter (
22- {required this .redactText,
23- required this .redactImages,
24- required this .logger,
25- @visibleForTesting AssetBundle ? rootAssetBundle})
26- : _rootAssetBundle = rootAssetBundle ?? rootBundle;
19+ /// Used to test _obscureElementOrParent
20+ @visibleForTesting
21+ bool throwInObscure = false ;
22+
23+ WidgetFilter (this .config, this .logger);
2724
2825 void obscure (BuildContext context, double pixelRatio, Rect bounds) {
2926 _pixelRatio = pixelRatio;
3027 _bounds = bounds;
3128 items.clear ();
3229 if (context is Element ) {
33- _obscure (context);
30+ _process (context);
3431 } else {
35- context.visitChildElements (_obscure );
32+ context.visitChildElements (_process );
3633 }
3734 }
3835
39- void _obscure (Element element) {
36+ void _process (Element element) {
4037 final widget = element.widget;
4138
4239 if (! _isVisible (widget)) {
@@ -47,47 +44,64 @@ class WidgetFilter {
4744 return ;
4845 }
4946
50- final obscured = _obscureIfNeeded (element, widget);
51- if (! obscured) {
52- element.visitChildElements (_obscure);
47+ final decision = config.shouldMask (element, widget);
48+ switch (decision) {
49+ case SentryMaskingDecision .mask:
50+ final item = _obscureElementOrParent (element, widget);
51+ if (item != null ) {
52+ items.add (item);
53+ }
54+ break ;
55+ case SentryMaskingDecision .unmask:
56+ logger (SentryLevel .debug, "WidgetFilter unmasked: $widget " );
57+ break ;
58+ case SentryMaskingDecision .continueProcessing:
59+ // If this element should not be obscured, visit and check its children.
60+ element.visitChildElements (_process);
61+ break ;
5362 }
5463 }
5564
65+ /// Determine the color and bounding box of the widget.
66+ /// If the widget is offscreen, returns null.
67+ /// If the widget cannot be obscured, obscures the parent.
5668 @pragma ('vm:prefer-inline' )
57- bool _obscureIfNeeded (Element element, Widget widget) {
58- Color ? color;
59-
60- if (redactText && widget is Text ) {
61- color = widget.style? .color;
62- } else if (redactText && widget is EditableText ) {
63- 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 ;
69+ WidgetFilterItem ? _obscureElementOrParent (Element element, Widget widget) {
70+ while (true ) {
71+ try {
72+ return _obscure (element, widget);
73+ } catch (e, stackTrace) {
74+ final parent = element.parent;
75+ if (! _warnedWidgets.contains (widget.hashCode)) {
76+ _warnedWidgets.add (widget.hashCode);
77+ logger (
78+ SentryLevel .warning,
79+ 'WidgetFilter cannot mask widget $widget : $e .'
80+ 'Obscuring the parent instead: ${parent ?.widget }.' ,
81+ stackTrace: stackTrace);
7182 }
83+ if (parent == null ) {
84+ return WidgetFilterItem (_defaultColor, _bounds);
85+ }
86+ element = parent;
87+ widget = element.widget;
7288 }
73- color = widget.color;
74- } else {
75- // No other type is currently obscured.
76- return false ;
77- }
78-
79- final renderObject = element.renderObject;
80- if (renderObject is ! RenderBox ) {
81- _cantObscure (widget, "its renderObject is not a RenderBox" );
82- return false ;
8389 }
90+ }
8491
85- var rect = _boundingBox (renderObject);
92+ /// Determine the color and bounding box of the widget.
93+ /// If the widget is offscreen, returns null.
94+ /// This function may throw in which case the caller is responsible for
95+ /// calling it again on the parent element.
96+ @pragma ('vm:prefer-inline' )
97+ WidgetFilterItem ? _obscure (Element element, Widget widget) {
98+ final RenderBox renderBox = element.renderObject as RenderBox ;
99+ var rect = _boundingBox (renderBox);
86100
87101 // If it's a clipped render object, use parent's offset and size.
88102 // This helps with text fields which often have oversized render objects.
89- if (renderObject .parent is RenderStack ) {
90- final renderStack = (renderObject .parent as RenderStack );
103+ if (renderBox .parent is RenderStack ) {
104+ final renderStack = (renderBox .parent as RenderStack );
91105 final clipBehavior = renderStack.clipBehavior;
92106 if (clipBehavior == Clip .hardEdge ||
93107 clipBehavior == Clip .antiAlias ||
@@ -102,19 +116,37 @@ class WidgetFilter {
102116 logger (SentryLevel .debug, "WidgetFilter skipping offscreen: $widget " );
103117 return true ;
104118 }());
105- return false ;
119+ return null ;
106120 }
107121
108- items.add (WidgetFilterItem (color ?? _defaultColor, rect));
109122 assert (() {
110- logger (SentryLevel .debug, "WidgetFilter obscuring : $widget " );
123+ logger (SentryLevel .debug, "WidgetFilter masking : $widget " );
111124 return true ;
112125 }());
113126
114- return true ;
127+ Color ? color;
128+ if (widget is Text ) {
129+ color = (widget).style? .color;
130+ } else if (widget is EditableText ) {
131+ color = (widget).style.color;
132+ } else if (widget is Image ) {
133+ color = (widget).color;
134+ }
135+
136+ // test-only code
137+ assert (() {
138+ if (throwInObscure) {
139+ throwInObscure = false ;
140+ return false ;
141+ }
142+ return true ;
143+ }());
144+
145+ return WidgetFilterItem (color ?? _defaultColor, rect);
115146 }
116147
117148 // We cut off some widgets early because they're not visible at all.
149+ @pragma ('vm:prefer-inline' )
118150 bool _isVisible (Widget widget) {
119151 if (widget is Visibility ) {
120152 return widget.visible;
@@ -128,9 +160,10 @@ class WidgetFilter {
128160 return true ;
129161 }
130162
131- @visibleForTesting
163+ @internal
132164 @pragma ('vm:prefer-inline' )
133- bool isBuiltInAssetImage (AssetBundleImageProvider image) {
165+ static bool isBuiltInAssetImage (
166+ AssetBundleImageProvider image, AssetBundle rootAssetBundle) {
134167 late final AssetBundle ? bundle;
135168 if (image is AssetImage ) {
136169 bundle = image.bundle;
@@ -140,17 +173,8 @@ class WidgetFilter {
140173 return false ;
141174 }
142175 return (bundle == null ||
143- bundle == _rootAssetBundle ||
144- (bundle is SentryAssetBundle && bundle.bundle == _rootAssetBundle));
145- }
146-
147- @pragma ('vm:prefer-inline' )
148- void _cantObscure (Widget widget, String message) {
149- if (! _warnedWidgets.contains (widget.hashCode)) {
150- _warnedWidgets.add (widget.hashCode);
151- logger (SentryLevel .warning,
152- "WidgetFilter cannot obscure widget $widget : $message " );
153- }
176+ bundle == rootAssetBundle ||
177+ (bundle is SentryAssetBundle && bundle.bundle == rootAssetBundle));
154178 }
155179
156180 @pragma ('vm:prefer-inline' )
@@ -165,9 +189,21 @@ class WidgetFilter {
165189 }
166190}
167191
192+ @internal
168193class WidgetFilterItem {
169194 final Color color;
170195 final Rect bounds;
171196
172197 const WidgetFilterItem (this .color, this .bounds);
173198}
199+
200+ extension on Element {
201+ Element ? get parent {
202+ Element ? result;
203+ visitAncestorElements ((el) {
204+ result = el;
205+ return false ;
206+ });
207+ return result;
208+ }
209+ }
0 commit comments