Skip to content
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

Symbolicate Dart stacktrace on Flutter Android and iOS without debug images from native sdks #2256

Merged
merged 43 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
034d78c
add symbolication
buenaflor Aug 28, 2024
db74ff3
update implementation
buenaflor Aug 28, 2024
e6c1650
update
buenaflor Aug 28, 2024
05ce21e
update
buenaflor Aug 28, 2024
1e3a44a
update
buenaflor Aug 28, 2024
a1e8aa1
update
buenaflor Aug 28, 2024
dc02362
update
buenaflor Aug 28, 2024
345d81b
update comment
buenaflor Aug 28, 2024
a260686
update
buenaflor Aug 28, 2024
4caf3ad
update
buenaflor Aug 28, 2024
966def4
update
buenaflor Aug 28, 2024
a6ffe97
fix
buenaflor Aug 28, 2024
188b0ee
update
buenaflor Aug 28, 2024
ef8ab0a
fix tests
buenaflor Aug 28, 2024
558a9dc
fix initial value test
buenaflor Aug 28, 2024
666b65f
Update comment and test
buenaflor Aug 29, 2024
621a095
update
buenaflor Aug 29, 2024
c3ec5e8
Update NeedsSymbolication
buenaflor Aug 29, 2024
df5d983
revert sample
buenaflor Aug 29, 2024
859ac24
revert
buenaflor Aug 29, 2024
cd8b486
update
buenaflor Aug 29, 2024
6369c35
update naming
buenaflor Aug 29, 2024
f7d358e
update naming and comments of flag
buenaflor Aug 29, 2024
db3bf92
set stacktrace in hint
buenaflor Aug 29, 2024
07f0b6b
update
buenaflor Aug 29, 2024
775279d
add changelog
buenaflor Aug 29, 2024
28e031c
Merge branch 'main' into feat/dart-symbolication
buenaflor Aug 29, 2024
88cfa1f
update
buenaflor Aug 29, 2024
e10f9b7
fix test
buenaflor Aug 29, 2024
72c2844
fix test
buenaflor Aug 29, 2024
16ded43
cache debug image
buenaflor Aug 29, 2024
09864a7
updaet
buenaflor Aug 29, 2024
b50b2d8
update var name
buenaflor Aug 29, 2024
30a12fd
updaet
buenaflor Aug 29, 2024
05044c2
update naming
buenaflor Aug 29, 2024
d592ffd
improve names
buenaflor Aug 29, 2024
11ef042
break early safeguard for parsing stacktrace and dont throw in hex fo…
buenaflor Sep 4, 2024
6dab4d8
revert load native image list integration
buenaflor Sep 4, 2024
22741a3
update
buenaflor Sep 4, 2024
465b643
Merge branch 'main' into feat/dart-symbolication
buenaflor Sep 4, 2024
45b2e65
Merge branch 'main' into feat/dart-symbolication
buenaflor Sep 9, 2024
e1d019b
fix analyze
buenaflor Sep 9, 2024
6a5a555
fix analyze
buenaflor Sep 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## Unreleased

### Features

- Add `enableDartSymbolication` option to Sentry.init() for **Flutter iOS, macOS and Android** ([#2256](https://github.com/getsentry/sentry-dart/pull/2256))
- This flag enables symbolication of Dart stack traces when native debug images are not available.
- Useful when using Sentry.init() instead of SentryFlutter.init() in Flutter projects for example due to size limitations.
- `true` by default but automatically set to `false` when using SentryFlutter.init() because the SentryFlutter fetches debug images from the native SDK integrations.

### Dependencies

- Bump Cocoa SDK from v8.35.1 to v8.36.0 ([#2252](https://github.com/getsentry/sentry-dart/pull/2252))
Expand Down
187 changes: 187 additions & 0 deletions dart/lib/src/debug_image_extractor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
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;
}

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,

Check warning on line 127 in dart/lib/src/debug_image_extractor.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/debug_image_extractor.dart#L127

Added line #L127 was not covered by tests
'Code ID must be at least 32 hexadecimal characters long');
return null;
}

final first16Bytes = codeId.substring(0, 32);
final byteData = _parseHexToBytes(first16Bytes);

if (byteData.isEmpty) {
_options.logger(

Check warning on line 136 in dart/lib/src/debug_image_extractor.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/debug_image_extractor.dart#L136

Added line #L136 was not covered by tests
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) {
throw ArgumentError('Invalid hex string');

Check warning on line 146 in dart/lib/src/debug_image_extractor.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/debug_image_extractor.dart#L146

Added line #L146 was not covered by tests
}
if (hex.startsWith('0x')) {
hex = hex.substring(2);

Check warning on line 149 in dart/lib/src/debug_image_extractor.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/debug_image_extractor.dart#L149

Added line #L149 was not covered by tests
}

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,

Check warning on line 176 in dart/lib/src/debug_image_extractor.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/debug_image_extractor.dart#L176

Added line #L176 was not covered by tests
'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)}';
}
}
74 changes: 74 additions & 0 deletions dart/lib/src/load_dart_debug_images_integration.dart
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(

Check warning on line 38 in dart/lib/src/load_dart_debug_images_integration.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/load_dart_debug_images_integration.dart#L38

Added line #L38 was not covered by tests
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;
}
}
5 changes: 5 additions & 0 deletions dart/lib/src/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:async';
import 'package:meta/meta.dart';

import 'dart_exception_type_identifier.dart';
import 'load_dart_debug_images_integration.dart';
import 'metrics/metrics_api.dart';
import 'run_zoned_guarded_integration.dart';
import 'event_processor/enricher/enricher_event_processor.dart';
Expand Down Expand Up @@ -83,6 +84,10 @@ class Sentry {
options.addIntegrationByIndex(0, IsolateErrorIntegration());
}

if (options.enableDartSymbolication) {
options.addIntegration(LoadDartDebugImagesIntegration());
}

options.addEventProcessor(EnricherEventProcessor(options));
options.addEventProcessor(ExceptionEventProcessor(options));
options.addEventProcessor(DeduplicationEventProcessor(options));
Expand Down
2 changes: 2 additions & 0 deletions dart/lib/src/sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'client_reports/client_report_recorder.dart';
import 'client_reports/discard_reason.dart';
import 'event_processor.dart';
import 'hint.dart';
import 'load_dart_debug_images_integration.dart';
import 'metrics/metric.dart';
import 'metrics/metrics_aggregator.dart';
import 'protocol.dart';
Expand Down Expand Up @@ -117,6 +118,7 @@ class SentryClient {
SentryEvent? preparedEvent = _prepareEvent(event, stackTrace: stackTrace);

hint ??= Hint();
hint.set(hintRawStackTraceKey, stackTrace.toString());

if (scope != null) {
preparedEvent = await scope.applyToEvent(preparedEvent, hint);
Expand Down
10 changes: 10 additions & 0 deletions dart/lib/src/sentry_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,16 @@ class SentryOptions {
_ignoredExceptionsForType.contains(exception.runtimeType);
}

/// Enables Dart symbolication for stack traces in Flutter.
///
/// If true, the SDK will attempt to symbolicate Dart stack traces when
/// [Sentry.init] is used instead of `SentryFlutter.init`. This is useful
/// when native debug images are not available.
///
/// Automatically set to `false` when using `SentryFlutter.init`, as it uses
/// native SDKs for setting up symbolication on iOS, macOS, and Android.
bool enableDartSymbolication = true;

@internal
late ClientReportRecorder recorder = NoOpClientReportRecorder();

Expand Down
Loading
Loading