diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index 4bb8ecb4fe..76e3285be8 100644 --- a/.github/workflows/flutter.yml +++ b/.github/workflows/flutter.yml @@ -103,7 +103,7 @@ jobs: with: name: sentry_flutter file: ./flutter/coverage/lcov.info - functionalities: "search" # remove after https://github.com/codecov/codecov-action/issues/600 + functionalities: 'search' # remove after https://github.com/codecov/codecov-action/issues/600 token: ${{ secrets.CODECOV_TOKEN }} - uses: VeryGoodOpenSource/very_good_coverage@c953fca3e24a915e111cc6f55f03f756dcb3964c # pin@v3.0.0 @@ -111,7 +111,8 @@ jobs: with: path: './flutter/coverage/lcov.info' min_coverage: 90 - exclude: 'lib/src/native/cocoa/binding.dart' + # 'native/c' for now because we run coverage on Linux where these are not tested yet. + exclude: 'lib/src/native/cocoa/binding.dart lib/src/native/c/*' - name: Build ${{ matrix.target }} working-directory: flutter/example diff --git a/.github/workflows/update-deps.yml b/.github/workflows/update-deps.yml index 459b4f3687..b33bfa4b7a 100644 --- a/.github/workflows/update-deps.yml +++ b/.github/workflows/update-deps.yml @@ -3,7 +3,7 @@ name: Update Dependencies on: # Run every day. schedule: - - cron: "0 3 * * *" + - cron: '0 3 * * *' # And on on every PR merge so we get the updated dependencies ASAP, and to make sure the changelog doesn't conflict. push: branches: @@ -27,6 +27,14 @@ jobs: secrets: api-token: ${{ secrets.CI_DEPLOY_KEY }} + native: + uses: getsentry/github-workflows/.github/workflows/updater.yml@v2 + with: + path: flutter/scripts/update-native.sh + name: Native SDK + secrets: + api-token: ${{ secrets.CI_DEPLOY_KEY }} + metrics-flutter: uses: getsentry/github-workflows/.github/workflows/updater.yml@v2 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index c15213b151..a769358c80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Emit `transaction.data` inside `contexts.trace.data` ([#2284](https://github.com/getsentry/sentry-dart/pull/2284)) - Blocking app starts if "appLaunchedInForeground" is false. (Android only) ([#2291](https://github.com/getsentry/sentry-dart/pull/2291)) +- Windows native error & obfuscation support ([#2286](https://github.com/getsentry/sentry-dart/pull/2286)) ### Enhancements diff --git a/dart/lib/src/debug_image_extractor.dart b/dart/lib/src/debug_image_extractor.dart deleted file mode 100644 index 99776ee12b..0000000000 --- a/dart/lib/src/debug_image_extractor.dart +++ /dev/null @@ -1,195 +0,0 @@ -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)}'; - } -} diff --git a/dart/lib/src/load_dart_debug_images_integration.dart b/dart/lib/src/load_dart_debug_images_integration.dart index 0932cac9af..4b7a35c39f 100644 --- a/dart/lib/src/load_dart_debug_images_integration.dart +++ b/dart/lib/src/load_dart_debug_images_integration.dart @@ -1,77 +1,160 @@ +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; + import '../sentry.dart'; -import 'debug_image_extractor.dart'; class LoadDartDebugImagesIntegration extends Integration { @override void call(Hub hub, SentryOptions options) { - options.addEventProcessor(_LoadImageIntegrationEventProcessor( - DebugImageExtractor(options), options)); - options.sdk.addIntegration('loadDartImageIntegration'); + if (options.enableDartSymbolication) { + options.addEventProcessor(LoadImageIntegrationEventProcessor(options)); + options.sdk.addIntegration('loadDartImageIntegration'); + } } } -const hintRawStackTraceKey = 'raw_stacktrace'; - -class _LoadImageIntegrationEventProcessor implements EventProcessor { - _LoadImageIntegrationEventProcessor(this._debugImageExtractor, this._options); +@internal +class LoadImageIntegrationEventProcessor implements EventProcessor { + LoadImageIntegrationEventProcessor(this._options); final SentryOptions _options; - final DebugImageExtractor _debugImageExtractor; + + // We don't need to always create the debug image, so we cache it here. + DebugImage? _debugImage; @override Future apply(SentryEvent event, Hint hint) async { - final rawStackTrace = hint.get(hintRawStackTraceKey) as String?; - if (!_options.enableDartSymbolication || - !event.needsSymbolication() || - rawStackTrace == null) { - return event; + final stackTrace = event.stacktrace; + if (stackTrace != null) { + final debugImage = getAppDebugImage(stackTrace); + if (debugImage != null) { + late final DebugMeta debugMeta; + if (event.debugMeta != null) { + final images = List.from(event.debugMeta!.images); + images.add(debugImage); + debugMeta = event.debugMeta!.copyWith(images: images); + } else { + debugMeta = DebugMeta(images: [debugImage]); + } + return event.copyWith(debugMeta: debugMeta); + } } - try { - final syntheticImage = _debugImageExtractor.extractFrom(rawStackTrace); - if (syntheticImage == null) { - return event; - } + return event; + } - return event.copyWith(debugMeta: DebugMeta(images: [syntheticImage])); - } catch (e, stackTrace) { + DebugImage? getAppDebugImage(SentryStackTrace stackTrace) { + // Don't return the debug image if the stack trace doesn't have native info. + if (stackTrace.baseAddr == null || + stackTrace.buildId == null || + !stackTrace.frames.any((f) => f.platform == 'native')) { + return null; + } + try { + _debugImage ??= createDebugImage(stackTrace); + } catch (e, stack) { _options.logger( SentryLevel.info, - "Couldn't add Dart debug image to event. " - 'The event will still be reported.', + "Couldn't add Dart debug image to event. The event will still be reported.", exception: e, - stackTrace: stackTrace, + stackTrace: stack, ); if (_options.automatedTestMode) { rethrow; } - return event; } + return _debugImage; } -} -extension NeedsSymbolication on SentryEvent { - bool needsSymbolication() { - if (this is SentryTransaction) { - return false; + @visibleForTesting + DebugImage? createDebugImage(SentryStackTrace stackTrace) { + if (stackTrace.buildId == null || stackTrace.baseAddr == null) { + _options.logger(SentryLevel.warning, + 'Cannot create DebugImage without a build ID and image base address.'); + return null; } - final frames = _getStacktraceFrames(); - if (frames == null) { - return false; + + // Type and DebugID are required for proper symbolication + late final String type; + late final String debugId; + + // CodeFile is required so that the debug image shows up properly in the UI. + // It doesn't need to exist and is not used for symbolication. + late final String codeFile; + + final platform = _options.platformChecker.platform; + + if (platform.isAndroid || platform.isWindows) { + type = 'elf'; + debugId = _convertBuildIdToDebugId(stackTrace.buildId!, platform.endian); + if (platform.isAndroid) { + codeFile = 'libapp.so'; + } else if (platform.isWindows) { + codeFile = 'data/app.so'; + } + } else if (platform.isIOS || platform.isMacOS) { + type = 'macho'; + debugId = _formatHexToUuid(stackTrace.buildId!); + codeFile = 'App.Framework/App'; + } else { + _options.logger( + SentryLevel.warning, + 'Unsupported platform for creating Dart debug images.', + ); + return null; } - return frames.any((frame) => 'native' == frame?.platform); + + return DebugImage( + type: type, + imageAddr: stackTrace.baseAddr, + debugId: debugId, + codeId: stackTrace.buildId, + codeFile: codeFile, + ); + } + + /// See https://github.com/getsentry/symbolic/blob/7dc28dd04c06626489c7536cfe8c7be8f5c48804/symbolic-debuginfo/src/elf.rs#L709-L734 + /// Converts an ELF object identifier into a `DebugId`. + /// + /// The identifier data is first truncated or extended to match 16 byte size of + /// Uuids. If the data is declared in little endian, the first three Uuid fields + /// are flipped to match the big endian expected by the breakpad processor. + /// + /// The `DebugId::appendix` field is always `0` for ELF. + String _convertBuildIdToDebugId(String buildId, Endian endian) { + // Make sure that we have exactly UUID_SIZE bytes available + const uuidSize = 16 * 2; + final data = Uint8List(uuidSize); + final len = buildId.length.clamp(0, uuidSize); + data.setAll(0, buildId.codeUnits.take(len)); + + if (endian == Endian.little) { + // The file ELF file targets a little endian architecture. Convert to + // network byte order (big endian) to match the Breakpad processor's + // expectations. For big endian object files, this is not needed. + // To manipulate this as hex, we create an Uint16 view. + final data16 = Uint16List.view(data.buffer); + data16.setRange(0, 4, data16.sublist(0, 4).reversed); + data16.setRange(4, 6, data16.sublist(4, 6).reversed); + data16.setRange(6, 8, data16.sublist(6, 8).reversed); + } + return _formatHexToUuid(String.fromCharCodes(data)); } - Iterable? _getStacktraceFrames() { - if (exceptions?.isNotEmpty == true) { - return exceptions?.first.stackTrace?.frames; + String _formatHexToUuid(String hex) { + if (hex.length == 36) { + return hex; } - if (threads?.isNotEmpty == true) { - var stacktraces = threads?.map((e) => e.stacktrace); - return stacktraces - ?.where((element) => element != null) - .expand((element) => element!.frames); + if (hex.length != 32) { + throw ArgumentError.value(hex, 'hexUUID', + '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)}'; } } diff --git a/dart/lib/src/platform/platform.dart b/dart/lib/src/platform/platform.dart index dffd3e81fd..ab2f94dd5f 100644 --- a/dart/lib/src/platform/platform.dart +++ b/dart/lib/src/platform/platform.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import '_io_platform.dart' if (dart.library.html) '_html_platform.dart' if (dart.library.js_interop) '_web_platform.dart' as platform; @@ -17,6 +19,9 @@ abstract class Platform { /// Get the local hostname for the system. String get localHostname; + /// Endianness of this platform. + Endian get endian => Endian.host; + /// True if the operating system is Linux. bool get isLinux => (operatingSystem == 'linux'); diff --git a/dart/lib/src/platform_checker.dart b/dart/lib/src/platform_checker.dart index 5169ca25f0..334d73f43f 100644 --- a/dart/lib/src/platform_checker.dart +++ b/dart/lib/src/platform_checker.dart @@ -44,10 +44,10 @@ class PlatformChecker { // the OS checks return true when the browser runs on the checked platform. // Example: platform.isAndroid return true if the browser is used on an // Android device. - if (platform.isAndroid || platform.isIOS || platform.isMacOS) { - return true; - } - return false; + return platform.isAndroid || + platform.isIOS || + platform.isMacOS || + platform.isWindows; } static bool _isWebWithWasmSupport() { diff --git a/dart/lib/src/protocol/sentry_event.dart b/dart/lib/src/protocol/sentry_event.dart index 1b2765c426..fe7e0af47f 100644 --- a/dart/lib/src/protocol/sentry_event.dart +++ b/dart/lib/src/protocol/sentry_event.dart @@ -1,4 +1,5 @@ import 'package:meta/meta.dart'; +import 'package:collection/collection.dart'; import '../protocol.dart'; import '../throwable_mechanism.dart'; @@ -411,4 +412,10 @@ class SentryEvent with SentryEventLike { if (threadJson?.isNotEmpty ?? false) 'threads': {'values': threadJson}, }; } + + // Returns first non-null stack trace of this event + @internal + SentryStackTrace? get stacktrace => + exceptions?.firstWhereOrNull((e) => e.stackTrace != null)?.stackTrace ?? + threads?.firstWhereOrNull((t) => t.stacktrace != null)?.stacktrace; } diff --git a/dart/lib/src/protocol/sentry_stack_frame.dart b/dart/lib/src/protocol/sentry_stack_frame.dart index edba949e9f..84880cda5e 100644 --- a/dart/lib/src/protocol/sentry_stack_frame.dart +++ b/dart/lib/src/protocol/sentry_stack_frame.dart @@ -91,6 +91,7 @@ class SentryStackFrame { /// The "package" the frame was contained in. final String? package; + // TODO what is this? doesn't seem to be part of the spec https://develop.sentry.dev/sdk/event-payloads/stacktrace/ final bool? native; /// This can override the platform for a single frame. Otherwise, the platform of the event is assumed. This can be used for multi-platform stack traces diff --git a/dart/lib/src/protocol/sentry_stack_trace.dart b/dart/lib/src/protocol/sentry_stack_trace.dart index 949318ec4c..8aceaaf269 100644 --- a/dart/lib/src/protocol/sentry_stack_trace.dart +++ b/dart/lib/src/protocol/sentry_stack_trace.dart @@ -12,6 +12,8 @@ class SentryStackTrace { this.lang, this.snapshot, this.unknown, + @internal this.baseAddr, + @internal this.buildId, }) : _frames = frames, _registers = Map.from(registers ?? {}); @@ -46,6 +48,12 @@ class SentryStackTrace { /// signal. final bool? snapshot; + @internal + final String? baseAddr; + + @internal + final String? buildId; + @internal final Map? unknown; @@ -91,5 +99,7 @@ class SentryStackTrace { lang: lang ?? this.lang, snapshot: snapshot ?? this.snapshot, unknown: unknown, + baseAddr: baseAddr, + buildId: buildId, ); } diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 3e1dcaefcf..91aca8bcbb 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -7,7 +7,6 @@ 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'; @@ -126,7 +125,6 @@ class SentryClient { SentryEvent? preparedEvent = _prepareEvent(event, stackTrace: stackTrace); hint ??= Hint(); - hint.set(hintRawStackTraceKey, stackTrace.toString()); if (scope != null) { preparedEvent = await scope.applyToEvent(preparedEvent, hint); @@ -278,9 +276,8 @@ class SentryClient { // https://develop.sentry.dev/sdk/event-payloads/stacktrace/ if (stackTrace != null || _options.attachStacktrace) { stackTrace ??= getCurrentStackTrace(); - final frames = _stackTraceFactory.getStackFrames(stackTrace); - - if (frames.isNotEmpty) { + final sentryStackTrace = _stackTraceFactory.parse(stackTrace); + if (sentryStackTrace.frames.isNotEmpty) { event = event.copyWith(threads: [ ...?event.threads, SentryThread( @@ -288,7 +285,7 @@ class SentryClient { id: isolateId, crashed: false, current: true, - stacktrace: SentryStackTrace(frames: frames), + stacktrace: sentryStackTrace, ), ]); } diff --git a/dart/lib/src/sentry_exception_factory.dart b/dart/lib/src/sentry_exception_factory.dart index 9ee2148c14..6f766014ac 100644 --- a/dart/lib/src/sentry_exception_factory.dart +++ b/dart/lib/src/sentry_exception_factory.dart @@ -47,13 +47,10 @@ class SentryExceptionFactory { SentryStackTrace? sentryStackTrace; if (stackTrace != null) { - final frames = _stacktraceFactory.getStackFrames(stackTrace); - - if (frames.isNotEmpty) { - sentryStackTrace = SentryStackTrace( - frames: frames, - snapshot: snapshot, - ); + sentryStackTrace = + _stacktraceFactory.parse(stackTrace).copyWith(snapshot: snapshot); + if (sentryStackTrace.frames.isEmpty) { + sentryStackTrace = null; } } diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index c9a9511c29..83fb887124 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -366,8 +366,8 @@ class SentryOptions { /// [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. + /// Automatically set to `false` when using `SentryFlutter.init` on a platform + /// with a native integration (e.g. Android, iOS, ...). bool enableDartSymbolication = true; @internal diff --git a/dart/lib/src/sentry_stack_trace_factory.dart b/dart/lib/src/sentry_stack_trace_factory.dart index 7aab228a3e..efcca49c70 100644 --- a/dart/lib/src/sentry_stack_trace_factory.dart +++ b/dart/lib/src/sentry_stack_trace_factory.dart @@ -9,22 +9,28 @@ import 'sentry_options.dart'; class SentryStackTraceFactory { final SentryOptions _options; - final _absRegex = RegExp(r'^\s*#[0-9]+ +abs +([A-Fa-f0-9]+)'); - final _frameRegex = RegExp(r'^\s*#', multiLine: true); - + static final _absRegex = RegExp(r'^\s*#[0-9]+ +abs +([A-Fa-f0-9]+)'); + static final _frameRegex = RegExp(r'^\s*#', multiLine: true); + static final _buildIdRegex = RegExp(r"build_id[:=] *'([A-Fa-f0-9]+)'"); + static final _baseAddrRegex = RegExp(r'isolate_dso_base[:=] *([A-Fa-f0-9]+)'); static final SentryStackFrame _asynchronousGapFrameJson = SentryStackFrame(absPath: ''); SentryStackTraceFactory(this._options); /// returns the [SentryStackFrame] list from a stackTrace ([StackTrace] or [String]) + @Deprecated('Use parse() instead') List getStackFrames(dynamic stackTrace) { - final chain = _parseStackTrace(stackTrace); + return parse(stackTrace).frames; + } + + SentryStackTrace parse(dynamic stackTrace) { + final parsed = _parseStackTrace(stackTrace); final frames = []; var onlyAsyncGap = true; - for (var t = 0; t < chain.traces.length; t += 1) { - final trace = chain.traces[t]; + for (var t = 0; t < parsed.traces.length; t += 1) { + final trace = parsed.traces[t]; // NOTE: We want to keep the Sentry frames for crash detection // this does not affect grouping since they're not marked as inApp @@ -37,17 +43,23 @@ class SentryStackTraceFactory { } // fill asynchronous gap - if (t < chain.traces.length - 1) { + if (t < parsed.traces.length - 1) { frames.add(_asynchronousGapFrameJson); } } - return onlyAsyncGap ? [] : frames.reversed.toList(); + return SentryStackTrace( + frames: onlyAsyncGap ? [] : frames.reversed.toList(), + baseAddr: parsed.baseAddr, + buildId: parsed.buildId, + ); } - Chain _parseStackTrace(dynamic stackTrace) { - if (stackTrace is Chain || stackTrace is Trace) { - return Chain.forTrace(stackTrace); + _StackInfo _parseStackTrace(dynamic stackTrace) { + if (stackTrace is Chain) { + return _StackInfo(stackTrace.traces); + } else if (stackTrace is Trace) { + return _StackInfo([stackTrace]); } // We need to convert to string and split the headers manually, otherwise @@ -62,16 +74,24 @@ class SentryStackTraceFactory { // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** // pid: 19226, tid: 6103134208, name io.flutter.ui // os: macos arch: arm64 comp: no sim: no + // build_id: 'bca64abfdfcc84d231bb8f1ccdbfbd8d' // isolate_dso_base: 10fa20000, vm_dso_base: 10fa20000 // isolate_instructions: 10fa27070, vm_instructions: 10fa21e20 // #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 // #01 abs 000000723d637527 _kDartIsolateSnapshotInstructions+0x1e5527 final startOffset = _frameRegex.firstMatch(stackTrace)?.start ?? 0; - return Chain.parse( + final chain = Chain.parse( startOffset == 0 ? stackTrace : stackTrace.substring(startOffset)); + final info = _StackInfo(chain.traces); + info.buildId = _buildIdRegex.firstMatch(stackTrace)?.group(1); + info.baseAddr = _baseAddrRegex.firstMatch(stackTrace)?.group(1); + if (info.baseAddr != null) { + info.baseAddr = '0x${info.baseAddr}'; + } + return info; } - return Chain([]); + return _StackInfo([]); } /// converts [Frame] to [SentryStackFrame] @@ -81,26 +101,23 @@ class SentryStackTraceFactory { if (frame is UnparsedFrame && member != null) { // if --split-debug-info is enabled, thats what we see: - // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** - // pid: 19226, tid: 6103134208, name io.flutter.ui - // os: macos arch: arm64 comp: no sim: no - // isolate_dso_base: 10fa20000, vm_dso_base: 10fa20000 - // isolate_instructions: 10fa27070, vm_instructions: 10fa21e20 // #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 - // #01 abs 000000723d637527 _kDartIsolateSnapshotInstructions+0x1e5527 // we are only interested on the #01, 02... items which contains the 'abs' addresses. final match = _absRegex.firstMatch(member); if (match != null) { return SentryStackFrame( instructionAddr: '0x${match.group(1)!}', - platform: 'native', // to trigger symbolication & native LoadImageList + // 'native' triggers the [LoadImageListIntegration] and server-side symbolication + platform: 'native', ); } // We shouldn't get here. If we do, it means there's likely an issue in // the parsing so let's fall back and post a stack trace as is, so that at // least we get an indication something's wrong and are able to fix it. + _options.logger( + SentryLevel.debug, "Failed to parse stack frame: $member"); } final platform = _options.platformChecker.isWeb ? 'javascript' : 'dart'; @@ -182,3 +199,11 @@ class SentryStackTraceFactory { return _options.considerInAppFramesByDefault; } } + +class _StackInfo { + String? baseAddr; + String? buildId; + final List traces; + + _StackInfo(this.traces); +} diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml index 6141a2eba6..46b4a52923 100644 --- a/dart/pubspec.yaml +++ b/dart/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: meta: ^1.3.0 stack_trace: ^1.10.0 uuid: '>=3.0.0 <5.0.0' + collection: ^1.16.0 dev_dependencies: build_runner: ^2.3.0 @@ -31,7 +32,6 @@ dev_dependencies: lints: '>=2.0.0 <5.0.0' test: ^1.21.1 yaml: ^3.1.0 # needed for version match (code and pubspec) - collection: ^1.16.0 coverage: ^1.3.0 intl: '>=0.17.0 <1.0.0' version: ^3.0.2 diff --git a/dart/test/debug_image_extractor_test.dart b/dart/test/debug_image_extractor_test.dart deleted file mode 100644 index 7a0ad7d12f..0000000000 --- a/dart/test/debug_image_extractor_test.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'package:test/test.dart'; -import 'package:sentry/src/debug_image_extractor.dart'; - -import 'mocks/mock_platform.dart'; -import 'mocks/mock_platform_checker.dart'; -import 'test_utils.dart'; - -void main() { - group(DebugImageExtractor, () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - test('returns null for invalid stack trace', () { - final stackTrace = 'Invalid stack trace'; - final extractor = fixture.getSut(platform: MockPlatform.android()); - final debugImage = extractor.extractFrom(stackTrace); - - expect(debugImage, isNull); - }); - - test('extracts correct debug ID for Android with short debugId', () { - final stackTrace = ''' -*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** -build_id: 'b680cb890f9e3c12a24b172d050dec73' -isolate_dso_base: 20000000 -'''; - final extractor = fixture.getSut(platform: MockPlatform.android()); - final debugImage = extractor.extractFrom(stackTrace); - - expect( - debugImage?.debugId, equals('89cb80b6-9e0f-123c-a24b-172d050dec73')); - }); - - test('extracts correct debug ID for Android with long debugId', () { - final stackTrace = ''' -*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** -build_id: 'f1c3bcc0279865fe3058404b2831d9e64135386c' -isolate_dso_base: 30000000 -'''; - final extractor = fixture.getSut(platform: MockPlatform.android()); - final debugImage = extractor.extractFrom(stackTrace); - - expect( - debugImage?.debugId, equals('c0bcc3f1-9827-fe65-3058-404b2831d9e6')); - }); - - test('extracts correct debug ID for iOS', () { - final stackTrace = ''' -*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** -build_id: 'b680cb890f9e3c12a24b172d050dec73' -isolate_dso_base: 30000000 -'''; - final extractor = fixture.getSut(platform: MockPlatform.iOS()); - final debugImage = extractor.extractFrom(stackTrace); - - expect( - debugImage?.debugId, equals('b680cb89-0f9e-3c12-a24b-172d050dec73')); - expect(debugImage?.codeId, isNull); - }); - - test('sets correct type based on platform', () { - final stackTrace = ''' -*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** -build_id: 'b680cb890f9e3c12a24b172d050dec73' -isolate_dso_base: 40000000 -'''; - final androidExtractor = fixture.getSut(platform: MockPlatform.android()); - final iosExtractor = fixture.getSut(platform: MockPlatform.iOS()); - - final androidDebugImage = androidExtractor.extractFrom(stackTrace); - final iosDebugImage = iosExtractor.extractFrom(stackTrace); - - expect(androidDebugImage?.type, equals('elf')); - expect(iosDebugImage?.type, equals('macho')); - }); - - test('debug image is null on unsupported platforms', () { - final stackTrace = ''' -*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** -build_id: 'b680cb890f9e3c12a24b172d050dec73' -isolate_dso_base: 40000000 -'''; - final extractor = fixture.getSut(platform: MockPlatform.linux()); - - final debugImage = extractor.extractFrom(stackTrace); - - expect(debugImage, isNull); - }); - - test('debugImage is cached after first extraction', () { - final stackTrace = ''' -*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** -build_id: 'b680cb890f9e3c12a24b172d050dec73' -isolate_dso_base: 10000000 -'''; - final extractor = fixture.getSut(platform: MockPlatform.android()); - - // First extraction - final debugImage1 = extractor.extractFrom(stackTrace); - expect(debugImage1, isNotNull); - expect(extractor.debugImageForTesting, equals(debugImage1)); - - // Second extraction - final debugImage2 = extractor.extractFrom(stackTrace); - expect(debugImage2, equals(debugImage1)); - }); - }); -} - -class Fixture { - DebugImageExtractor getSut({required MockPlatform platform}) { - final options = defaultTestOptions(MockPlatformChecker(platform: platform)); - return DebugImageExtractor(options); - } -} diff --git a/dart/test/load_dart_debug_images_integration_test.dart b/dart/test/load_dart_debug_images_integration_test.dart index e7f06525ce..84d478ab08 100644 --- a/dart/test/load_dart_debug_images_integration_test.dart +++ b/dart/test/load_dart_debug_images_integration_test.dart @@ -1,8 +1,11 @@ @TestOn('vm') library dart_test; +import 'dart:async'; + import 'package:sentry/sentry.dart'; import 'package:sentry/src/load_dart_debug_images_integration.dart'; +import 'package:sentry/src/sentry_stack_trace_factory.dart'; import 'package:test/test.dart'; import 'mocks/mock_platform.dart'; @@ -10,16 +13,17 @@ import 'mocks/mock_platform_checker.dart'; import 'test_utils.dart'; void main() { - group(LoadDartDebugImagesIntegration, () { - late Fixture fixture; + final platforms = [ + MockPlatform.iOS(), + MockPlatform.macOS(), + MockPlatform.android(), + MockPlatform.windows(), + ]; - final platforms = [ - MockPlatform.iOS(), - MockPlatform.macOS(), - MockPlatform.android(), - ]; + for (final platform in platforms) { + group('$LoadDartDebugImagesIntegration $platform', () { + late Fixture fixture; - for (final platform in platforms) { setUp(() { fixture = Fixture(); fixture.options.platformChecker = @@ -37,25 +41,23 @@ void main() { expect(fixture.options.eventProcessors.length, 1); expect( fixture.options.eventProcessors.first.runtimeType.toString(), - '_LoadImageIntegrationEventProcessor', + 'LoadImageIntegrationEventProcessor', ); }); test( 'Event processor does not add debug image if symbolication is not needed', () async { - final event = _getEvent(needsSymbolication: false); - final processor = fixture.options.eventProcessors.first; - final resultEvent = await processor.apply(event, Hint()); + final event = fixture.newEvent(needsSymbolication: false); + final resultEvent = await fixture.process(event); expect(resultEvent, equals(event)); }); test('Event processor does not add debug image if stackTrace is null', () async { - final event = _getEvent(); - final processor = fixture.options.eventProcessors.first; - final resultEvent = await processor.apply(event, Hint()); + final event = fixture.newEvent(); + final resultEvent = await fixture.process(event); expect(resultEvent, equals(event)); }); @@ -64,47 +66,175 @@ void main() { 'Event processor does not add debug image if enableDartSymbolication is false', () async { fixture.options.enableDartSymbolication = false; - final event = _getEvent(); - final processor = fixture.options.eventProcessors.first; - final resultEvent = await processor.apply(event, Hint()); + final event = fixture.newEvent(); + final resultEvent = await fixture.process(event); expect(resultEvent, equals(event)); }); test('Event processor adds debug image when symbolication is needed', () async { - final stackTrace = ''' + final debugImage = await fixture.parseAndProcess(''' *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** build_id: 'b680cb890f9e3c12a24b172d050dec73' isolate_dso_base: 10000000 -'''; - SentryEvent event = _getEvent(); - final processor = fixture.options.eventProcessors.first; - final resultEvent = await processor.apply( - event, Hint()..set(hintRawStackTraceKey, stackTrace)); + #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 +'''); + expect(debugImage?.debugId, isNotEmpty); + expect(debugImage?.imageAddr, equals('0x10000000')); + }); - expect(resultEvent?.debugMeta?.images.length, 1); - final debugImage = resultEvent?.debugMeta?.images.first; + test( + 'Event processor does not add debug image on second stack trace without image address', + () async { + final debugImage = await fixture.parseAndProcess(''' +*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** +build_id: 'b680cb890f9e3c12a24b172d050dec73' +isolate_dso_base: 10000000 + #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 +'''); expect(debugImage?.debugId, isNotEmpty); expect(debugImage?.imageAddr, equals('0x10000000')); + + final event = fixture.newEvent(stackTrace: fixture.parse(''' +*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** + #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 +''')); + final resultEvent = await fixture.process(event); + expect(resultEvent?.debugMeta?.images, isEmpty); + }); + + test('returns null for invalid stack trace', () async { + final event = + fixture.newEvent(stackTrace: fixture.parse('Invalid stack trace')); + final resultEvent = await fixture.process(event); + expect(resultEvent?.debugMeta?.images, isEmpty); + }); + + test('extracts correct debug ID with short debugId', () async { + final debugImage = await fixture.parseAndProcess(''' +*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** +build_id: 'b680cb890f9e3c12a24b172d050dec73' +isolate_dso_base: 20000000 + #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 +'''); + + if (platform.isIOS || platform.isMacOS) { + expect(debugImage?.debugId, 'b680cb89-0f9e-3c12-a24b-172d050dec73'); + } else { + expect(debugImage?.debugId, '89cb80b6-9e0f-123c-a24b-172d050dec73'); + } + }); + + test('extracts correct debug ID for Android with long debugId', () async { + final debugImage = await fixture.parseAndProcess(''' +*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** +build_id: 'f1c3bcc0279865fe3058404b2831d9e64135386c' +isolate_dso_base: 30000000 + #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 +'''); + + expect(debugImage?.debugId, + equals('c0bcc3f1-9827-fe65-3058-404b2831d9e6')); + }, skip: !platform.isAndroid); + + test('sets correct type based on platform', () async { + final debugImage = await fixture.parseAndProcess(''' +*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** +build_id: 'b680cb890f9e3c12a24b172d050dec73' +isolate_dso_base: 40000000 + #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 +'''); + + if (platform.isAndroid || platform.isWindows) { + expect(debugImage?.type, 'elf'); + } else if (platform.isIOS || platform.isMacOS) { + expect(debugImage?.type, 'macho'); + } else { + fail('missing case for platform $platform'); + } + }); + + test('sets codeFile based on platform', () async { + final debugImage = await fixture.parseAndProcess(''' +*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** +build_id: 'b680cb890f9e3c12a24b172d050dec73' +isolate_dso_base: 40000000 + #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 +'''); + + if (platform.isAndroid) { + expect(debugImage?.codeFile, 'libapp.so'); + } else if (platform.isWindows) { + expect(debugImage?.codeFile, 'data/app.so'); + } else if (platform.isIOS || platform.isMacOS) { + expect(debugImage?.codeFile, 'App.Framework/App'); + } else { + fail('missing case for platform $platform'); + } + }); + + test('debugImage is cached after first extraction', () async { + final stackTrace = ''' +*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** +build_id: 'b680cb890f9e3c12a24b172d050dec73' +isolate_dso_base: 10000000 + #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 +'''; + // First extraction + final debugImage1 = await fixture.parseAndProcess(stackTrace); + expect(debugImage1, isNotNull); + + // Second extraction + final debugImage2 = await fixture.parseAndProcess(stackTrace); + expect(debugImage2, equals(debugImage1)); }); - } + }); + } + + test('debug image is null on unsupported platforms', () async { + final fixture = Fixture() + ..options.platformChecker = + MockPlatformChecker(platform: MockPlatform.linux()); + final event = fixture.newEvent(stackTrace: fixture.parse(''' +*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** +build_id: 'b680cb890f9e3c12a24b172d050dec73' +isolate_dso_base: 40000000 + #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 +''')); + final resultEvent = await fixture.process(event); + expect(resultEvent?.debugMeta?.images.length, 0); }); } class Fixture { final options = defaultTestOptions(); + late final factory = SentryStackTraceFactory(options); Fixture() { final integration = LoadDartDebugImagesIntegration(); integration.call(Hub(options), options); } -} -SentryEvent _getEvent({bool needsSymbolication = true}) { - final frame = - SentryStackFrame(platform: needsSymbolication ? 'native' : 'dart'); - final st = SentryStackTrace(frames: [frame]); - return SentryEvent( - threads: [SentryThread(stacktrace: st)], debugMeta: DebugMeta()); + SentryStackTrace parse(String stacktrace) => factory.parse(stacktrace); + + SentryEvent newEvent( + {bool needsSymbolication = true, SentryStackTrace? stackTrace}) { + stackTrace ??= SentryStackTrace(frames: [ + SentryStackFrame(platform: needsSymbolication ? null : 'dart') + ]); + return SentryEvent( + threads: [SentryThread(stacktrace: stackTrace)], + debugMeta: DebugMeta()); + } + + FutureOr process(SentryEvent event) => + options.eventProcessors.first.apply(event, Hint()); + + Future parseAndProcess(String stacktrace) async { + final event = newEvent(stackTrace: parse(stacktrace)); + final resultEvent = await process(event); + expect(resultEvent?.debugMeta?.images.length, 1); + return resultEvent?.debugMeta?.images.first; + } } diff --git a/dart/test/mocks/mock_platform.dart b/dart/test/mocks/mock_platform.dart index 75025a4a15..21dc234b09 100644 --- a/dart/test/mocks/mock_platform.dart +++ b/dart/test/mocks/mock_platform.dart @@ -1,9 +1,13 @@ +import 'dart:typed_data'; + import 'package:sentry/src/platform/platform.dart'; import 'no_such_method_provider.dart'; class MockPlatform extends Platform with NoSuchMethodProvider { - MockPlatform({String? os}) : operatingSystem = os ?? ''; + MockPlatform({String? os, Endian? endian}) + : operatingSystem = os ?? '', + endian = endian ?? Endian.host; factory MockPlatform.android() { return MockPlatform(os: 'android'); @@ -26,5 +30,11 @@ class MockPlatform extends Platform with NoSuchMethodProvider { } @override - String operatingSystem; + final String operatingSystem; + + @override + final Endian endian; + + @override + String toString() => operatingSystem; } diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 1766148a16..843279ed36 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -98,10 +98,8 @@ void main() { final exception = SentryException( type: 'Exception', value: 'an exception', - stackTrace: SentryStackTrace( - frames: SentryStackTraceFactory(fixture.options) - .getStackFrames('#0 baz (file:///pathto/test.dart:50:3)'), - ), + stackTrace: SentryStackTraceFactory(fixture.options) + .parse('#0 baz (file:///pathto/test.dart:50:3)'), ); final event = SentryEvent(exceptions: [exception]); diff --git a/dart/test/sentry_exception_factory_test.dart b/dart/test/sentry_exception_factory_test.dart index a3129fb8f3..f2e12fce10 100644 --- a/dart/test/sentry_exception_factory_test.dart +++ b/dart/test/sentry_exception_factory_test.dart @@ -5,7 +5,11 @@ import 'package:test/test.dart'; import 'test_utils.dart'; void main() { - final fixture = Fixture(); + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); test('getSentryException with frames', () { SentryException sentryException; @@ -206,6 +210,26 @@ void main() { expect(sentryException.stackTrace!.snapshot, true); }); + + test('sets stacktrace build id and image address', () { + final sentryException = fixture + .getSut(attachStacktrace: false) + .getSentryException(Object(), stackTrace: StackTraceErrorStackTrace()); + + final sentryStackTrace = sentryException.stackTrace!; + expect(sentryStackTrace.baseAddr, '0x752602b000'); + expect(sentryStackTrace.buildId, 'bca64abfdfcc84d231bb8f1ccdbfbd8d'); + }); + + test('sets null build id and image address if not present', () { + final sentryException = fixture + .getSut(attachStacktrace: false) + .getSentryException(Object(), stackTrace: null); + + final sentryStackTrace = sentryException.stackTrace!; + expect(sentryStackTrace.baseAddr, isNull); + expect(sentryStackTrace.buildId, isNull); + }); } class CustomError extends Error {} @@ -233,26 +257,7 @@ class StackTraceError extends Error { return ''' $prefix -pid: 9437, tid: 10069, name 1.ui -os: android arch: arm64 comp: yes sim: no -build_id: 'bca64abfdfcc84d231bb8f1ccdbfbd8d' -isolate_dso_base: 752602b000, vm_dso_base: 752602b000 -isolate_instructions: 7526344980, vm_instructions: 752633f000 -#00 abs 00000075266c2fbf virt 0000000000697fbf _kDartIsolateSnapshotInstructions+0x37e63f -#1 abs 000000752685211f virt 000000000082711f _kDartIsolateSnapshotInstructions+0x50d79f -#2 abs 0000007526851cb3 virt 0000000000826cb3 _kDartIsolateSnapshotInstructions+0x50d333 -#3 abs 0000007526851c63 virt 0000000000826c63 _kDartIsolateSnapshotInstructions+0x50d2e3 -#4 abs 0000007526851bf3 virt 0000000000826bf3 _kDartIsolateSnapshotInstructions+0x50d273 -#5 abs 0000007526a0b44b virt 00000000009e044b _kDartIsolateSnapshotInstructions+0x6c6acb -#6 abs 0000007526a068a7 virt 00000000009db8a7 _kDartIsolateSnapshotInstructions+0x6c1f27 -#7 abs 0000007526b57a2b virt 0000000000b2ca2b _kDartIsolateSnapshotInstructions+0x8130ab -#8 abs 0000007526b5d93b virt 0000000000b3293b _kDartIsolateSnapshotInstructions+0x818fbb -#9 abs 0000007526a2333b virt 00000000009f833b _kDartIsolateSnapshotInstructions+0x6de9bb -#10 abs 0000007526937957 virt 000000000090c957 _kDartIsolateSnapshotInstructions+0x5f2fd7 -#11 abs 0000007526a243a3 virt 00000000009f93a3 _kDartIsolateSnapshotInstructions+0x6dfa23 -#12 abs 000000752636273b virt 000000000033773b _kDartIsolateSnapshotInstructions+0x1ddbb -#13 abs 0000007526a36ac3 virt 0000000000a0bac3 _kDartIsolateSnapshotInstructions+0x6f2143 -#14 abs 00000075263626af virt 00000000003376af _kDartIsolateSnapshotInstructions+0x1dd2f'''; +${StackTraceErrorStackTrace()}'''; } } diff --git a/dart/test/stack_trace_test.dart b/dart/test/stack_trace_test.dart index f63bc0f227..3b250ddc81 100644 --- a/dart/test/stack_trace_test.dart +++ b/dart/test/stack_trace_test.dart @@ -110,12 +110,11 @@ void main() { group('encodeStackTrace', () { test('encodes a simple stack trace', () { - final frames = Fixture() - .getSut(considerInAppFramesByDefault: true) - .getStackFrames(''' + final frames = + Fixture().getSut(considerInAppFramesByDefault: true).parse(''' #0 baz (file:///pathto/test.dart:50:3) #1 bar (file:///pathto/test.dart:46:9) - ''').map((frame) => frame.toJson()); + ''').frames.map((frame) => frame.toJson()); expect(frames, [ { @@ -139,14 +138,26 @@ void main() { ]); }); + test('obsoleted getStackFrames works as expected', () { + final sut = Fixture().getSut(considerInAppFramesByDefault: true); + final trace = ''' +#0 baz (file:///pathto/test.dart:50:3) +#1 bar (file:///pathto/test.dart:46:9) + '''; + final frames1 = sut.parse(trace).frames.map((frame) => frame.toJson()); + // ignore: deprecated_member_use_from_same_package + final frames2 = sut.getStackFrames(trace).map((frame) => frame.toJson()); + + expect(frames1, equals(frames2)); + }); + test('encodes an asynchronous stack trace', () { - final frames = Fixture() - .getSut(considerInAppFramesByDefault: true) - .getStackFrames(''' + final frames = + Fixture().getSut(considerInAppFramesByDefault: true).parse(''' #0 baz (file:///pathto/test.dart:50:3) #1 bar (file:///pathto/test.dart:46:9) - ''').map((frame) => frame.toJson()); + ''').frames.map((frame) => frame.toJson()); expect(frames, [ { @@ -201,7 +212,8 @@ isolate_instructions: 10fa27070, vm_instructions: 10fa21e20 for (var traceString in stackTraces) { final frames = Fixture() .getSut(considerInAppFramesByDefault: true) - .getStackFrames(traceString) + .parse(traceString) + .frames .map((frame) => frame.toJson()); expect( @@ -221,13 +233,12 @@ isolate_instructions: 10fa27070, vm_instructions: 10fa21e20 }); test('parses normal stack trace', () { - final frames = Fixture() - .getSut(considerInAppFramesByDefault: true) - .getStackFrames(''' + final frames = + Fixture().getSut(considerInAppFramesByDefault: true).parse(''' #0 asyncThrows (file:/foo/bar/main.dart:404) #1 MainScaffold.build. (package:example/main.dart:131) #2 PlatformDispatcher._dispatchPointerDataPacket (dart:ui/platform_dispatcher.dart:341) - ''').map((frame) => frame.toJson()); + ''').frames.map((frame) => frame.toJson()); expect(frames, [ { 'filename': 'platform_dispatcher.dart', @@ -260,9 +271,10 @@ isolate_instructions: 10fa27070, vm_instructions: 10fa21e20 test('remove frames if only async gap is left', () { final frames = Fixture() .getSut(considerInAppFramesByDefault: true) - .getStackFrames(StackTrace.fromString(''' + .parse(StackTrace.fromString(''' ''')) + .frames .map((frame) => frame.toJson()); expect(frames.isEmpty, true); }); diff --git a/flutter/.gitignore b/flutter/.gitignore index 068bf84155..30db743ffc 100644 --- a/flutter/.gitignore +++ b/flutter/.gitignore @@ -10,4 +10,4 @@ build/ .cxx/ .vscode/launch.json -cocoa_bindings_temp +temp diff --git a/flutter/example/.gitignore b/flutter/example/.gitignore index c063aec688..856624edb0 100644 --- a/flutter/example/.gitignore +++ b/flutter/example/.gitignore @@ -43,3 +43,6 @@ app.*.map.json # sqflite web/sqflite_sw.js web/sqlite3.wasm + +# Sentry native local storage +.sentry-native diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 8de265083d..f6b879b294 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -571,6 +571,14 @@ class MainScaffold extends StatelessWidget { if (UniversalPlatform.isIOS || UniversalPlatform.isMacOS) const CocoaExample(), if (UniversalPlatform.isAndroid) const AndroidExample(), + // ignore: invalid_use_of_internal_member + if (SentryFlutter.native != null) + ElevatedButton( + onPressed: () async { + SentryFlutter.nativeCrash(); + }, + child: const Text('Sentry.nativeCrash'), + ), ].map((widget) { if (kIsWeb) { // Add vertical padding to web so the tooltip doesn't obstruct the clicking of the button below. @@ -767,12 +775,6 @@ class AndroidExample extends StatelessWidget { }, child: const Text('Platform exception'), ), - ElevatedButton( - onPressed: () async { - SentryFlutter.nativeCrash(); - }, - child: const Text('Sentry.nativeCrash'), - ), ]); } } @@ -885,12 +887,6 @@ class CocoaExample extends StatelessWidget { }, child: const Text('Objective-C SEGFAULT'), ), - ElevatedButton( - onPressed: () async { - SentryFlutter.nativeCrash(); - }, - child: const Text('Sentry.nativeCrash'), - ), ], ); } diff --git a/flutter/example/run.sh b/flutter/example/run.sh index 025a6f31b4..b329d61be9 100755 --- a/flutter/example/run.sh +++ b/flutter/example/run.sh @@ -35,6 +35,9 @@ elif [ "$1" == "web" ]; then elif [ "$1" == "macos" ]; then flutter build macos --split-debug-info=$symbolsDir --obfuscate launchCmd='./build/macos/Build/Products/Release/sentry_flutter_example.app/Contents/MacOS/sentry_flutter_example' +elif [ "$1" == "windows" ]; then + flutter build windows --split-debug-info=$symbolsDir --obfuscate + launchCmd='./build/windows/x64/runner/Release/sentry_flutter_example.exe' else if [ "$1" == "" ]; then echo -e "[\033[92mrun\033[0m] Pass the platform you'd like to run: android, ios, web" diff --git a/flutter/example/windows/runner/main.cpp b/flutter/example/windows/runner/main.cpp index 11ea9c69a7..08f9f5fc8f 100644 --- a/flutter/example/windows/runner/main.cpp +++ b/flutter/example/windows/runner/main.cpp @@ -9,9 +9,9 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. - if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + // if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { CreateAndAttachConsole(); - } + // } // Initialize COM, so that it is available for use in the library and/or // plugins. diff --git a/flutter/ffi-native.yaml b/flutter/ffi-native.yaml new file mode 100644 index 0000000000..cdb28b223b --- /dev/null +++ b/flutter/ffi-native.yaml @@ -0,0 +1,75 @@ +# Run with `dart ffigen --config ffi-native.yaml`. +name: SentryNative +description: Sentry Native SDK FFI binding. +output: lib/src/native/c/binding.dart +headers: + entry-points: + - ./temp/sentry-native.h +exclude-all-by-default: true +functions: + include: + - sentry_init + - sentry_close + - sentry_options_new + - sentry_options_set_dsn + - sentry_options_set_debug + - sentry_options_set_environment + - sentry_options_set_release + - sentry_options_set_auto_session_tracking + - sentry_options_set_dist + - sentry_options_set_max_breadcrumbs + - sentry_options_set_handler_path + - sentry_set_user + - sentry_remove_user + - sentry_add_breadcrumb + - sentry_set_context + - sentry_remove_context + - sentry_set_extra + - sentry_remove_extra + - sentry_set_tag + - sentry_remove_tag + - sentry_get_modules_list + - sentry_value_get_type + - sentry_value_get_length + - sentry_value_get_by_index + - sentry_value_decref + - sentry_value_set_by_key + - sentry_value_get_by_key + - sentry_value_remove_by_key + - sentry_value_is_null + - sentry_value_is_true + - sentry_value_new_null + - sentry_value_new_int32 + - sentry_value_new_double + - sentry_value_new_string + - sentry_value_new_list + - sentry_value_new_object + - sentry_value_new_bool + - sentry_value_append + - sentry_value_as_int32 + - sentry_value_as_double + - sentry_value_as_string + - sentry_value_as_list + - sentry_value_as_object + # For tests only: + - sentry_sdk_version + - sentry_sdk_name + - sentry_options_free + - sentry_options_get_dsn + - sentry_options_get_debug + - sentry_options_get_environment + - sentry_options_get_release + - sentry_options_get_auto_session_tracking + - sentry_options_get_dist + - sentry_options_get_max_breadcrumbs + rename: + 'sentry_(.*)': '$1' +structs: + dependency-only: opaque +unions: + dependency-only: opaque +comments: + style: any + length: full +preamble: | + // ignore_for_file: unused_field diff --git a/flutter/lib/src/integrations/load_image_list_integration.dart b/flutter/lib/src/integrations/load_image_list_integration.dart index 776c86640d..3643dcb83d 100644 --- a/flutter/lib/src/integrations/load_image_list_integration.dart +++ b/flutter/lib/src/integrations/load_image_list_integration.dart @@ -1,13 +1,12 @@ import 'dart:async'; import 'package:sentry/sentry.dart'; +// ignore: implementation_imports +import 'package:sentry/src/load_dart_debug_images_integration.dart'; + import '../native/sentry_native_binding.dart'; import '../sentry_flutter_options.dart'; -// ignore: implementation_imports -import 'package:sentry/src/load_dart_debug_images_integration.dart' - show NeedsSymbolication; - /// Loads the native debug image list for stack trace symbolication. class LoadImageListIntegration extends Integration { /// TODO: rename to LoadNativeDebugImagesIntegration in the next major version @@ -18,7 +17,7 @@ class LoadImageListIntegration extends Integration { @override void call(Hub hub, SentryFlutterOptions options) { options.addEventProcessor( - _LoadImageListIntegrationEventProcessor(_native), + _LoadImageListIntegrationEventProcessor(options, _native), ); options.sdk.addIntegration('loadImageListIntegration'); @@ -26,14 +25,32 @@ class LoadImageListIntegration extends Integration { } class _LoadImageListIntegrationEventProcessor implements EventProcessor { - _LoadImageListIntegrationEventProcessor(this._native); + _LoadImageListIntegrationEventProcessor(this._options, this._native); + final SentryFlutterOptions _options; final SentryNativeBinding _native; + late final _dartProcessor = LoadImageIntegrationEventProcessor(_options); + @override Future apply(SentryEvent event, Hint hint) async { - if (event.needsSymbolication()) { - final images = await _native.loadDebugImages(); + // ignore: invalid_use_of_internal_member + final stackTrace = event.stacktrace; + + // if the stacktrace has native frames, we load native debug images. + if (stackTrace != null && + stackTrace.frames.any((frame) => 'native' == frame.platform)) { + var images = await _native.loadDebugImages(stackTrace); + + // On windows, we need to add the ELF debug image of the AOT code. + // See https://github.com/flutter/flutter/issues/154840 + if (_options.platformChecker.platform.isWindows) { + final debugImage = _dartProcessor.getAppDebugImage(stackTrace); + if (debugImage != null) { + images ??= List.empty(); + images.add(debugImage); + } + } if (images != null) { return event.copyWith(debugMeta: DebugMeta(images: images)); } diff --git a/flutter/lib/src/native/c/binding.dart b/flutter/lib/src/native/c/binding.dart new file mode 100644 index 0000000000..5d944edf0b --- /dev/null +++ b/flutter/lib/src/native/c/binding.dart @@ -0,0 +1,894 @@ +// ignore_for_file: unused_field + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +// ignore_for_file: type=lint +import 'dart:ffi' as ffi; + +/// Sentry Native SDK FFI binding. +class SentryNative { + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + SentryNative(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; + + /// The symbols are looked up with [lookup]. + SentryNative.fromLookup( + ffi.Pointer Function(String symbolName) + lookup) + : _lookup = lookup; + + /// Decrements the reference count on the value. + void value_decref( + sentry_value_u value, + ) { + return _value_decref( + value, + ); + } + + late final _value_decrefPtr = + _lookup>( + 'sentry_value_decref'); + late final _value_decref = + _value_decrefPtr.asFunction(); + + /// Creates a null value. + sentry_value_u value_new_null() { + return _value_new_null(); + } + + late final _value_new_nullPtr = + _lookup>( + 'sentry_value_new_null'); + late final _value_new_null = + _value_new_nullPtr.asFunction(); + + /// Creates a new 32-bit signed integer value. + sentry_value_u value_new_int32( + int value, + ) { + return _value_new_int32( + value, + ); + } + + late final _value_new_int32Ptr = + _lookup>( + 'sentry_value_new_int32'); + late final _value_new_int32 = + _value_new_int32Ptr.asFunction(); + + /// Creates a new double value. + sentry_value_u value_new_double( + double value, + ) { + return _value_new_double( + value, + ); + } + + late final _value_new_doublePtr = + _lookup>( + 'sentry_value_new_double'); + late final _value_new_double = + _value_new_doublePtr.asFunction(); + + /// Creates a new boolean value. + sentry_value_u value_new_bool( + int value, + ) { + return _value_new_bool( + value, + ); + } + + late final _value_new_boolPtr = + _lookup>( + 'sentry_value_new_bool'); + late final _value_new_bool = + _value_new_boolPtr.asFunction(); + + /// Creates a new null terminated string. + sentry_value_u value_new_string( + ffi.Pointer value, + ) { + return _value_new_string( + value, + ); + } + + late final _value_new_stringPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_value_new_string'); + late final _value_new_string = _value_new_stringPtr + .asFunction)>(); + + /// Creates a new list value. + sentry_value_u value_new_list() { + return _value_new_list(); + } + + late final _value_new_listPtr = + _lookup>( + 'sentry_value_new_list'); + late final _value_new_list = + _value_new_listPtr.asFunction(); + + /// Creates a new object. + sentry_value_u value_new_object() { + return _value_new_object(); + } + + late final _value_new_objectPtr = + _lookup>( + 'sentry_value_new_object'); + late final _value_new_object = + _value_new_objectPtr.asFunction(); + + /// Returns the type of the value passed. + int value_get_type( + sentry_value_u value, + ) { + return _value_get_type( + value, + ); + } + + late final _value_get_typePtr = + _lookup>( + 'sentry_value_get_type'); + late final _value_get_type = + _value_get_typePtr.asFunction(); + + /// Sets a key to a value in the map. + /// + /// This moves the ownership of the value into the map. The caller does not + /// have to call `sentry_value_decref` on it. + int value_set_by_key( + sentry_value_u value, + ffi.Pointer k, + sentry_value_u v, + ) { + return _value_set_by_key( + value, + k, + v, + ); + } + + late final _value_set_by_keyPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(sentry_value_u, ffi.Pointer, + sentry_value_u)>>('sentry_value_set_by_key'); + late final _value_set_by_key = _value_set_by_keyPtr.asFunction< + int Function(sentry_value_u, ffi.Pointer, sentry_value_u)>(); + + /// This removes a value from the map by key. + int value_remove_by_key( + sentry_value_u value, + ffi.Pointer k, + ) { + return _value_remove_by_key( + value, + k, + ); + } + + late final _value_remove_by_keyPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(sentry_value_u, + ffi.Pointer)>>('sentry_value_remove_by_key'); + late final _value_remove_by_key = _value_remove_by_keyPtr + .asFunction)>(); + + /// Appends a value to a list. + /// + /// This moves the ownership of the value into the list. The caller does not + /// have to call `sentry_value_decref` on it. + int value_append( + sentry_value_u value, + sentry_value_u v, + ) { + return _value_append( + value, + v, + ); + } + + late final _value_appendPtr = _lookup< + ffi.NativeFunction>( + 'sentry_value_append'); + late final _value_append = _value_appendPtr + .asFunction(); + + /// Looks up a value in a map by key. If missing a null value is returned. + /// The returned value is borrowed. + sentry_value_u value_get_by_key( + sentry_value_u value, + ffi.Pointer k, + ) { + return _value_get_by_key( + value, + k, + ); + } + + late final _value_get_by_keyPtr = _lookup< + ffi.NativeFunction< + sentry_value_u Function(sentry_value_u, + ffi.Pointer)>>('sentry_value_get_by_key'); + late final _value_get_by_key = _value_get_by_keyPtr.asFunction< + sentry_value_u Function(sentry_value_u, ffi.Pointer)>(); + + /// Looks up a value in a list by index. If missing a null value is returned. + /// The returned value is borrowed. + sentry_value_u value_get_by_index( + sentry_value_u value, + int index, + ) { + return _value_get_by_index( + value, + index, + ); + } + + late final _value_get_by_indexPtr = _lookup< + ffi + .NativeFunction>( + 'sentry_value_get_by_index'); + late final _value_get_by_index = _value_get_by_indexPtr + .asFunction(); + + /// Returns the length of the given map or list. + /// + /// If an item is not a list or map the return value is 0. + int value_get_length( + sentry_value_u value, + ) { + return _value_get_length( + value, + ); + } + + late final _value_get_lengthPtr = + _lookup>( + 'sentry_value_get_length'); + late final _value_get_length = + _value_get_lengthPtr.asFunction(); + + /// Converts a value into a 32bit signed integer. + int value_as_int32( + sentry_value_u value, + ) { + return _value_as_int32( + value, + ); + } + + late final _value_as_int32Ptr = + _lookup>( + 'sentry_value_as_int32'); + late final _value_as_int32 = + _value_as_int32Ptr.asFunction(); + + /// Converts a value into a double value. + double value_as_double( + sentry_value_u value, + ) { + return _value_as_double( + value, + ); + } + + late final _value_as_doublePtr = + _lookup>( + 'sentry_value_as_double'); + late final _value_as_double = + _value_as_doublePtr.asFunction(); + + /// Returns the value as c string. + ffi.Pointer value_as_string( + sentry_value_u value, + ) { + return _value_as_string( + value, + ); + } + + late final _value_as_stringPtr = _lookup< + ffi.NativeFunction Function(sentry_value_u)>>( + 'sentry_value_as_string'); + late final _value_as_string = _value_as_stringPtr + .asFunction Function(sentry_value_u)>(); + + /// Returns `true` if the value is boolean true. + int value_is_true( + sentry_value_u value, + ) { + return _value_is_true( + value, + ); + } + + late final _value_is_truePtr = + _lookup>( + 'sentry_value_is_true'); + late final _value_is_true = + _value_is_truePtr.asFunction(); + + /// Returns `true` if the value is null. + int value_is_null( + sentry_value_u value, + ) { + return _value_is_null( + value, + ); + } + + late final _value_is_nullPtr = + _lookup>( + 'sentry_value_is_null'); + late final _value_is_null = + _value_is_nullPtr.asFunction(); + + /// Creates a new options struct. + /// Can be freed with `sentry_options_free`. + ffi.Pointer options_new() { + return _options_new(); + } + + late final _options_newPtr = + _lookup Function()>>( + 'sentry_options_new'); + late final _options_new = + _options_newPtr.asFunction Function()>(); + + /// Deallocates previously allocated sentry options. + void options_free( + ffi.Pointer opts, + ) { + return _options_free( + opts, + ); + } + + late final _options_freePtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_options_free'); + late final _options_free = _options_freePtr + .asFunction)>(); + + /// Sets the DSN. + void options_set_dsn( + ffi.Pointer opts, + ffi.Pointer dsn, + ) { + return _options_set_dsn( + opts, + dsn, + ); + } + + late final _options_set_dsnPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_dsn'); + late final _options_set_dsn = _options_set_dsnPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + /// Gets the DSN. + ffi.Pointer options_get_dsn( + ffi.Pointer opts, + ) { + return _options_get_dsn( + opts, + ); + } + + late final _options_get_dsnPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('sentry_options_get_dsn'); + late final _options_get_dsn = _options_get_dsnPtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); + + /// Sets the release. + void options_set_release( + ffi.Pointer opts, + ffi.Pointer release, + ) { + return _options_set_release( + opts, + release, + ); + } + + late final _options_set_releasePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_release'); + late final _options_set_release = _options_set_releasePtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + /// Gets the release. + ffi.Pointer options_get_release( + ffi.Pointer opts, + ) { + return _options_get_release( + opts, + ); + } + + late final _options_get_releasePtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('sentry_options_get_release'); + late final _options_get_release = _options_get_releasePtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); + + /// Sets the environment. + void options_set_environment( + ffi.Pointer opts, + ffi.Pointer environment, + ) { + return _options_set_environment( + opts, + environment, + ); + } + + late final _options_set_environmentPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_environment'); + late final _options_set_environment = _options_set_environmentPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + /// Gets the environment. + ffi.Pointer options_get_environment( + ffi.Pointer opts, + ) { + return _options_get_environment( + opts, + ); + } + + late final _options_get_environmentPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Pointer)>>( + 'sentry_options_get_environment'); + late final _options_get_environment = _options_get_environmentPtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); + + /// Sets the dist. + void options_set_dist( + ffi.Pointer opts, + ffi.Pointer dist, + ) { + return _options_set_dist( + opts, + dist, + ); + } + + late final _options_set_distPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_dist'); + late final _options_set_dist = _options_set_distPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + /// Gets the dist. + ffi.Pointer options_get_dist( + ffi.Pointer opts, + ) { + return _options_get_dist( + opts, + ); + } + + late final _options_get_distPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('sentry_options_get_dist'); + late final _options_get_dist = _options_get_distPtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); + + /// Enables or disables debug printing mode. + void options_set_debug( + ffi.Pointer opts, + int debug, + ) { + return _options_set_debug( + opts, + debug, + ); + } + + late final _options_set_debugPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Int)>>('sentry_options_set_debug'); + late final _options_set_debug = _options_set_debugPtr + .asFunction, int)>(); + + /// Returns the current value of the debug flag. + int options_get_debug( + ffi.Pointer opts, + ) { + return _options_get_debug( + opts, + ); + } + + late final _options_get_debugPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_options_get_debug'); + late final _options_get_debug = _options_get_debugPtr + .asFunction)>(); + + /// Sets the number of breadcrumbs being tracked and attached to events. + /// + /// Defaults to 100. + void options_set_max_breadcrumbs( + ffi.Pointer opts, + int max_breadcrumbs, + ) { + return _options_set_max_breadcrumbs( + opts, + max_breadcrumbs, + ); + } + + late final _options_set_max_breadcrumbsPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Size)>>('sentry_options_set_max_breadcrumbs'); + late final _options_set_max_breadcrumbs = _options_set_max_breadcrumbsPtr + .asFunction, int)>(); + + /// Gets the number of breadcrumbs being tracked and attached to events. + int options_get_max_breadcrumbs( + ffi.Pointer opts, + ) { + return _options_get_max_breadcrumbs( + opts, + ); + } + + late final _options_get_max_breadcrumbsPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_options_get_max_breadcrumbs'); + late final _options_get_max_breadcrumbs = _options_get_max_breadcrumbsPtr + .asFunction)>(); + + /// Enables or disables automatic session tracking. + /// + /// Automatic session tracking is enabled by default and is equivalent to calling + /// `sentry_start_session` after startup. + /// There can only be one running session, and the current session will always be + /// closed implicitly by `sentry_close`, when starting a new session with + /// `sentry_start_session`, or manually by calling `sentry_end_session`. + void options_set_auto_session_tracking( + ffi.Pointer opts, + int val, + ) { + return _options_set_auto_session_tracking( + opts, + val, + ); + } + + late final _options_set_auto_session_trackingPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Int)>>('sentry_options_set_auto_session_tracking'); + late final _options_set_auto_session_tracking = + _options_set_auto_session_trackingPtr + .asFunction, int)>(); + + /// Returns true if automatic session tracking is enabled. + int options_get_auto_session_tracking( + ffi.Pointer opts, + ) { + return _options_get_auto_session_tracking( + opts, + ); + } + + late final _options_get_auto_session_trackingPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_options_get_auto_session_tracking'); + late final _options_get_auto_session_tracking = + _options_get_auto_session_trackingPtr + .asFunction)>(); + + /// Sets the path to the crashpad handler if the crashpad backend is used. + /// + /// The path defaults to the `crashpad_handler`/`crashpad_handler.exe` + /// executable, depending on platform, which is expected to be present in the + /// same directory as the app executable. + /// + /// It is recommended that library users set an explicit handler path, depending + /// on the directory/executable structure of their app. + /// + /// `path` is assumed to be in platform-specific filesystem path encoding. + /// API Users on windows are encouraged to use `sentry_options_set_handler_pathw` + /// instead. + void options_set_handler_path( + ffi.Pointer opts, + ffi.Pointer path, + ) { + return _options_set_handler_path( + opts, + path, + ); + } + + late final _options_set_handler_pathPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_handler_path'); + late final _options_set_handler_path = + _options_set_handler_pathPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer)>(); + + /// Initializes the Sentry SDK with the specified options. + /// + /// This takes ownership of the options. After the options have been set + /// they cannot be modified any more. + /// Depending on the configured transport and backend, this function might not be + /// fully thread-safe. + /// Returns 0 on success. + int init( + ffi.Pointer options, + ) { + return _init( + options, + ); + } + + late final _initPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_init'); + late final _init = + _initPtr.asFunction)>(); + + /// Shuts down the sentry client and forces transports to flush out. + /// + /// Returns 0 on success. + /// + /// Note that this does not uninstall any crash handler installed by our + /// backends, which will still process crashes after `sentry_close()`, except + /// when using `crashpad` on Linux or the `inproc` backend. + /// + /// Further note that this function will block the thread it was called from + /// until the sentry background worker has finished its work or it timed out, + /// whichever comes first. + int close() { + return _close(); + } + + late final _closePtr = + _lookup>('sentry_close'); + late final _close = _closePtr.asFunction(); + + /// This will lazily load and cache a list of all the loaded libraries. + /// + /// Returns a new reference to an immutable, frozen list. + /// The reference must be released with `sentry_value_decref`. + sentry_value_u get_modules_list() { + return _get_modules_list(); + } + + late final _get_modules_listPtr = + _lookup>( + 'sentry_get_modules_list'); + late final _get_modules_list = + _get_modules_listPtr.asFunction(); + + /// Adds the breadcrumb to be sent in case of an event. + void add_breadcrumb( + sentry_value_u breadcrumb, + ) { + return _add_breadcrumb( + breadcrumb, + ); + } + + late final _add_breadcrumbPtr = + _lookup>( + 'sentry_add_breadcrumb'); + late final _add_breadcrumb = + _add_breadcrumbPtr.asFunction(); + + /// Sets the specified user. + void set_user( + sentry_value_u user, + ) { + return _set_user( + user, + ); + } + + late final _set_userPtr = + _lookup>( + 'sentry_set_user'); + late final _set_user = + _set_userPtr.asFunction(); + + /// Removes a user. + void remove_user() { + return _remove_user(); + } + + late final _remove_userPtr = + _lookup>('sentry_remove_user'); + late final _remove_user = _remove_userPtr.asFunction(); + + /// Sets a tag. + void set_tag( + ffi.Pointer key, + ffi.Pointer value, + ) { + return _set_tag( + key, + value, + ); + } + + late final _set_tagPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, ffi.Pointer)>>('sentry_set_tag'); + late final _set_tag = _set_tagPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + /// Removes the tag with the specified key. + void remove_tag( + ffi.Pointer key, + ) { + return _remove_tag( + key, + ); + } + + late final _remove_tagPtr = + _lookup)>>( + 'sentry_remove_tag'); + late final _remove_tag = + _remove_tagPtr.asFunction)>(); + + /// Sets extra information. + void set_extra( + ffi.Pointer key, + sentry_value_u value, + ) { + return _set_extra( + key, + value, + ); + } + + late final _set_extraPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, sentry_value_u)>>('sentry_set_extra'); + late final _set_extra = _set_extraPtr + .asFunction, sentry_value_u)>(); + + /// Removes the extra with the specified key. + void remove_extra( + ffi.Pointer key, + ) { + return _remove_extra( + key, + ); + } + + late final _remove_extraPtr = + _lookup)>>( + 'sentry_remove_extra'); + late final _remove_extra = + _remove_extraPtr.asFunction)>(); + + /// Sets a context object. + void set_context( + ffi.Pointer key, + sentry_value_u value, + ) { + return _set_context( + key, + value, + ); + } + + late final _set_contextPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, sentry_value_u)>>('sentry_set_context'); + late final _set_context = _set_contextPtr + .asFunction, sentry_value_u)>(); + + /// Removes the context object with the specified key. + void remove_context( + ffi.Pointer key, + ) { + return _remove_context( + key, + ); + } + + late final _remove_contextPtr = + _lookup)>>( + 'sentry_remove_context'); + late final _remove_context = + _remove_contextPtr.asFunction)>(); + + /// Sentry SDK version. + ffi.Pointer sdk_version() { + return _sdk_version(); + } + + late final _sdk_versionPtr = + _lookup Function()>>( + 'sentry_sdk_version'); + late final _sdk_version = + _sdk_versionPtr.asFunction Function()>(); + + /// Sentry SDK name set during build time. + /// Deprecated: Please use sentry_options_get_sdk_name instead. + ffi.Pointer sdk_name() { + return _sdk_name(); + } + + late final _sdk_namePtr = + _lookup Function()>>( + 'sentry_sdk_name'); + late final _sdk_name = + _sdk_namePtr.asFunction Function()>(); +} + +/// Represents a sentry protocol value. +/// +/// The members of this type should never be accessed. They are only here +/// so that alignment for the type can be properly determined. +/// +/// Values must be released with `sentry_value_decref`. This lowers the +/// internal refcount by one. If the refcount hits zero it's freed. Some +/// values like primitives have no refcount (like null) so operations on +/// those are no-ops. +/// +/// In addition values can be frozen. Some values like primitives are always +/// frozen but lists and dicts are not and can be frozen on demand. This +/// automatically happens for some shared values in the event payload like +/// the module list. +class sentry_value_u extends ffi.Union { + @ffi.Uint64() + external int _bits; + + @ffi.Double() + external double _double; +} + +/// Type of a sentry value. +abstract class sentry_value_type_t { + static const int SENTRY_VALUE_TYPE_NULL = 0; + static const int SENTRY_VALUE_TYPE_BOOL = 1; + static const int SENTRY_VALUE_TYPE_INT32 = 2; + static const int SENTRY_VALUE_TYPE_DOUBLE = 3; + static const int SENTRY_VALUE_TYPE_STRING = 4; + static const int SENTRY_VALUE_TYPE_LIST = 5; + static const int SENTRY_VALUE_TYPE_OBJECT = 6; +} + +/// The Sentry Client Options. +/// +/// See https://docs.sentry.io/platforms/native/configuration/ +class sentry_options_s extends ffi.Opaque {} diff --git a/flutter/lib/src/native/c/sentry_native.dart b/flutter/lib/src/native/c/sentry_native.dart new file mode 100644 index 0000000000..d3f86df2d7 --- /dev/null +++ b/flutter/lib/src/native/c/sentry_native.dart @@ -0,0 +1,389 @@ +import 'dart:async'; +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; +import 'package:meta/meta.dart'; + +import '../../../sentry_flutter.dart'; +import '../native_app_start.dart'; +import '../native_frames.dart'; +import '../sentry_native_binding.dart'; +import '../sentry_native_invoker.dart'; +import 'binding.dart' as binding; +import 'utils.dart'; + +@internal +class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { + @override + final SentryFlutterOptions options; + + @visibleForTesting + static final native = binding.SentryNative(DynamicLibrary.open('sentry.dll')); + + @visibleForTesting + static String? crashpadPath; + + SentryNative(this.options); + + void _logNotSupported(String operation) => options.logger( + SentryLevel.debug, 'SentryNative: $operation is not supported'); + + @override + FutureOr init(Hub hub) { + if (!options.enableNativeCrashHandling) { + options.logger( + SentryLevel.info, 'SentryNative crash handling is disabled'); + } else { + tryCatchSync("init", () { + final cOptions = createOptions(options); + final code = native.init(cOptions); + if (code != 0) { + throw StateError( + "Failed to initialize native SDK - init() exit code: $code"); + } + }); + } + } + + @visibleForTesting + Pointer createOptions( + SentryFlutterOptions options) { + final c = FreeableFactory(); + try { + final cOptions = native.options_new(); + native.options_set_dsn(cOptions, c.str(options.dsn)); + native.options_set_debug(cOptions, options.debug ? 1 : 0); + native.options_set_environment(cOptions, c.str(options.environment)); + native.options_set_release(cOptions, c.str(options.release)); + native.options_set_auto_session_tracking( + cOptions, options.enableAutoSessionTracking ? 1 : 0); + native.options_set_dist(cOptions, c.str(options.dist)); + native.options_set_max_breadcrumbs(cOptions, options.maxBreadcrumbs); + if (options.proxy != null) { + // sentry-native expects a single string and it doesn't support different types or authentication + options.logger(SentryLevel.warning, + 'SentryNative: setting a proxy is currently not supported'); + } + + if (crashpadPath != null) { + native.options_set_handler_path(cOptions, c.str(crashpadPath)); + } + + return cOptions; + } finally { + c.freeAll(); + } + } + + @override + FutureOr close() { + tryCatchSync('close', native.close); + } + + @override + FutureOr fetchNativeAppStart() => null; + + @override + bool get supportsCaptureEnvelope => false; + + @override + FutureOr captureEnvelope( + Uint8List envelopeData, bool containsUnhandledException) { + throw UnsupportedError('$SentryNative.captureEnvelope() is not suppurted'); + } + + @override + FutureOr beginNativeFrames() {} + + @override + FutureOr endNativeFrames(SentryId id) => null; + + @override + FutureOr setUser(SentryUser? user) { + if (user == null) { + tryCatchSync('remove_user', native.remove_user); + } else { + tryCatchSync('set_user', () { + var cUser = user.toJson().toNativeValue(options.logger); + native.set_user(cUser); + }); + } + } + + @override + FutureOr addBreadcrumb(Breadcrumb breadcrumb) { + tryCatchSync('add_breadcrumb', () { + var cBreadcrumb = breadcrumb.toJson().toNativeValue(options.logger); + native.add_breadcrumb(cBreadcrumb); + }); + } + + @override + FutureOr clearBreadcrumbs() { + _logNotSupported('clearing breadcrumbs'); + } + + @override + bool get supportsLoadContexts => false; + + @override + FutureOr?> loadContexts() { + _logNotSupported('loading contexts'); + return null; + } + + @override + FutureOr setContexts(String key, dynamic value) { + tryCatchSync('set_context', () { + final cValue = dynamicToNativeValue(value, options.logger); + if (cValue != null) { + final cKey = key.toNativeUtf8(); + native.set_context(cKey.cast(), cValue); + malloc.free(cKey); + } else { + options.logger(SentryLevel.warning, + 'SentryNative: failed to set context $key - value couldn\'t be converted to native'); + } + }); + } + + @override + FutureOr removeContexts(String key) { + tryCatchSync('remove_context', () { + final cKey = key.toNativeUtf8(); + native.remove_context(cKey.cast()); + malloc.free(cKey); + }); + } + + @override + FutureOr setExtra(String key, dynamic value) { + tryCatchSync('set_extra', () { + final cValue = dynamicToNativeValue(value, options.logger); + if (cValue != null) { + final cKey = key.toNativeUtf8(); + native.set_extra(cKey.cast(), cValue); + malloc.free(cKey); + } else { + options.logger(SentryLevel.warning, + 'SentryNative: failed to set extra $key - value couldn\'t be converted to native'); + } + }); + } + + @override + FutureOr removeExtra(String key) { + tryCatchSync('remove_extra', () { + final cKey = key.toNativeUtf8(); + native.remove_extra(cKey.cast()); + malloc.free(cKey); + }); + } + + @override + FutureOr setTag(String key, String value) { + tryCatchSync('set_tag', () { + final c = FreeableFactory(); + native.set_tag(c.str(key), c.str(value)); + c.freeAll(); + }); + } + + @override + FutureOr removeTag(String key) { + tryCatchSync('remove_tag', () { + final cKey = key.toNativeUtf8(); + native.remove_tag(cKey.cast()); + malloc.free(cKey); + }); + } + + @override + int? startProfiler(SentryId traceId) => + throw UnsupportedError("Not supported on this platform"); + + @override + FutureOr discardProfiler(SentryId traceId) => + throw UnsupportedError("Not supported on this platform"); + + @override + FutureOr?> collectProfile( + SentryId traceId, int startTimeNs, int endTimeNs) => + throw UnsupportedError("Not supported on this platform"); + + @override + FutureOr displayRefreshRate() { + _logNotSupported('collecting display refresh rate'); + return null; + } + + @override + FutureOr?> loadDebugImages(SentryStackTrace stackTrace) => + tryCatchAsync('get_module_list', () async { + final cImages = native.get_modules_list(); + try { + if (native.value_get_type(cImages) != + binding.sentry_value_type_t.SENTRY_VALUE_TYPE_LIST) { + return null; + } + + final images = List.generate( + native.value_get_length(cImages), (index) { + final cImage = native.value_get_by_index(cImages, index); + return DebugImage( + type: cImage.get('type').castPrimitive(options.logger) ?? '', + imageAddr: cImage.get('image_addr').castPrimitive(options.logger), + imageSize: cImage.get('image_size').castPrimitive(options.logger), + codeFile: cImage.get('code_file').castPrimitive(options.logger), + debugId: cImage.get('debug_id').castPrimitive(options.logger), + debugFile: cImage.get('debug_file').castPrimitive(options.logger), + codeId: cImage.get('code_id').castPrimitive(options.logger), + ); + }); + return images; + } finally { + native.value_decref(cImages); + } + }); + + @override + FutureOr pauseAppHangTracking() {} + + @override + FutureOr resumeAppHangTracking() {} + + @override + FutureOr nativeCrash() { + Pointer.fromAddress(1).cast().toDartString(); + } + + @override + FutureOr captureReplay(bool isCrash) { + _logNotSupported('capturing replay'); + return SentryId.empty(); + } +} + +extension on binding.sentry_value_u { + void setNativeValue(String key, binding.sentry_value_u? value) { + final cKey = key.toNativeUtf8(); + if (value == null) { + SentryNative.native.value_remove_by_key(this, cKey.cast()); + } else { + SentryNative.native.value_set_by_key(this, cKey.cast(), value); + } + malloc.free(cKey); + } + + binding.sentry_value_u get(String key) { + final cKey = key.toNativeUtf8(); + try { + return SentryNative.native.value_get_by_key(this, cKey.cast()); + } finally { + malloc.free(cKey); + } + } + + T? castPrimitive(SentryLogger logger) { + if (SentryNative.native.value_is_null(this) == 1) { + return null; + } + final type = SentryNative.native.value_get_type(this); + switch (type) { + case binding.sentry_value_type_t.SENTRY_VALUE_TYPE_NULL: + return null; + case binding.sentry_value_type_t.SENTRY_VALUE_TYPE_BOOL: + return (SentryNative.native.value_is_true(this) == 1) as T; + case binding.sentry_value_type_t.SENTRY_VALUE_TYPE_INT32: + return SentryNative.native.value_as_int32(this) as T; + case binding.sentry_value_type_t.SENTRY_VALUE_TYPE_DOUBLE: + return SentryNative.native.value_as_double(this) as T; + case binding.sentry_value_type_t.SENTRY_VALUE_TYPE_STRING: + return SentryNative.native + .value_as_string(this) + .cast() + .toDartString() as T; + default: + logger(SentryLevel.warning, + 'SentryNative: cannot read native value type: $type'); + return null; + } + } +} + +binding.sentry_value_u? dynamicToNativeValue( + dynamic value, SentryLogger logger) { + if (value is String) { + return value.toNativeValue(); + } else if (value is int) { + return value.toNativeValue(); + } else if (value is double) { + return value.toNativeValue(); + } else if (value is bool) { + return value.toNativeValue(); + } else if (value is Map) { + return value.toNativeValue(logger); + } else if (value is List) { + return value.toNativeValue(logger); + } else if (value == null) { + return SentryNative.native.value_new_null(); + } else { + logger(SentryLevel.warning, + 'SentryNative: unsupported data for for conversion: ${value.runtimeType} ($value)'); + return null; + } +} + +extension on String { + binding.sentry_value_u toNativeValue() { + final cValue = toNativeUtf8(); + final result = SentryNative.native.value_new_string(cValue.cast()); + malloc.free(cValue); + return result; + } +} + +extension on int { + binding.sentry_value_u toNativeValue() { + if (this >= -2147483648 && this <= 2147483647) { + return SentryNative.native.value_new_int32(this); + } else { + return toString().toNativeValue(); + } + } +} + +extension on double { + binding.sentry_value_u toNativeValue() => + SentryNative.native.value_new_double(this); +} + +extension on bool { + binding.sentry_value_u toNativeValue() => + SentryNative.native.value_new_bool(this ? 1 : 0); +} + +extension on Map { + binding.sentry_value_u toNativeValue(SentryLogger logger) { + final cObject = SentryNative.native.value_new_object(); + for (final entry in entries) { + final cValue = dynamicToNativeValue(entry.value, logger); + cObject.setNativeValue(entry.key, cValue); + } + return cObject; + } +} + +extension on List { + binding.sentry_value_u toNativeValue(SentryLogger logger) { + final cObject = SentryNative.native.value_new_list(); + for (final value in this) { + final cValue = dynamicToNativeValue(value, logger); + if (cValue != null) { + SentryNative.native.value_append(cObject, cValue); + } + } + return cObject; + } +} diff --git a/flutter/lib/src/native/c/utils.dart b/flutter/lib/src/native/c/utils.dart new file mode 100644 index 0000000000..34786404e5 --- /dev/null +++ b/flutter/lib/src/native/c/utils.dart @@ -0,0 +1,24 @@ +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; + +/// Creates and collects native pointers that need to be freed. +class FreeableFactory { + final _allocated = []; + + Pointer str(String? dartString) { + if (dartString == null) { + return nullptr; + } + final ptr = dartString.toNativeUtf8(); + _allocated.add(ptr); + return ptr.cast(); + } + + void freeAll() { + for (final ptr in _allocated) { + malloc.free(ptr); + } + _allocated.clear(); + } +} diff --git a/flutter/lib/src/native/factory_real.dart b/flutter/lib/src/native/factory_real.dart index 67af20e2e8..92b0f6e545 100644 --- a/flutter/lib/src/native/factory_real.dart +++ b/flutter/lib/src/native/factory_real.dart @@ -1,4 +1,5 @@ import '../../sentry_flutter.dart'; +import 'c/sentry_native.dart'; import 'cocoa/sentry_native_cocoa.dart'; import 'java/sentry_native_java.dart'; import 'sentry_native_binding.dart'; @@ -10,6 +11,8 @@ SentryNativeBinding createBinding(SentryFlutterOptions options) { return SentryNativeCocoa(options); } else if (platform.isAndroid) { return SentryNativeJava(options); + } else if (platform.isWindows) { + return SentryNative(options); } else { return SentryNativeChannel(options); } diff --git a/flutter/lib/src/native/sentry_native_binding.dart b/flutter/lib/src/native/sentry_native_binding.dart index 44ee6432b5..00fa6c4104 100644 --- a/flutter/lib/src/native/sentry_native_binding.dart +++ b/flutter/lib/src/native/sentry_native_binding.dart @@ -10,55 +10,59 @@ import 'native_frames.dart'; /// Provide typed methods to access native layer. @internal abstract class SentryNativeBinding { - Future init(Hub hub); + FutureOr init(Hub hub); - Future close(); + FutureOr close(); - Future fetchNativeAppStart(); + FutureOr fetchNativeAppStart(); - Future captureEnvelope( + bool get supportsCaptureEnvelope; + + FutureOr captureEnvelope( Uint8List envelopeData, bool containsUnhandledException); - Future beginNativeFrames(); + FutureOr beginNativeFrames(); + + FutureOr endNativeFrames(SentryId id); - Future endNativeFrames(SentryId id); + FutureOr setUser(SentryUser? user); - Future setUser(SentryUser? user); + FutureOr addBreadcrumb(Breadcrumb breadcrumb); - Future addBreadcrumb(Breadcrumb breadcrumb); + FutureOr clearBreadcrumbs(); - Future clearBreadcrumbs(); + bool get supportsLoadContexts; - Future?> loadContexts(); + FutureOr?> loadContexts(); - Future setContexts(String key, dynamic value); + FutureOr setContexts(String key, dynamic value); - Future removeContexts(String key); + FutureOr removeContexts(String key); - Future setExtra(String key, dynamic value); + FutureOr setExtra(String key, dynamic value); - Future removeExtra(String key); + FutureOr removeExtra(String key); - Future setTag(String key, String value); + FutureOr setTag(String key, String value); - Future removeTag(String key); + FutureOr removeTag(String key); int? startProfiler(SentryId traceId); - Future discardProfiler(SentryId traceId); + FutureOr discardProfiler(SentryId traceId); - Future displayRefreshRate(); + FutureOr displayRefreshRate(); - Future?> collectProfile( + FutureOr?> collectProfile( SentryId traceId, int startTimeNs, int endTimeNs); - Future?> loadDebugImages(); + FutureOr?> loadDebugImages(SentryStackTrace stackTrace); - Future pauseAppHangTracking(); + FutureOr pauseAppHangTracking(); - Future resumeAppHangTracking(); + FutureOr resumeAppHangTracking(); - Future nativeCrash(); + FutureOr nativeCrash(); - Future captureReplay(bool isCrash); + FutureOr captureReplay(bool isCrash); } diff --git a/flutter/lib/src/native/sentry_native_channel.dart b/flutter/lib/src/native/sentry_native_channel.dart index 4b2fa464e8..7af9213fb3 100644 --- a/flutter/lib/src/native/sentry_native_channel.dart +++ b/flutter/lib/src/native/sentry_native_channel.dart @@ -86,6 +86,9 @@ class SentryNativeChannel return (json != null) ? NativeAppStart.fromJson(json) : null; } + @override + bool get supportsCaptureEnvelope => true; + @override Future captureEnvelope( Uint8List envelopeData, bool containsUnhandledException) { @@ -93,6 +96,9 @@ class SentryNativeChannel 'captureEnvelope', [envelopeData, containsUnhandledException]); } + @override + bool get supportsLoadContexts => true; + @override Future?> loadContexts() => channel.invokeMapMethod('loadContexts'); @@ -178,7 +184,7 @@ class SentryNativeChannel }); @override - Future?> loadDebugImages() => + Future?> loadDebugImages(SentryStackTrace stackTrace) => tryCatchAsync('loadDebugImages', () async { final images = await channel .invokeListMethod>('loadImageList'); diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index e190115149..56a36d4f21 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -115,7 +115,9 @@ mixin SentryFlutter { // Not all platforms have a native integration. if (_native != null) { - options.transport = FileSystemTransport(_native!, options); + if (_native!.supportsCaptureEnvelope) { + options.transport = FileSystemTransport(_native!, options); + } options.addScopeObserver(NativeScopeObserver(_native!)); } @@ -170,7 +172,9 @@ mixin SentryFlutter { final native = _native; if (native != null) { integrations.add(NativeSdkIntegration(native)); - integrations.add(LoadContextsIntegration(native)); + if (native.supportsLoadContexts) { + integrations.add(LoadContextsIntegration(native)); + } integrations.add(LoadImageListIntegration(native)); options.enableDartSymbolication = false; } @@ -247,22 +251,22 @@ mixin SentryFlutter { /// Pauses the app hang tracking. /// Only for iOS and macOS. - static Future pauseAppHangTracking() { + static Future pauseAppHangTracking() async { if (_native == null) { _logNativeIntegrationNotAvailable("pauseAppHangTracking"); - return Future.value(); + } else { + await _native!.pauseAppHangTracking(); } - return _native!.pauseAppHangTracking(); } /// Resumes the app hang tracking. /// Only for iOS and macOS - static Future resumeAppHangTracking() { + static Future resumeAppHangTracking() async { if (_native == null) { _logNativeIntegrationNotAvailable("resumeAppHangTracking"); - return Future.value(); + } else { + await _native!.resumeAppHangTracking(); } - return _native!.resumeAppHangTracking(); } @internal @@ -276,7 +280,7 @@ mixin SentryFlutter { /// Use `nativeCrash()` to crash the native implementation and test/debug the crash reporting for native code. /// This should not be used in production code. /// Only for Android, iOS and macOS - static Future nativeCrash() { + static Future nativeCrash() async { if (_native == null) { _logNativeIntegrationNotAvailable("nativeCrash"); return Future.value(); diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index f60a97ac5c..626d3b84d7 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -40,6 +40,10 @@ dev_dependencies: remove_from_coverage: ^2.0.0 flutter_localizations: sdk: flutter + ffigen: + git: + url: https://github.com/getsentry/ffigen + ref: 6aa2c2642f507eab3df83373189170797a9fa5e7 flutter: plugin: @@ -57,4 +61,6 @@ flutter: linux: pluginClass: SentryFlutterPlugin windows: - ffiPlugin: true + # Note, we cannot use `ffiPlugin: true` because flutter tooling won't add `target_link_libraries()` + # so sentry-native won't even build during the build process (since it doesn't need to). + pluginClass: SentryFlutterPlugin diff --git a/flutter/scripts/generate-cocoa-bindings.sh b/flutter/scripts/generate-cocoa-bindings.sh index 9af4cb2084..3415ee3f62 100755 --- a/flutter/scripts/generate-cocoa-bindings.sh +++ b/flutter/scripts/generate-cocoa-bindings.sh @@ -14,18 +14,8 @@ cocoa_version="${1:-$(./scripts/update-cocoa.sh get-version)}" cd "$(dirname "$0")/../" -# Remove dependency on script exit (even in case of an error). -trap "dart pub remove ffigen" EXIT - -# Currently we add the dependency only when the code needs to be generated because it depends -# on Dart SDK 3.2.0 which isn't available on with Flutter stable yet. -# Leaving the dependency in pubspec would block all contributors. -# As for why this is coming from a fork - because we need a specific version of ffigen including PR 607 but not PR 601 -# which starts generating code not compatible with Dart SDK 2.17. The problem is they were merged in the wrong order... -dart pub add 'dev:ffigen:{"git":{"url":"https://github.com/getsentry/ffigen","ref":"6aa2c2642f507eab3df83373189170797a9fa5e7"}}' - # Download Cocoa SDK (we need the headers) -temp="cocoa_bindings_temp" +temp="temp" rm -rf $temp mkdir -p $temp curl -Lv --fail-with-body https://github.com/getsentry/sentry-cocoa/releases/download/$cocoa_version/Sentry.xcframework.zip -o $temp/Sentry.xcframework.zip diff --git a/flutter/scripts/generate-native-bindings.ps1 b/flutter/scripts/generate-native-bindings.ps1 new file mode 100644 index 0000000000..67d84de610 --- /dev/null +++ b/flutter/scripts/generate-native-bindings.ps1 @@ -0,0 +1,21 @@ +# This is a PowerShell script instead of a bash script because it needs to run on Windows during local development. +Push-Location "$PSScriptRoot/../" +try +{ + New-Item temp -ItemType Directory -ErrorAction SilentlyContinue | Out-Null + + $props = ConvertFrom-StringData (Get-Content sentry-native/CMakeCache.txt -Raw) + Invoke-WebRequest -Uri "https://raw.githubusercontent.com/getsentry/sentry-native/$($props.version)/include/sentry.h" -OutFile temp/sentry-native.h + + $binding = 'lib/src/native/c/binding.dart' + dart run ffigen --config ffi-native.yaml + $content = Get-Content $binding -Raw + $content = $content -replace 'final class', 'class' + $content | Set-Content -NoNewline -Encoding utf8 $binding + dart format $binding + Get-Item $binding +} +finally +{ + Pop-Location +} diff --git a/flutter/scripts/update-native.sh b/flutter/scripts/update-native.sh new file mode 100644 index 0000000000..abddc3a980 --- /dev/null +++ b/flutter/scripts/update-native.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/../sentry-native" +file='CMakeCache.txt' +content=$(cat $file) +regex="(version)=([0-9\.]+(\-[a-z0-9\.]+)?)" +if ! [[ $content =~ $regex ]]; then + echo "Failed to find the plugin version in $file" + exit 1 +fi + +case $1 in +get-version) + echo "${BASH_REMATCH[2]}" + ;; +get-repo) + echo "$content" | grep -w repo | cut -d '=' -f 2 | tr -d '\n' + ;; +set-version) + newValue="${BASH_REMATCH[1]}=$2" + echo "${content/${BASH_REMATCH[0]}/$newValue}" >$file + pwsh ../scripts/generate-native-bindings.ps1 "$2" + ;; +*) + echo "Unknown argument $1" + exit 1 + ;; +esac diff --git a/flutter/sentry-native/CMakeCache.txt b/flutter/sentry-native/CMakeCache.txt new file mode 100644 index 0000000000..6e747be3f6 --- /dev/null +++ b/flutter/sentry-native/CMakeCache.txt @@ -0,0 +1,5 @@ +# This is not actually a CMakeCache.txt file, but a load_cache() requires the name. +# Basically, this is a properties file we use both in CMake and update-deps.yml to update dependencies. + +repo=https://github.com/getsentry/sentry-native +version=0.7.8 diff --git a/flutter/sentry-native/sentry-native.cmake b/flutter/sentry-native/sentry-native.cmake new file mode 100644 index 0000000000..0a5fc7884f --- /dev/null +++ b/flutter/sentry-native/sentry-native.cmake @@ -0,0 +1,31 @@ +load_cache("${CMAKE_CURRENT_LIST_DIR}" READ_WITH_PREFIX SENTRY_NATIVE_ repo version) +message(STATUS "Fetching Sentry native version: ${SENTRY_NATIVE_version} from ${SENTRY_NATIVE_repo}") + +set(SENTRY_SDK_NAME "sentry.native.flutter" CACHE STRING "The SDK name to report when sending events." FORCE) +set(SENTRY_BACKEND "crashpad" CACHE STRING "The sentry backend responsible for reporting crashes" FORCE) +set(SENTRY_BUILD_SHARED_LIBS ON CACHE BOOL "Build shared libraries (.dll/.so) instead of static ones (.lib/.a)" FORCE) + +include(FetchContent) +FetchContent_Declare( + sentry-native + GIT_REPOSITORY ${SENTRY_NATIVE_repo} + GIT_TAG ${SENTRY_NATIVE_version} + EXCLUDE_FROM_ALL +) + +FetchContent_MakeAvailable(sentry-native) + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(sentry_flutter_bundled_libraries + $ + $ + PARENT_SCOPE +) + +# `*_plugin` is the name of the plugin library as expected by flutter. +# We don't actually need a plugin here, we just need to get the native library linked +# The following generated code achieves that: +# https://github.com/flutter/flutter/blob/ebfaa45c7d23374a7f3f596adea62ae1dd4e5845/packages/flutter_tools/lib/src/flutter_plugins.dart#L591-L596 +add_library(sentry_flutter_plugin ALIAS sentry) diff --git a/flutter/temp/native-test/dist/sentry.dll b/flutter/temp/native-test/dist/sentry.dll deleted file mode 100644 index 21615e9b7d..0000000000 Binary files a/flutter/temp/native-test/dist/sentry.dll and /dev/null differ diff --git a/flutter/test/file_system_transport_test.dart b/flutter/test/file_system_transport_test.dart index 84bef4babe..91340f7fb6 100644 --- a/flutter/test/file_system_transport_test.dart +++ b/flutter/test/file_system_transport_test.dart @@ -159,6 +159,10 @@ class Fixture { final options = defaultTestOptions(); final binding = MockSentryNativeBinding(); + Fixture() { + when(binding.captureEnvelope(any, any)).thenReturn(null); + } + FileSystemTransport getSut() { return FileSystemTransport(binding, options); } diff --git a/flutter/test/initialization_test.dart b/flutter/test/initialization_test.dart index d36e549339..527b51b247 100644 --- a/flutter/test/initialization_test.dart +++ b/flutter/test/initialization_test.dart @@ -10,8 +10,6 @@ import 'mocks.dart'; // https://github.com/getsentry/sentry-dart/issues/508 // There are no asserts, test are succesfull if no exceptions are thrown. void main() { - final native = NativeChannelFixture(); - void optionsInitializer(SentryFlutterOptions options) { // LoadReleaseIntegration throws because package_info channel is not available options.removeIntegration( @@ -20,12 +18,12 @@ void main() { test('async re-initilization', () async { await SentryFlutter.init(optionsInitializer, - options: defaultTestOptions()..methodChannel = native.channel); + options: defaultTestOptions()..autoInitializeNativeSdk = false); await Sentry.close(); await SentryFlutter.init(optionsInitializer, - options: defaultTestOptions()..methodChannel = native.channel); + options: defaultTestOptions()..autoInitializeNativeSdk = false); await Sentry.close(); }); diff --git a/flutter/test/integrations/load_image_list_test.dart b/flutter/test/integrations/load_image_list_test.dart index 35e59b7599..95e31a625c 100644 --- a/flutter/test/integrations/load_image_list_test.dart +++ b/flutter/test/integrations/load_image_list_test.dart @@ -26,8 +26,8 @@ void main() { setUp(() async { fixture = IntegrationTestFixture(LoadImageListIntegration.new); - when(fixture.binding.loadDebugImages()) - .thenAnswer((_) async => imageList); + when(fixture.binding.loadDebugImages(any)) + .thenAnswer((_) async => imageList.toList()); await fixture.registerIntegration(); }); @@ -44,14 +44,14 @@ void main() { await fixture.hub.captureException(StateError('error'), stackTrace: StackTrace.current); - verifyNever(fixture.binding.loadDebugImages()); + verifyNever(fixture.binding.loadDebugImages(any)); }); test('Native layer is not called if the event has no stack traces', () async { await fixture.hub.captureException(StateError('error')); - verifyNever(fixture.binding.loadDebugImages()); + verifyNever(fixture.binding.loadDebugImages(any)); }); test('Native layer is called because stack traces are not symbolicated', @@ -67,7 +67,7 @@ void main() { #01 abs 000000723d637527 virt 00000000001f0527 _kDartIsolateSnapshotInstructions+0x1e5527 '''); - verify(fixture.binding.loadDebugImages()).called(1); + verify(fixture.binding.loadDebugImages(any)).called(1); }); test('Event processor adds image list to the event', () async { @@ -100,7 +100,7 @@ void main() { expect(fixture.options.eventProcessors.length, 1); await fixture.hub.captureMessage('error'); - verifyNever(fixture.binding.loadDebugImages()); + verifyNever(fixture.binding.loadDebugImages(any)); }); }); } diff --git a/flutter/test/integrations/native_sdk_integration_test.dart b/flutter/test/integrations/native_sdk_integration_test.dart index 7c25cec7b7..8645a80aa2 100644 --- a/flutter/test/integrations/native_sdk_integration_test.dart +++ b/flutter/test/integrations/native_sdk_integration_test.dart @@ -15,6 +15,8 @@ void main() { setUp(() { fixture = IntegrationTestFixture(NativeSdkIntegration.new); + when(fixture.binding.init(any)).thenReturn(null); + when(fixture.binding.close()).thenReturn(null); }); test('adds integration', () async { diff --git a/flutter/test/mocks.mocks.dart b/flutter/test/mocks.mocks.dart index 3f94ca0274..40e2d854c6 100644 --- a/flutter/test/mocks.mocks.dart +++ b/flutter/test/mocks.mocks.dart @@ -4,7 +4,7 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i7; -import 'dart:typed_data' as _i13; +import 'dart:typed_data' as _i12; import 'package:flutter/services.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; @@ -14,8 +14,7 @@ import 'package:sentry/src/metrics/metrics_api.dart' as _i5; import 'package:sentry/src/profiling.dart' as _i9; import 'package:sentry/src/sentry_tracer.dart' as _i3; import 'package:sentry_flutter/sentry_flutter.dart' as _i2; -import 'package:sentry_flutter/src/native/native_app_start.dart' as _i12; -import 'package:sentry_flutter/src/native/native_frames.dart' as _i14; +import 'package:sentry_flutter/src/native/native_frames.dart' as _i13; import 'package:sentry_flutter/src/native/sentry_native_binding.dart' as _i11; import 'mocks.dart' as _i6; @@ -1304,191 +1303,110 @@ class MockSentryNativeBinding extends _i1.Mock } @override - _i7.Future init(_i2.Hub? hub) => (super.noSuchMethod( - Invocation.method( - #init, - [hub], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + bool get supportsCaptureEnvelope => (super.noSuchMethod( + Invocation.getter(#supportsCaptureEnvelope), + returnValue: false, + ) as bool); @override - _i7.Future close() => (super.noSuchMethod( - Invocation.method( - #close, - [], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + bool get supportsLoadContexts => (super.noSuchMethod( + Invocation.getter(#supportsLoadContexts), + returnValue: false, + ) as bool); @override - _i7.Future<_i12.NativeAppStart?> fetchNativeAppStart() => (super.noSuchMethod( - Invocation.method( - #fetchNativeAppStart, - [], - ), - returnValue: _i7.Future<_i12.NativeAppStart?>.value(), - ) as _i7.Future<_i12.NativeAppStart?>); + _i7.FutureOr init(_i2.Hub? hub) => + (super.noSuchMethod(Invocation.method( + #init, + [hub], + )) as _i7.FutureOr); @override - _i7.Future captureEnvelope( - _i13.Uint8List? envelopeData, + _i7.FutureOr captureEnvelope( + _i12.Uint8List? envelopeData, bool? containsUnhandledException, ) => - (super.noSuchMethod( - Invocation.method( - #captureEnvelope, - [ - envelopeData, - containsUnhandledException, - ], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); - - @override - _i7.Future beginNativeFrames() => (super.noSuchMethod( - Invocation.method( - #beginNativeFrames, - [], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); - - @override - _i7.Future<_i14.NativeFrames?> endNativeFrames(_i2.SentryId? id) => - (super.noSuchMethod( - Invocation.method( - #endNativeFrames, - [id], - ), - returnValue: _i7.Future<_i14.NativeFrames?>.value(), - ) as _i7.Future<_i14.NativeFrames?>); - - @override - _i7.Future setUser(_i2.SentryUser? user) => (super.noSuchMethod( - Invocation.method( - #setUser, - [user], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); - - @override - _i7.Future addBreadcrumb(_i2.Breadcrumb? breadcrumb) => - (super.noSuchMethod( - Invocation.method( - #addBreadcrumb, - [breadcrumb], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + (super.noSuchMethod(Invocation.method( + #captureEnvelope, + [ + envelopeData, + containsUnhandledException, + ], + )) as _i7.FutureOr); @override - _i7.Future clearBreadcrumbs() => (super.noSuchMethod( - Invocation.method( - #clearBreadcrumbs, - [], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + _i7.FutureOr<_i13.NativeFrames?> endNativeFrames(_i2.SentryId? id) => + (super.noSuchMethod(Invocation.method( + #endNativeFrames, + [id], + )) as _i7.FutureOr<_i13.NativeFrames?>); @override - _i7.Future?> loadContexts() => (super.noSuchMethod( - Invocation.method( - #loadContexts, - [], - ), - returnValue: _i7.Future?>.value(), - ) as _i7.Future?>); + _i7.FutureOr addBreadcrumb(_i2.Breadcrumb? breadcrumb) => + (super.noSuchMethod(Invocation.method( + #addBreadcrumb, + [breadcrumb], + )) as _i7.FutureOr); @override - _i7.Future setContexts( + _i7.FutureOr setContexts( String? key, dynamic value, ) => - (super.noSuchMethod( - Invocation.method( - #setContexts, - [ - key, - value, - ], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + (super.noSuchMethod(Invocation.method( + #setContexts, + [ + key, + value, + ], + )) as _i7.FutureOr); @override - _i7.Future removeContexts(String? key) => (super.noSuchMethod( - Invocation.method( - #removeContexts, - [key], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + _i7.FutureOr removeContexts(String? key) => + (super.noSuchMethod(Invocation.method( + #removeContexts, + [key], + )) as _i7.FutureOr); @override - _i7.Future setExtra( + _i7.FutureOr setExtra( String? key, dynamic value, ) => - (super.noSuchMethod( - Invocation.method( - #setExtra, - [ - key, - value, - ], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + (super.noSuchMethod(Invocation.method( + #setExtra, + [ + key, + value, + ], + )) as _i7.FutureOr); @override - _i7.Future removeExtra(String? key) => (super.noSuchMethod( - Invocation.method( - #removeExtra, - [key], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + _i7.FutureOr removeExtra(String? key) => + (super.noSuchMethod(Invocation.method( + #removeExtra, + [key], + )) as _i7.FutureOr); @override - _i7.Future setTag( + _i7.FutureOr setTag( String? key, String? value, ) => - (super.noSuchMethod( - Invocation.method( - #setTag, - [ - key, - value, - ], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + (super.noSuchMethod(Invocation.method( + #setTag, + [ + key, + value, + ], + )) as _i7.FutureOr); @override - _i7.Future removeTag(String? key) => (super.noSuchMethod( - Invocation.method( - #removeTag, - [key], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + _i7.FutureOr removeTag(String? key) => + (super.noSuchMethod(Invocation.method( + #removeTag, + [key], + )) as _i7.FutureOr); @override int? startProfiler(_i2.SentryId? traceId) => @@ -1498,84 +1416,38 @@ class MockSentryNativeBinding extends _i1.Mock )) as int?); @override - _i7.Future discardProfiler(_i2.SentryId? traceId) => - (super.noSuchMethod( - Invocation.method( - #discardProfiler, - [traceId], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); - - @override - _i7.Future displayRefreshRate() => (super.noSuchMethod( - Invocation.method( - #displayRefreshRate, - [], - ), - returnValue: _i7.Future.value(), - ) as _i7.Future); + _i7.FutureOr discardProfiler(_i2.SentryId? traceId) => + (super.noSuchMethod(Invocation.method( + #discardProfiler, + [traceId], + )) as _i7.FutureOr); @override - _i7.Future?> collectProfile( + _i7.FutureOr?> collectProfile( _i2.SentryId? traceId, int? startTimeNs, int? endTimeNs, ) => - (super.noSuchMethod( - Invocation.method( - #collectProfile, - [ - traceId, - startTimeNs, - endTimeNs, - ], - ), - returnValue: _i7.Future?>.value(), - ) as _i7.Future?>); - - @override - _i7.Future?> loadDebugImages() => (super.noSuchMethod( - Invocation.method( - #loadDebugImages, - [], - ), - returnValue: _i7.Future?>.value(), - ) as _i7.Future?>); - - @override - _i7.Future pauseAppHangTracking() => (super.noSuchMethod( - Invocation.method( - #pauseAppHangTracking, - [], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); - - @override - _i7.Future resumeAppHangTracking() => (super.noSuchMethod( - Invocation.method( - #resumeAppHangTracking, - [], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + (super.noSuchMethod(Invocation.method( + #collectProfile, + [ + traceId, + startTimeNs, + endTimeNs, + ], + )) as _i7.FutureOr?>); @override - _i7.Future nativeCrash() => (super.noSuchMethod( - Invocation.method( - #nativeCrash, - [], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + _i7.FutureOr?> loadDebugImages( + _i2.SentryStackTrace? stackTrace) => + (super.noSuchMethod(Invocation.method( + #loadDebugImages, + [stackTrace], + )) as _i7.FutureOr?>); @override - _i7.Future<_i2.SentryId> captureReplay(bool? isCrash) => (super.noSuchMethod( + _i7.FutureOr<_i2.SentryId> captureReplay(bool? isCrash) => + (super.noSuchMethod( Invocation.method( #captureReplay, [isCrash], @@ -1587,7 +1459,7 @@ class MockSentryNativeBinding extends _i1.Mock [isCrash], ), )), - ) as _i7.Future<_i2.SentryId>); + ) as _i7.FutureOr<_i2.SentryId>); } /// A class which mocks [Hub]. diff --git a/flutter/test/native_scope_observer_test.dart b/flutter/test/native_scope_observer_test.dart index 9d7a8fd2a9..e22136bc1b 100644 --- a/flutter/test/native_scope_observer_test.dart +++ b/flutter/test/native_scope_observer_test.dart @@ -18,6 +18,7 @@ void main() { }); test('addBreadcrumbCalls', () async { + when(mock.addBreadcrumb(any)).thenReturn(null); final breadcrumb = Breadcrumb(); await sut.addBreadcrumb(breadcrumb); @@ -25,12 +26,14 @@ void main() { }); test('clearBreadcrumbsCalls', () async { + when(mock.clearBreadcrumbs()).thenReturn(null); await sut.clearBreadcrumbs(); verify(mock.clearBreadcrumbs()).called(1); }); test('removeContextsCalls', () async { + when(mock.removeContexts(any)).thenReturn(null); await sut.removeContexts('fixture-key'); expect( @@ -38,36 +41,43 @@ void main() { }); test('removeExtraCalls', () async { + when(mock.removeExtra(any)).thenReturn(null); await sut.removeExtra('fixture-key'); expect(verify(mock.removeExtra(captureAny)).captured.single, 'fixture-key'); }); test('removeTagCalls', () async { + when(mock.removeTag(any)).thenReturn(null); await sut.removeTag('fixture-key'); expect(verify(mock.removeTag(captureAny)).captured.single, 'fixture-key'); }); test('setContextsCalls', () async { + when(mock.setContexts(any, any)).thenReturn(null); await sut.setContexts('fixture-key', 'fixture-value'); verify(mock.setContexts('fixture-key', 'fixture-value')).called(1); }); test('setExtraCalls', () async { + when(mock.setExtra(any, any)).thenReturn(null); await sut.setExtra('fixture-key', 'fixture-value'); verify(mock.setExtra('fixture-key', 'fixture-value')).called(1); }); test('setTagCalls', () async { + when(mock.setTag(any, any)).thenReturn(null); await sut.setTag('fixture-key', 'fixture-value'); verify(mock.setTag('fixture-key', 'fixture-value')).called(1); }); test('setUserCalls', () async { + when(mock.setUser(any)).thenReturn(null); + final user = SentryUser(id: 'foo bar'); await sut.setUser(user); diff --git a/flutter/test/profiling_test.dart b/flutter/test/profiling_test.dart index 5ab944e53f..510daac9f0 100644 --- a/flutter/test/profiling_test.dart +++ b/flutter/test/profiling_test.dart @@ -68,6 +68,8 @@ void main() { }); test('dispose() calls native discard() exactly once', () async { + when(mock.discardProfiler(any)).thenReturn(null); + sut.dispose(); sut.dispose(); // Additional calls must not have an effect. diff --git a/flutter/test/sentry_flutter_test.dart b/flutter/test/sentry_flutter_test.dart index 7cd3794285..233f4539bf 100644 --- a/flutter/test/sentry_flutter_test.dart +++ b/flutter/test/sentry_flutter_test.dart @@ -59,6 +59,7 @@ void main() { setUp(() async { native = NativeChannelFixture(); + SentryFlutter.native = null; }); group('Test platform integrations', () { @@ -229,7 +230,8 @@ void main() { Transport transport = MockTransport(); final sentryFlutterOptions = defaultTestOptions( getPlatformChecker(platform: MockPlatform.windows())) - ..methodChannel = native.channel; + // We need to disable native init because sentry.dll is not available here. + ..autoInitializeNativeSdk = false; await SentryFlutter.init( (options) async { @@ -248,7 +250,7 @@ void main() { ); testScopeObserver( - options: sentryFlutterOptions, expectedHasNativeScopeObserver: false); + options: sentryFlutterOptions, expectedHasNativeScopeObserver: true); testConfiguration( integrations: integrations, @@ -269,10 +271,8 @@ void main() { beforeIntegration: WidgetsFlutterBindingIntegration, afterIntegration: OnErrorIntegration); - expect(SentryFlutter.native, isNull); + expect(SentryFlutter.native, isNotNull); expect(Sentry.currentHub.profilerFactory, isNull); - - await Sentry.close(); }, testOn: 'vm'); test('Linux', () async { @@ -629,7 +629,7 @@ void main() { () async { final sentryFlutterOptions = defaultTestOptions( getPlatformChecker(platform: MockPlatform.android(), isWeb: true)); - SentryFlutter.native = MockSentryNativeBinding(); + SentryFlutter.native = mockNativeBinding(); await SentryFlutter.init( (options) { expect(options.enableDartSymbolication, false); @@ -643,7 +643,7 @@ void main() { }); test('resumeAppHangTracking calls native method when available', () async { - SentryFlutter.native = MockSentryNativeBinding(); + SentryFlutter.native = mockNativeBinding(); when(SentryFlutter.native?.resumeAppHangTracking()) .thenAnswer((_) => Future.value()); @@ -662,7 +662,7 @@ void main() { }); test('pauseAppHangTracking calls native method when available', () async { - SentryFlutter.native = MockSentryNativeBinding(); + SentryFlutter.native = mockNativeBinding(); when(SentryFlutter.native?.pauseAppHangTracking()) .thenAnswer((_) => Future.value()); @@ -721,6 +721,16 @@ void main() { }); } +MockSentryNativeBinding mockNativeBinding() { + final result = MockSentryNativeBinding(); + when(result.supportsLoadContexts).thenReturn(true); + when(result.supportsCaptureEnvelope).thenReturn(true); + when(result.captureEnvelope(any, any)).thenReturn(null); + when(result.init(any)).thenReturn(null); + when(result.close()).thenReturn(null); + return result; +} + void appRunner() {} void loadTestPackage() { diff --git a/flutter/test/sentry_native/sentry_native_test.dart b/flutter/test/sentry_native/sentry_native_test.dart new file mode 100644 index 0000000000..3b720df7af --- /dev/null +++ b/flutter/test/sentry_native/sentry_native_test.dart @@ -0,0 +1,15 @@ +// We must conditionally import the actual test code, otherwise tests fail on +// a browser. @TestOn('vm') doesn't help by itself in this case because imports +// are still evaluated, thus causing a compilation failure. +@TestOn('vm && windows') +library sentry_native_test; + +import 'package:flutter_test/flutter_test.dart'; + +// ignore: unused_import +import 'sentry_native_test_web.dart' + if (dart.library.io) 'sentry_native_test_ffi.dart' as actual; + +// Defining main() here allows us to manually run/debug from VSCode. +// If we didn't need that, we could just `export` above. +void main() => actual.main(); diff --git a/flutter/test/sentry_native/sentry_native_test_ffi.dart b/flutter/test/sentry_native/sentry_native_test_ffi.dart new file mode 100644 index 0000000000..75fbf89c96 --- /dev/null +++ b/flutter/test/sentry_native/sentry_native_test_ffi.dart @@ -0,0 +1,301 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry/src/platform/platform.dart' as platform; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/native/c/sentry_native.dart'; +import 'package:sentry_flutter/src/native/factory.dart'; + +import '../mocks.dart'; +import '../mocks.mocks.dart'; + +late final String repoRootDir; +late final List expectedDistFiles; + +// NOTE: Don't run/debug this main(), it likely won't work. +// You can use main() in `sentry_native_test.dart`. +void main() { + repoRootDir = Directory.current.path.endsWith('/test') + ? Directory.current.parent.path + : Directory.current.path; + + expectedDistFiles = [ + 'sentry.dll', + 'crashpad_handler.exe', + 'crashpad_wer.dll', + ]; + + setUpAll(() async { + Directory.current = + await _buildSentryNative('$repoRootDir/temp/native-test'); + SentryNative.crashpadPath = + '${Directory.current.path}/${expectedDistFiles.firstWhere((f) => f.startsWith('crashpad_handler'))}'; + }); + + late SentryNative sut; + late SentryFlutterOptions options; + + setUp(() { + options = SentryFlutterOptions(dsn: fakeDsn) + // ignore: invalid_use_of_internal_member + ..automatedTestMode = true + ..debug = true; + sut = createBinding(options) as SentryNative; + }); + + test('expected output files', () { + for (var name in expectedDistFiles) { + if (!File(name).existsSync()) { + fail('Native distribution file $name does not exist'); + } + } + }); + + test('options', () { + options + ..debug = true + ..environment = 'foo' + ..release = 'foo@bar+1' + ..enableAutoSessionTracking = true + ..dist = 'distfoo' + ..maxBreadcrumbs = 42; + + final cOptions = sut.createOptions(options); + try { + expect( + SentryNative.native + .options_get_dsn(cOptions) + .cast() + .toDartString(), + fakeDsn); + expect( + SentryNative.native + .options_get_environment(cOptions) + .cast() + .toDartString(), + 'foo'); + expect( + SentryNative.native + .options_get_release(cOptions) + .cast() + .toDartString(), + 'foo@bar+1'); + expect( + SentryNative.native.options_get_auto_session_tracking(cOptions), 1); + expect(SentryNative.native.options_get_max_breadcrumbs(cOptions), 42); + } finally { + SentryNative.native.options_free(cOptions); + } + }); + + test('SDK version', () { + expect(_configuredSentryNativeVersion.length, greaterThanOrEqualTo(5)); + expect(SentryNative.native.sdk_version().cast().toDartString(), + _configuredSentryNativeVersion); + }); + + test('SDK name', () { + expect(SentryNative.native.sdk_name().cast().toDartString(), + 'sentry.native.flutter'); + }); + + test('init', () async { + addTearDown(sut.close); + await sut.init(MockHub()); + }); + + test('app start', () { + expect(sut.fetchNativeAppStart(), null); + }); + + test('frames tracking', () { + sut.beginNativeFrames(); + expect(sut.endNativeFrames(SentryId.newId()), null); + }); + + test('hang tracking', () { + sut.pauseAppHangTracking(); + sut.resumeAppHangTracking(); + }); + + test('setUser', () async { + final user = SentryUser( + id: "fixture-id", + username: 'username', + email: 'mail@domain.tld', + ipAddress: '1.2.3.4', + name: 'User Name', + data: { + 'str': 'foo-bar', + 'double': 1.0, + 'int': 1, + 'int64': 0x7FFFFFFF + 1, + 'boo': true, + 'inner-map': {'str': 'inner'}, + 'unsupported': Object() + }, + ); + + await sut.setUser(user); + }); + + test('addBreadcrumb', () async { + final breadcrumb = Breadcrumb( + type: 'type', + message: 'message', + category: 'category', + ); + await sut.addBreadcrumb(breadcrumb); + }); + + test('clearBreadcrumbs', () async { + await sut.clearBreadcrumbs(); + }); + + test('displayRefreshRate', () async { + expect(sut.displayRefreshRate(), isNull); + }); + + test('setContexts', () async { + final value = {'object': Object()}; + await sut.setContexts('fixture-key', value); + }); + + test('removeContexts', () async { + await sut.removeContexts('fixture-key'); + }); + + test('setExtra', () async { + final value = {'object': Object()}; + await sut.setExtra('fixture-key', value); + }); + + test('removeExtra', () async { + await sut.removeExtra('fixture-key'); + }); + + test('setTag', () async { + await sut.setTag('fixture-key', 'fixture-value'); + }); + + test('removeTag', () async { + await sut.removeTag('fixture-key'); + }); + + test('startProfiler', () { + expect(() => sut.startProfiler(SentryId.newId()), throwsUnsupportedError); + }); + + test('discardProfiler', () async { + expect(() => sut.discardProfiler(SentryId.newId()), throwsUnsupportedError); + }); + + test('collectProfile', () async { + final traceId = SentryId.newId(); + const startTime = 42; + const endTime = 50; + expect(() => sut.collectProfile(traceId, startTime, endTime), + throwsUnsupportedError); + }); + + test('captureEnvelope', () async { + final data = Uint8List.fromList([1, 2, 3]); + expect(() => sut.captureEnvelope(data, false), throwsUnsupportedError); + }); + + test('loadContexts', () async { + expect(await sut.loadContexts(), isNull); + }); + + test('loadDebugImages', () async { + final list = await sut.loadDebugImages(SentryStackTrace(frames: [])); + expect(list, isNotEmpty); + expect(list![0].type, 'pe'); + expect(list[0].debugId!.length, greaterThan(30)); + expect(list[0].debugFile, isNotEmpty); + expect(list[0].imageSize, greaterThan(0)); + expect(list[0].imageAddr, startsWith('0x')); + expect(list[0].imageAddr?.length, greaterThan(2)); + expect(list[0].codeId!.length, greaterThan(10)); + expect(list[0].codeFile, isNotEmpty); + expect( + File(list[0].codeFile!), + (File file) => file.existsSync(), + ); + }); +} + +/// Runs [command] with command's stdout and stderr being forwrarded to +/// test runner's respective streams. It buffers stdout and returns it. +/// +/// Returns [_CommandResult] with exitCode and stdout as a single sting +Future _exec(String executable, List arguments) async { + final process = await Process.start(executable, arguments); + + // forward standard streams + unawaited(stderr.addStream(process.stderr)); + unawaited(stdout.addStream(process.stdout)); + + int exitCode = await process.exitCode; + if (exitCode != 0) { + throw Exception( + "$executable ${arguments.join(' ')} failed with exit code $exitCode"); + } +} + +/// Compile sentry-native using CMake, as if it was part of a Flutter app. +/// Returns the directory containing built libraries +Future _buildSentryNative(String nativeTestRoot) async { + final cmakeBuildDir = '$nativeTestRoot/build'; + final cmakeConfDir = '$nativeTestRoot/conf'; + final buildOutputDir = '$nativeTestRoot/dist/'; + + if (!_builtVersionIsExpected(cmakeBuildDir, buildOutputDir)) { + Directory(cmakeConfDir).createSync(recursive: true); + Directory(buildOutputDir).createSync(recursive: true); + File('$cmakeConfDir/main.c').writeAsStringSync(''' +int main(int argc, char *argv[]) { return 0; } +'''); + File('$cmakeConfDir/CMakeLists.txt').writeAsStringSync(''' +cmake_minimum_required(VERSION 3.14) +project(sentry-native-flutter-test) +add_subdirectory(../../../${platform.instance.operatingSystem} plugin) +add_executable(\${CMAKE_PROJECT_NAME} main.c) +target_link_libraries(\${CMAKE_PROJECT_NAME} PRIVATE sentry_flutter_plugin) + +# Same as generated_plugins.cmake +list(APPEND PLUGIN_BUNDLED_LIBRARIES \$) +list(APPEND PLUGIN_BUNDLED_LIBRARIES \${sentry_flutter_bundled_libraries}) +install(FILES "\${PLUGIN_BUNDLED_LIBRARIES}" DESTINATION "${buildOutputDir.replaceAll('\\', '/')}" COMPONENT Runtime) +'''); + await _exec('cmake', ['-B', cmakeBuildDir, cmakeConfDir]); + await _exec('cmake', + ['--build', cmakeBuildDir, '--config', 'Release', '--parallel']); + await _exec('cmake', ['--install', cmakeBuildDir, '--config', 'Release']); + } + return buildOutputDir; +} + +bool _builtVersionIsExpected(String cmakeBuildDir, String buildOutputDir) { + final buildCmake = File( + '$cmakeBuildDir/_deps/sentry-native-build/sentry-config-version.cmake'); + if (!buildCmake.existsSync()) return false; + + if (!buildCmake + .readAsStringSync() + .contains('set(PACKAGE_VERSION "$_configuredSentryNativeVersion")')) { + return false; + } + + return !expectedDistFiles + .any((name) => !File('$buildOutputDir/$name').existsSync()); +} + +final _configuredSentryNativeVersion = + File('$repoRootDir/sentry-native/CMakeCache.txt') + .readAsLinesSync() + .map((line) => line.startsWith('version=') ? line.substring(8) : null) + .firstWhere((line) => line != null)!; diff --git a/flutter/test/sentry_native/sentry_native_test_web.dart b/flutter/test/sentry_native/sentry_native_test_web.dart new file mode 100644 index 0000000000..ab73b3a234 --- /dev/null +++ b/flutter/test/sentry_native/sentry_native_test_web.dart @@ -0,0 +1 @@ +void main() {} diff --git a/flutter/test/sentry_native_channel_test.dart b/flutter/test/sentry_native_channel_test.dart index 0428349d49..fe5ed79dd6 100644 --- a/flutter/test/sentry_native_channel_test.dart +++ b/flutter/test/sentry_native_channel_test.dart @@ -279,7 +279,7 @@ void main() { when(channel.invokeMethod('loadImageList')) .thenAnswer((invocation) async => json); - final data = await sut.loadDebugImages(); + final data = await sut.loadDebugImages(SentryStackTrace(frames: [])); expect(data?.map((v) => v.toJson()), json); }); diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index b71abd847d..6467aa1fff 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -47,6 +47,7 @@ void main() { setUp(() { mockBinding = MockSentryNativeBinding(); + when(mockBinding.beginNativeFrames()).thenReturn(null); SentryFlutter.native = mockBinding; }); diff --git a/flutter/windows/CMakeLists.txt b/flutter/windows/CMakeLists.txt index b7d3459e35..f992a75fdb 100644 --- a/flutter/windows/CMakeLists.txt +++ b/flutter/windows/CMakeLists.txt @@ -4,14 +4,8 @@ # customers of the plugin. cmake_minimum_required(VERSION 3.14) -# Project-level configuration. -set(PROJECT_NAME "sentry_flutter") -project(${PROJECT_NAME} LANGUAGES CXX) +include("${CMAKE_CURRENT_SOURCE_DIR}/../sentry-native/sentry-native.cmake") -# List of absolute paths to libraries that should be bundled with the plugin. -# This list could contain prebuilt libraries, or libraries created by an -# external build triggered from this build file. -set(sentry_flutter_bundled_libraries - "" - PARENT_SCOPE -) +# Even though sentry_flutter doesn't actually provide a useful plugin, we need to accomodate the Flutter tooling. +# sentry_flutter/sentry_flutter_plugin.h is included by the flutter-tool generated plugin registrar: +target_include_directories(sentry INTERFACE ${CMAKE_CURRENT_LIST_DIR}) diff --git a/flutter/windows/sentry_flutter/sentry_flutter_plugin.h b/flutter/windows/sentry_flutter/sentry_flutter_plugin.h new file mode 100644 index 0000000000..328a651d12 --- /dev/null +++ b/flutter/windows/sentry_flutter/sentry_flutter_plugin.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +#if defined(__cplusplus) +extern "C" { +#endif + +void SentryFlutterPluginRegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar) {} + +#if defined(__cplusplus) +} // extern "C" +#endif