Skip to content

Commit 3fb08ac

Browse files
committed
Merge branch 'fix/flutter-multiview-support' of https://github.com/getsentry/sentry-dart into fix/flutter-multiview-support
* 'fix/flutter-multiview-support' of https://github.com/getsentry/sentry-dart: Update CHANGELOG.md release: 8.9.0 chore: rename errorSampleRate to onErrorSampleRate (#2270) fix: repost replay screenshots on android while idle (#2275) feat: capture touch breadcrumbs for all buttons (#2242) Symbolicate Dart stacktrace on Flutter Android and iOS without debug images from native sdks (#2256) Fix: Support allowUrls, denyUrls (#2271) chore(deps): update Flutter SDK (metrics) to v3.24.2 (#2272)
2 parents 1d82e56 + 6883e29 commit 3fb08ac

File tree

56 files changed

+1290
-478
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1290
-478
lines changed

CHANGELOG.md

+10-6
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
# Changelog
22

3-
## Unreleased
3+
## 8.9.0
44

55
### Features
66

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), [#2236](https://github.com/getsentry/sentry-dart/pull/2236)).
8-
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), [#2236](https://github.com/getsentry/sentry-dart/pull/2236), [#2275](https://github.com/getsentry/sentry-dart/pull/2275), [#2270](https://github.com/getsentry/sentry-dart/pull/2270)).
98
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/)):
109

1110
```dart
1211
await SentryFlutter.init(
1312
(options) {
1413
...
1514
options.experimental.replay.sessionSampleRate = 1.0;
16-
options.experimental.replay.errorSampleRate = 1.0;
15+
options.experimental.replay.onErrorSampleRate = 1.0;
1716
},
1817
appRunner: () => runApp(MyApp()),
1918
);
@@ -27,12 +26,17 @@
2726
...
2827
options.allowUrls = ["^https://sentry.com.*\$", "my-custom-domain"];
2928
options.denyUrls = ["^.*ends-with-this\$", "denied-url"];
30-
options.denyUrls = ["^.*ends-with-this\$", "denied-url"];
3129
},
3230
appRunner: () => runApp(MyApp()),
3331
);
3432
```
3533

34+
- Collect touch breadcrumbs for all buttons, not just those with `key` specified. ([#2242](https://github.com/getsentry/sentry-dart/pull/2242))
35+
- Add `enableDartSymbolication` option to Sentry.init() for **Flutter iOS, macOS and Android** ([#2256](https://github.com/getsentry/sentry-dart/pull/2256))
36+
- This flag enables symbolication of Dart stack traces when native debug images are not available.
37+
- Useful when using Sentry.init() instead of SentryFlutter.init() in Flutter projects for example due to size limitations.
38+
- `true` by default but automatically set to `false` when using SentryFlutter.init() because the SentryFlutter fetches debug images from the native SDK integrations.
39+
3640
### Dependencies
3741

3842
- Bump Cocoa SDK from v8.35.1 to v8.36.0 ([#2252](https://github.com/getsentry/sentry-dart/pull/2252))
@@ -199,7 +203,7 @@ SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]),
199203
(options) {
200204
...
201205
options.experimental.replay.sessionSampleRate = 1.0;
202-
options.experimental.replay.errorSampleRate = 1.0;
206+
options.experimental.replay.onErrorSampleRate = 1.0;
203207
},
204208
appRunner: () => runApp(MyApp()),
205209
);
+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import 'dart:typed_data';
2+
import 'package:meta/meta.dart';
3+
import 'package:uuid/uuid.dart';
4+
5+
import '../sentry.dart';
6+
7+
// Regular expressions for parsing header lines
8+
const String _headerStartLine =
9+
'*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***';
10+
final RegExp _buildIdRegex = RegExp(r"build_id(?:=|: )'([\da-f]+)'");
11+
final RegExp _isolateDsoBaseLineRegex =
12+
RegExp(r'isolate_dso_base(?:=|: )([\da-f]+)');
13+
14+
/// Extracts debug information from stack trace header.
15+
/// Needed for symbolication of Dart stack traces without native debug images.
16+
@internal
17+
class DebugImageExtractor {
18+
DebugImageExtractor(this._options);
19+
20+
final SentryOptions _options;
21+
22+
// We don't need to always parse the debug image, so we cache it here.
23+
DebugImage? _debugImage;
24+
25+
@visibleForTesting
26+
DebugImage? get debugImageForTesting => _debugImage;
27+
28+
DebugImage? extractFrom(String stackTraceString) {
29+
if (_debugImage != null) {
30+
return _debugImage;
31+
}
32+
_debugImage = _extractDebugInfoFrom(stackTraceString).toDebugImage();
33+
return _debugImage;
34+
}
35+
36+
_DebugInfo _extractDebugInfoFrom(String stackTraceString) {
37+
String? buildId;
38+
String? isolateDsoBase;
39+
40+
final lines = stackTraceString.split('\n');
41+
42+
for (final line in lines) {
43+
if (_isHeaderStartLine(line)) {
44+
continue;
45+
}
46+
// Stop parsing as soon as we get to the stack frames
47+
// This should never happen but is a safeguard to avoid looping
48+
// through every line of the stack trace
49+
if (line.contains("#00 abs")) {
50+
break;
51+
}
52+
53+
buildId ??= _extractBuildId(line);
54+
isolateDsoBase ??= _extractIsolateDsoBase(line);
55+
56+
// Early return if all needed information is found
57+
if (buildId != null && isolateDsoBase != null) {
58+
return _DebugInfo(buildId, isolateDsoBase, _options);
59+
}
60+
}
61+
62+
return _DebugInfo(buildId, isolateDsoBase, _options);
63+
}
64+
65+
bool _isHeaderStartLine(String line) {
66+
return line.contains(_headerStartLine);
67+
}
68+
69+
String? _extractBuildId(String line) {
70+
final buildIdMatch = _buildIdRegex.firstMatch(line);
71+
return buildIdMatch?.group(1);
72+
}
73+
74+
String? _extractIsolateDsoBase(String line) {
75+
final isolateMatch = _isolateDsoBaseLineRegex.firstMatch(line);
76+
return isolateMatch?.group(1);
77+
}
78+
}
79+
80+
class _DebugInfo {
81+
final String? buildId;
82+
final String? isolateDsoBase;
83+
final SentryOptions _options;
84+
85+
_DebugInfo(this.buildId, this.isolateDsoBase, this._options);
86+
87+
DebugImage? toDebugImage() {
88+
if (buildId == null || isolateDsoBase == null) {
89+
_options.logger(SentryLevel.warning,
90+
'Cannot create DebugImage without buildId and isolateDsoBase.');
91+
return null;
92+
}
93+
94+
String type;
95+
String? imageAddr;
96+
String? debugId;
97+
String? codeId;
98+
99+
final platform = _options.platformChecker.platform;
100+
101+
// Default values for all platforms
102+
imageAddr = '0x$isolateDsoBase';
103+
104+
if (platform.isAndroid) {
105+
type = 'elf';
106+
debugId = _convertCodeIdToDebugId(buildId!);
107+
codeId = buildId;
108+
} else if (platform.isIOS || platform.isMacOS) {
109+
type = 'macho';
110+
debugId = _formatHexToUuid(buildId!);
111+
// `codeId` is not needed for iOS/MacOS.
112+
} else {
113+
_options.logger(
114+
SentryLevel.warning,
115+
'Unsupported platform for creating Dart debug images.',
116+
);
117+
return null;
118+
}
119+
120+
return DebugImage(
121+
type: type,
122+
imageAddr: imageAddr,
123+
debugId: debugId,
124+
codeId: codeId,
125+
);
126+
}
127+
128+
// Debug identifier is the little-endian UUID representation of the first 16-bytes of
129+
// the build ID on ELF images.
130+
String? _convertCodeIdToDebugId(String codeId) {
131+
codeId = codeId.replaceAll(' ', '');
132+
if (codeId.length < 32) {
133+
_options.logger(SentryLevel.warning,
134+
'Code ID must be at least 32 hexadecimal characters long');
135+
return null;
136+
}
137+
138+
final first16Bytes = codeId.substring(0, 32);
139+
final byteData = _parseHexToBytes(first16Bytes);
140+
141+
if (byteData == null || byteData.isEmpty) {
142+
_options.logger(
143+
SentryLevel.warning, 'Failed to convert code ID to debug ID');
144+
return null;
145+
}
146+
147+
return bigToLittleEndianUuid(UuidValue.fromByteList(byteData).uuid);
148+
}
149+
150+
Uint8List? _parseHexToBytes(String hex) {
151+
if (hex.length % 2 != 0) {
152+
_options.logger(
153+
SentryLevel.warning, 'Invalid hex string during debug image parsing');
154+
return null;
155+
}
156+
if (hex.startsWith('0x')) {
157+
hex = hex.substring(2);
158+
}
159+
160+
var bytes = Uint8List(hex.length ~/ 2);
161+
for (var i = 0; i < hex.length; i += 2) {
162+
bytes[i ~/ 2] = int.parse(hex.substring(i, i + 2), radix: 16);
163+
}
164+
return bytes;
165+
}
166+
167+
String bigToLittleEndianUuid(String bigEndianUuid) {
168+
final byteArray =
169+
Uuid.parse(bigEndianUuid, validationMode: ValidationMode.nonStrict);
170+
171+
final reversedByteArray = Uint8List.fromList([
172+
...byteArray.sublist(0, 4).reversed,
173+
...byteArray.sublist(4, 6).reversed,
174+
...byteArray.sublist(6, 8).reversed,
175+
...byteArray.sublist(8, 10),
176+
...byteArray.sublist(10),
177+
]);
178+
179+
return Uuid.unparse(reversedByteArray);
180+
}
181+
182+
String? _formatHexToUuid(String hex) {
183+
if (hex.length != 32) {
184+
_options.logger(SentryLevel.warning,
185+
'Hex input must be a 32-character hexadecimal string');
186+
return null;
187+
}
188+
189+
return '${hex.substring(0, 8)}-'
190+
'${hex.substring(8, 12)}-'
191+
'${hex.substring(12, 16)}-'
192+
'${hex.substring(16, 20)}-'
193+
'${hex.substring(20)}';
194+
}
195+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import '../sentry.dart';
2+
import 'debug_image_extractor.dart';
3+
4+
class LoadDartDebugImagesIntegration extends Integration<SentryOptions> {
5+
@override
6+
void call(Hub hub, SentryOptions options) {
7+
options.addEventProcessor(_LoadImageIntegrationEventProcessor(
8+
DebugImageExtractor(options), options));
9+
options.sdk.addIntegration('loadDartImageIntegration');
10+
}
11+
}
12+
13+
const hintRawStackTraceKey = 'raw_stacktrace';
14+
15+
class _LoadImageIntegrationEventProcessor implements EventProcessor {
16+
_LoadImageIntegrationEventProcessor(this._debugImageExtractor, this._options);
17+
18+
final SentryOptions _options;
19+
final DebugImageExtractor _debugImageExtractor;
20+
21+
@override
22+
Future<SentryEvent?> apply(SentryEvent event, Hint hint) async {
23+
final rawStackTrace = hint.get(hintRawStackTraceKey) as String?;
24+
if (!_options.enableDartSymbolication ||
25+
!event.needsSymbolication() ||
26+
rawStackTrace == null) {
27+
return event;
28+
}
29+
30+
try {
31+
final syntheticImage = _debugImageExtractor.extractFrom(rawStackTrace);
32+
if (syntheticImage == null) {
33+
return event;
34+
}
35+
36+
return event.copyWith(debugMeta: DebugMeta(images: [syntheticImage]));
37+
} catch (e, stackTrace) {
38+
_options.logger(
39+
SentryLevel.info,
40+
"Couldn't add Dart debug image to event. "
41+
'The event will still be reported.',
42+
exception: e,
43+
stackTrace: stackTrace,
44+
);
45+
return event;
46+
}
47+
}
48+
}
49+
50+
extension NeedsSymbolication on SentryEvent {
51+
bool needsSymbolication() {
52+
if (this is SentryTransaction) {
53+
return false;
54+
}
55+
final frames = _getStacktraceFrames();
56+
if (frames == null) {
57+
return false;
58+
}
59+
return frames.any((frame) => 'native' == frame?.platform);
60+
}
61+
62+
Iterable<SentryStackFrame?>? _getStacktraceFrames() {
63+
if (exceptions?.isNotEmpty == true) {
64+
return exceptions?.first.stackTrace?.frames;
65+
}
66+
if (threads?.isNotEmpty == true) {
67+
var stacktraces = threads?.map((e) => e.stacktrace);
68+
return stacktraces
69+
?.where((element) => element != null)
70+
.expand((element) => element!.frames);
71+
}
72+
return null;
73+
}
74+
}

dart/lib/src/protocol/breadcrumb.dart

+5-30
Original file line numberDiff line numberDiff line change
@@ -105,42 +105,17 @@ class Breadcrumb {
105105
String? viewId,
106106
String? viewClass,
107107
}) {
108-
final newData = data ?? {};
109-
var path = '';
110-
111-
if (viewId != null) {
112-
newData['view.id'] = viewId;
113-
path = viewId;
114-
}
115-
116-
if (newData.containsKey('label')) {
117-
if (path.isEmpty) {
118-
path = newData['label'];
119-
} else {
120-
path = "$path, label: ${newData['label']}";
121-
}
122-
}
123-
124-
if (viewClass != null) {
125-
newData['view.class'] = viewClass;
126-
if (path.isEmpty) {
127-
path = viewClass;
128-
} else {
129-
path = "$viewClass($path)";
130-
}
131-
}
132-
133-
if (path.isNotEmpty && !newData.containsKey('path')) {
134-
newData['path'] = path;
135-
}
136-
137108
return Breadcrumb(
138109
message: message,
139110
level: level,
140111
category: 'ui.$subCategory',
141112
type: 'user',
142113
timestamp: timestamp,
143-
data: newData,
114+
data: {
115+
if (viewId != null) 'view.id': viewId,
116+
if (viewClass != null) 'view.class': viewClass,
117+
if (data != null) ...data,
118+
},
144119
);
145120
}
146121

dart/lib/src/sentry.dart

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:async';
33
import 'package:meta/meta.dart';
44

55
import 'dart_exception_type_identifier.dart';
6+
import 'load_dart_debug_images_integration.dart';
67
import 'metrics/metrics_api.dart';
78
import 'run_zoned_guarded_integration.dart';
89
import 'event_processor/enricher/enricher_event_processor.dart';
@@ -83,6 +84,10 @@ class Sentry {
8384
options.addIntegrationByIndex(0, IsolateErrorIntegration());
8485
}
8586

87+
if (options.enableDartSymbolication) {
88+
options.addIntegration(LoadDartDebugImagesIntegration());
89+
}
90+
8691
options.addEventProcessor(EnricherEventProcessor(options));
8792
options.addEventProcessor(ExceptionEventProcessor(options));
8893
options.addEventProcessor(DeduplicationEventProcessor(options));

0 commit comments

Comments
 (0)