From c238d73f459bad20554e5f01cf16337e93d77906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Andra=C5=A1ec?= Date: Mon, 6 Mar 2023 12:31:48 +0000 Subject: [PATCH] Allow sentry user to control resolution of captured Flutter screenshots (#1288) --- CHANGELOG.md | 4 ++ flutter/example/lib/main.dart | 1 + flutter/lib/sentry_flutter.dart | 1 + .../screenshot_event_processor.dart | 16 +++++++- .../screenshot/sentry_screenshot_quality.dart | 20 +++++++++ flutter/lib/src/sentry_flutter_options.dart | 4 ++ .../screenshot_event_processor_test.dart | 41 ++++++++++++++++++- 7 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 flutter/lib/src/screenshot/sentry_screenshot_quality.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cb31ae85..46738ab39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#6140) - [diff](https://github.com/getsentry/sentry-java/compare/6.13.1...6.14.0) +### Features + +- Allow sentry user to control resolution of captured Flutter screenshots ([#1288](https://github.com/getsentry/sentry-dart/pull/1288)) + ## 6.21.0 ### Features diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 9610d82f5..5b6492a74 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -50,6 +50,7 @@ Future setupSentry(AppRunner appRunner, String dsn) async { options.sendDefaultPii = true; options.reportSilentFlutterErrors = true; options.attachScreenshot = true; + options.screenshotQuality = SentryScreenshotQuality.low; options.attachViewHierarchy = true; // We can enable Sentry debug logging during development. This is likely // going to log too much for your app, but can be useful when figuring out diff --git a/flutter/lib/sentry_flutter.dart b/flutter/lib/sentry_flutter.dart index 069e6a1bd..508091c9e 100644 --- a/flutter/lib/sentry_flutter.dart +++ b/flutter/lib/sentry_flutter.dart @@ -9,5 +9,6 @@ export 'src/flutter_sentry_attachment.dart'; export 'src/sentry_asset_bundle.dart'; export 'src/integrations/on_error_integration.dart'; export 'src/screenshot/sentry_screenshot_widget.dart'; +export 'src/screenshot/sentry_screenshot_quality.dart'; export 'src/user_interaction/sentry_user_interaction_widget.dart'; export 'src/binding_wrapper.dart'; diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index 3191fee36..3ce48e548 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -1,6 +1,8 @@ import 'dart:async'; +import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui show ImageByteFormat; +import 'dart:ui'; import 'package:sentry/sentry.dart'; import '../screenshot/sentry_screenshot_widget.dart'; @@ -44,9 +46,9 @@ class ScreenshotEventProcessor extends EventProcessor { try { final renderObject = sentryScreenshotWidgetGlobalKey.currentContext?.findRenderObject(); - if (renderObject is RenderRepaintBoundary) { - final image = await renderObject.toImage(pixelRatio: 1); + final pixelRatio = window.devicePixelRatio; + var image = await renderObject.toImage(pixelRatio: pixelRatio); // At the time of writing there's no other image format available which // Sentry understands. @@ -56,7 +58,17 @@ class ScreenshotEventProcessor extends EventProcessor { return null; } + final targetResolution = _options.screenshotQuality.targetResolution(); + if (targetResolution != null) { + var ratioWidth = targetResolution / image.width; + var ratioHeight = targetResolution / image.height; + var ratio = min(ratioWidth, ratioHeight); + if (ratio > 0.0 && ratio < 1.0) { + image = await renderObject.toImage(pixelRatio: ratio * pixelRatio); + } + } final byteData = await image.toByteData(format: ui.ImageByteFormat.png); + final bytes = byteData?.buffer.asUint8List(); if (bytes?.isNotEmpty == true) { return bytes; diff --git a/flutter/lib/src/screenshot/sentry_screenshot_quality.dart b/flutter/lib/src/screenshot/sentry_screenshot_quality.dart new file mode 100644 index 000000000..d42e62296 --- /dev/null +++ b/flutter/lib/src/screenshot/sentry_screenshot_quality.dart @@ -0,0 +1,20 @@ +/// The quality of the attached screenshot +enum SentryScreenshotQuality { + full, + high, + medium, + low; + + int? targetResolution() { + switch (this) { + case SentryScreenshotQuality.full: + return null; // Keep current scale + case SentryScreenshotQuality.high: + return 1920; + case SentryScreenshotQuality.medium: + return 1280; + case SentryScreenshotQuality.low: + return 854; + } + } +} diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index af6c3f1eb..66b817d56 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'binding_wrapper.dart'; import 'renderer/renderer.dart'; +import 'screenshot/sentry_screenshot_quality.dart'; /// This class adds options which are only availble in a Flutter environment. /// Note that some of these options require native Sentry integration, which is @@ -163,6 +164,9 @@ class SentryFlutterOptions extends SentryOptions { /// The [SentryScreenshotWidget] has to be the root widget of the app. bool attachScreenshot = false; + /// The quality of the attached screenshot + SentryScreenshotQuality screenshotQuality = SentryScreenshotQuality.high; + /// Enable or disable automatic breadcrumbs for User interactions Using [Listener] /// /// Requires adding the [SentryUserInteractionWidget] to the widget tree. diff --git a/flutter/test/event_processor/screenshot_event_processor_test.dart b/flutter/test/event_processor/screenshot_event_processor_test.dart index 6b49f6457..0535e8ff1 100644 --- a/flutter/test/event_processor/screenshot_event_processor_test.dart +++ b/flutter/test/event_processor/screenshot_event_processor_test.dart @@ -1,5 +1,8 @@ @Tags(['canvasKit']) // Web renderer where this test can run +import 'dart:math'; +import 'dart:ui'; + import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sentry_flutter/src/event_processor/screenshot_event_processor.dart'; @@ -9,13 +12,15 @@ import 'package:sentry_flutter/sentry_flutter.dart'; void main() { late Fixture fixture; + setUp(() { fixture = Fixture(); TestWidgetsFlutterBinding.ensureInitialized(); }); Future _addScreenshotAttachment( - WidgetTester tester, FlutterRenderer renderer, bool added) async { + WidgetTester tester, FlutterRenderer renderer, bool added, + {int? expectedMaxWidthOrHeight}) async { // Run with real async https://stackoverflow.com/a/54021863 await tester.runAsync(() async { final sut = fixture.getSut(renderer); @@ -30,6 +35,16 @@ void main() { await sut.apply(event, hint: hint); expect(hint.screenshot != null, added); + if (expectedMaxWidthOrHeight != null) { + final bytes = await hint.screenshot?.bytes; + final codec = await instantiateImageCodec(bytes!); + final frameInfo = await codec.getNextFrame(); + final image = frameInfo.image; + expect( + max(image.width, image.height).toDouble(), + moreOrLessEquals(expectedMaxWidthOrHeight.toDouble(), epsilon: 1.0), + ); + } }); } @@ -51,6 +66,30 @@ void main() { (tester) async { await _addScreenshotAttachment(tester, FlutterRenderer.unknown, false); }); + + testWidgets('does add screenshot in correct resolution for low', + (tester) async { + final height = SentryScreenshotQuality.low.targetResolution()!; + fixture.options.screenshotQuality = SentryScreenshotQuality.low; + await _addScreenshotAttachment(tester, FlutterRenderer.skia, true, + expectedMaxWidthOrHeight: height); + }); + + testWidgets('does add screenshot in correct resolution for medium', + (tester) async { + final height = SentryScreenshotQuality.medium.targetResolution()!; + fixture.options.screenshotQuality = SentryScreenshotQuality.medium; + await _addScreenshotAttachment(tester, FlutterRenderer.skia, true, + expectedMaxWidthOrHeight: height); + }); + + testWidgets('does add screenshot in correct resolution for high', + (tester) async { + final widthOrHeight = SentryScreenshotQuality.high.targetResolution()!; + fixture.options.screenshotQuality = SentryScreenshotQuality.high; + await _addScreenshotAttachment(tester, FlutterRenderer.skia, true, + expectedMaxWidthOrHeight: widthOrHeight); + }); } class Fixture {