Skip to content

Commit

Permalink
Merge branch 'main' into feat/support-allow-urls-deny-urls
Browse files Browse the repository at this point in the history
  • Loading branch information
martinhaintz authored Sep 6, 2024
2 parents 10639ce + 3adbea9 commit b6f758f
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 63 deletions.
17 changes: 9 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,30 @@

### Features

- Support allowUrls and denyUrls for Flutter Web ([#2227](https://github.com/getsentry/sentry-dart/pull/2227))
- Session replay Alpha for Android and iOS ([#2208](https://github.com/getsentry/sentry-dart/pull/2208), [#2269](https://github.com/getsentry/sentry-dart/pull/2269), [#2236](https://github.com/getsentry/sentry-dart/pull/2236)).

To try out replay, you can set following options (access is limited to early access orgs on Sentry. If you're interested, [sign up for the waitlist](https://sentry.io/lp/mobile-replay-beta/)):

```dart
await SentryFlutter.init(
(options) {
...
options.allowUrls = ["^https://sentry.com.*\$", "my-custom-domain"];
options.denyUrls = ["^.*ends-with-this\$", "denied-url"];
options.experimental.replay.sessionSampleRate = 1.0;
options.experimental.replay.errorSampleRate = 1.0;
},
appRunner: () => runApp(MyApp()),
);
```

- Session replay Alpha for Android and iOS ([#2208](https://github.com/getsentry/sentry-dart/pull/2208)).

To try out replay, you can set following options (access is limited to early access orgs on Sentry. If you're interested, [sign up for the waitlist](https://sentry.io/lp/mobile-replay-beta/)):
- Support allowUrls and denyUrls for Flutter Web ([#2227](https://github.com/getsentry/sentry-dart/pull/2227))

```dart
await SentryFlutter.init(
(options) {
...
options.experimental.replay.sessionSampleRate = 1.0;
options.experimental.replay.errorSampleRate = 1.0;
options.allowUrls = ["^https://sentry.com.*\$", "my-custom-domain"];
options.denyUrls = ["^.*ends-with-this\$", "denied-url"];
options.denyUrls = ["^.*ends-with-this\$", "denied-url"];
},
appRunner: () => runApp(MyApp()),
);
Expand Down
10 changes: 10 additions & 0 deletions flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,8 @@ Future<void> showDialogWithTextAndImage(BuildContext context) async {
await DefaultAssetBundle.of(context).loadString('assets/lorem-ipsum.txt');

if (!context.mounted) return;
final imageBytes =
await DefaultAssetBundle.of(context).load('assets/sentry-wordmark.png');
await showDialog<void>(
context: context,

Check notice on line 1049 in flutter/example/lib/main.dart

View workflow job for this annotation

GitHub Actions / analyze / analyze

Don't use 'BuildContext's across async gaps.

Try rewriting the code to not use the 'BuildContext', or guard the use with a 'mounted' check. See https://dart.dev/diagnostics/use_build_context_synchronously to learn more about this problem.
// gets tracked if using SentryNavigatorObserver
Expand All @@ -1056,7 +1058,15 @@ Future<void> showDialogWithTextAndImage(BuildContext context) async {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Use various ways an image is included in the app.
// Local asset images are not obscured in replay recording.
Image.asset('assets/sentry-wordmark.png'),
Image.asset('assets/sentry-wordmark.png', bundle: rootBundle),
Image.asset('assets/sentry-wordmark.png',
bundle: DefaultAssetBundle.of(context)),
Image.network(
'https://www.gstatic.com/recaptcha/api2/logo_48.png'),
Image.memory(imageBytes.buffer.asUint8List()),
Text(text),
],
),
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 @@ -10,7 +10,7 @@ export 'src/sentry_flutter.dart';
export 'src/sentry_flutter_options.dart';
export 'src/sentry_replay_options.dart';
export 'src/flutter_sentry_attachment.dart';
export 'src/sentry_asset_bundle.dart';
export 'src/sentry_asset_bundle.dart' show SentryAssetBundle;
export 'src/integrations/on_error_integration.dart';
export 'src/screenshot/sentry_screenshot_widget.dart';
export 'src/screenshot/sentry_screenshot_quality.dart';
Expand Down
72 changes: 56 additions & 16 deletions flutter/lib/src/replay/widget_filter.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
import 'package:sentry/sentry.dart';

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

@internal
class WidgetFilter {
Expand All @@ -14,11 +16,14 @@ class WidgetFilter {
late double _pixelRatio;
late Rect _bounds;
final _warnedWidgets = <int>{};
final AssetBundle _rootAssetBundle;

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

void obscure(BuildContext context, double pixelRatio, Rect bounds) {
_pixelRatio = pixelRatio;
Expand Down Expand Up @@ -57,6 +62,14 @@ class WidgetFilter {
} else if (redactText && 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;
}
}
color = widget.color;
} else {
// No other type is currently obscured.
Expand All @@ -65,25 +78,25 @@ class WidgetFilter {

final renderObject = element.renderObject;
if (renderObject is! RenderBox) {
_cantObscure(widget, "it's renderObject is not a RenderBox");
_cantObscure(widget, "its renderObject is not a RenderBox");
return false;
}

final size = element.size;
if (size == null) {
_cantObscure(widget, "it's renderObject has a null size");
return false;
var rect = _boundingBox(renderObject);

// If it's a clipped render object, use parent's offset and size.
// This helps with text fields which often have oversized render objects.
if (renderObject.parent is RenderStack) {
final renderStack = (renderObject.parent as RenderStack);
final clipBehavior = renderStack.clipBehavior;
if (clipBehavior == Clip.hardEdge ||
clipBehavior == Clip.antiAlias ||
clipBehavior == Clip.antiAliasWithSaveLayer) {
final clipRect = _boundingBox(renderStack);
rect = rect.intersect(clipRect);
}
}

final offset = renderObject.localToGlobal(Offset.zero);

final rect = Rect.fromLTWH(
offset.dx * _pixelRatio,
offset.dy * _pixelRatio,
size.width * _pixelRatio,
size.height * _pixelRatio,
);

if (!rect.overlaps(_bounds)) {
assert(() {
logger(SentryLevel.debug, "WidgetFilter skipping offscreen: $widget");
Expand Down Expand Up @@ -115,6 +128,22 @@ class WidgetFilter {
return true;
}

@visibleForTesting
@pragma('vm:prefer-inline')
bool isBuiltInAssetImage(AssetBundleImageProvider image) {
late final AssetBundle? bundle;
if (image is AssetImage) {
bundle = image.bundle;
} else if (image is ExactAssetImage) {
bundle = image.bundle;
} else {
return false;
}
return (bundle == null ||
bundle == _rootAssetBundle ||
(bundle is SentryAssetBundle && bundle.bundle == _rootAssetBundle));
}

@pragma('vm:prefer-inline')
void _cantObscure(Widget widget, String message) {
if (!_warnedWidgets.contains(widget.hashCode)) {
Expand All @@ -123,6 +152,17 @@ class WidgetFilter {
"WidgetFilter cannot obscure widget $widget: $message");
}
}

@pragma('vm:prefer-inline')
Rect _boundingBox(RenderBox box) {
final offset = box.localToGlobal(Offset.zero);
return Rect.fromLTWH(
offset.dx * _pixelRatio,
offset.dy * _pixelRatio,
box.size.width * _pixelRatio,
box.size.height * _pixelRatio,
);
}
}

class WidgetFilterItem {
Expand Down
7 changes: 7 additions & 0 deletions flutter/lib/src/sentry_asset_bundle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'dart:ui';

import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'package:sentry/sentry.dart';

typedef _StringParser<T> = Future<T> Function(String value);
Expand Down Expand Up @@ -375,3 +376,9 @@ class SentryAssetBundle implements AssetBundle {
as Future<T>;
}
}

@internal
extension SentryAssetBundleInternal on SentryAssetBundle {
/// Returns the wrapped [AssetBundle].
AssetBundle get bundle => _bundle;
}
91 changes: 56 additions & 35 deletions flutter/test/replay/test_widget.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sentry_flutter/sentry_flutter.dart';

Future<Element> pumpTestElement(WidgetTester tester) async {
Future<Element> pumpTestElement(WidgetTester tester,
{List<Widget>? children}) async {
await tester.pumpWidget(
MaterialApp(
home: SentryWidget(
Expand All @@ -14,25 +14,42 @@ Future<Element> pumpTestElement(WidgetTester tester) async {
child: Opacity(
opacity: 0.5,
child: Column(
children: <Widget>[
newImage(),
const Padding(
padding: EdgeInsets.all(15),
child: Center(child: Text('Centered text')),
),
ElevatedButton(
onPressed: () {},
child: Text('Button title'),
),
newImage(),
// Invisible widgets won't be obscured.
Visibility(visible: false, child: Text('Invisible text')),
Visibility(visible: false, child: newImage()),
Opacity(opacity: 0, child: Text('Invisible text')),
Opacity(opacity: 0, child: newImage()),
Offstage(offstage: true, child: Text('Offstage text')),
Offstage(offstage: true, child: newImage()),
],
children: children ??
<Widget>[
newImage(),
const Padding(
padding: EdgeInsets.all(15),
child: Center(child: Text('Centered text')),
),
ElevatedButton(
onPressed: () {},
child: Text('Button title'),
),
newImage(),
// Invisible widgets won't be obscured.
Visibility(visible: false, child: Text('Invisible text')),
Visibility(visible: false, child: newImage()),
Opacity(opacity: 0, child: Text('Invisible text')),
Opacity(opacity: 0, child: newImage()),
Offstage(offstage: true, child: Text('Offstage text')),
Offstage(offstage: true, child: newImage()),
Text(dummyText),
SizedBox(
width: 100,
height: 20,
child: Stack(children: [
Positioned(
top: 0,
left: 0,
width: 50,
child: Text(dummyText)),
Positioned(
top: 0,
left: 0,
width: 50,
child: newImage(width: 500, height: 500)),
]))
],
),
),
),
Expand All @@ -43,17 +60,21 @@ Future<Element> pumpTestElement(WidgetTester tester) async {
return TestWidgetsFlutterBinding.instance.rootElement!;
}

Image newImage() => Image.memory(
Uint8List.fromList([
66, 77, 142, 0, 0, 0, 0, 0, 0, 0, 138, 0, 0, 0, 124, 0, 0, 0, 1, 0,
0, 0, 255, 255, 255, 255, 1, 0, 32, 0, 3, 0, 0, 0, 4, 0, 0, 0, 19,
11, 0, 0, 19, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0,
255, 0, 0, 255, 0, 0, 0, 0, 0, 0, 255, 66, 71, 82, 115, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 135, 135, 135, 255,
// This comment prevents dartfmt reformatting this to single-item lines.
]),
width: 1,
height: 1,
final testImageData = Uint8List.fromList([
66, 77, 142, 0, 0, 0, 0, 0, 0, 0, 138, 0, 0, 0, 124, 0, 0, 0, 1, 0,
0, 0, 255, 255, 255, 255, 1, 0, 32, 0, 3, 0, 0, 0, 4, 0, 0, 0, 19,
11, 0, 0, 19, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0,
255, 0, 0, 255, 0, 0, 0, 0, 0, 0, 255, 66, 71, 82, 115, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 135, 135, 135, 255,
// This comment prevents dartfmt reformatting this to single-item lines.
]);

Image newImage({double width = 1, double height = 1}) => Image.memory(
testImageData,
width: width,
height: height,
);

const dummyText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
Loading

0 comments on commit b6f758f

Please sign in to comment.