Skip to content

Commit 17e05e9

Browse files
committed
Merge branch 'main' into feat/improve-app-start-integration
2 parents a7fd92d + 2e93bab commit 17e05e9

Some content is hidden

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

42 files changed

+1464
-509
lines changed

CHANGELOG.md

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,40 @@
44

55
### Features
66

7-
- Support allowUrls and denyUrls for Flutter Web ([#2227](https://github.com/getsentry/sentry-dart/pull/2227))
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)).
8+
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/)):
89

910
```dart
1011
await SentryFlutter.init(
1112
(options) {
1213
...
13-
options.allowUrls = ["^https://sentry.com.*\$", "my-custom-domain"];
14-
options.denyUrls = ["^.*ends-with-this\$", "denied-url"];
14+
options.experimental.replay.sessionSampleRate = 1.0;
15+
options.experimental.replay.onErrorSampleRate = 1.0;
1516
},
1617
appRunner: () => runApp(MyApp()),
1718
);
1819
```
1920

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

2423
```dart
2524
await SentryFlutter.init(
2625
(options) {
2726
...
28-
options.experimental.replay.sessionSampleRate = 1.0;
29-
options.experimental.replay.errorSampleRate = 1.0;
27+
options.allowUrls = ["^https://sentry.com.*\$", "my-custom-domain"];
28+
options.denyUrls = ["^.*ends-with-this\$", "denied-url"];
29+
options.denyUrls = ["^.*ends-with-this\$", "denied-url"];
3030
},
3131
appRunner: () => runApp(MyApp()),
3232
);
3333
```
3434

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

3743
- Bump Cocoa SDK from v8.35.1 to v8.36.0 ([#2252](https://github.com/getsentry/sentry-dart/pull/2252))
@@ -198,7 +204,7 @@ SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]),
198204
(options) {
199205
...
200206
options.experimental.replay.sessionSampleRate = 1.0;
201-
options.experimental.replay.errorSampleRate = 1.0;
207+
options.experimental.replay.onErrorSampleRate = 1.0;
202208
},
203209
appRunner: () => runApp(MyApp()),
204210
);
Lines changed: 195 additions & 0 deletions
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+
}
Lines changed: 74 additions & 0 deletions
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

Lines changed: 5 additions & 30 deletions
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

Lines changed: 5 additions & 0 deletions
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)