Skip to content

Commit

Permalink
use view hierachy for screenshots
Browse files Browse the repository at this point in the history
  • Loading branch information
martinhaintz committed Oct 15, 2024
1 parent 4d763a5 commit 42be7f3
Show file tree
Hide file tree
Showing 18 changed files with 236 additions and 189 deletions.
2 changes: 1 addition & 1 deletion flutter/lib/sentry_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export 'src/sentry_replay_options.dart';
export 'src/flutter_sentry_attachment.dart';
export 'src/sentry_asset_bundle.dart' show SentryAssetBundle;
export 'src/integrations/on_error_integration.dart';
export 'src/replay/masking_config.dart' show SentryMaskingDecision;
export 'src/screenshot/masking_config.dart' show SentryMaskingDecision;
export 'src/screenshot/sentry_mask_widget.dart';
export 'src/screenshot/sentry_unmask_widget.dart';
export 'src/screenshot/sentry_screenshot_widget.dart';
Expand Down
168 changes: 98 additions & 70 deletions flutter/lib/src/event_processor/screenshot_event_processor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import 'dart:typed_data';
import 'dart:ui';

import 'package:sentry/sentry.dart';
import '../screenshot/recorder.dart';
import '../screenshot/recorder_config.dart';
import '../screenshot/sentry_screenshot_widget.dart';
import '../sentry_flutter_options.dart';
import 'package:flutter/rendering.dart';
Expand All @@ -19,6 +21,8 @@ class ScreenshotEventProcessor implements EventProcessor {
bool get _hasSentryScreenshotWidget =>
sentryScreenshotWidgetGlobalKey.currentContext != null;

Uint8List? _screenshotCache;

@override
Future<SentryEvent?> apply(SentryEvent event, Hint hint) async {
if (event is SentryTransaction) {
Expand Down Expand Up @@ -75,83 +79,107 @@ class ScreenshotEventProcessor implements EventProcessor {
return event;
}

final bytes = await _createScreenshot();
if (bytes != null) {
hint.screenshot = SentryAttachment.fromScreenshotData(bytes);
// ignore: deprecated_member_use
var recorder = ScreenshotRecorder(
ScreenshotRecorderConfig(
width: window.display.size.width.toInt(),
height: window.display.size.height.toInt()),
_options);

await recorder.capture((Image image) async {
_screenshotCache = await _convertImageToUint8List(image);
});

if (_screenshotCache != null) {
hint.screenshot = SentryAttachment.fromScreenshotData(_screenshotCache!);
}
_screenshotCache = null;
return event;
}

Future<Uint8List?> _createScreenshot() async {
try {
final renderObject =
sentryScreenshotWidgetGlobalKey.currentContext?.findRenderObject();
if (renderObject is RenderRepaintBoundary) {
// ignore: deprecated_member_use
final pixelRatio = window.devicePixelRatio;
var imageResult = _getImage(renderObject, pixelRatio);
Image image;
if (imageResult is Future<Image>) {
image = await imageResult;
} else {
image = imageResult;
}
// At the time of writing there's no other image format available which
// Sentry understands.

if (image.width == 0 || image.height == 0) {
_options.logger(SentryLevel.debug,
'View\'s width and height is zeroed, not taking screenshot.');
return null;
}
// Future<Uint8List?> _createScreenshot() async {
// try {
// final renderObject =
// sentryScreenshotWidgetGlobalKey.currentContext?.findRenderObject();
// if (renderObject is RenderRepaintBoundary) {
// // ignore: deprecated_member_use
// final pixelRatio = window.devicePixelRatio;
// var imageResult = _getImage(renderObject, pixelRatio);
// Image image;
// if (imageResult is Future<Image>) {
// image = await imageResult;
// } else {
// image = imageResult;
// }
// // At the time of writing there's no other image format available which
// // Sentry understands.
//
// if (image.width == 0 || image.height == 0) {
// _options.logger(SentryLevel.debug,
// 'View\'s width and height is zeroed, not taking screenshot.');
// 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) {
// imageResult = _getImage(renderObject, ratio * pixelRatio);
// if (imageResult is Future<Image>) {
// image = await imageResult;
// } else {
// image = imageResult;
// }
// }
// }
// final byteData = await image.toByteData(format: ImageByteFormat.png);
//
// final bytes = byteData?.buffer.asUint8List();
// if (bytes?.isNotEmpty == true) {
// return bytes;
// } else {
// _options.logger(SentryLevel.debug,
// 'Screenshot is 0 bytes, not attaching the image.');
// return null;
// }
// }
// } catch (exception, stackTrace) {
// _options.logger(
// SentryLevel.error,
// 'Taking screenshot failed.',
// exception: exception,
// stackTrace: stackTrace,
// );
// if (_options.automatedTestMode) {
// rethrow;
// }
// }
// 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) {
imageResult = _getImage(renderObject, ratio * pixelRatio);
if (imageResult is Future<Image>) {
image = await imageResult;
} else {
image = imageResult;
}
}
}
final byteData = await image.toByteData(format: ImageByteFormat.png);
Future<Uint8List?> _convertImageToUint8List(Image image) async {
final byteData = await image.toByteData(format: ImageByteFormat.png);

final bytes = byteData?.buffer.asUint8List();
if (bytes?.isNotEmpty == true) {
return bytes;
} else {
_options.logger(SentryLevel.debug,
'Screenshot is 0 bytes, not attaching the image.');
return null;
}
}
} catch (exception, stackTrace) {
final bytes = byteData?.buffer.asUint8List();
if (bytes?.isNotEmpty == true) {
return bytes;
} else {
_options.logger(
SentryLevel.error,
'Taking screenshot failed.',
exception: exception,
stackTrace: stackTrace,
);
if (_options.automatedTestMode) {
rethrow;
}
}
return null;
}

FutureOr<Image> _getImage(
RenderRepaintBoundary repaintBoundary, double pixelRatio) {
// This one is a hack to use https://api.flutter.dev/flutter/rendering/RenderRepaintBoundary/toImage.html on versions older than 3.7 and https://api.flutter.dev/flutter/rendering/RenderRepaintBoundary/toImageSync.html on versions equal or newer than 3.7
try {
return (repaintBoundary as dynamic).toImageSync(pixelRatio: pixelRatio)
as Image;
} on NoSuchMethodError catch (_) {
return repaintBoundary.toImage(pixelRatio: pixelRatio);
SentryLevel.debug, 'Screenshot is 0 bytes, not attaching the image.');
return null;
}
}
//
// FutureOr<Image> _getImage(
// RenderRepaintBoundary repaintBoundary, double pixelRatio) {
// // This one is a hack to use https://api.flutter.dev/flutter/rendering/RenderRepaintBoundary/toImage.html on versions older than 3.7 and https://api.flutter.dev/flutter/rendering/RenderRepaintBoundary/toImageSync.html on versions equal or newer than 3.7
// try {
// return (repaintBoundary as dynamic).toImageSync(pixelRatio: pixelRatio)
// as Image;
// } on NoSuchMethodError catch (_) {
// return repaintBoundary.toImage(pixelRatio: pixelRatio);
// }
// }
}
4 changes: 2 additions & 2 deletions flutter/lib/src/native/cocoa/sentry_native_cocoa.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import 'package:meta/meta.dart';

import '../../../sentry_flutter.dart';
import '../../event_processor/replay_event_processor.dart';
import '../../screenshot/recorder.dart';
import '../../screenshot/recorder_config.dart';
import '../../replay/integration.dart';
import '../../replay/recorder.dart';
import '../../replay/recorder_config.dart';
import '../sentry_native_channel.dart';
import 'binding.dart' as cocoa;

Expand Down
2 changes: 1 addition & 1 deletion flutter/lib/src/native/java/sentry_native_java.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import '../../../sentry_flutter.dart';
import '../../event_processor/replay_event_processor.dart';
import '../../replay/integration.dart';
import '../../replay/scheduled_recorder.dart';
import '../../replay/recorder_config.dart';
import '../../replay/scheduled_recorder_config.dart';
import '../sentry_native_channel.dart';

// Note: currently this doesn't do anything. Later, it shall be used with
Expand Down
5 changes: 3 additions & 2 deletions flutter/lib/src/replay/scheduled_recorder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import 'dart:async';
import 'dart:ui';

import 'package:meta/meta.dart';
import 'package:sentry_flutter/src/replay/scheduled_recorder_config.dart';

import '../../sentry_flutter.dart';
import 'recorder.dart';
import 'recorder_config.dart';
import '../screenshot/recorder.dart';
import '../screenshot/recorder_config.dart';
import 'scheduler.dart';

@internal
Expand Down
11 changes: 11 additions & 0 deletions flutter/lib/src/replay/scheduled_recorder_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import '../screenshot/recorder_config.dart';

class ScheduledScreenshotRecorderConfig extends ScreenshotRecorderConfig {
final int frameRate;

const ScheduledScreenshotRecorderConfig({
super.width,
super.height,
required this.frameRate,
});
}
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class ScreenshotRecorder {
try {
await callback(finalImage);
} finally {
finalImage.dispose();
finalImage.dispose(); // image needs to be disposed manually
}
} finally {
picture.dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,3 @@ class ScreenshotRecorderConfig {
return min(width! / srcWidth, height! / srcHeight);
}
}

class ScheduledScreenshotRecorderConfig extends ScreenshotRecorderConfig {
final int frameRate;

const ScheduledScreenshotRecorderConfig({
super.width,
super.height,
required this.frameRate,
});
}
File renamed without changes.
2 changes: 2 additions & 0 deletions flutter/lib/src/sentry_flutter_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'event_processor/screenshot_event_processor.dart';
import 'screenshot/sentry_screenshot_widget.dart';
import 'sentry_flutter.dart';
import 'sentry_replay_options.dart';
import 'sentry_screenshot_options.dart';
import 'user_interaction/sentry_user_interaction_widget.dart';

/// This class adds options which are only available in a Flutter environment.
Expand Down Expand Up @@ -380,6 +381,7 @@ class SentryFlutterOptions extends SentryOptions {
class _SentryFlutterExperimentalOptions {
/// Replay recording configuration.
final replay = SentryReplayOptions();
final screenshot = SentryScreenshotOptions();
}

/// Callback being executed in [ScreenshotEventProcessor], deciding if a
Expand Down
Loading

0 comments on commit 42be7f3

Please sign in to comment.