Skip to content

Commit 7563246

Browse files
authored
Merge branch 'main' into feat/capture-touch-breadcrumbs
2 parents ed4cab2 + a40bb7c commit 7563246

23 files changed

+548
-57
lines changed

CHANGELOG.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
### Features
66

7-
- Session replay Alpha for Android and iOS ([#2208](https://github.com/getsentry/sentry-dart/pull/2208)).
7+
- 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)).
88

99
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/)):
1010

@@ -19,6 +19,19 @@
1919
);
2020
```
2121

22+
- Support allowUrls and denyUrls for Flutter Web ([#2227](https://github.com/getsentry/sentry-dart/pull/2227))
23+
24+
```dart
25+
await SentryFlutter.init(
26+
(options) {
27+
...
28+
options.allowUrls = ["^https://sentry.com.*\$", "my-custom-domain"];
29+
options.denyUrls = ["^.*ends-with-this\$", "denied-url"];
30+
},
31+
appRunner: () => runApp(MyApp()),
32+
);
33+
```
34+
2235
- Collect touch breadcrumbs for all buttons, not just those with `key` specified. ([#2242](https://github.com/getsentry/sentry-dart/pull/2242))
2336

2437
### Dependencies
@@ -27,12 +40,17 @@
2740
- [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8360)
2841
- [diff](https://github.com/getsentry/sentry-cocoa/compare/8.35.1...8.36.0)
2942

43+
### Fixes
44+
45+
- Only access renderObject if `hasSize` is true ([#2263](https://github.com/getsentry/sentry-dart/pull/2263))
46+
3047
## 8.8.0
3148

3249
### Features
3350

3451
- Add `SentryFlutter.nativeCrash()` using MethodChannels for Android and iOS ([#2239](https://github.com/getsentry/sentry-dart/pull/2239))
3552
- This can be used to test if native crash reporting works
53+
3654
- Add `ignoreRoutes` parameter to `SentryNavigatorObserver`. ([#2218](https://github.com/getsentry/sentry-dart/pull/2218))
3755
- This will ignore the Routes and prevent the Route from being pushed to the Sentry server.
3856
- Ignored routes will also create no TTID and TTFD spans.

dart/lib/src/sentry_client.dart

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import 'transport/rate_limiter.dart';
2626
import 'transport/spotlight_http_transport.dart';
2727
import 'transport/task_queue.dart';
2828
import 'utils/isolate_utils.dart';
29+
import 'utils/regex_utils.dart';
2930
import 'utils/stacktrace_utils.dart';
3031
import 'version.dart';
3132

@@ -196,7 +197,7 @@ class SentryClient {
196197
}
197198

198199
var message = event.message!.formatted;
199-
return _isMatchingRegexPattern(message, _options.ignoreErrors);
200+
return isMatchingRegexPattern(message, _options.ignoreErrors);
200201
}
201202

202203
SentryEvent _prepareEvent(SentryEvent event, {dynamic stackTrace}) {
@@ -415,7 +416,7 @@ class SentryClient {
415416
}
416417

417418
var name = transaction.tracer.name;
418-
return _isMatchingRegexPattern(name, _options.ignoreTransactions);
419+
return isMatchingRegexPattern(name, _options.ignoreTransactions);
419420
}
420421

421422
/// Reports the [envelope] to Sentry.io.
@@ -593,11 +594,4 @@ class SentryClient {
593594
SentryId.empty(),
594595
);
595596
}
596-
597-
bool _isMatchingRegexPattern(String value, List<String> regexPattern,
598-
{bool caseSensitive = false}) {
599-
final combinedRegexPattern = regexPattern.join('|');
600-
final regExp = RegExp(combinedRegexPattern, caseSensitive: caseSensitive);
601-
return regExp.hasMatch(value);
602-
}
603597
}

dart/lib/src/sentry_options.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,12 @@ class SentryOptions {
186186

187187
/// The ignoreErrors tells the SDK which errors should be not sent to the sentry server.
188188
/// If an null or an empty list is used, the SDK will send all transactions.
189+
/// To use regex add the `^` and the `$` to the string.
189190
List<String> ignoreErrors = [];
190191

191192
/// The ignoreTransactions tells the SDK which transactions should be not sent to the sentry server.
192193
/// If null or an empty list is used, the SDK will send all transactions.
194+
/// To use regex add the `^` and the `$` to the string.
193195
List<String> ignoreTransactions = [];
194196

195197
final List<String> _inAppExcludes = [];

dart/lib/src/transport/spotlight_http_transport.dart

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,6 @@ class SpotlightHttpTransport extends Transport {
3838
Future<void> _sendToSpotlight(SentryEnvelope envelope) async {
3939
envelope.header.sentAt = _options.clock();
4040

41-
// Screenshots do not work currently https://github.com/getsentry/spotlight/issues/274
42-
envelope.items
43-
.removeWhere((element) => element.header.contentType == 'image/png');
44-
4541
final spotlightRequest = await _requestHandler.createRequest(envelope);
4642

4743
final response = await _options.httpClient

dart/lib/src/utils/regex_utils.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import 'package:meta/meta.dart';
2+
3+
@internal
4+
bool isMatchingRegexPattern(String value, List<String> regexPattern,
5+
{bool caseSensitive = false}) {
6+
final combinedRegexPattern = regexPattern.join('|');
7+
final regExp = RegExp(combinedRegexPattern, caseSensitive: caseSensitive);
8+
return regExp.hasMatch(value);
9+
}

dart/test/utils/regex_utils_test.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import 'package:sentry/src/utils/regex_utils.dart';
2+
import 'package:test/test.dart';
3+
4+
void main() {
5+
group('regex_utils', () {
6+
final testString = "this is a test";
7+
8+
test('testString contains string pattern', () {
9+
expect(isMatchingRegexPattern(testString, ["is"]), isTrue);
10+
});
11+
12+
test('testString does not contain string pattern', () {
13+
expect(isMatchingRegexPattern(testString, ["not"]), isFalse);
14+
});
15+
16+
test('testString contains regex pattern', () {
17+
expect(isMatchingRegexPattern(testString, ["^this.*\$"]), isTrue);
18+
});
19+
20+
test('testString does not contain regex pattern', () {
21+
expect(isMatchingRegexPattern(testString, ["^is.*\$"]), isFalse);
22+
});
23+
});
24+
}

flutter/example/lib/main.dart

Lines changed: 10 additions & 0 deletions
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/ios/Classes/SentryFlutterPluginApple.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin {
182182
case "nativeCrash":
183183
crash()
184184

185-
case "sendReplayForEvent":
185+
case "captureReplay":
186186
#if canImport(UIKit) && !SENTRY_NO_UIKIT && (os(iOS) || os(tvOS))
187187
PrivateSentrySDKOnly.captureReplay()
188188
result(PrivateSentrySDKOnly.getReplayId())

flutter/lib/sentry_flutter.dart

Lines changed: 1 addition & 1 deletion
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';
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import 'dart:html' as html show window, Window;
2+
3+
import '../../../sentry_flutter.dart';
4+
import 'url_filter_event_processor.dart';
5+
// ignore: implementation_imports
6+
import 'package:sentry/src/utils/regex_utils.dart';
7+
8+
// ignore_for_file: invalid_use_of_internal_member
9+
10+
UrlFilterEventProcessor urlFilterEventProcessor(SentryFlutterOptions options) =>
11+
WebUrlFilterEventProcessor(options);
12+
13+
class WebUrlFilterEventProcessor implements UrlFilterEventProcessor {
14+
WebUrlFilterEventProcessor(
15+
this._options,
16+
);
17+
18+
final SentryFlutterOptions _options;
19+
20+
@override
21+
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+
}
28+
29+
if (_options.allowUrls.isNotEmpty &&
30+
!isMatchingRegexPattern(lastPath, _options.allowUrls)) {
31+
return null;
32+
}
33+
34+
if (_options.denyUrls.isNotEmpty &&
35+
isMatchingRegexPattern(lastPath, _options.denyUrls)) {
36+
return null;
37+
}
38+
39+
return event;
40+
}
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+
}
54+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import '../../../sentry_flutter.dart';
2+
import 'url_filter_event_processor.dart';
3+
4+
UrlFilterEventProcessor urlFilterEventProcessor(SentryFlutterOptions _) =>
5+
IoUrlFilterEventProcessor();
6+
7+
class IoUrlFilterEventProcessor implements UrlFilterEventProcessor {
8+
@override
9+
SentryEvent apply(SentryEvent event, Hint hint) => event;
10+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import '../../../sentry_flutter.dart';
2+
import 'io_url_filter_event_processor.dart'
3+
if (dart.library.html) 'html_url_filter_event_processor.dart'
4+
if (dart.library.js_interop) 'web_url_filter_event_processor.dart';
5+
6+
abstract class UrlFilterEventProcessor implements EventProcessor {
7+
factory UrlFilterEventProcessor(SentryFlutterOptions options) =>
8+
urlFilterEventProcessor(options);
9+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// We would lose compatibility with old dart versions by adding web to pubspec.
2+
// ignore: depend_on_referenced_packages
3+
import 'package:web/web.dart' as web show window, Window;
4+
5+
import '../../../sentry_flutter.dart';
6+
import 'url_filter_event_processor.dart';
7+
// ignore: implementation_imports
8+
import 'package:sentry/src/utils/regex_utils.dart';
9+
10+
// ignore_for_file: invalid_use_of_internal_member
11+
12+
UrlFilterEventProcessor urlFilterEventProcessor(SentryFlutterOptions options) =>
13+
WebUrlFilterEventProcessor(options);
14+
15+
class WebUrlFilterEventProcessor implements UrlFilterEventProcessor {
16+
WebUrlFilterEventProcessor(
17+
this._options,
18+
);
19+
20+
final SentryFlutterOptions _options;
21+
22+
@override
23+
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+
}
30+
31+
if (_options.allowUrls.isNotEmpty &&
32+
!isMatchingRegexPattern(lastPath, _options.allowUrls)) {
33+
return null;
34+
}
35+
36+
if (_options.denyUrls.isNotEmpty &&
37+
isMatchingRegexPattern(lastPath, _options.denyUrls)) {
38+
return null;
39+
}
40+
41+
return event;
42+
}
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+
}
56+
}

flutter/lib/src/replay/widget_filter.dart

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import 'package:flutter/services.dart';
12
import 'package:flutter/widgets.dart';
23
import 'package:meta/meta.dart';
3-
import 'package:sentry/sentry.dart';
44

55
import '../../sentry_flutter.dart';
6+
import '../sentry_asset_bundle.dart';
67

78
@internal
89
class WidgetFilter {
@@ -14,11 +15,14 @@ class WidgetFilter {
1415
late double _pixelRatio;
1516
late Rect _bounds;
1617
final _warnedWidgets = <int>{};
18+
final AssetBundle _rootAssetBundle;
1719

1820
WidgetFilter(
1921
{required this.redactText,
2022
required this.redactImages,
21-
required this.logger});
23+
required this.logger,
24+
@visibleForTesting AssetBundle? rootAssetBundle})
25+
: _rootAssetBundle = rootAssetBundle ?? rootBundle;
2226

2327
void obscure(BuildContext context, double pixelRatio, Rect bounds) {
2428
_pixelRatio = pixelRatio;
@@ -57,6 +61,14 @@ class WidgetFilter {
5761
} else if (redactText && widget is EditableText) {
5862
color = widget.style.color;
5963
} else if (redactImages && widget is Image) {
64+
if (widget.image is AssetBundleImageProvider) {
65+
final image = widget.image as AssetBundleImageProvider;
66+
if (isBuiltInAssetImage(image)) {
67+
logger(SentryLevel.debug,
68+
"WidgetFilter skipping asset: $widget ($image).");
69+
return false;
70+
}
71+
}
6072
color = widget.color;
6173
} else {
6274
// No other type is currently obscured.
@@ -115,6 +127,22 @@ class WidgetFilter {
115127
return true;
116128
}
117129

130+
@visibleForTesting
131+
@pragma('vm:prefer-inline')
132+
bool isBuiltInAssetImage(AssetBundleImageProvider image) {
133+
late final AssetBundle? bundle;
134+
if (image is AssetImage) {
135+
bundle = image.bundle;
136+
} else if (image is ExactAssetImage) {
137+
bundle = image.bundle;
138+
} else {
139+
return false;
140+
}
141+
return (bundle == null ||
142+
bundle == _rootAssetBundle ||
143+
(bundle is SentryAssetBundle && bundle.bundle == _rootAssetBundle));
144+
}
145+
118146
@pragma('vm:prefer-inline')
119147
void _cantObscure(Widget widget, String message) {
120148
if (!_warnedWidgets.contains(widget.hashCode)) {

0 commit comments

Comments
 (0)