Skip to content

Commit 45b2e65

Browse files
authored
Merge branch 'main' into feat/dart-symbolication
2 parents 465b643 + 77db8d4 commit 45b2e65

11 files changed

+239
-190
lines changed

CHANGELOG.md

+9-7
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,30 @@
99
- Useful when using Sentry.init() instead of SentryFlutter.init() in Flutter projects for example due to size limitations.
1010
- `true` by default but automatically set to `false` when using SentryFlutter.init() because the SentryFlutter fetches debug images from the native SDK integrations.
1111
- Support allowUrls and denyUrls for Flutter Web ([#2227](https://github.com/getsentry/sentry-dart/pull/2227))
12+
- 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)).
13+
14+
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/)):
1215

1316
```dart
1417
await SentryFlutter.init(
1518
(options) {
1619
...
17-
options.allowUrls = ["^https://sentry.com.*\$", "my-custom-domain"];
18-
options.denyUrls = ["^.*ends-with-this\$", "denied-url"];
20+
options.experimental.replay.sessionSampleRate = 1.0;
21+
options.experimental.replay.errorSampleRate = 1.0;
1922
},
2023
appRunner: () => runApp(MyApp()),
2124
);
2225
```
2326

24-
- Session replay Alpha for Android and iOS ([#2208](https://github.com/getsentry/sentry-dart/pull/2208)).
25-
26-
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/)):
27+
- Support allowUrls and denyUrls for Flutter Web ([#2227](https://github.com/getsentry/sentry-dart/pull/2227))
2728

2829
```dart
2930
await SentryFlutter.init(
3031
(options) {
3132
...
32-
options.experimental.replay.sessionSampleRate = 1.0;
33-
options.experimental.replay.errorSampleRate = 1.0;
33+
options.allowUrls = ["^https://sentry.com.*\$", "my-custom-domain"];
34+
options.denyUrls = ["^.*ends-with-this\$", "denied-url"];
35+
options.denyUrls = ["^.*ends-with-this\$", "denied-url"];
3436
},
3537
appRunner: () => runApp(MyApp()),
3638
);

flutter/example/lib/main.dart

+10
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,8 @@ Future<void> showDialogWithTextAndImage(BuildContext context) async {
10431043
await DefaultAssetBundle.of(context).loadString('assets/lorem-ipsum.txt');
10441044

10451045
if (!context.mounted) return;
1046+
final imageBytes =
1047+
await DefaultAssetBundle.of(context).load('assets/sentry-wordmark.png');
10461048
await showDialog<void>(
10471049
context: context,
10481050
// gets tracked if using SentryNavigatorObserver
@@ -1056,7 +1058,15 @@ Future<void> showDialogWithTextAndImage(BuildContext context) async {
10561058
child: Column(
10571059
mainAxisSize: MainAxisSize.min,
10581060
children: [
1061+
// Use various ways an image is included in the app.
1062+
// Local asset images are not obscured in replay recording.
10591063
Image.asset('assets/sentry-wordmark.png'),
1064+
Image.asset('assets/sentry-wordmark.png', bundle: rootBundle),
1065+
Image.asset('assets/sentry-wordmark.png',
1066+
bundle: DefaultAssetBundle.of(context)),
1067+
Image.network(
1068+
'https://www.gstatic.com/recaptcha/api2/logo_48.png'),
1069+
Image.memory(imageBytes.buffer.asUint8List()),
10601070
Text(text),
10611071
],
10621072
),

flutter/lib/sentry_flutter.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export 'src/sentry_flutter.dart';
1010
export 'src/sentry_flutter_options.dart';
1111
export 'src/sentry_replay_options.dart';
1212
export 'src/flutter_sentry_attachment.dart';
13-
export 'src/sentry_asset_bundle.dart';
13+
export 'src/sentry_asset_bundle.dart' show SentryAssetBundle;
1414
export 'src/integrations/on_error_integration.dart';
1515
export 'src/screenshot/sentry_screenshot_widget.dart';
1616
export 'src/screenshot/sentry_screenshot_quality.dart';

flutter/lib/src/event_processor/url_filter/html_url_filter_event_processor.dart

+4-21
Original file line numberDiff line numberDiff line change
@@ -15,40 +15,23 @@ class WebUrlFilterEventProcessor implements UrlFilterEventProcessor {
1515
this._options,
1616
);
1717

18+
final html.Window _window = html.window;
1819
final SentryFlutterOptions _options;
1920

2021
@override
2122
SentryEvent? apply(SentryEvent event, Hint hint) {
22-
final frames = _getStacktraceFrames(event);
23-
final lastPath = frames?.first?.absPath;
24-
25-
if (lastPath == null) {
26-
return event;
27-
}
23+
final url = _window.location.toString();
2824

2925
if (_options.allowUrls.isNotEmpty &&
30-
!isMatchingRegexPattern(lastPath, _options.allowUrls)) {
26+
!isMatchingRegexPattern(url, _options.allowUrls)) {
3127
return null;
3228
}
3329

3430
if (_options.denyUrls.isNotEmpty &&
35-
isMatchingRegexPattern(lastPath, _options.denyUrls)) {
31+
isMatchingRegexPattern(url, _options.denyUrls)) {
3632
return null;
3733
}
3834

3935
return event;
4036
}
41-
42-
Iterable<SentryStackFrame?>? _getStacktraceFrames(SentryEvent event) {
43-
if (event.exceptions?.isNotEmpty == true) {
44-
return event.exceptions?.first.stackTrace?.frames;
45-
}
46-
if (event.threads?.isNotEmpty == true) {
47-
final stacktraces = event.threads?.map((e) => e.stacktrace);
48-
return stacktraces
49-
?.where((element) => element != null)
50-
.expand((element) => element!.frames);
51-
}
52-
return null;
53-
}
5437
}

flutter/lib/src/event_processor/url_filter/web_url_filter_event_processor.dart

+4-21
Original file line numberDiff line numberDiff line change
@@ -17,40 +17,23 @@ class WebUrlFilterEventProcessor implements UrlFilterEventProcessor {
1717
this._options,
1818
);
1919

20+
final web.Window _window = web.window;
2021
final SentryFlutterOptions _options;
2122

2223
@override
2324
SentryEvent? apply(SentryEvent event, Hint hint) {
24-
final frames = _getStacktraceFrames(event);
25-
final lastPath = frames?.first?.absPath;
26-
27-
if (lastPath == null) {
28-
return event;
29-
}
25+
final url = _window.location.toString();
3026

3127
if (_options.allowUrls.isNotEmpty &&
32-
!isMatchingRegexPattern(lastPath, _options.allowUrls)) {
28+
!isMatchingRegexPattern(url, _options.allowUrls)) {
3329
return null;
3430
}
3531

3632
if (_options.denyUrls.isNotEmpty &&
37-
isMatchingRegexPattern(lastPath, _options.denyUrls)) {
33+
isMatchingRegexPattern(url, _options.denyUrls)) {
3834
return null;
3935
}
4036

4137
return event;
4238
}
43-
44-
Iterable<SentryStackFrame?>? _getStacktraceFrames(SentryEvent event) {
45-
if (event.exceptions?.isNotEmpty == true) {
46-
return event.exceptions?.first.stackTrace?.frames;
47-
}
48-
if (event.threads?.isNotEmpty == true) {
49-
final stacktraces = event.threads?.map((e) => e.stacktrace);
50-
return stacktraces
51-
?.where((element) => element != null)
52-
.expand((element) => element!.frames);
53-
}
54-
return null;
55-
}
5639
}

flutter/lib/src/replay/widget_filter.dart

+56-16
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import 'package:flutter/rendering.dart';
2+
import 'package:flutter/services.dart';
13
import 'package:flutter/widgets.dart';
24
import 'package:meta/meta.dart';
3-
import 'package:sentry/sentry.dart';
45

56
import '../../sentry_flutter.dart';
7+
import '../sentry_asset_bundle.dart';
68

79
@internal
810
class WidgetFilter {
@@ -14,11 +16,14 @@ class WidgetFilter {
1416
late double _pixelRatio;
1517
late Rect _bounds;
1618
final _warnedWidgets = <int>{};
19+
final AssetBundle _rootAssetBundle;
1720

1821
WidgetFilter(
1922
{required this.redactText,
2023
required this.redactImages,
21-
required this.logger});
24+
required this.logger,
25+
@visibleForTesting AssetBundle? rootAssetBundle})
26+
: _rootAssetBundle = rootAssetBundle ?? rootBundle;
2227

2328
void obscure(BuildContext context, double pixelRatio, Rect bounds) {
2429
_pixelRatio = pixelRatio;
@@ -57,6 +62,14 @@ class WidgetFilter {
5762
} else if (redactText && widget is EditableText) {
5863
color = widget.style.color;
5964
} else if (redactImages && widget is Image) {
65+
if (widget.image is AssetBundleImageProvider) {
66+
final image = widget.image as AssetBundleImageProvider;
67+
if (isBuiltInAssetImage(image)) {
68+
logger(SentryLevel.debug,
69+
"WidgetFilter skipping asset: $widget ($image).");
70+
return false;
71+
}
72+
}
6073
color = widget.color;
6174
} else {
6275
// No other type is currently obscured.
@@ -65,25 +78,25 @@ class WidgetFilter {
6578

6679
final renderObject = element.renderObject;
6780
if (renderObject is! RenderBox) {
68-
_cantObscure(widget, "it's renderObject is not a RenderBox");
81+
_cantObscure(widget, "its renderObject is not a RenderBox");
6982
return false;
7083
}
7184

72-
final size = element.size;
73-
if (size == null) {
74-
_cantObscure(widget, "it's renderObject has a null size");
75-
return false;
85+
var rect = _boundingBox(renderObject);
86+
87+
// If it's a clipped render object, use parent's offset and size.
88+
// This helps with text fields which often have oversized render objects.
89+
if (renderObject.parent is RenderStack) {
90+
final renderStack = (renderObject.parent as RenderStack);
91+
final clipBehavior = renderStack.clipBehavior;
92+
if (clipBehavior == Clip.hardEdge ||
93+
clipBehavior == Clip.antiAlias ||
94+
clipBehavior == Clip.antiAliasWithSaveLayer) {
95+
final clipRect = _boundingBox(renderStack);
96+
rect = rect.intersect(clipRect);
97+
}
7698
}
7799

78-
final offset = renderObject.localToGlobal(Offset.zero);
79-
80-
final rect = Rect.fromLTWH(
81-
offset.dx * _pixelRatio,
82-
offset.dy * _pixelRatio,
83-
size.width * _pixelRatio,
84-
size.height * _pixelRatio,
85-
);
86-
87100
if (!rect.overlaps(_bounds)) {
88101
assert(() {
89102
logger(SentryLevel.debug, "WidgetFilter skipping offscreen: $widget");
@@ -115,6 +128,22 @@ class WidgetFilter {
115128
return true;
116129
}
117130

131+
@visibleForTesting
132+
@pragma('vm:prefer-inline')
133+
bool isBuiltInAssetImage(AssetBundleImageProvider image) {
134+
late final AssetBundle? bundle;
135+
if (image is AssetImage) {
136+
bundle = image.bundle;
137+
} else if (image is ExactAssetImage) {
138+
bundle = image.bundle;
139+
} else {
140+
return false;
141+
}
142+
return (bundle == null ||
143+
bundle == _rootAssetBundle ||
144+
(bundle is SentryAssetBundle && bundle.bundle == _rootAssetBundle));
145+
}
146+
118147
@pragma('vm:prefer-inline')
119148
void _cantObscure(Widget widget, String message) {
120149
if (!_warnedWidgets.contains(widget.hashCode)) {
@@ -123,6 +152,17 @@ class WidgetFilter {
123152
"WidgetFilter cannot obscure widget $widget: $message");
124153
}
125154
}
155+
156+
@pragma('vm:prefer-inline')
157+
Rect _boundingBox(RenderBox box) {
158+
final offset = box.localToGlobal(Offset.zero);
159+
return Rect.fromLTWH(
160+
offset.dx * _pixelRatio,
161+
offset.dy * _pixelRatio,
162+
box.size.width * _pixelRatio,
163+
box.size.height * _pixelRatio,
164+
);
165+
}
126166
}
127167

128168
class WidgetFilterItem {

flutter/lib/src/sentry_asset_bundle.dart

+7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'dart:ui';
77

88
import 'package:flutter/services.dart';
99
import 'package:flutter/material.dart';
10+
import 'package:meta/meta.dart';
1011
import 'package:sentry/sentry.dart';
1112

1213
typedef _StringParser<T> = Future<T> Function(String value);
@@ -375,3 +376,9 @@ class SentryAssetBundle implements AssetBundle {
375376
as Future<T>;
376377
}
377378
}
379+
380+
@internal
381+
extension SentryAssetBundleInternal on SentryAssetBundle {
382+
/// Returns the wrapped [AssetBundle].
383+
AssetBundle get bundle => _bundle;
384+
}

0 commit comments

Comments
 (0)