diff --git a/CHANGELOG.md b/CHANGELOG.md index b12c189997..1d2fb4e2b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- Session replay Alpha for Android and iOS ([#2208](https://github.com/getsentry/sentry-dart/pull/2208), [#2269](https://github.com/getsentry/sentry-dart/pull/2269)). +- Session replay Alpha for Android and iOS ([#2208](https://github.com/getsentry/sentry-dart/pull/2208), [#2269](https://github.com/getsentry/sentry-dart/pull/2269), [#2236](https://github.com/getsentry/sentry-dart/pull/2236)). To try out replay, you can set following options (access is limited to early access orgs on Sentry. If you're interested, [sign up for the waitlist](https://sentry.io/lp/mobile-replay-beta/)): @@ -27,6 +27,7 @@ ... options.allowUrls = ["^https://sentry.com.*\$", "my-custom-domain"]; options.denyUrls = ["^.*ends-with-this\$", "denied-url"]; + options.denyUrls = ["^.*ends-with-this\$", "denied-url"]; }, appRunner: () => runApp(MyApp()), ); diff --git a/flutter/lib/src/replay/widget_filter.dart b/flutter/lib/src/replay/widget_filter.dart index f269dd041f..1f66a42f1a 100644 --- a/flutter/lib/src/replay/widget_filter.dart +++ b/flutter/lib/src/replay/widget_filter.dart @@ -1,3 +1,4 @@ +import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; @@ -77,25 +78,25 @@ class WidgetFilter { final renderObject = element.renderObject; if (renderObject is! RenderBox) { - _cantObscure(widget, "it's renderObject is not a RenderBox"); + _cantObscure(widget, "its renderObject is not a RenderBox"); return false; } - final size = element.size; - if (size == null) { - _cantObscure(widget, "it's renderObject has a null size"); - return false; + var rect = _boundingBox(renderObject); + + // If it's a clipped render object, use parent's offset and size. + // This helps with text fields which often have oversized render objects. + if (renderObject.parent is RenderStack) { + final renderStack = (renderObject.parent as RenderStack); + final clipBehavior = renderStack.clipBehavior; + if (clipBehavior == Clip.hardEdge || + clipBehavior == Clip.antiAlias || + clipBehavior == Clip.antiAliasWithSaveLayer) { + final clipRect = _boundingBox(renderStack); + rect = rect.intersect(clipRect); + } } - final offset = renderObject.localToGlobal(Offset.zero); - - final rect = Rect.fromLTWH( - offset.dx * _pixelRatio, - offset.dy * _pixelRatio, - size.width * _pixelRatio, - size.height * _pixelRatio, - ); - if (!rect.overlaps(_bounds)) { assert(() { logger(SentryLevel.debug, "WidgetFilter skipping offscreen: $widget"); @@ -151,6 +152,17 @@ class WidgetFilter { "WidgetFilter cannot obscure widget $widget: $message"); } } + + @pragma('vm:prefer-inline') + Rect _boundingBox(RenderBox box) { + final offset = box.localToGlobal(Offset.zero); + return Rect.fromLTWH( + offset.dx * _pixelRatio, + offset.dy * _pixelRatio, + box.size.width * _pixelRatio, + box.size.height * _pixelRatio, + ); + } } class WidgetFilterItem { diff --git a/flutter/test/replay/test_widget.dart b/flutter/test/replay/test_widget.dart index c5321ce750..d800a3ef12 100644 --- a/flutter/test/replay/test_widget.dart +++ b/flutter/test/replay/test_widget.dart @@ -33,6 +33,22 @@ Future pumpTestElement(WidgetTester tester, Opacity(opacity: 0, child: newImage()), Offstage(offstage: true, child: Text('Offstage text')), Offstage(offstage: true, child: newImage()), + Text(dummyText), + SizedBox( + width: 100, + height: 20, + child: Stack(children: [ + Positioned( + top: 0, + left: 0, + width: 50, + child: Text(dummyText)), + Positioned( + top: 0, + left: 0, + width: 50, + child: newImage(width: 500, height: 500)), + ])) ], ), ), @@ -55,4 +71,10 @@ final testImageData = Uint8List.fromList([ // This comment prevents dartfmt reformatting this to single-item lines. ]); -Image newImage() => Image.memory(testImageData, width: 1, height: 1); +Image newImage({double width = 1, double height = 1}) => Image.memory( + testImageData, + width: width, + height: height, + ); + +const dummyText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'; diff --git a/flutter/test/replay/widget_filter_test.dart b/flutter/test/replay/widget_filter_test.dart index 3f4136b90a..e5787431bd 100644 --- a/flutter/test/replay/widget_filter_test.dart +++ b/flutter/test/replay/widget_filter_test.dart @@ -21,12 +21,15 @@ void main() async { rootAssetBundle: rootBundle, ); + boundsRect(WidgetFilterItem item) => + '${item.bounds.width.floor()}x${item.bounds.height.floor()}'; + group('redact text', () { testWidgets('redacts the correct number of elements', (tester) async { final sut = createSut(redactText: true); final element = await pumpTestElement(tester); sut.obscure(element, 1.0, defaultBounds); - expect(sut.items.length, 2); + expect(sut.items.length, 4); }); testWidgets('does not redact text when disabled', (tester) async { @@ -43,6 +46,17 @@ void main() async { sut.obscure(element, 1.0, Rect.fromLTRB(0, 0, 100, 100)); expect(sut.items.length, 1); }); + + testWidgets('correctly determines sizes', (tester) async { + final sut = createSut(redactText: true); + final element = await pumpTestElement(tester); + sut.obscure(element, 1.0, defaultBounds); + expect(sut.items.length, 4); + expect(boundsRect(sut.items[0]), '624x48'); + expect(boundsRect(sut.items[1]), '169x20'); + expect(boundsRect(sut.items[2]), '800x192'); + expect(boundsRect(sut.items[3]), '50x20'); + }); }); group('redact images', () { @@ -50,7 +64,7 @@ void main() async { final sut = createSut(redactImages: true); final element = await pumpTestElement(tester); sut.obscure(element, 1.0, defaultBounds); - expect(sut.items.length, 2); + expect(sut.items.length, 3); }); // Note: we cannot currently test actual asset images without either: @@ -93,6 +107,16 @@ void main() async { sut.obscure(element, 1.0, Rect.fromLTRB(0, 0, 500, 100)); expect(sut.items.length, 1); }); + + testWidgets('correctly determines sizes', (tester) async { + final sut = createSut(redactImages: true); + final element = await pumpTestElement(tester); + sut.obscure(element, 1.0, defaultBounds); + expect(sut.items.length, 3); + expect(boundsRect(sut.items[0]), '1x1'); + expect(boundsRect(sut.items[1]), '1x1'); + expect(boundsRect(sut.items[2]), '50x20'); + }); }); }