Skip to content

Commit ec669f4

Browse files
committed
refactor widget filter to handle errors gracefully
1 parent fa68455 commit ec669f4

File tree

1 file changed

+77
-39
lines changed

1 file changed

+77
-39
lines changed

flutter/lib/src/replay/widget_filter.dart

Lines changed: 77 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ class WidgetFilter {
2222
_bounds = bounds;
2323
items.clear();
2424
if (context is Element) {
25-
_obscure(context);
25+
_process(context);
2626
} else {
27-
context.visitChildElements(_obscure);
27+
context.visitChildElements(_process);
2828
}
2929
}
3030

31-
void _obscure(Element element) {
31+
void _process(Element element) {
3232
final widget = element.widget;
3333

3434
if (!_isVisible(widget)) {
@@ -39,46 +39,73 @@ class WidgetFilter {
3939
return;
4040
}
4141

42-
final obscured = _obscureIfNeeded(element, widget);
43-
if (!obscured) {
44-
element.visitChildElements(_obscure);
42+
if (!_shouldObscure(element, widget)) {
43+
// If this element should not be obscured, visit and check its children.
44+
element.visitChildElements(_process);
45+
} else {
46+
final item = _obscureElementOrParent(element, widget);
47+
if (item != null) {
48+
items.add(item);
49+
}
4550
}
4651
}
4752

4853
@pragma('vm:prefer-inline')
49-
bool _obscureIfNeeded(Element element, Widget widget) {
54+
bool _shouldObscure(Element element, Widget widget) {
55+
// Check if we should mask this widget based on the configuration.
5056
final maskingConfig = config[widget.runtimeType];
5157
if (maskingConfig == null) {
5258
return false;
5359
} else if (!maskingConfig.shouldMask(element, widget)) {
54-
logger(SentryLevel.debug, "WidgetFilter skipping: $widget");
55-
return false;
56-
}
57-
58-
Color? color;
59-
if (widget is Text) {
60-
color = widget.style?.color;
61-
} else if (widget is EditableText) {
62-
color = widget.style.color;
63-
} else if (widget is Image) {
64-
color = widget.color;
65-
} else {
66-
// No other type is currently obscured.
60+
assert(() {
61+
logger(SentryLevel.debug, "WidgetFilter skipping: $widget");
62+
return true;
63+
}());
6764
return false;
6865
}
66+
return true;
67+
}
6968

70-
final renderObject = element.renderObject;
71-
if (renderObject is! RenderBox) {
72-
_cantObscure(widget, "its renderObject is not a RenderBox");
73-
return false;
69+
/// Determine the color and bounding box of the widget.
70+
/// If the widget is offscreen, returns null.
71+
/// If the widget cannot be obscured, obscures the parent.
72+
@pragma('vm:prefer-inline')
73+
WidgetFilterItem? _obscureElementOrParent(Element element, Widget widget) {
74+
while (true) {
75+
try {
76+
return _obscure(element, widget);
77+
} catch (e, stackTrace) {
78+
final parent = element.parent;
79+
if (!_warnedWidgets.contains(widget.hashCode)) {
80+
_warnedWidgets.add(widget.hashCode);
81+
logger(
82+
SentryLevel.warning,
83+
'WidgetFilter cannot obscure widget $widget: $e.'
84+
'Obscuring the parent instead: ${parent?.widget}.',
85+
stackTrace: stackTrace);
86+
}
87+
if (parent == null) {
88+
return WidgetFilterItem(_defaultColor, _bounds);
89+
}
90+
element = parent;
91+
widget = element.widget;
92+
}
7493
}
94+
}
7595

76-
var rect = _boundingBox(renderObject);
96+
/// Determine the color and bounding box of the widget.
97+
/// If the widget is offscreen, returns null.
98+
/// This function may throw in which case the caller is responsible for
99+
/// calling it again on the parent element.
100+
@pragma('vm:prefer-inline')
101+
WidgetFilterItem? _obscure(Element element, Widget widget) {
102+
final RenderBox renderBox = element.renderObject as RenderBox;
103+
var rect = _boundingBox(renderBox);
77104

78105
// If it's a clipped render object, use parent's offset and size.
79106
// This helps with text fields which often have oversized render objects.
80-
if (renderObject.parent is RenderStack) {
81-
final renderStack = (renderObject.parent as RenderStack);
107+
if (renderBox.parent is RenderStack) {
108+
final renderStack = (renderBox.parent as RenderStack);
82109
final clipBehavior = renderStack.clipBehavior;
83110
if (clipBehavior == Clip.hardEdge ||
84111
clipBehavior == Clip.antiAlias ||
@@ -93,19 +120,28 @@ class WidgetFilter {
93120
logger(SentryLevel.debug, "WidgetFilter skipping offscreen: $widget");
94121
return true;
95122
}());
96-
return false;
123+
return null;
97124
}
98125

99-
items.add(WidgetFilterItem(color ?? _defaultColor, rect));
100126
assert(() {
101127
logger(SentryLevel.debug, "WidgetFilter obscuring: $widget");
102128
return true;
103129
}());
104130

105-
return true;
131+
Color? color;
132+
if (widget is Text) {
133+
color = (widget).style?.color;
134+
} else if (widget is EditableText) {
135+
color = (widget).style.color;
136+
} else if (widget is Image) {
137+
color = (widget).color;
138+
}
139+
140+
return WidgetFilterItem(color ?? _defaultColor, rect);
106141
}
107142

108143
// We cut off some widgets early because they're not visible at all.
144+
@pragma('vm:prefer-inline')
109145
bool _isVisible(Widget widget) {
110146
if (widget is Visibility) {
111147
return widget.visible;
@@ -136,15 +172,6 @@ class WidgetFilter {
136172
(bundle is SentryAssetBundle && bundle.bundle == rootAssetBundle));
137173
}
138174

139-
@pragma('vm:prefer-inline')
140-
void _cantObscure(Widget widget, String message) {
141-
if (!_warnedWidgets.contains(widget.hashCode)) {
142-
_warnedWidgets.add(widget.hashCode);
143-
logger(SentryLevel.warning,
144-
"WidgetFilter cannot obscure widget $widget: $message");
145-
}
146-
}
147-
148175
@pragma('vm:prefer-inline')
149176
Rect _boundingBox(RenderBox box) {
150177
final offset = box.localToGlobal(Offset.zero);
@@ -194,3 +221,14 @@ class WidgetFilterMaskingConfig {
194221
}
195222
}
196223
}
224+
225+
extension on Element {
226+
Element? get parent {
227+
Element? result;
228+
visitAncestorElements((el) {
229+
result = el;
230+
return false;
231+
});
232+
return result;
233+
}
234+
}

0 commit comments

Comments
 (0)