Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix/screenshot masking during changes #2553

Merged
merged 29 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2b514dd
capture screenshots in sequence until they're stable
vaind Jan 2, 2025
d0c6c01
improve timestamps
vaind Jan 2, 2025
6f0f351
fix tests & future handling
vaind Jan 7, 2025
8bc4df7
example: debug log only in debug mode
vaind Jan 7, 2025
c990fe2
formatting
vaind Jan 7, 2025
7636b2c
min-version compat
vaind Jan 7, 2025
ae34a38
linter issue
vaind Jan 8, 2025
9bedc01
fix scheduler tests
vaind Jan 8, 2025
32f5b35
cleanup
vaind Jan 8, 2025
9d4a3f9
fix: scheduler test
vaind Jan 8, 2025
4439231
Merge branch 'main' into fix/screenshot-masking-during-changes
vaind Jan 8, 2025
8550861
improve performance
vaind Jan 9, 2025
05574bb
improve screenshot comparison performance
vaind Jan 10, 2025
ae1c567
comments
vaind Jan 10, 2025
d010424
screenshot list-equal fix & tests
vaind Jan 10, 2025
f62e5ee
min-version failure
vaind Jan 10, 2025
b78c583
rename retrier to stabilizer
vaind Jan 10, 2025
9509e44
chore: changelog
vaind Jan 10, 2025
46acddd
Merge branch 'main' into fix/screenshot-masking-during-changes
vaind Jan 10, 2025
0418629
capture stable issue screenshots when masking is enabled
vaind Jan 10, 2025
183e995
fixes and tests
vaind Jan 10, 2025
4ccd5ea
better logging
vaind Jan 10, 2025
e7ab100
cocoa replay fix
vaind Jan 10, 2025
622d859
fix dart2js tests
vaind Jan 10, 2025
e9cebf5
fix screenshot capture hanging
vaind Jan 10, 2025
07aec1d
fix integration test
vaind Jan 11, 2025
134b015
time out if we can't take a screenshot for events
vaind Jan 11, 2025
17df8d7
update ios app size metrics check
vaind Jan 11, 2025
223495c
fix: oom
vaind Jan 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@

- Add `beforeCapture` for View Hierarchy ([#2523](https://github.com/getsentry/sentry-dart/pull/2523))
- View hierarchy calls are now debounced for 2 seconds.

### Enhancements

- Replay: improve iOS native interop performance ([#2530](https://github.com/getsentry/sentry-dart/pull/2530))
- Replay: improve orientation change tracking accuracy on Android ([#2540](https://github.com/getsentry/sentry-dart/pull/2540))

### Fixes

- Replay: fix masking for frames captured during UI changes ([#2553](https://github.com/getsentry/sentry-dart/pull/2553))

### Dependencies

- Bump Cocoa SDK from v8.42.0 to v8.43.0 ([#2542](https://github.com/getsentry/sentry-dart/pull/2542), [#2548](https://github.com/getsentry/sentry-dart/pull/2548))
Expand Down
3 changes: 3 additions & 0 deletions flutter/example/devtools_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
2 changes: 2 additions & 0 deletions flutter/example/integration_test/integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ void main() {
const fakeDsn = 'https://[email protected]/1234567';

IntegrationTestWidgetsFlutterBinding.ensureInitialized();
IntegrationTestWidgetsFlutterBinding.instance.framePolicy =
LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;

tearDown(() async {
await Sentry.close();
Expand Down
2 changes: 1 addition & 1 deletion flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Future<void> setupSentry(
// 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
// configuration issues, e.g. finding out why your events are not uploaded.
options.debug = true;
options.debug = kDebugMode;
options.spotlight = Spotlight(enabled: true);
options.enableTimeToFullDisplayTracing = true;
options.enableMetrics = true;
Expand Down
44 changes: 31 additions & 13 deletions flutter/lib/src/event_processor/screenshot_event_processor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import '../screenshot/recorder.dart';
import '../screenshot/recorder_config.dart';
import 'package:flutter/widgets.dart' as widget;

import '../screenshot/stabilizer.dart';
import '../utils/debouncer.dart';

class ScreenshotEventProcessor implements EventProcessor {
Expand Down Expand Up @@ -125,20 +126,37 @@ class ScreenshotEventProcessor implements EventProcessor {
}

@internal
Future<Uint8List?> createScreenshot() =>
_recorder.capture(_convertImageToUint8List);

Future<Uint8List?> _convertImageToUint8List(Screenshot screenshot) async {
final byteData =
await screenshot.image.toByteData(format: ImageByteFormat.png);

final bytes = byteData?.buffer.asUint8List();
if (bytes?.isNotEmpty == true) {
return bytes;
Future<Uint8List?> createScreenshot() async {
if (_options.experimental.privacyForScreenshots == null) {
return _recorder.capture((screenshot) =>
screenshot.pngData.then((v) => v.buffer.asUint8List()));
} else {
_options.logger(
SentryLevel.debug, 'Screenshot is 0 bytes, not attaching the image.');
return null;
// If masking is enabled, we need to use [ScreenshotStabilizer].
final completer = Completer<Uint8List?>();
final stabilizer = ScreenshotStabilizer(
_recorder, _options,
(screenshot) async {
final pngData = await screenshot.pngData;
completer.complete(pngData.buffer.asUint8List());
},
// This limits the amount of time to take a stable masked screenshot.
maxTries: 5,
// We need to force the frame the frame or this could hang indefinitely.
frameSchedulingMode: FrameSchedulingMode.forced,
);
try {
unawaited(
stabilizer.capture(Duration.zero).onError(completer.completeError));
// DO NOT return completer.future directly - we need to dispose first.
return await completer.future.timeout(const Duration(seconds: 1),
onTimeout: () {
_options.logger(
SentryLevel.warning, 'Timed out taking a stable screenshot.');
return null;
});
} finally {
stabilizer.dispose();
}
}
}
}
40 changes: 40 additions & 0 deletions flutter/lib/src/native/cocoa/cocoa_replay_recorder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'dart:async';
import 'dart:typed_data';

import 'package:meta/meta.dart';

import '../../../sentry_flutter.dart';
import '../../replay/replay_recorder.dart';
import '../../screenshot/recorder.dart';
import '../../screenshot/recorder_config.dart';
import '../../screenshot/stabilizer.dart';

@internal
class CocoaReplayRecorder {
final SentryFlutterOptions _options;
final ScreenshotRecorder _recorder;
late final ScreenshotStabilizer<void> _stabilizer;
var _completer = Completer<Uint8List?>();

CocoaReplayRecorder(this._options)
: _recorder =
ReplayScreenshotRecorder(ScreenshotRecorderConfig(), _options) {
_stabilizer = ScreenshotStabilizer(_recorder, _options, (screenshot) async {
final pngData = await screenshot.pngData;
_options.logger(
SentryLevel.debug,
'Replay: captured screenshot ('
'${screenshot.width}x${screenshot.height} pixels, '
'${pngData.lengthInBytes} bytes)');
_completer.complete(pngData.buffer.asUint8List());
});
}

Future<Uint8List?> captureScreenshot() async {
_completer = Completer<Uint8List?>();
_stabilizer.ensureFrameAndAddCallback((msSinceEpoch) {
_stabilizer.capture(msSinceEpoch).onError(_completer.completeError);
});
return _completer.future;
}
}
42 changes: 5 additions & 37 deletions flutter/lib/src/native/cocoa/sentry_native_cocoa.dart
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:typed_data';
import 'dart:ui';

import 'package:meta/meta.dart';

import '../../../sentry_flutter.dart';
import '../../replay/replay_config.dart';
import '../../replay/replay_recorder.dart';
import '../../screenshot/recorder.dart';
import '../../screenshot/recorder_config.dart';
import '../native_memory.dart';
import '../sentry_native_channel.dart';
import 'binding.dart' as cocoa;
import 'cocoa_replay_recorder.dart';

@internal
class SentryNativeCocoa extends SentryNativeChannel {
late final _lib = cocoa.SentryCocoa(DynamicLibrary.process());
ScreenshotRecorder? _replayRecorder;
CocoaReplayRecorder? _replayRecorder;
SentryId? _replayId;

SentryNativeCocoa(super.options);
Expand All @@ -33,12 +29,12 @@ class SentryNativeCocoa extends SentryNativeChannel {
channel.setMethodCallHandler((call) async {
switch (call.method) {
case 'captureReplayScreenshot':
_replayRecorder ??=
ReplayScreenshotRecorder(ScreenshotRecorderConfig(), options);
_replayRecorder ??= CocoaReplayRecorder(options);

final replayId = call.arguments['replayId'] == null
? null
: SentryId.fromId(call.arguments['replayId'] as String);

if (_replayId != replayId) {
_replayId = replayId;
hub.configureScope((s) {
Expand All @@ -47,35 +43,7 @@ class SentryNativeCocoa extends SentryNativeChannel {
});
}

final widgetsBinding = options.bindingUtils.instance;
if (widgetsBinding == null) {
options.logger(SentryLevel.warning,
'Replay: failed to capture screenshot, WidgetsBinding.instance is null');
return null;
}

final completer = Completer<Uint8List?>();
widgetsBinding.ensureVisualUpdate();
widgetsBinding.addPostFrameCallback((_) {
_replayRecorder?.capture((screenshot) async {
final image = screenshot.image;
final imageData =
await image.toByteData(format: ImageByteFormat.png);
if (imageData != null) {
options.logger(
SentryLevel.debug,
'Replay: captured screenshot ('
'${image.width}x${image.height} pixels, '
'${imageData.lengthInBytes} bytes)');
return imageData.buffer.asUint8List();
} else {
options.logger(SentryLevel.warning,
'Replay: failed to convert screenshot to PNG');
}
}).then(completer.complete, onError: completer.completeError);
});
final uint8List = await completer.future;

final uint8List = await _replayRecorder!.captureScreenshot();
// Malloc memory and copy the data. Native must free it.
return uint8List?.toNativeMemory().toJson();
default:
Expand Down
16 changes: 9 additions & 7 deletions flutter/lib/src/native/java/android_replay_recorder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:meta/meta.dart';

import '../../../sentry_flutter.dart';
import '../../replay/scheduled_recorder.dart';
import '../../screenshot/screenshot.dart';
import '../sentry_safe_method_channel.dart';

@internal
Expand All @@ -15,19 +16,20 @@ class AndroidReplayRecorder extends ScheduledScreenshotRecorder {
}

Future<void> _addReplayScreenshot(
ScreenshotPng screenshot, bool isNewlyCaptured) async {
Screenshot screenshot, bool isNewlyCaptured) async {
final timestamp = screenshot.timestamp.millisecondsSinceEpoch;
final filePath = "$_cacheDir/$timestamp.png";

options.logger(
SentryLevel.debug,
'$logName: saving ${isNewlyCaptured ? 'new' : 'repeated'} screenshot to'
' $filePath (${screenshot.width}x${screenshot.height} pixels, '
'${screenshot.data.lengthInBytes} bytes)');
try {
final pngData = await screenshot.pngData;
options.logger(
SentryLevel.debug,
'$logName: saving ${isNewlyCaptured ? 'new' : 'repeated'} screenshot to'
' $filePath (${screenshot.width}x${screenshot.height} pixels, '
'${pngData.lengthInBytes} bytes)');
await options.fileSystem
.file(filePath)
.writeAsBytes(screenshot.data.buffer.asUint8List(), flush: true);
.writeAsBytes(pngData.buffer.asUint8List(), flush: true);

await _channel.invokeMethod(
'addReplayScreenshot',
Expand Down
4 changes: 2 additions & 2 deletions flutter/lib/src/replay/replay_recorder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ class ReplayScreenshotRecorder extends ScreenshotRecorder {

@override
@protected
Future<void> executeTask(void Function() task, Flow flow) {
// Future() schedules the task to be executed asynchronously with TImer.run.
Future<void> executeTask(Future<void> Function() task, Flow flow) {
// Future() schedules the task to be executed asynchronously with Timer.run.
return Future(task);
}
}
Loading
Loading