Skip to content

Commit

Permalink
refactor: change redaction logic to custom filters
Browse files Browse the repository at this point in the history
  • Loading branch information
vaind committed Sep 26, 2024
1 parent a4c4f8c commit fa68455
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 49 deletions.
7 changes: 2 additions & 5 deletions flutter/lib/src/replay/recorder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,8 @@ class ScreenshotRecorder {

ScreenshotRecorder(this.config, this.options) {
final replayOptions = options.experimental.replay;
if (replayOptions.redactAllText || replayOptions.redactAllImages) {
_widgetFilter = WidgetFilter(
redactText: replayOptions.redactAllText,
redactImages: replayOptions.redactAllImages,
logger: options.logger);
if (replayOptions.maskingConfig.isNotEmpty) {
_widgetFilter = WidgetFilter(replayOptions.maskingConfig, options.logger);
}
}

Expand Down
75 changes: 49 additions & 26 deletions flutter/lib/src/replay/widget_filter.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';

Expand All @@ -10,20 +9,13 @@ import '../sentry_asset_bundle.dart';
class WidgetFilter {
final items = <WidgetFilterItem>[];
final SentryLogger logger;
final bool redactText;
final bool redactImages;
final Map<Type, WidgetFilterMaskingConfig> config;
static const _defaultColor = Color.fromARGB(255, 0, 0, 0);
late double _pixelRatio;
late Rect _bounds;
final _warnedWidgets = <int>{};
final AssetBundle _rootAssetBundle;

WidgetFilter(
{required this.redactText,
required this.redactImages,
required this.logger,
@visibleForTesting AssetBundle? rootAssetBundle})
: _rootAssetBundle = rootAssetBundle ?? rootBundle;
WidgetFilter(this.config, this.logger);

void obscure(BuildContext context, double pixelRatio, Rect bounds) {
_pixelRatio = pixelRatio;
Expand Down Expand Up @@ -55,21 +47,20 @@ class WidgetFilter {

@pragma('vm:prefer-inline')
bool _obscureIfNeeded(Element element, Widget widget) {
Color? color;
final maskingConfig = config[widget.runtimeType];
if (maskingConfig == null) {
return false;
} else if (!maskingConfig.shouldMask(element, widget)) {
logger(SentryLevel.debug, "WidgetFilter skipping: $widget");
return false;
}

if (redactText && widget is Text) {
Color? color;
if (widget is Text) {
color = widget.style?.color;
} else if (redactText && widget is EditableText) {
} else if (widget is EditableText) {
color = widget.style.color;
} else if (redactImages && widget is Image) {
if (widget.image is AssetBundleImageProvider) {
final image = widget.image as AssetBundleImageProvider;
if (isBuiltInAssetImage(image)) {
logger(SentryLevel.debug,
"WidgetFilter skipping asset: $widget ($image).");
return false;
}
}
} else if (widget is Image) {
color = widget.color;
} else {
// No other type is currently obscured.
Expand Down Expand Up @@ -128,9 +119,10 @@ class WidgetFilter {
return true;
}

@visibleForTesting
@internal
@pragma('vm:prefer-inline')
bool isBuiltInAssetImage(AssetBundleImageProvider image) {
static bool isBuiltInAssetImage(
AssetBundleImageProvider image, AssetBundle rootAssetBundle) {
late final AssetBundle? bundle;
if (image is AssetImage) {
bundle = image.bundle;
Expand All @@ -140,8 +132,8 @@ class WidgetFilter {
return false;
}
return (bundle == null ||
bundle == _rootAssetBundle ||
(bundle is SentryAssetBundle && bundle.bundle == _rootAssetBundle));
bundle == rootAssetBundle ||
(bundle is SentryAssetBundle && bundle.bundle == rootAssetBundle));
}

@pragma('vm:prefer-inline')
Expand All @@ -165,9 +157,40 @@ class WidgetFilter {
}
}

@internal
class WidgetFilterItem {
final Color color;
final Rect bounds;

const WidgetFilterItem(this.color, this.bounds);
}

@internal
class WidgetFilterMaskingConfig {
static const mask = WidgetFilterMaskingConfig._(1, 'mask');
static const show = WidgetFilterMaskingConfig._(2, 'mask');

final int index;
final String _name;
final bool Function(Element, Widget)? _shouldMask;

const WidgetFilterMaskingConfig._(this.index, this._name)
: _shouldMask = null;
const WidgetFilterMaskingConfig.custom(this._shouldMask)
: index = 3,
_name = 'custom';

@override
String toString() => "$WidgetFilterMaskingConfig.$_name";

bool shouldMask(Element element, Widget widget) {
switch (this) {
case mask:
return true;
case show:
return false;
default:
return _shouldMask!(element, widget);
}
}
}
68 changes: 66 additions & 2 deletions flutter/lib/src/sentry_replay_options.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';

import 'replay/widget_filter.dart';

/// Configuration of the experimental replay feature.
class SentryReplayOptions {
SentryReplayOptions() {
redactAllText = true;
redactAllImages = true;
}

double? _sessionSampleRate;

/// A percentage of sessions in which a replay will be created.
Expand All @@ -27,12 +36,67 @@ class SentryReplayOptions {
/// Redact all text content. Draws a rectangle of text bounds with text color
/// on top. Currently, only [Text] and [EditableText] Widgets are redacted.
/// Default is enabled.
var redactAllText = true;
set redactAllText(bool value) {
if (value) {
mask(Text);
mask(EditableText);
} else {
unmask(Text);
unmask(EditableText);
}
}

/// Redact all image content. Draws a rectangle of image bounds with image's
/// dominant color on top. Currently, only [Image] widgets are redacted.
/// Default is enabled.
var redactAllImages = true;
set redactAllImages(bool value) {
if (value) {
maskIfTrue(Image, (element, widget) {
widget as Image;
if (widget.image is AssetBundleImageProvider) {
final image = widget.image as AssetBundleImageProvider;
if (WidgetFilter.isBuiltInAssetImage(image, rootBundle)) {
return false;
}
}
return true;
});
} else {
unmask(Image);
}
}

Map<Type, WidgetFilterMaskingConfig> _maskingConfig = {};
bool _finished = false;

/// Once accessed, masking confing cannot change anymore.
@internal
Map<Type, WidgetFilterMaskingConfig> get maskingConfig {
if (_finished) {
return _maskingConfig;
}
_finished = true;
final result =
Map<Type, WidgetFilterMaskingConfig>.unmodifiable(_maskingConfig);
_maskingConfig = result;
return result;
}

/// Mask given widget type in the replay.
void mask(Type type) {
_maskingConfig[type] = WidgetFilterMaskingConfig.mask;
}

/// Unmask given widget type in the replay.
void unmask(Type type) {
_maskingConfig.remove(type);
}

/// Unmask given widget type in the replay if it's masked by default rules
/// [redactAllText] or [redactAllImages].
void maskIfTrue(Type type, bool Function(Element, Widget) shouldMask) {
_maskingConfig[type] = WidgetFilterMaskingConfig.custom(shouldMask);
}

@internal
bool get isEnabled =>
Expand Down
39 changes: 23 additions & 16 deletions flutter/test/replay/widget_filter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ void main() async {
final rootBundle = TestAssetBundle();
final otherBundle = TestAssetBundle();

final createSut =
({bool redactImages = false, bool redactText = false}) => WidgetFilter(
logger: (level, message, {exception, logger, stackTrace}) {},
redactImages: redactImages,
redactText: redactText,
rootAssetBundle: rootBundle,
);
final createSut = ({bool redactImages = false, bool redactText = false}) {
final replayOptions = SentryReplayOptions();
replayOptions.redactAllImages = redactImages;
replayOptions.redactAllText = redactText;
return WidgetFilter(replayOptions.maskingConfig,
(level, message, {exception, logger, stackTrace}) {});
};

boundsRect(WidgetFilterItem item) =>
'${item.bounds.width.floor()}x${item.bounds.height.floor()}';
Expand Down Expand Up @@ -75,20 +75,27 @@ void main() async {
testWidgets(
'recognizes ${newAssetImage('').runtimeType} from the root bundle',
(tester) async {
final sut = createSut(redactImages: true);

expect(sut.isBuiltInAssetImage(newAssetImage('')), isTrue);
expect(sut.isBuiltInAssetImage(newAssetImage('', bundle: rootBundle)),
expect(WidgetFilter.isBuiltInAssetImage(newAssetImage(''), rootBundle),
isTrue);
expect(
WidgetFilter.isBuiltInAssetImage(
newAssetImage('', bundle: rootBundle), rootBundle),
isTrue);
expect(sut.isBuiltInAssetImage(newAssetImage('', bundle: otherBundle)),
expect(
WidgetFilter.isBuiltInAssetImage(
newAssetImage('', bundle: otherBundle), rootBundle),
isFalse);
expect(
sut.isBuiltInAssetImage(newAssetImage('',
bundle: SentryAssetBundle(bundle: rootBundle))),
WidgetFilter.isBuiltInAssetImage(
newAssetImage('',
bundle: SentryAssetBundle(bundle: rootBundle)),
rootBundle),
isTrue);
expect(
sut.isBuiltInAssetImage(newAssetImage('',
bundle: SentryAssetBundle(bundle: otherBundle))),
WidgetFilter.isBuiltInAssetImage(
newAssetImage('',
bundle: SentryAssetBundle(bundle: otherBundle)),
rootBundle),
isFalse);
});
}
Expand Down

0 comments on commit fa68455

Please sign in to comment.