Skip to content

Commit

Permalink
Merge branch 'fix/flutter-multiview-support' of https://github.com/ge…
Browse files Browse the repository at this point in the history
…tsentry/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)
  • Loading branch information
martinhaintz committed Sep 10, 2024
2 parents 1d82e56 + 6883e29 commit 3fb08ac
Show file tree
Hide file tree
Showing 56 changed files with 1,290 additions and 478 deletions.
16 changes: 10 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
# Changelog

## Unreleased
## 8.9.0

### Features

- 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)).

- 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)).
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/)):

```dart
await SentryFlutter.init(
(options) {
...
options.experimental.replay.sessionSampleRate = 1.0;
options.experimental.replay.errorSampleRate = 1.0;
options.experimental.replay.onErrorSampleRate = 1.0;
},
appRunner: () => runApp(MyApp()),
);
Expand All @@ -27,12 +26,17 @@
...
options.allowUrls = ["^https://sentry.com.*\$", "my-custom-domain"];
options.denyUrls = ["^.*ends-with-this\$", "denied-url"];
options.denyUrls = ["^.*ends-with-this\$", "denied-url"];
},
appRunner: () => runApp(MyApp()),
);
```

- Collect touch breadcrumbs for all buttons, not just those with `key` specified. ([#2242](https://github.com/getsentry/sentry-dart/pull/2242))
- 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 Expand Up @@ -199,7 +203,7 @@ SentryNavigatorObserver(ignoreRoutes: ["/ignoreThisRoute"]),
(options) {
...
options.experimental.replay.sessionSampleRate = 1.0;
options.experimental.replay.errorSampleRate = 1.0;
options.experimental.replay.onErrorSampleRate = 1.0;
},
appRunner: () => runApp(MyApp()),
);
Expand Down
195 changes: 195 additions & 0 deletions dart/lib/src/debug_image_extractor.dart
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)}';
}
}
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(
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;
}
}
35 changes: 5 additions & 30 deletions dart/lib/src/protocol/breadcrumb.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,42 +105,17 @@ class Breadcrumb {
String? viewId,
String? viewClass,
}) {
final newData = data ?? {};
var path = '';

if (viewId != null) {
newData['view.id'] = viewId;
path = viewId;
}

if (newData.containsKey('label')) {
if (path.isEmpty) {
path = newData['label'];
} else {
path = "$path, label: ${newData['label']}";
}
}

if (viewClass != null) {
newData['view.class'] = viewClass;
if (path.isEmpty) {
path = viewClass;
} else {
path = "$viewClass($path)";
}
}

if (path.isNotEmpty && !newData.containsKey('path')) {
newData['path'] = path;
}

return Breadcrumb(
message: message,
level: level,
category: 'ui.$subCategory',
type: 'user',
timestamp: timestamp,
data: newData,
data: {
if (viewId != null) 'view.id': viewId,
if (viewClass != null) 'view.class': viewClass,
if (data != null) ...data,
},
);
}

Expand Down
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
Loading

0 comments on commit 3fb08ac

Please sign in to comment.