From d0312c9616494d7365a21482a4564461e3bbd14d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Andra=C5=A1ec?= Date: Tue, 9 Jan 2024 10:10:06 +0000 Subject: [PATCH] Add `beforeScreenshotCallback` to `SentryFlutterOptions` (#1805) --- CHANGELOG.md | 1 + .../screenshot_event_processor.dart | 26 ++++++ flutter/lib/src/native/cocoa/binding.dart | 23 +++-- flutter/lib/src/sentry_flutter_options.dart | 17 +++- .../screenshot_event_processor_test.dart | 88 ++++++++++++++++++- 5 files changed, 139 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 864bc01d69..f822c59ae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - APM for isar ([#1726](https://github.com/getsentry/sentry-dart/pull/1726)) - Add isar breadcrumbs ([#1800](https://github.com/getsentry/sentry-dart/pull/1800)) - Starting with Flutter 3.16, Sentry adds the [`appFlavor`](https://api.flutter.dev/flutter/services/appFlavor-constant.html) to the `flutter_context` ([#1799](https://github.com/getsentry/sentry-dart/pull/1799)) +- Add beforeScreenshotCallback to SentryFlutterOptions ([#1805](https://github.com/getsentry/sentry-dart/pull/1805)) ### Dependencies diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index 263722fdad..48e0a4c4d6 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -30,6 +30,32 @@ class ScreenshotEventProcessor implements EventProcessor { _hasSentryScreenshotWidget) { return event; } + final beforeScreenshot = _options.beforeScreenshot; + if (beforeScreenshot != null) { + try { + final result = beforeScreenshot(event, hint: hint); + bool takeScreenshot; + if (result is Future) { + takeScreenshot = await result; + } else { + takeScreenshot = result; + } + if (!takeScreenshot) { + return event; + } + } catch (exception, stackTrace) { + _options.logger( + SentryLevel.error, + 'The beforeScreenshot callback threw an exception', + exception: exception, + stackTrace: stackTrace, + ); + // ignore: invalid_use_of_internal_member + if (_options.automatedTestMode) { + rethrow; + } + } + } final renderer = _options.rendererWrapper.getRenderer(); diff --git a/flutter/lib/src/native/cocoa/binding.dart b/flutter/lib/src/native/cocoa/binding.dart index d614c12b1b..41350a3a2c 100644 --- a/flutter/lib/src/native/cocoa/binding.dart +++ b/flutter/lib/src/native/cocoa/binding.dart @@ -37603,8 +37603,7 @@ class ObjCBlock_bool_ObjCObject_ffiUnsignedLong_bool extends _ObjCBlockBase { ObjCBlock_bool_ObjCObject_ffiUnsignedLong_bool.fromFunctionPointer( SentryCocoa lib, ffi.Pointer< - ffi - .NativeFunction< + ffi.NativeFunction< ffi.Bool Function(ffi.Pointer arg0, ffi.UnsignedLong arg1, ffi.Pointer arg2)>> ptr) @@ -42032,17 +42031,15 @@ class ObjCBlock_bool_ObjCObject_bool extends _ObjCBlockBase { ffi.Pointer arg1)>> ptr) : this._( - lib - ._newBlock1( - _cFuncTrampoline ??= ffi.Pointer.fromFunction< - ffi.Bool Function( - ffi.Pointer<_ObjCBlock> block, - ffi.Pointer arg0, - ffi.Pointer arg1)>( - _ObjCBlock_bool_ObjCObject_bool_fnPtrTrampoline, - false) - .cast(), - ptr.cast()), + lib._newBlock1( + _cFuncTrampoline ??= ffi.Pointer.fromFunction< + ffi.Bool Function( + ffi.Pointer<_ObjCBlock> block, + ffi.Pointer arg0, + ffi.Pointer arg1)>( + _ObjCBlock_bool_ObjCObject_bool_fnPtrTrampoline, false) + .cast(), + ptr.cast()), lib); static ffi.Pointer? _cFuncTrampoline; diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index 3806397465..ee722f8e9e 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:meta/meta.dart'; import 'package:sentry/sentry.dart'; import 'package:flutter/widgets.dart'; @@ -5,8 +7,9 @@ import 'package:flutter/widgets.dart'; import 'binding_wrapper.dart'; import 'renderer/renderer.dart'; import 'screenshot/sentry_screenshot_quality.dart'; +import 'event_processor/screenshot_event_processor.dart'; -/// This class adds options which are only availble in a Flutter environment. +/// This class adds options which are only available in a Flutter environment. /// Note that some of these options require native Sentry integration, which is /// not available on all platforms. class SentryFlutterOptions extends SentryOptions { @@ -169,6 +172,11 @@ class SentryFlutterOptions extends SentryOptions { /// Only attach a screenshot when the app is resumed. bool attachScreenshotOnlyWhenResumed = false; + /// Sets a callback which is executed before capturing screenshots. Only + /// relevant if `attachScreenshot` is set to true. When false is returned + /// from the function, no screenshot will be attached. + BeforeScreenshotCallback? beforeScreenshot; + /// Enable or disable automatic breadcrumbs for User interactions Using [Listener] /// /// Requires adding the [SentryUserInteractionWidget] to the widget tree. @@ -289,3 +297,10 @@ class SentryFlutterOptions extends SentryOptions { /// The [navigatorKey] is used to add information of the currently used locale to the contexts. GlobalKey? navigatorKey; } + +/// Callback being executed in [ScreenshotEventProcessor], deciding if a +/// screenshot should be recorded and attached. +typedef BeforeScreenshotCallback = FutureOr Function( + SentryEvent event, { + Hint? hint, +}); diff --git a/flutter/test/event_processor/screenshot_event_processor_test.dart b/flutter/test/event_processor/screenshot_event_processor_test.dart index e0ed2eb182..4beeea3ef3 100644 --- a/flutter/test/event_processor/screenshot_event_processor_test.dart +++ b/flutter/test/event_processor/screenshot_event_processor_test.dart @@ -14,6 +14,9 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); late Fixture fixture; + late SentryEvent event; + late Hint hint; + setUp(() { fixture = Fixture(); }); @@ -34,8 +37,8 @@ void main() { textDirection: TextDirection.ltr))); final throwable = Exception(); - final event = SentryEvent(throwable: throwable); - final hint = Hint(); + event = SentryEvent(throwable: throwable); + hint = Hint(); await sut.apply(event, hint: hint); expect(hint.screenshot != null, added); @@ -91,6 +94,87 @@ void main() { await _addScreenshotAttachment(tester, null, added: true, isWeb: false, expectedMaxWidthOrHeight: widthOrHeight); }); + + group('beforeScreenshot', () { + testWidgets('does add screenshot if beforeScreenshot returns true', + (tester) async { + fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) { + return true; + }; + await _addScreenshotAttachment(tester, FlutterRenderer.canvasKit, + added: true, isWeb: false); + }); + + testWidgets('does add screenshot if async beforeScreenshot returns true', + (tester) async { + fixture.options.beforeScreenshot = + (SentryEvent event, {Hint? hint}) async { + await Future.delayed(Duration(milliseconds: 1)); + return true; + }; + await _addScreenshotAttachment(tester, FlutterRenderer.canvasKit, + added: true, isWeb: false); + }); + + testWidgets('does not add screenshot if beforeScreenshot returns false', + (tester) async { + fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) { + return false; + }; + await _addScreenshotAttachment(tester, FlutterRenderer.canvasKit, + added: false, isWeb: false); + }); + + testWidgets( + 'does not add screenshot if async beforeScreenshot returns false', + (tester) async { + fixture.options.beforeScreenshot = + (SentryEvent event, {Hint? hint}) async { + await Future.delayed(Duration(milliseconds: 1)); + return false; + }; + await _addScreenshotAttachment(tester, FlutterRenderer.canvasKit, + added: false, isWeb: false); + }); + + testWidgets('does add screenshot if beforeScreenshot throws', + (tester) async { + fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) { + throw Error(); + }; + await _addScreenshotAttachment(tester, FlutterRenderer.canvasKit, + added: true, isWeb: false); + }); + + testWidgets('does add screenshot if async beforeScreenshot throws', + (tester) async { + fixture.options.beforeScreenshot = + (SentryEvent event, {Hint? hint}) async { + await Future.delayed(Duration(milliseconds: 1)); + throw Error(); + }; + await _addScreenshotAttachment(tester, FlutterRenderer.canvasKit, + added: true, isWeb: false); + }); + + testWidgets('passes event & hint to beforeScreenshot callback', + (tester) async { + SentryEvent? beforeScreenshotEvent; + Hint? beforeScreenshotHint; + + fixture.options.beforeScreenshot = (SentryEvent event, {Hint? hint}) { + beforeScreenshotEvent = event; + beforeScreenshotHint = hint; + return true; + }; + + await _addScreenshotAttachment(tester, FlutterRenderer.canvasKit, + added: true, isWeb: false); + + expect(beforeScreenshotEvent, event); + expect(beforeScreenshotHint, hint); + }); + }); } class Fixture {