-
-
Notifications
You must be signed in to change notification settings - Fork 237
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
base: main
Are you sure you want to change the base?
Changes from all commits
42be7f3
49a413b
d283df9
fdc7d6d
ea94d47
981039c
4671c53
f722191
22f22b0
9bb14ad
0969b7b
73a4224
c2daf00
3606202
35ebb0b
ba079eb
57a0823
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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)) | ||
Comment on lines
5
to
+8
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The changelog entry should be under |
||
|
||
## 8.10.0-beta.2 | ||
|
||
|
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; | ||
|
||
|
@@ -30,7 +30,7 @@ | |
_hasSentryScreenshotWidget) { | ||
return event; | ||
} | ||
final beforeScreenshot = _options.beforeScreenshot; | ||
final beforeScreenshot = _options.screenshot.beforeScreenshot; | ||
if (beforeScreenshot != null) { | ||
try { | ||
final result = beforeScreenshot(event, hint: hint); | ||
|
@@ -67,91 +67,43 @@ | |
return event; | ||
} | ||
|
||
if (_options.attachScreenshotOnlyWhenResumed && | ||
if (_options.screenshot.attachScreenshotOnlyWhenResumed && | ||
widget.WidgetsBinding.instance.lifecycleState != | ||
AppLifecycleState.resumed) { | ||
_options.logger(SentryLevel.debug, | ||
'Only attaching screenshots when application state is resumed.'); | ||
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.screenshot.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( | ||
SentryLevel.debug, 'Screenshot is 0 bytes, not attaching the image.'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ever happened to you while testing? Looks like a bug. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @vaind I looked up the |
||
return null; | ||
} | ||
} | ||
} |
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 | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -99,6 +99,8 @@ | |||||||||||||||||||||||||||||
// ignore: invalid_use_of_internal_member | ||||||||||||||||||||||||||||||
runZonedGuardedOnError: runZonedGuardedOnError, | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
// TODO: Remove when we synced SS and SR configurations and have a single default configuration | ||||||||||||||||||||||||||||||
_setRedactionOptions(options); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
if (_native != null) { | ||||||||||||||||||||||||||||||
// ignore: invalid_use_of_internal_member | ||||||||||||||||||||||||||||||
|
@@ -243,6 +245,27 @@ | |||||||||||||||||||||||||||||
options.sdk = sdk; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
/// Screen redaction was previously introduced with the SessionReplay feature. | ||||||||||||||||||||||||||||||
/// Screen redaction is enabled by default for SessionReplay. | ||||||||||||||||||||||||||||||
/// As we also to use this feature for Screenshot, which previously was not | ||||||||||||||||||||||||||||||
/// capable of redacting the screenshot, we need to disable redaction for Screenshot by default | ||||||||||||||||||||||||||||||
/// so we don`t break the existing behavior. | ||||||||||||||||||||||||||||||
/// As we have only one central place to configure the redaction, | ||||||||||||||||||||||||||||||
/// we need to set the redaction options to full fill the above default settings. | ||||||||||||||||||||||||||||||
/// The plan is to unify this behaviour with the next major release. | ||||||||||||||||||||||||||||||
Comment on lines
+248
to
+255
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
suggestion to keep it a bit shorter |
||||||||||||||||||||||||||||||
static void _setRedactionOptions(SentryFlutterOptions options) { | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
minor: since we changed the wording from redact -> mask, it might make sense to change it |
||||||||||||||||||||||||||||||
if (options.experimental.privacy != null) { | ||||||||||||||||||||||||||||||
return; | ||||||||||||||||||||||||||||||
} else if (options.screenshot.attachScreenshot == true && | ||||||||||||||||||||||||||||||
!options.experimental.replay.isEnabled) { | ||||||||||||||||||||||||||||||
options.experimental.privacy = SentryPrivacyOptions() | ||||||||||||||||||||||||||||||
..maskAllText = false | ||||||||||||||||||||||||||||||
..maskAllImages = false; | ||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||
options.experimental.privacy = SentryPrivacyOptions(); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
martinhaintz marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
/// 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 { | ||||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the wording is a bit confusing from a user perspective we can improve it by saying
Allow Screenshots to be masked for privacy reasons
or something like thatAlso show an exampl how a user can enable it and please add the screenshot & replay masking behaviour (the one that I commented) as sub bullet points for more context so it's clear how it behaves