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 all 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
3 changes: 2 additions & 1 deletion .github/file-filters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ high_risk_code: &high_risk_code
- "flutter/lib/src/integrations/native_app_start_integration.dart"
- "flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt"
- "flutter/ios/Classes/SentryFlutterPluginApple.swift"

- "flutter/lib/src/screenshot/recorder.dart"
- "flutter/lib/src/screenshot/widget_filter.dart"
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,33 @@

### Features

- Support for screenshot PII content masking ([#2361](https://github.com/getsentry/sentry-dart/pull/2361))
By default, masking is enabled for SessionReplay. To also enable it for screenshots captured with events, you can specify `options.experimental.privacy`:
```dart
await SentryFlutter.init(
(options) {
...
// the defaults are:
options.experimental.privacy.maskAllText = true;
options.experimental.privacy.maskAllImages = true;
options.experimental.privacy.maskAssetImages = false;
// you cal also set up custom masking, for example:
options.experimental.privacy.mask<WebView>();
},
appRunner: () => runApp(MyApp()),
);
```
Actually, just accessing this field will cause it to be initialized with the default settings to mask all text and images:
```dart
await SentryFlutter.init(
(options) {
...
// this has a side-effect of creating the default privacy configuration, thus enabling Screenshot masking:
options.experimental.privacy;
},
appRunner: () => runApp(MyApp()),
);
```
- Linux native error & obfuscation support ([#2431](https://github.com/getsentry/sentry-dart/pull/2431))
- Improve Device context on plain Dart and Flutter desktop apps ([#2441](https://github.com/getsentry/sentry-dart/pull/2441))

Expand Down
5 changes: 4 additions & 1 deletion flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ Future<void> setupSentry(
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
Expand All @@ -92,6 +91,10 @@ Future<void> setupSentry(
options.experimental.replay.sessionSampleRate = 1.0;
options.experimental.replay.onErrorSampleRate = 1.0;

// This has a side-effect of creating the default privacy configuration,
// thus enabling Screenshot masking. No need to actually change it.
options.experimental.privacy;

_isIntegrationTest = isIntegrationTest;
if (_isIntegrationTest) {
options.dist = '1';
Expand Down
3 changes: 2 additions & 1 deletion flutter/lib/sentry_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ export 'src/navigation/sentry_navigator_observer.dart';
export 'src/sentry_flutter.dart';
export 'src/sentry_flutter_options.dart';
export 'src/sentry_replay_options.dart';
export 'src/sentry_privacy_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
111 changes: 37 additions & 74 deletions flutter/lib/src/event_processor/screenshot_event_processor.dart
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import 'dart:async';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui';

import 'package:flutter/rendering.dart';
import 'package:meta/meta.dart';
import 'package:sentry/sentry.dart';
import '../screenshot/sentry_screenshot_widget.dart';
import '../sentry_flutter_options.dart';
import '../../sentry_flutter.dart';
import '../renderer/renderer.dart';
import '../screenshot/recorder.dart';
import '../screenshot/recorder_config.dart';
import 'package:flutter/widgets.dart' as widget;

class ScreenshotEventProcessor implements EventProcessor {
final SentryFlutterOptions _options;

ScreenshotEventProcessor(this._options);
late final ScreenshotRecorder _recorder;

ScreenshotEventProcessor(this._options) {
final targetResolution = _options.screenshotQuality.targetResolution();

_recorder = ScreenshotRecorder(
ScreenshotRecorderConfig(
width: targetResolution,
height: targetResolution,
),
_options,
isReplayRecorder: false,
);
}

@override
Future<SentryEvent?> apply(SentryEvent event, Hint hint) async {
Expand Down Expand Up @@ -77,84 +88,36 @@
return event;
}

final bytes = await createScreenshot();
if (bytes != null) {
hint.screenshot = SentryAttachment.fromScreenshotData(bytes);
Uint8List? screenshotData = await createScreenshot();

if (screenshotData != null) {
hint.screenshot = SentryAttachment.fromScreenshotData(screenshotData);
}

return event;
}

@internal
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;
}
}
return null;
return screenshotData;
}

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 118 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#L118

Added line #L118 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;
}
}
}
2 changes: 1 addition & 1 deletion flutter/lib/src/integrations/screenshot_integration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import '../event_processor/screenshot_event_processor.dart';
import '../sentry_flutter_options.dart';

/// Adds [ScreenshotEventProcessor] to options event processors if
/// [SentryFlutterOptions.attachScreenshot] is true
/// [SentryFlutterOptions.screenshot.attach] is true
class ScreenshotIntegration implements Integration<SentryFlutterOptions> {
SentryFlutterOptions? _options;
ScreenshotEventProcessor? _screenshotEventProcessor;
Expand Down
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
29 changes: 0 additions & 29 deletions flutter/lib/src/replay/recorder_config.dart

This file was deleted.

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 @@ -4,8 +4,8 @@ import 'dart:ui';
import 'package:meta/meta.dart';

import '../../sentry_flutter.dart';
import 'recorder.dart';
import 'recorder_config.dart';
import '../screenshot/recorder.dart';
import 'scheduled_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;

ScheduledScreenshotRecorderConfig({
super.width,
super.height,
required this.frameRate,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import 'dart:ui';

import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart' as widgets;
import 'package:meta/meta.dart';

import '../../sentry_flutter.dart';
Expand All @@ -17,12 +18,23 @@
final ScreenshotRecorderConfig config;
@protected
final SentryFlutterOptions options;
final String _logName;
WidgetFilter? _widgetFilter;
bool warningLogged = false;

ScreenshotRecorder(this.config, this.options) {
final maskingConfig = options.experimental.replay.buildMaskingConfig();
if (maskingConfig.length > 0) {
bool _warningLogged = false;

// TODO: remove in the next major release, see recorder_test.dart.
@visibleForTesting
bool get hasWidgetFilter => _widgetFilter != null;

// TODO: remove [isReplayRecorder] parameter in the next major release, see _SentryFlutterExperimentalOptions.
ScreenshotRecorder(this.config, this.options, {bool isReplayRecorder = true})
: _logName = isReplayRecorder ? 'ReplayRecorder' : 'ScreenshotRecorder' {
// see `options.experimental.privacy` docs for details
final privacyOptions = isReplayRecorder
? options.experimental.privacyForReplay
: options.experimental.privacyForScreenshots;
final maskingConfig = privacyOptions?.buildMaskingConfig();
if (maskingConfig != null && maskingConfig.length > 0) {
_widgetFilter = WidgetFilter(maskingConfig, options.logger);
}
}
Expand All @@ -31,12 +43,10 @@
final context = sentryScreenshotWidgetGlobalKey.currentContext;
final renderObject = context?.findRenderObject() as RenderRepaintBoundary?;
if (context == null || renderObject == null) {
if (!warningLogged) {
options.logger(
SentryLevel.warning,
"Replay: SentryScreenshotWidget is not attached. "
"Skipping replay capture.");
warningLogged = true;
if (!_warningLogged) {
options.logger(SentryLevel.warning,
"$_logName: SentryScreenshotWidget is not attached, skipping capture.");
_warningLogged = true;

Check warning on line 49 in flutter/lib/src/screenshot/recorder.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/screenshot/recorder.dart#L46-L49

Added lines #L46 - L49 were not covered by tests
}
return;
}
Expand All @@ -49,7 +59,9 @@
// On iOS, the screenshot resolution is not adjusted.
final srcWidth = renderObject.size.width;
final srcHeight = renderObject.size.height;
final pixelRatio = config.getPixelRatio(srcWidth, srcHeight);

final pixelRatio = config.getPixelRatio(srcWidth, srcHeight) ??
widgets.MediaQuery.of(context).devicePixelRatio;

// First, we synchronously capture the image and enumerate widgets on the main UI loop.
final futureImage = renderObject.toImage(pixelRatio: pixelRatio);
Expand Down Expand Up @@ -87,18 +99,19 @@
try {
await callback(finalImage);
} finally {
finalImage.dispose();
finalImage.dispose(); // image needs to be disposed manually
}
} finally {
picture.dispose();
}

options.logger(
SentryLevel.debug,
"Replay: captured a screenshot in ${watch.elapsedMilliseconds}"
"$_logName: captured a screenshot in ${watch.elapsedMilliseconds}"
" ms ($blockingTime ms blocking).");
} catch (e, stackTrace) {
options.logger(SentryLevel.error, "Replay: failed to capture screenshot.",
options.logger(
SentryLevel.error, "$_logName: failed to capture screenshot.",

Check warning on line 114 in flutter/lib/src/screenshot/recorder.dart

View check run for this annotation

Codecov / codecov/patch

flutter/lib/src/screenshot/recorder.dart#L113-L114

Added lines #L113 - L114 were not covered by tests
exception: e, stackTrace: stackTrace);
if (options.automatedTestMode) {
rethrow;
Expand Down
Loading
Loading