-
-
Notifications
You must be signed in to change notification settings - Fork 276
Symbolicate Dart stacktrace on Flutter Android and iOS without debug images from native sdks #2256
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
43 commits
Select commit
Hold shift + click to select a range
034d78c
add symbolication
buenaflor db74ff3
update implementation
buenaflor e6c1650
update
buenaflor 05ce21e
update
buenaflor 1e3a44a
update
buenaflor a1e8aa1
update
buenaflor dc02362
update
buenaflor 345d81b
update comment
buenaflor a260686
update
buenaflor 4caf3ad
update
buenaflor 966def4
update
buenaflor a6ffe97
fix
buenaflor 188b0ee
update
buenaflor ef8ab0a
fix tests
buenaflor 558a9dc
fix initial value test
buenaflor 666b65f
Update comment and test
buenaflor 621a095
update
buenaflor c3ec5e8
Update NeedsSymbolication
buenaflor df5d983
revert sample
buenaflor 859ac24
revert
buenaflor cd8b486
update
buenaflor 6369c35
update naming
buenaflor f7d358e
update naming and comments of flag
buenaflor db3bf92
set stacktrace in hint
buenaflor 07f0b6b
update
buenaflor 775279d
add changelog
buenaflor 28e031c
Merge branch 'main' into feat/dart-symbolication
buenaflor 88cfa1f
update
buenaflor e10f9b7
fix test
buenaflor 72c2844
fix test
buenaflor 16ded43
cache debug image
buenaflor 09864a7
updaet
buenaflor b50b2d8
update var name
buenaflor 30a12fd
updaet
buenaflor 05044c2
update naming
buenaflor d592ffd
improve names
buenaflor 11ef042
break early safeguard for parsing stacktrace and dont throw in hex fo…
buenaflor 6dab4d8
revert load native image list integration
buenaflor 22741a3
update
buenaflor 465b643
Merge branch 'main' into feat/dart-symbolication
buenaflor 45b2e65
Merge branch 'main' into feat/dart-symbolication
buenaflor e1d019b
fix analyze
buenaflor 6a5a555
fix analyze
buenaflor File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| import 'dart:typed_data'; | ||
| import 'package:meta/meta.dart'; | ||
| import 'package:uuid/uuid.dart'; | ||
|
|
||
| import '../sentry.dart'; | ||
|
|
||
| // Regular expressions for parsing header lines | ||
| const String _headerStartLine = | ||
| '*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***'; | ||
| final RegExp _buildIdRegex = RegExp(r"build_id(?:=|: )'([\da-f]+)'"); | ||
| final RegExp _isolateDsoBaseLineRegex = | ||
| RegExp(r'isolate_dso_base(?:=|: )([\da-f]+)'); | ||
|
|
||
| /// Extracts debug information from stack trace header. | ||
| /// Needed for symbolication of Dart stack traces without native debug images. | ||
| @internal | ||
| class DebugImageExtractor { | ||
| DebugImageExtractor(this._options); | ||
|
|
||
| final SentryOptions _options; | ||
|
|
||
| // We don't need to always parse the debug image, so we cache it here. | ||
| DebugImage? _debugImage; | ||
|
|
||
| @visibleForTesting | ||
| DebugImage? get debugImageForTesting => _debugImage; | ||
|
|
||
| DebugImage? extractFrom(String stackTraceString) { | ||
| if (_debugImage != null) { | ||
| return _debugImage; | ||
| } | ||
| _debugImage = _extractDebugInfoFrom(stackTraceString).toDebugImage(); | ||
| return _debugImage; | ||
| } | ||
|
|
||
| _DebugInfo _extractDebugInfoFrom(String stackTraceString) { | ||
| String? buildId; | ||
| String? isolateDsoBase; | ||
|
|
||
| final lines = stackTraceString.split('\n'); | ||
|
|
||
| for (final line in lines) { | ||
| if (_isHeaderStartLine(line)) { | ||
| continue; | ||
| } | ||
| // Stop parsing as soon as we get to the stack frames | ||
| // This should never happen but is a safeguard to avoid looping | ||
| // through every line of the stack trace | ||
| if (line.contains("#00 abs")) { | ||
| break; | ||
| } | ||
|
|
||
| buildId ??= _extractBuildId(line); | ||
| isolateDsoBase ??= _extractIsolateDsoBase(line); | ||
|
|
||
| // Early return if all needed information is found | ||
| if (buildId != null && isolateDsoBase != null) { | ||
| return _DebugInfo(buildId, isolateDsoBase, _options); | ||
| } | ||
| } | ||
|
|
||
| return _DebugInfo(buildId, isolateDsoBase, _options); | ||
| } | ||
|
|
||
| bool _isHeaderStartLine(String line) { | ||
| return line.contains(_headerStartLine); | ||
| } | ||
|
|
||
| String? _extractBuildId(String line) { | ||
| final buildIdMatch = _buildIdRegex.firstMatch(line); | ||
| return buildIdMatch?.group(1); | ||
| } | ||
|
|
||
| String? _extractIsolateDsoBase(String line) { | ||
| final isolateMatch = _isolateDsoBaseLineRegex.firstMatch(line); | ||
| return isolateMatch?.group(1); | ||
| } | ||
| } | ||
|
|
||
| class _DebugInfo { | ||
| final String? buildId; | ||
| final String? isolateDsoBase; | ||
| final SentryOptions _options; | ||
|
|
||
| _DebugInfo(this.buildId, this.isolateDsoBase, this._options); | ||
|
|
||
| DebugImage? toDebugImage() { | ||
| if (buildId == null || isolateDsoBase == null) { | ||
| _options.logger(SentryLevel.warning, | ||
| 'Cannot create DebugImage without buildId and isolateDsoBase.'); | ||
| return null; | ||
| } | ||
|
|
||
| String type; | ||
| String? imageAddr; | ||
| String? debugId; | ||
| String? codeId; | ||
|
|
||
| final platform = _options.platformChecker.platform; | ||
|
|
||
| // Default values for all platforms | ||
| imageAddr = '0x$isolateDsoBase'; | ||
|
|
||
| if (platform.isAndroid) { | ||
| type = 'elf'; | ||
| debugId = _convertCodeIdToDebugId(buildId!); | ||
| codeId = buildId; | ||
| } else if (platform.isIOS || platform.isMacOS) { | ||
| type = 'macho'; | ||
| debugId = _formatHexToUuid(buildId!); | ||
| // `codeId` is not needed for iOS/MacOS. | ||
| } else { | ||
| _options.logger( | ||
| SentryLevel.warning, | ||
| 'Unsupported platform for creating Dart debug images.', | ||
| ); | ||
| return null; | ||
| } | ||
|
|
||
| return DebugImage( | ||
| type: type, | ||
| imageAddr: imageAddr, | ||
| debugId: debugId, | ||
| codeId: codeId, | ||
| ); | ||
| } | ||
|
|
||
| // Debug identifier is the little-endian UUID representation of the first 16-bytes of | ||
| // the build ID on ELF images. | ||
| String? _convertCodeIdToDebugId(String codeId) { | ||
| codeId = codeId.replaceAll(' ', ''); | ||
| if (codeId.length < 32) { | ||
| _options.logger(SentryLevel.warning, | ||
| 'Code ID must be at least 32 hexadecimal characters long'); | ||
| return null; | ||
| } | ||
|
|
||
| final first16Bytes = codeId.substring(0, 32); | ||
| final byteData = _parseHexToBytes(first16Bytes); | ||
|
|
||
| if (byteData == null || byteData.isEmpty) { | ||
| _options.logger( | ||
| SentryLevel.warning, 'Failed to convert code ID to debug ID'); | ||
| return null; | ||
| } | ||
|
|
||
| return bigToLittleEndianUuid(UuidValue.fromByteList(byteData).uuid); | ||
| } | ||
|
|
||
| Uint8List? _parseHexToBytes(String hex) { | ||
| if (hex.length % 2 != 0) { | ||
| _options.logger( | ||
| SentryLevel.warning, 'Invalid hex string during debug image parsing'); | ||
| return null; | ||
| } | ||
| if (hex.startsWith('0x')) { | ||
| hex = hex.substring(2); | ||
| } | ||
|
|
||
| var bytes = Uint8List(hex.length ~/ 2); | ||
| for (var i = 0; i < hex.length; i += 2) { | ||
| bytes[i ~/ 2] = int.parse(hex.substring(i, i + 2), radix: 16); | ||
| } | ||
| return bytes; | ||
| } | ||
|
|
||
| String bigToLittleEndianUuid(String bigEndianUuid) { | ||
| final byteArray = | ||
| Uuid.parse(bigEndianUuid, validationMode: ValidationMode.nonStrict); | ||
|
|
||
| final reversedByteArray = Uint8List.fromList([ | ||
| ...byteArray.sublist(0, 4).reversed, | ||
| ...byteArray.sublist(4, 6).reversed, | ||
| ...byteArray.sublist(6, 8).reversed, | ||
| ...byteArray.sublist(8, 10), | ||
| ...byteArray.sublist(10), | ||
| ]); | ||
|
|
||
| return Uuid.unparse(reversedByteArray); | ||
| } | ||
|
|
||
| String? _formatHexToUuid(String hex) { | ||
| if (hex.length != 32) { | ||
| _options.logger(SentryLevel.warning, | ||
| 'Hex input must be a 32-character hexadecimal string'); | ||
| return null; | ||
| } | ||
|
|
||
| return '${hex.substring(0, 8)}-' | ||
| '${hex.substring(8, 12)}-' | ||
| '${hex.substring(12, 16)}-' | ||
| '${hex.substring(16, 20)}-' | ||
| '${hex.substring(20)}'; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import '../sentry.dart'; | ||
| import 'debug_image_extractor.dart'; | ||
|
|
||
| class LoadDartDebugImagesIntegration extends Integration<SentryOptions> { | ||
| @override | ||
| void call(Hub hub, SentryOptions options) { | ||
| options.addEventProcessor(_LoadImageIntegrationEventProcessor( | ||
| DebugImageExtractor(options), options)); | ||
| options.sdk.addIntegration('loadDartImageIntegration'); | ||
| } | ||
| } | ||
|
|
||
| const hintRawStackTraceKey = 'raw_stacktrace'; | ||
|
|
||
| class _LoadImageIntegrationEventProcessor implements EventProcessor { | ||
| _LoadImageIntegrationEventProcessor(this._debugImageExtractor, this._options); | ||
|
|
||
| final SentryOptions _options; | ||
| final DebugImageExtractor _debugImageExtractor; | ||
|
|
||
| @override | ||
| Future<SentryEvent?> apply(SentryEvent event, Hint hint) async { | ||
| final rawStackTrace = hint.get(hintRawStackTraceKey) as String?; | ||
| if (!_options.enableDartSymbolication || | ||
| !event.needsSymbolication() || | ||
| rawStackTrace == null) { | ||
| return event; | ||
| } | ||
|
|
||
| try { | ||
| final syntheticImage = _debugImageExtractor.extractFrom(rawStackTrace); | ||
| if (syntheticImage == null) { | ||
| return event; | ||
| } | ||
|
|
||
| return event.copyWith(debugMeta: DebugMeta(images: [syntheticImage])); | ||
| } catch (e, stackTrace) { | ||
| _options.logger( | ||
| SentryLevel.info, | ||
| "Couldn't add Dart debug image to event. " | ||
| 'The event will still be reported.', | ||
| exception: e, | ||
| stackTrace: stackTrace, | ||
| ); | ||
| return event; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| extension NeedsSymbolication on SentryEvent { | ||
| bool needsSymbolication() { | ||
| if (this is SentryTransaction) { | ||
| return false; | ||
| } | ||
| final frames = _getStacktraceFrames(); | ||
| if (frames == null) { | ||
| return false; | ||
| } | ||
| return frames.any((frame) => 'native' == frame?.platform); | ||
| } | ||
|
|
||
| Iterable<SentryStackFrame?>? _getStacktraceFrames() { | ||
| if (exceptions?.isNotEmpty == true) { | ||
| return exceptions?.first.stackTrace?.frames; | ||
| } | ||
| if (threads?.isNotEmpty == true) { | ||
| var stacktraces = threads?.map((e) => e.stacktrace); | ||
| return stacktraces | ||
| ?.where((element) => element != null) | ||
| .expand((element) => element!.frames); | ||
| } | ||
| return null; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.