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

redact screenshots via view hierarchy #2361

Merged
merged 63 commits into from
Nov 24, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
42be7f3
use view hierachy for screenshots
martinhaintz Oct 15, 2024
49a413b
fix tests and distinguish between ScheduledScreenshotRecorderConfig a…
martinhaintz Oct 15, 2024
d283df9
Merge branch 'main' into feat/redact-screenshots-via-view-hierarchy
martinhaintz Oct 15, 2024
fdc7d6d
removed unused imports
martinhaintz Oct 16, 2024
ea94d47
Merge branch 'main' into feat/redact-screenshots-via-view-hierarchy
martinhaintz Oct 16, 2024
981039c
remove unused test
martinhaintz Oct 16, 2024
4671c53
add changelog
martinhaintz Oct 16, 2024
f722191
rename variable
martinhaintz Oct 22, 2024
22f22b0
add internal
martinhaintz Oct 22, 2024
9bb14ad
split into screenshot, screenreplay and redaction options
martinhaintz Oct 24, 2024
0969b7b
fix comments
martinhaintz Oct 24, 2024
73a4224
Merge branch 'main' into feat/redact-screenshots-via-view-hierarchy
martinhaintz Oct 24, 2024
c2daf00
export redaction options and remove unused dependencies
martinhaintz Oct 25, 2024
3606202
renaming to SentryPrivacyOptions
martinhaintz Oct 25, 2024
35ebb0b
add explanation for setRedactionOptions
martinhaintz Oct 29, 2024
ba079eb
Merge branch 'main' into feat/redact-screenshots-via-view-hierarchy
martinhaintz Oct 29, 2024
57a0823
fix deprecation warnings
martinhaintz Oct 29, 2024
5e45d77
Update flutter/lib/src/sentry_flutter.dart
martinhaintz Nov 11, 2024
487fd27
Update flutter/lib/src/sentry_flutter.dart
martinhaintz Nov 11, 2024
4e062e8
Update flutter/lib/src/sentry_privacy_options.dart
martinhaintz Nov 11, 2024
21851d9
feat: non-nullable privacy setting (#2382)
vaind Nov 11, 2024
9699edc
Merge branch 'main' into feat/redact-screenshots-via-view-hierarchy
martinhaintz Nov 11, 2024
6768268
keep recorder instance in memory
martinhaintz Nov 11, 2024
eee9c23
Update flutter/lib/src/sentry_screenshot_options.dart
martinhaintz Nov 11, 2024
2481c04
update comment for sentry screenshot options
martinhaintz Nov 11, 2024
3a82edf
fixed screenshot size mismatch
martinhaintz Nov 12, 2024
b565f3a
renamed some variables to make it more readable
martinhaintz Nov 12, 2024
3febb1f
fix unit tests
martinhaintz Nov 12, 2024
1d99b52
reorganized tests
martinhaintz Nov 12, 2024
a223200
update changelog
martinhaintz Nov 12, 2024
9f6f01a
Merge branch 'main' into feat/redact-screenshots-via-view-hierarchy
martinhaintz Nov 12, 2024
2821d78
screenshots now also working with canvas kit
martinhaintz Nov 12, 2024
b201e6d
Merge branch 'feat/redact-screenshots-via-view-hierarchy' of https://…
martinhaintz Nov 12, 2024
ec80659
Update CHANGELOG.md
martinhaintz Nov 13, 2024
e3c45f4
Update flutter/lib/src/sentry_flutter_options.dart
martinhaintz Nov 13, 2024
23cd66c
Update flutter/lib/src/sentry_screenshot_options.dart
martinhaintz Nov 13, 2024
673ef0b
renamed screenshot settings
martinhaintz Nov 13, 2024
b9772aa
removed quality from recorder config.
martinhaintz Nov 13, 2024
feb579d
Update CHANGELOG.md
martinhaintz Nov 14, 2024
d14704b
Update CHANGELOG.md
martinhaintz Nov 14, 2024
9c4966c
Update flutter/lib/src/event_processor/screenshot_event_processor.dart
martinhaintz Nov 14, 2024
ef8d6b3
add test for screenshots with flutter html renderer
martinhaintz Nov 14, 2024
879d445
Update flutter/lib/src/sentry_screenshot_options.dart
martinhaintz Nov 14, 2024
02c8c41
Merge branch 'feat/redact-screenshots-via-view-hierarchy' of https://…
martinhaintz Nov 14, 2024
93a8a7f
add changelog for supported html renderer
martinhaintz Nov 14, 2024
606454f
removed unused import
martinhaintz Nov 14, 2024
b8e14be
revert: screenshot recorder resolution handling (#2409)
vaind Nov 14, 2024
2332d9d
fix pixelRatio
martinhaintz Nov 14, 2024
080989d
Update flutter/test/replay/scheduled_recorder_test.dart
martinhaintz Nov 14, 2024
ca18328
fix unit tests
martinhaintz Nov 14, 2024
198d1a3
Merge branch 'main' into feat/redact-screenshots-via-view-hierarchy
martinhaintz Nov 14, 2024
efd66c8
Update flutter/lib/src/screenshot/recorder.dart
martinhaintz Nov 14, 2024
0671009
reverted changes for html renderer as it is not working
martinhaintz Nov 15, 2024
de3550c
fix unit tests and errors
martinhaintz Nov 15, 2024
cc1d836
add comment for quality
martinhaintz Nov 15, 2024
8202004
moved code back to previous position
martinhaintz Nov 15, 2024
823598a
rearrange imports
martinhaintz Nov 15, 2024
11b4ddd
Merge branch 'main' into feat/redact-screenshots-via-view-hierarchy
vaind Nov 15, 2024
d25e4cf
move screenshot options into global options
martinhaintz Nov 20, 2024
d3a0a2a
change wording from redaction to masking and similar.
martinhaintz Nov 20, 2024
d6212ac
Merge branch 'main' into feat/redact-screenshots-via-view-hierarchy
martinhaintz Nov 20, 2024
0805b74
Merge branch 'main' into feat/redact-screenshots-via-view-hierarchy
martinhaintz Nov 24, 2024
3fbe924
add import for screenshot event processor
martinhaintz Nov 24, 2024
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Enhancements

- Cache parsed DSN ([#2365](https://github.com/getsentry/sentry-dart/pull/2365))
- Switching from traditional screenshot to view hierarchy for screenshots which allows redacting ([#2361](https://github.com/getsentry/sentry-dart/pull/2361))
martinhaintz marked this conversation as resolved.
Show resolved Hide resolved
martinhaintz marked this conversation as resolved.
Show resolved Hide resolved
martinhaintz marked this conversation as resolved.
Show resolved Hide resolved

## 8.10.0-beta.2

Expand Down
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
97 changes: 24 additions & 73 deletions flutter/lib/src/event_processor/screenshot_event_processor.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import 'dart:async';
import 'dart:math';
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';
import '../renderer/renderer.dart';
import 'package:flutter/widgets.dart' as widget;

Expand Down Expand Up @@ -75,83 +75,34 @@
return event;
}

final bytes = await _createScreenshot();
if (bytes != null) {
hint.screenshot = SentryAttachment.fromScreenshotData(bytes);
}
return event;
}
// ignore: deprecated_member_use
var recorder = ScreenshotRecorder(
ScreenshotRecorderConfig(quality: _options.screenshotQuality),
_options);

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.
Uint8List? _screenshotData;

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);
await recorder.capture((Image image) async {
_screenshotData = await _convertImageToUint8List(image);
});

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;
}
if (_screenshotData != null) {
hint.screenshot = SentryAttachment.fromScreenshotData(_screenshotData!);
}
return null;

return event;
}

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);
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(

Check warning on line 103 in flutter/lib/src/event_processor/screenshot_event_processor.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/event_processor/screenshot_event_processor.dart#L103

Added line #L103 was not covered by tests
SentryLevel.debug, 'Screenshot is 0 bytes, not attaching the image.');
vaind marked this conversation as resolved.
Show resolved Hide resolved
return null;
}
}
}
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
4 changes: 2 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,10 @@ import 'dart:async';
import 'dart:ui';

import 'package:meta/meta.dart';
import 'scheduled_recorder_config.dart';

import '../../sentry_flutter.dart';
import 'recorder.dart';
import 'recorder_config.dart';
import '../screenshot/recorder.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,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import 'dart:ui';

import 'package:flutter/rendering.dart';
import 'package:meta/meta.dart';
import '../sentry_redaction_options.dart';

import '../../sentry_flutter.dart';
import 'masking_config.dart';
import 'recorder_config.dart';
import 'widget_filter.dart';

Expand All @@ -21,7 +23,12 @@ class ScreenshotRecorder {
bool warningLogged = false;

ScreenshotRecorder(this.config, this.options) {
final maskingConfig = options.experimental.replay.buildMaskingConfig();
/// TODO: Rewrite when default redaction value are synced with SS & SR
final SentryMaskingConfig maskingConfig =
(options.experimental.sentryRedactingOptions ??
SentryRedactingOptions())
.buildMaskingConfig();

if (maskingConfig.length > 0) {
_widgetFilter = WidgetFilter(maskingConfig, options.logger);
}
Expand Down Expand Up @@ -82,12 +89,16 @@ class ScreenshotRecorder {
final picture = recorder.endRecording();

try {
final finalImage = await picture.toImage(
(srcWidth * pixelRatio).round(), (srcHeight * pixelRatio).round());
Image finalImage;
final targetHeight =
config.quality.calculateHeight(srcWidth.toInt(), srcHeight.toInt());
final targetWidth =
config.quality.calculateWidth(srcWidth.toInt(), srcHeight.toInt());
finalImage = await picture.toImage(targetWidth, targetHeight);
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 @@ -2,12 +2,19 @@ import 'dart:math';

import 'package:meta/meta.dart';

import '../../sentry_flutter.dart';

@internal
class ScreenshotRecorderConfig {
final int? width;
final int? height;
final SentryScreenshotQuality quality;
martinhaintz marked this conversation as resolved.
Show resolved Hide resolved

const ScreenshotRecorderConfig({this.width, this.height});
const ScreenshotRecorderConfig({
this.width,
this.height,
this.quality = SentryScreenshotQuality.full,
});

double getPixelRatio(double srcWidth, double srcHeight) {
assert((width == null) == (height == null));
Expand All @@ -17,13 +24,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,
});
}
30 changes: 30 additions & 0 deletions flutter/lib/src/screenshot/sentry_screenshot_quality.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:meta/meta.dart';

/// The quality of the attached screenshot
enum SentryScreenshotQuality {
full,
Expand All @@ -17,4 +19,32 @@ enum SentryScreenshotQuality {
return 854;
}
}

@internal
int calculateHeight(int width, int height) {
martinhaintz marked this conversation as resolved.
Show resolved Hide resolved
if (this == SentryScreenshotQuality.full) {
return height;
} else {
if (height > width) {
return targetResolution()!;
} else {
var ratio = targetResolution()! / width;
return (height * ratio).round();
}
}
}

@internal
int calculateWidth(int width, int height) {
if (this == SentryScreenshotQuality.full) {
return width;
} else {
if (width > height) {
return targetResolution()!;
} else {
var ratio = targetResolution()! / height;
return (width * ratio).round();
}
}
}
}
16 changes: 16 additions & 0 deletions flutter/lib/src/sentry_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import 'native/sentry_native_binding.dart';
import 'profiling.dart';
import 'renderer/renderer.dart';
import 'sentry_redaction_options.dart';
import 'span_frame_metrics_collector.dart';
import 'version.dart';
import 'view_hierarchy/view_hierarchy_integration.dart';
Expand Down Expand Up @@ -149,6 +150,8 @@
SentryFlutterOptions options,
bool isOnErrorSupported,
) {
_setRedactionOptions(options);

final integrations = <Integration>[];
final platformChecker = options.platformChecker;

Expand Down Expand Up @@ -243,6 +246,19 @@
options.sdk = sdk;
}

static void _setRedactionOptions(SentryFlutterOptions options) {
martinhaintz marked this conversation as resolved.
Show resolved Hide resolved
if (options.experimental.sentryRedactingOptions != null) {
return;
} else if (options.screenshot.attachScreenshot == true &&
!options.experimental.replay.isEnabled) {
options.experimental.sentryRedactingOptions = SentryRedactingOptions()
..maskAllText = false
..maskAllImages = false;

Check warning on line 256 in flutter/lib/src/sentry_flutter.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/sentry_flutter.dart#L253-L256

Added lines #L253 - L256 were not covered by tests
} else {
options.experimental.sentryRedactingOptions = SentryRedactingOptions();
}
}

/// Reports the time it took for the screen to be fully displayed.
/// This requires the [SentryFlutterOptions.enableTimeToFullDisplayTracing] option to be set to `true`.
static Future<void> reportFullyDisplayed() async {
Expand Down
Loading
Loading