Skip to content

Commit

Permalink
Merge branch 'main' into fix/linux-crashpad-handler
Browse files Browse the repository at this point in the history
  • Loading branch information
vaind authored Nov 25, 2024
2 parents 5c70b02 + 4c13d97 commit 77a5bc5
Show file tree
Hide file tree
Showing 29 changed files with 486 additions and 334 deletions.
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 @@ class ScreenshotEventProcessor implements EventProcessor {
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(
SentryLevel.debug, 'Screenshot is 0 bytes, not attaching the image.');
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,
});
}
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';
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 @@ class ScreenshotRecorder {
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 @@ class ScreenshotRecorder {
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;
}
return;
}
Expand All @@ -49,7 +59,9 @@ class ScreenshotRecorder {
// 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 @@ class ScreenshotRecorder {
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.",
exception: e, stackTrace: stackTrace);
if (options.automatedTestMode) {
rethrow;
Expand Down
Loading

0 comments on commit 77a5bc5

Please sign in to comment.