From 20926fca22a75ad0f1aaaebc7693f5d17570674c Mon Sep 17 00:00:00 2001 From: Ivan Dlugos <6349682+vaind@users.noreply.github.com> Date: Thu, 10 Oct 2024 13:02:42 +0200 Subject: [PATCH] feat: windows native error & obfuscation support (#2286) * chore: update windows code based on latest flutter template with plugin_ffi * feat: add sentry-native library to the windows build * add sentry-native bindings * basic tests * more native binding functions * improve native tests * more native binding calls * load debug image list * add native feature support flags * clean * update native crash example * post-merge fixup * example: allways show console in the windows sample app * fix: windows build with crashpad * update test script * tmp * tmp * tmp * remove cmake_minimum_required * windows native obfuscated stack trace support * move native debug image resolution to sentry_native.dart * fix native test setup * further improve native test build * update debug id conversion code * fix native init test * test fixes * cleanup * improve native test * ensure crashpad_wer is distributed * post-merge failing test fixes * move stacktrace parsing back to stacktrace factory * fix debug image integration and tests * tmp: use sentry.init in the flutter example * fixup dart deps * comment fix * rename: SentryStackTraceFactory create() to parse() * linter isssues * analyzer issue * fix tests on web * fixup code coverage test * chore: update stacktrace tests * fix missing deps * formatting * add missing override annotations * merge sentry-native debug image creation to dart * analyzer issues * disable analysis on generated code * chore: update changelog * analyzer issues * fix tests * Revert "tmp: use sentry.init in the flutter example" This reverts commit 1c7b63d535db6a4221f6c8f7211fb1f45cc41b6b. * test: fix flutter test * fix int.toNativeValue() range * comments * Update CHANGELOG.md --- .github/workflows/flutter.yml | 5 +- .github/workflows/update-deps.yml | 10 +- CHANGELOG.md | 1 + dart/lib/src/debug_image_extractor.dart | 195 ---- .../load_dart_debug_images_integration.dart | 169 +++- dart/lib/src/platform/platform.dart | 5 + dart/lib/src/platform_checker.dart | 8 +- dart/lib/src/protocol/sentry_event.dart | 7 + dart/lib/src/protocol/sentry_stack_frame.dart | 1 + dart/lib/src/protocol/sentry_stack_trace.dart | 10 + dart/lib/src/sentry_client.dart | 9 +- dart/lib/src/sentry_exception_factory.dart | 11 +- dart/lib/src/sentry_options.dart | 4 +- dart/lib/src/sentry_stack_trace_factory.dart | 65 +- dart/pubspec.yaml | 2 +- dart/test/debug_image_extractor_test.dart | 118 --- ...ad_dart_debug_images_integration_test.dart | 198 +++- dart/test/mocks/mock_platform.dart | 14 +- dart/test/sentry_client_test.dart | 6 +- dart/test/sentry_exception_factory_test.dart | 47 +- dart/test/stack_trace_test.dart | 40 +- flutter/.gitignore | 2 +- flutter/example/.gitignore | 3 + flutter/example/lib/main.dart | 20 +- flutter/example/run.sh | 3 + flutter/example/windows/runner/main.cpp | 4 +- flutter/ffi-native.yaml | 75 ++ .../load_image_list_integration.dart | 33 +- flutter/lib/src/native/c/binding.dart | 894 ++++++++++++++++++ flutter/lib/src/native/c/sentry_native.dart | 389 ++++++++ flutter/lib/src/native/c/utils.dart | 24 + flutter/lib/src/native/factory_real.dart | 3 + .../lib/src/native/sentry_native_binding.dart | 52 +- .../lib/src/native/sentry_native_channel.dart | 8 +- flutter/lib/src/sentry_flutter.dart | 22 +- flutter/pubspec.yaml | 8 +- flutter/scripts/generate-cocoa-bindings.sh | 12 +- flutter/scripts/generate-native-bindings.ps1 | 21 + flutter/scripts/update-native.sh | 29 + flutter/sentry-native/CMakeCache.txt | 5 + flutter/sentry-native/sentry-native.cmake | 31 + flutter/temp/native-test/dist/sentry.dll | Bin 280064 -> 0 bytes flutter/test/file_system_transport_test.dart | 4 + flutter/test/initialization_test.dart | 6 +- .../integrations/load_image_list_test.dart | 12 +- .../native_sdk_integration_test.dart | 2 + flutter/test/mocks.mocks.dart | 320 ++----- flutter/test/native_scope_observer_test.dart | 10 + flutter/test/profiling_test.dart | 2 + flutter/test/sentry_flutter_test.dart | 26 +- .../sentry_native/sentry_native_test.dart | 15 + .../sentry_native/sentry_native_test_ffi.dart | 301 ++++++ .../sentry_native/sentry_native_test_web.dart | 1 + flutter/test/sentry_native_channel_test.dart | 2 +- .../test/sentry_navigator_observer_test.dart | 1 + flutter/windows/CMakeLists.txt | 14 +- .../sentry_flutter/sentry_flutter_plugin.h | 13 + 57 files changed, 2496 insertions(+), 796 deletions(-) delete mode 100644 dart/lib/src/debug_image_extractor.dart delete mode 100644 dart/test/debug_image_extractor_test.dart create mode 100644 flutter/ffi-native.yaml create mode 100644 flutter/lib/src/native/c/binding.dart create mode 100644 flutter/lib/src/native/c/sentry_native.dart create mode 100644 flutter/lib/src/native/c/utils.dart create mode 100644 flutter/scripts/generate-native-bindings.ps1 create mode 100644 flutter/scripts/update-native.sh create mode 100644 flutter/sentry-native/CMakeCache.txt create mode 100644 flutter/sentry-native/sentry-native.cmake delete mode 100644 flutter/temp/native-test/dist/sentry.dll create mode 100644 flutter/test/sentry_native/sentry_native_test.dart create mode 100644 flutter/test/sentry_native/sentry_native_test_ffi.dart create mode 100644 flutter/test/sentry_native/sentry_native_test_web.dart create mode 100644 flutter/windows/sentry_flutter/sentry_flutter_plugin.h 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 21615e9b7d335ff796f8ef03339c05780c9e36f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 280064 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�-0)E$fH{&iSTzR59+v^ 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%0A8d$Wr2cmF@b#M0vCDY7zCl!lXFw{tv-~4gCcV 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 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