diff --git a/CHANGELOG.md b/CHANGELOG.md index d693771b73..aa493b1388 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Prepare future support for iOS and macOS obfuscated app symbolication using dSYM (requires Flutter `master` channel) ([#823](https://github.com/getsentry/sentry-dart/pull/823)) - Bump Android SDK from v6.3.1 to v6.4.1 ([#989](https://github.com/getsentry/sentry-dart/pull/989)) - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#641) - [diff](https://github.com/getsentry/sentry-java/compare/6.3.1...6.4.1) @@ -458,7 +459,7 @@ This should not break anything since the Dart's min. version is already 2.12.0 a ### Breaking Changes: * Fix: Plugin Registrant class moved to barrel file (#358) - * This changed the import from `import 'package:sentry_flutter/src/sentry_flutter_web.dart';` + * This changed the import from `import 'package:sentry_flutter/src/sentry_flutter_web.dart';` to `import 'package:sentry_flutter/sentry_flutter_web.dart';` * This could lead to breaking changes. Typically it shouldn't because the referencing file is auto-generated. * Fix: Prefix classes with Sentry (#357) diff --git a/dart/lib/src/protocol/debug_image.dart b/dart/lib/src/protocol/debug_image.dart index b75dea5bb0..9d5a0a441f 100644 --- a/dart/lib/src/protocol/debug_image.dart +++ b/dart/lib/src/protocol/debug_image.dart @@ -13,9 +13,12 @@ import 'package:meta/meta.dart'; class DebugImage { final String? uuid; - /// Required. Type of the debug image. Must be "macho". + /// Required. Type of the debug image. final String type; + // Name of the image. Sentry-cocoa only. + final String? name; + /// Required. Identifier of the dynamic library or executable. It is the value of the LC_UUID load command in the Mach header, formatted as UUID. final String? debugId; @@ -23,6 +26,10 @@ class DebugImage { /// Should be a string in hex representation prefixed with "0x". final String? imageAddr; + /// Optional. Preferred load address of the image in virtual memory, as declared in the headers of the image. + /// When loading an image, the operating system may still choose to place it at a different address. + final String? imageVmAddr; + /// Required. The size of the image in virtual memory. If missing, Sentry will assume that the image spans up to the next image, which might lead to invalid stack traces. final int? imageSize; @@ -40,7 +47,9 @@ class DebugImage { const DebugImage({ required this.type, + this.name, this.imageAddr, + this.imageVmAddr, this.debugId, this.debugFile, this.imageSize, @@ -54,7 +63,9 @@ class DebugImage { factory DebugImage.fromJson(Map json) { return DebugImage( type: json['type'], + name: json['name'], imageAddr: json['image_addr'], + imageVmAddr: json['image_vmaddr'], debugId: json['debug_id'], debugFile: json['debug_file'], imageSize: json['image_size'], @@ -79,6 +90,10 @@ class DebugImage { json['debug_id'] = debugId; } + if (name != null) { + json['name'] = name; + } + if (debugFile != null) { json['debug_file'] = debugFile; } @@ -91,6 +106,10 @@ class DebugImage { json['image_addr'] = imageAddr; } + if (imageVmAddr != null) { + json['image_vmaddr'] = imageVmAddr; + } + if (imageSize != null) { json['image_size'] = imageSize; } @@ -108,22 +127,26 @@ class DebugImage { DebugImage copyWith({ String? uuid, + String? name, String? type, String? debugId, String? debugFile, String? codeFile, String? imageAddr, + String? imageVmAddr, int? imageSize, String? arch, String? codeId, }) => DebugImage( uuid: uuid ?? this.uuid, + name: name ?? this.name, type: type ?? this.type, debugId: debugId ?? this.debugId, debugFile: debugFile ?? this.debugFile, codeFile: codeFile ?? this.codeFile, imageAddr: imageAddr ?? this.imageAddr, + imageVmAddr: imageVmAddr ?? this.imageVmAddr, imageSize: imageSize ?? this.imageSize, arch: arch ?? this.arch, codeId: codeId ?? this.codeId, diff --git a/dart/lib/src/sentry_stack_trace_factory.dart b/dart/lib/src/sentry_stack_trace_factory.dart index f4426a53f0..bb9f9c5d70 100644 --- a/dart/lib/src/sentry_stack_trace_factory.dart +++ b/dart/lib/src/sentry_stack_trace_factory.dart @@ -9,9 +9,8 @@ import 'sentry_options.dart'; class SentryStackTraceFactory { final SentryOptions _options; - final _absRegex = RegExp('abs +([A-Fa-f0-9]+)'); - static const _stackTraceViolateDartStandard = - 'This VM has been configured to produce stack traces that violate the Dart standard.'; + final _absRegex = RegExp(r'^\s*#[0-9]+ +abs +([A-Fa-f0-9]+)'); + final _frameRegex = RegExp(r'^\s*#', multiLine: true); static const _sentryPackagesIdentifier = [ 'sentry', @@ -24,39 +23,22 @@ class SentryStackTraceFactory { /// returns the [SentryStackFrame] list from a stackTrace ([StackTrace] or [String]) List getStackFrames(dynamic stackTrace) { - final chain = (stackTrace is StackTrace) - ? Chain.forTrace(stackTrace) - : (stackTrace is String) - ? Chain.parse(stackTrace) - : Chain.parse(''); - + final chain = _parseStackTrace(stackTrace); final frames = []; - var symbolicated = true; - for (var t = 0; t < chain.traces.length; t += 1) { final trace = chain.traces[t]; for (final frame in trace.frames) { // we don't want to add our own frames - if (_sentryPackagesIdentifier.contains(frame.package)) { + if (frame.package != null && + _sentryPackagesIdentifier.contains(frame.package)) { continue; } - final member = frame.member; - // ideally the language would offer us a native way of parsing it. - if (member != null && member.contains(_stackTraceViolateDartStandard)) { - symbolicated = false; - } - - final stackTraceFrame = encodeStackTraceFrame( - frame, - symbolicated: symbolicated, - ); - - if (stackTraceFrame == null) { - continue; + final stackTraceFrame = encodeStackTraceFrame(frame); + if (stackTraceFrame != null) { + frames.add(stackTraceFrame); } - frames.add(stackTraceFrame); } // fill asynchronous gap @@ -68,67 +50,84 @@ class SentryStackTraceFactory { return frames.reversed.toList(); } - /// converts [Frame] to [SentryStackFrame] - @visibleForTesting - SentryStackFrame? encodeStackTraceFrame( - Frame frame, { - bool symbolicated = true, - }) { - final member = frame.member; - - SentryStackFrame? sentryStackFrame; - - if (symbolicated) { - final fileName = frame.uri.pathSegments.isNotEmpty - ? frame.uri.pathSegments.last - : null; + Chain _parseStackTrace(dynamic stackTrace) { + if (stackTrace is Chain || stackTrace is Trace) { + return Chain.forTrace(stackTrace); + } - final abs = '$eventOrigin${_absolutePathForCrashReport(frame)}'; + // We need to convert to string and split the headers manually, otherwise + // they end up in the final stack trace as "unparsed" lines. + // Note: [Chain.forTrace] would call [stackTrace.toString()] too. + if (stackTrace is StackTrace) { + stackTrace = stackTrace.toString(); + } - sentryStackFrame = SentryStackFrame( - absPath: abs, - function: member, - // https://docs.sentry.io/development/sdk-dev/features/#in-app-frames - inApp: isInApp(frame), - fileName: fileName, - package: frame.package, - ); + if (stackTrace is String) { + // Remove headers (everything before the first line starting with '#'). + // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** + // 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 + + final startOffset = _frameRegex.firstMatch(stackTrace)?.start ?? 0; + return Chain.parse( + startOffset == 0 ? stackTrace : stackTrace.substring(startOffset)); + } + return Chain([]); + } - if (frame.line != null && frame.line! >= 0) { - sentryStackFrame = sentryStackFrame.copyWith(lineNo: frame.line); - } + /// converts [Frame] to [SentryStackFrame] + @visibleForTesting + SentryStackFrame? encodeStackTraceFrame(Frame frame) { + final member = frame.member; - if (frame.column != null && frame.column! >= 0) { - sentryStackFrame = sentryStackFrame.copyWith(colNo: frame.column); - } - } else if (member != null) { + if (frame is UnparsedFrame && member != null) { // if --split-debug-info is enabled, thats what we see: - // warning: This VM has been configured to produce stack traces that violate the Dart standard. - // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** - // unparsed pid: 30930, tid: 30990, name 1.ui - // unparsed build_id: '5346e01103ffeed44e97094ff7bfcc19' - // unparsed isolate_dso_base: 723d447000, vm_dso_base: 723d447000 - // unparsed isolate_instructions: 723d452000, vm_instructions: 723d449000 - // unparsed #00 abs 000000723d6346d7 virt 00000000001ed6d7 _kDartIsolateSnapshotInstructions+0x1e26d7 - // unparsed #01 abs 000000723d637527 virt 00000000001f0527 _kDartIsolateSnapshotInstructions+0x1e5527 - // unparsed #02 abs 000000723d4a41a7 virt 000000000005d1a7 _kDartIsolateSnapshotInstructions+0x521a7 - // unparsed #03 abs 000000723d624663 virt 00000000001dd663 _kDartIsolateSnapshotInstructions+0x1d2663 - // unparsed #04 abs 000000723d4b8c3b virt 0000000000071c3b _kDartIsolateSnapshotInstructions+0x66c3b + // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** + // 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 matches = _absRegex.allMatches(member); - - if (matches.isNotEmpty) { - final abs = matches.elementAt(0).group(1); - if (abs != null) { - sentryStackFrame = SentryStackFrame( - instructionAddr: '0x$abs', - platform: 'native', // to trigger symbolication - ); - } + final match = _absRegex.firstMatch(member); + if (match != null) { + return SentryStackFrame( + instructionAddr: '0x${match.group(1)!}', + platform: 'native', // to trigger symbolication & native LoadImageList + ); } + + // 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. } + final fileName = + frame.uri.pathSegments.isNotEmpty ? frame.uri.pathSegments.last : null; + final abs = '$eventOrigin${_absolutePathForCrashReport(frame)}'; + + var sentryStackFrame = SentryStackFrame( + absPath: abs, + function: member, + // https://docs.sentry.io/development/sdk-dev/features/#in-app-frames + inApp: isInApp(frame), + fileName: fileName, + package: frame.package, + ); + + if (frame.line != null && frame.line! >= 0) { + sentryStackFrame = sentryStackFrame.copyWith(lineNo: frame.line); + } + + if (frame.column != null && frame.column! >= 0) { + sentryStackFrame = sentryStackFrame.copyWith(colNo: frame.column); + } return sentryStackFrame; } diff --git a/dart/test/protocol/debug_image_test.dart b/dart/test/protocol/debug_image_test.dart index 427eb19b8a..d06dad4b5e 100644 --- a/dart/test/protocol/debug_image_test.dart +++ b/dart/test/protocol/debug_image_test.dart @@ -64,7 +64,9 @@ void main() { final copy = data.copyWith( type: 'type1', + name: 'name', imageAddr: 'imageAddr1', + imageVmAddr: 'imageVmAddr1', debugId: 'debugId1', debugFile: 'debugFile1', imageSize: 2, diff --git a/dart/test/stack_trace_test.dart b/dart/test/stack_trace_test.dart index 75781a74ca..c5e656aded 100644 --- a/dart/test/stack_trace_test.dart +++ b/dart/test/stack_trace_test.dart @@ -169,50 +169,84 @@ void main() { ]); }); - test('sets instruction_addr if stack trace violates dart standard', () { - final frames = Fixture() - .getSut(considerInAppFramesByDefault: true) - .getStackFrames(''' - warning: This VM has been configured to produce stack traces that violate the Dart standard. - unparsed #00 abs 000000723d6346d7 virt 00000000001ed6d7 _kDartIsolateSnapshotInstructions+0x1e26d7 - unparsed #01 abs 000000723d637527 virt 00000000001f0527 _kDartIsolateSnapshotInstructions+0x1e5527 - ''').map((frame) => frame.toJson()); + test('parses obfuscated stack trace', () { + final stackTraces = [ + // Older format up to Dart SDK v2.18 (Flutter v3.3) + ''' +warning: This VM has been configured to produce stack traces that violate the Dart standard. +*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** +pid: 30930, tid: 30990, name 1.ui +build_id: '5346e01103ffeed44e97094ff7bfcc19' +isolate_dso_base: 723d447000, vm_dso_base: 723d447000 +isolate_instructions: 723d452000, vm_instructions: 723d449000 + #00 abs 000000723d6346d7 virt 00000000001ed6d7 _kDartIsolateSnapshotInstructions+0x1e26d7 + #01 abs 000000723d637527 virt 00000000001f0527 _kDartIsolateSnapshotInstructions+0x1e5527 + ''', + // Newer format + ''' +*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** +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 + ''', + ]; - expect(frames, [ - { - 'platform': 'native', - 'instruction_addr': '0x000000723d637527', - }, - { - 'platform': 'native', - 'instruction_addr': '0x000000723d6346d7', - }, - ]); + for (var traceString in stackTraces) { + final frames = Fixture() + .getSut(considerInAppFramesByDefault: true) + .getStackFrames(traceString) + .map((frame) => frame.toJson()); + + expect( + frames, + [ + { + 'platform': 'native', + 'instruction_addr': '0x000000723d637527', + }, + { + 'platform': 'native', + 'instruction_addr': '0x000000723d6346d7', + }, + ], + reason: "Failed to parse StackTrace:$traceString"); + } }); - test('sets instruction_addr and ignores noise', () { + test('parses normal stack trace', () { final frames = Fixture() .getSut(considerInAppFramesByDefault: true) .getStackFrames(''' - warning: This VM has been configured to produce stack traces that violate the Dart standard. - *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** - unparsed pid: 30930, tid: 30990, name 1.ui - unparsed build_id: '5346e01103ffeed44e97094ff7bfcc19' - unparsed isolate_dso_base: 723d447000, vm_dso_base: 723d447000 - unparsed isolate_instructions: 723d452000, vm_instructions: 723d449000 - unparsed #00 abs 000000723d6346d7 virt 00000000001ed6d7 _kDartIsolateSnapshotInstructions+0x1e26d7 - unparsed #01 abs 000000723d637527 virt 00000000001f0527 _kDartIsolateSnapshotInstructions+0x1e5527 - ''').map((frame) => frame.toJson()); - +#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()); expect(frames, [ { - 'platform': 'native', - 'instruction_addr': '0x000000723d637527', + 'filename': 'platform_dispatcher.dart', + 'function': 'PlatformDispatcher._dispatchPointerDataPacket', + 'lineno': 341, + 'abs_path': '${eventOrigin}dart:ui/platform_dispatcher.dart', + 'in_app': false }, { - 'platform': 'native', - 'instruction_addr': '0x000000723d6346d7', + 'filename': 'main.dart', + 'package': 'example', + 'function': 'MainScaffold.build.', + 'lineno': 131, + 'abs_path': '${eventOrigin}package:example/main.dart', + 'in_app': true }, + { + 'filename': 'main.dart', + 'function': 'asyncThrows', + 'lineno': 404, + 'abs_path': '${eventOrigin}main.dart', + 'in_app': true + } ]); }); }); diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index d14cabc470..facf48e935 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -31,6 +31,10 @@ Future main() async { options.attachThreads = true; options.enableWindowMetricBreadcrumbs = true; options.addIntegration(LoggingIntegration()); + // We can enable Sentry debug logging during development. This is likely + // going to log too much for your app, but can be useful when figuring out + // configuration issues, e.g. finding out why your events are not uploaded. + options.debug = true; }, // Init your App. appRunner: () => runApp( diff --git a/flutter/example/run.sh b/flutter/example/run.sh index 38b779fd25..1d87c5570f 100755 --- a/flutter/example/run.sh +++ b/flutter/example/run.sh @@ -1,5 +1,5 @@ #!/bin/bash -set -e +set -euo pipefail # Build a release version of the app for a platform and upload debug symbols and source maps. @@ -15,21 +15,26 @@ export SENTRY_RELEASE=$(date +%Y-%m-%d_%H-%M-%S) echo -e "[\033[92mrun\033[0m] $1" +# using 'build' as the base dir because `flutter clean` will delete it, so we don't end up with leftover symbols from a previous build +symbolsDir=build/symbols + if [ "$1" == "ios" ]; then - # iOS does not support split-debug-info and obfuscate yet - flutter build ios - # TODO: Install the iOS app via CLI - #.. install build/ios/Release-iphoneos/Runner.app + flutter build ios --split-debug-info=$symbolsDir --obfuscate + # see https://github.com/ios-control/ios-deploy (or just install: `brew install ios-deploy`) + launchCmd='ios-deploy --justlaunch --bundle build/ios/Release-iphoneos/Runner.app' elif [ "$1" == "android" ]; then - flutter build apk --split-debug-info=symbols --obfuscate - adb install build/app/outputs/flutter-apk/app-release.apk - adb shell am start -n io.sentry.samples.flutter/io.sentry.samples.flutter.MainActivity + flutter build apk --split-debug-info=$symbolsDir --obfuscate + adb install build/app/outputs/flutter-apk/app-release.apk + launchCmd='adb shell am start -n io.sentry.samples.flutter/io.sentry.samples.flutter.MainActivity' echo -e "[\033[92mrun\033[0m] Android app installed" elif [ "$1" == "web" ]; then # Uses dart2js flutter build web --dart-define=SENTRY_RELEASE=$SENTRY_RELEASE --source-maps ls -lah $OUTPUT_FOLDER_WEB echo -e "[\033[92mrun\033[0m] Built: $OUTPUT_FOLDER_WEB" +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' else if [ "$1" == "" ]; then echo -e "[\033[92mrun\033[0m] Pass the platform you'd like to run: android, ios, web" @@ -56,8 +61,10 @@ if [ "$1" == "web" ]; then python3 -m http.server 8132 popd else + # 'symbols' directory contains the Dart debug info files but to include platform-specific ones, use the whole build dir instead. echo -e "[\033[92mrun\033[0m] Uploading debug information files" - # directory 'symbols' contain the Dart debug info files but to include platform ones, use current dir. - sentry-cli upload-dif --org $SENTRY_ORG --project $SENTRY_PROJECT . -fi + sentry-cli upload-dif --wait --org $SENTRY_ORG --project $SENTRY_PROJECT build + echo "Starting the built app: $($launchCmd)" + $launchCmd +fi diff --git a/flutter/ios/Classes/SentryFlutterPluginApple.swift b/flutter/ios/Classes/SentryFlutterPluginApple.swift index 2592b8b977..8b8f31523f 100644 --- a/flutter/ios/Classes/SentryFlutterPluginApple.swift +++ b/flutter/ios/Classes/SentryFlutterPluginApple.swift @@ -61,6 +61,9 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin { case "loadContexts": loadContexts(result: result) + case "loadImageList": + loadImageList(result: result) + case "initNativeSdk": initNativeSdk(call, result: result) @@ -176,6 +179,11 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin { } } + private func loadImageList(result: @escaping FlutterResult) { + let debugImages = PrivateSentrySDKOnly.getDebugImages() as [DebugMeta] + result(debugImages.map { $0.serialize() }) + } + private func initNativeSdk(_ call: FlutterMethodCall, result: @escaping FlutterResult) { guard let arguments = call.arguments as? [String: Any], !arguments.isEmpty else { print("Arguments is null or empty") diff --git a/flutter/lib/src/default_integrations.dart b/flutter/lib/src/default_integrations.dart index 3fc708dcf4..348d5aa419 100644 --- a/flutter/lib/src/default_integrations.dart +++ b/flutter/lib/src/default_integrations.dart @@ -5,7 +5,6 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:sentry/sentry.dart'; - import 'binding_utils.dart'; import 'sentry_flutter_options.dart'; import 'widgets_binding_observer.dart'; @@ -444,97 +443,74 @@ class WidgetsBindingIntegration extends Integration { } } -/// Loads the Android Image list for stack trace symbolication -class LoadAndroidImageListIntegration - extends Integration { +/// Loads the native debug image list for stack trace symbolication. +class LoadImageListIntegration extends Integration { final MethodChannel _channel; - LoadAndroidImageListIntegration(this._channel); + LoadImageListIntegration(this._channel); @override FutureOr call(Hub hub, SentryFlutterOptions options) { options.addEventProcessor( - _LoadAndroidImageListIntegrationEventProcessor(_channel, options), + _LoadImageListIntegrationEventProcessor(_channel, options), ); - options.sdk.addIntegration('loadAndroidImageListIntegration'); + options.sdk.addIntegration('loadImageListIntegration'); + } +} + +extension _NeedsSymbolication on SentryEvent { + bool needsSymbolication() { + if (this is SentryTransaction) return false; + final frames = exceptions?.first.stackTrace?.frames; + if (frames == null) return false; + return frames.any((frame) => 'native' == frame.platform); } } -class _LoadAndroidImageListIntegrationEventProcessor extends EventProcessor { - _LoadAndroidImageListIntegrationEventProcessor(this._channel, this._options); +class _LoadImageListIntegrationEventProcessor extends EventProcessor { + _LoadImageListIntegrationEventProcessor(this._channel, this._options); final MethodChannel _channel; final SentryFlutterOptions _options; @override FutureOr apply(SentryEvent event, {hint}) async { - if (event is SentryTransaction) { - return event; - } - - try { - final exceptions = event.exceptions; - if (exceptions != null && exceptions.first.stackTrace != null) { - final needsSymbolication = exceptions.first.stackTrace?.frames - .any((element) => 'native' == element.platform) ?? - false; - - // if there are no frames that require symbolication, we don't - // load the debug image list. - if (!needsSymbolication) { - return event; - } - } else { - return event; - } - - // we call on every event because the loaded image list is cached - // and it could be changed on the Native side. - final imageList = List>.from( - await _channel.invokeMethod('loadImageList'), - ); - - if (imageList.isEmpty) { - return event; - } - - final newDebugImages = []; - - for (final item in imageList) { - final codeFile = item['code_file'] as String?; - final codeId = item['code_id'] as String?; - final imageAddr = item['image_addr'] as String?; - final imageSize = item['image_size'] as int?; - final type = item['type'] as String; - final debugId = item['debug_id'] as String?; - final debugFile = item['debug_file'] as String?; - - final image = DebugImage( - type: type, - imageAddr: imageAddr, - imageSize: imageSize, - codeFile: codeFile, - debugId: debugId, - codeId: codeId, - debugFile: debugFile, + if (event.needsSymbolication()) { + try { + // we call on every event because the loaded image list is cached + // and it could be changed on the Native side. + final imageList = List>.from( + await _channel.invokeMethod('loadImageList'), + ); + return copyWithDebugImages(event, imageList); + } catch (exception, stackTrace) { + _options.logger( + SentryLevel.error, + 'loadImageList failed', + exception: exception, + stackTrace: stackTrace, ); - newDebugImages.add(image); } + } - final debugMeta = DebugMeta(images: newDebugImages); + return event; + } - event = event.copyWith(debugMeta: debugMeta); - } catch (exception, stackTrace) { - _options.logger( - SentryLevel.error, - 'loadImageList failed', - exception: exception, - stackTrace: stackTrace, - ); + static SentryEvent copyWithDebugImages( + SentryEvent event, List imageList) { + if (imageList.isEmpty) { + return event; } - return event; + final newDebugImages = []; + for (final obj in imageList) { + final jsonMap = Map.from(obj as Map); + final image = DebugImage.fromJson(jsonMap); + newDebugImages.add(image); + } + + return event.copyWith(debugMeta: DebugMeta(images: newDebugImages)); } } diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 2681a8e768..94962bec56 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -98,6 +98,8 @@ mixin SentryFlutter { SentryFlutterOptions options, ) { final integrations = []; + final platformChecker = options.platformChecker; + final platform = platformChecker.platform; // Will call WidgetsFlutterBinding.ensureInitialized() before all other integrations. integrations.add(WidgetsFlutterBindingIntegration()); @@ -111,22 +113,21 @@ mixin SentryFlutter { // The ordering here matters, as we'd like to first start the native integration. // That allow us to send events to the network and then the Flutter integrations. // Flutter Web doesn't need that, only Android and iOS. - if (options.platformChecker.hasNativeIntegration) { + if (platformChecker.hasNativeIntegration) { integrations.add(NativeSdkIntegration(channel)); } // Will enrich events with device context, native packages and integrations - if (options.platformChecker.hasNativeIntegration && - !options.platformChecker.isWeb && - (options.platformChecker.platform.isIOS || - options.platformChecker.platform.isMacOS)) { + if (platformChecker.hasNativeIntegration && + !platformChecker.isWeb && + (platform.isIOS || platform.isMacOS)) { integrations.add(LoadContextsIntegration(channel)); } - if (options.platformChecker.hasNativeIntegration && - !options.platformChecker.isWeb && - options.platformChecker.platform.isAndroid) { - integrations.add(LoadAndroidImageListIntegration(channel)); + if (platformChecker.hasNativeIntegration && + !platformChecker.isWeb && + (platform.isAndroid || platform.isIOS || platform.isMacOS)) { + integrations.add(LoadImageListIntegration(channel)); } integrations.add(DebugPrintIntegration()); @@ -136,7 +137,7 @@ mixin SentryFlutter { // in errors. integrations.add(LoadReleaseIntegration(packageLoader)); - if (options.platformChecker.hasNativeIntegration) { + if (platformChecker.hasNativeIntegration) { integrations.add(NativeAppStartIntegration( SentryNative(), () { diff --git a/flutter/test/load_android_image_list_test.dart b/flutter/test/load_android_image_list_test.dart deleted file mode 100644 index 4e9f81dda9..0000000000 --- a/flutter/test/load_android_image_list_test.dart +++ /dev/null @@ -1,165 +0,0 @@ -@TestOn('vm') - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; - -import 'mocks.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - late Fixture fixture; - - final imageList = [ - { - 'code_file': '/apex/com.android.art/javalib/arm64/boot.oat', - 'code_id': '13577ce71153c228ecf0eb73fc39f45010d487f8', - 'image_addr': '0x6f80b000', - 'image_size': 3092480, - 'type': 'elf', - 'debug_id': 'e77c5713-5311-28c2-ecf0-eb73fc39f450', - 'debug_file': 'test' - } - ]; - - setUp(() { - fixture = Fixture(); - fixture.channel.setMockMethodCallHandler((MethodCall methodCall) async { - return imageList; - }); - }); - - tearDown(() { - fixture.channel.setMockMethodCallHandler(null); - }); - - test('$LoadAndroidImageListIntegration adds itself to sdk.integrations', - () async { - final sut = fixture.getSut(); - - sut.call(fixture.hub, fixture.options); - - expect( - fixture.options.sdk.integrations - .contains('loadAndroidImageListIntegration'), - true, - ); - }); - - test('Native layer is not called as the event is symbolicated', () async { - var called = false; - - final sut = fixture.getSut(); - fixture.channel.setMockMethodCallHandler((MethodCall methodCall) async { - called = true; - return imageList; - }); - - sut.call(fixture.hub, fixture.options); - - expect(fixture.options.eventProcessors.length, 1); - - await fixture.hub - .captureException(StateError('error'), stackTrace: StackTrace.current); - - expect(called, false); - }); - - test('Native layer is not called as the event has no stack traces', () async { - var called = false; - - final sut = fixture.getSut(); - fixture.channel.setMockMethodCallHandler((MethodCall methodCall) async { - called = true; - return imageList; - }); - - sut.call(fixture.hub, fixture.options); - - await fixture.hub.captureException(StateError('error')); - - expect(called, false); - }); - - test('Native layer is called as stack traces are not symbolicated', () async { - var called = false; - - final sut = fixture.getSut(); - fixture.channel.setMockMethodCallHandler((MethodCall methodCall) async { - called = true; - return imageList; - }); - - sut.call(fixture.hub, fixture.options); - - await fixture.hub.captureException(StateError('error'), stackTrace: ''' - warning: This VM has been configured to produce stack traces that violate the Dart standard. - *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** - unparsed pid: 30930, tid: 30990, name 1.ui - unparsed build_id: '5346e01103ffeed44e97094ff7bfcc19' - unparsed isolate_dso_base: 723d447000, vm_dso_base: 723d447000 - unparsed isolate_instructions: 723d452000, vm_instructions: 723d449000 - unparsed #00 abs 000000723d6346d7 virt 00000000001ed6d7 _kDartIsolateSnapshotInstructions+0x1e26d7 - unparsed #01 abs 000000723d637527 virt 00000000001f0527 _kDartIsolateSnapshotInstructions+0x1e5527 - '''); - - expect(called, true); - }); - - test('Event processor adds image list to the event', () async { - final sut = fixture.getSut(); - - sut.call(fixture.hub, fixture.options); - - final ep = fixture.options.eventProcessors.first; - SentryEvent? event = getEvent(); - event = await ep.apply(event); - - expect(1, event!.debugMeta!.images.length); - }); - - test('Event processor asserts image list', () async { - final sut = fixture.getSut(); - - sut.call(fixture.hub, fixture.options); - final ep = fixture.options.eventProcessors.first; - SentryEvent? event = getEvent(); - event = await ep.apply(event); - - final image = event!.debugMeta!.images.first; - - expect('/apex/com.android.art/javalib/arm64/boot.oat', image.codeFile); - expect('13577ce71153c228ecf0eb73fc39f45010d487f8', image.codeId); - expect('0x6f80b000', image.imageAddr); - expect(3092480, image.imageSize); - expect('elf', image.type); - expect('e77c5713-5311-28c2-ecf0-eb73fc39f450', image.debugId); - expect('test', image.debugFile); - }); -} - -SentryEvent getEvent({bool symbolicated = false}) { - final frame = SentryStackFrame(platform: 'native'); - final st = SentryStackTrace(frames: [frame]); - final ex = SentryException( - type: 'type', - value: 'value', - stackTrace: st, - ); - return SentryEvent(exceptions: [ex]); -} - -class Fixture { - Fixture() { - hub = Hub(options); - } - - final channel = MethodChannel('sentry_flutter'); - final options = SentryFlutterOptions(dsn: fakeDsn); - - late Hub hub; - - LoadAndroidImageListIntegration getSut() { - return LoadAndroidImageListIntegration(channel); - } -} diff --git a/flutter/test/load_image_list_test.dart b/flutter/test/load_image_list_test.dart new file mode 100644 index 0000000000..6abe496026 --- /dev/null +++ b/flutter/test/load_image_list_test.dart @@ -0,0 +1,176 @@ +@TestOn('vm') + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import 'mocks.dart'; +import 'sentry_flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + late Fixture fixture; + + tearDown(() { + fixture.channel.setMockMethodCallHandler(null); + }); + + for (var platform in [ + MockPlatform.android(), + MockPlatform.iOs(), + MockPlatform.macOs() + ]) { + group(platform.operatingSystem, () { + final imageList = [ + { + 'code_file': '/apex/com.android.art/javalib/arm64/boot.oat', + 'code_id': '13577ce71153c228ecf0eb73fc39f45010d487f8', + 'image_addr': '0x6f80b000', + 'image_size': 3092480, + 'type': 'elf', + 'debug_id': 'e77c5713-5311-28c2-ecf0-eb73fc39f450', + 'debug_file': 'test' + } + ]; + + setUp(() { + fixture = Fixture(platform); + fixture.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return imageList; + }); + }); + + test('$LoadImageListIntegration adds itself to sdk.integrations', + () async { + final sut = fixture.getSut(); + + sut.call(fixture.hub, fixture.options); + + expect( + fixture.options.sdk.integrations.contains('loadImageListIntegration'), + true, + ); + }); + + test('Native layer is not called as the event is symbolicated', () async { + var called = false; + + final sut = fixture.getSut(); + fixture.channel.setMockMethodCallHandler((MethodCall methodCall) async { + called = true; + return imageList; + }); + + sut.call(fixture.hub, fixture.options); + + expect(fixture.options.eventProcessors.length, 1); + + await fixture.hub.captureException(StateError('error'), + stackTrace: StackTrace.current); + + expect(called, false); + }); + + test('Native layer is not called if the event has no stack traces', + () async { + var called = false; + + final sut = fixture.getSut(); + fixture.channel.setMockMethodCallHandler((MethodCall methodCall) async { + called = true; + return imageList; + }); + + sut.call(fixture.hub, fixture.options); + + await fixture.hub.captureException(StateError('error')); + + expect(called, false); + }); + + test('Native layer is called because stack traces are not symbolicated', + () async { + var called = false; + + final sut = fixture.getSut(); + fixture.channel.setMockMethodCallHandler((MethodCall methodCall) async { + called = true; + return imageList; + }); + + sut.call(fixture.hub, fixture.options); + + await fixture.hub.captureException(StateError('error'), stackTrace: ''' + warning: This VM has been configured to produce stack traces that violate the Dart standard. + *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** + pid: 30930, tid: 30990, name 1.ui + build_id: '5346e01103ffeed44e97094ff7bfcc19' + isolate_dso_base: 723d447000, vm_dso_base: 723d447000 + isolate_instructions: 723d452000, vm_instructions: 723d449000 + #00 abs 000000723d6346d7 virt 00000000001ed6d7 _kDartIsolateSnapshotInstructions+0x1e26d7 + #01 abs 000000723d637527 virt 00000000001f0527 _kDartIsolateSnapshotInstructions+0x1e5527 + '''); + + expect(called, true); + }); + + test('Event processor adds image list to the event', () async { + final sut = fixture.getSut(); + + sut.call(fixture.hub, fixture.options); + + final ep = fixture.options.eventProcessors.first; + SentryEvent? event = _getEvent(); + event = await ep.apply(event); + + expect(1, event!.debugMeta!.images.length); + }); + + test('Event processor asserts image list', () async { + final sut = fixture.getSut(); + + sut.call(fixture.hub, fixture.options); + final ep = fixture.options.eventProcessors.first; + SentryEvent? event = _getEvent(); + event = await ep.apply(event); + + final image = event!.debugMeta!.images.first; + + expect('/apex/com.android.art/javalib/arm64/boot.oat', image.codeFile); + expect('13577ce71153c228ecf0eb73fc39f45010d487f8', image.codeId); + expect('0x6f80b000', image.imageAddr); + expect(3092480, image.imageSize); + expect('elf', image.type); + expect('e77c5713-5311-28c2-ecf0-eb73fc39f450', image.debugId); + expect('test', image.debugFile); + }); + }); + } +} + +SentryEvent _getEvent() { + final frame = SentryStackFrame(platform: 'native'); + final st = SentryStackTrace(frames: [frame]); + final ex = SentryException( + type: 'type', + value: 'value', + stackTrace: st, + ); + return SentryEvent(exceptions: [ex]); +} + +class Fixture { + late final Hub hub; + late final SentryFlutterOptions options; + final channel = MethodChannel('sentry_flutter'); + + Fixture(MockPlatform platform) { + options = SentryFlutterOptions( + dsn: fakeDsn, checker: getPlatformChecker(platform: platform)); + hub = Hub(options); + } + + LoadImageListIntegration getSut() { + return LoadImageListIntegration(channel); + } +} diff --git a/flutter/test/sentry_flutter_test.dart b/flutter/test/sentry_flutter_test.dart index ed6cfde0d5..1836fa0ed7 100644 --- a/flutter/test/sentry_flutter_test.dart +++ b/flutter/test/sentry_flutter_test.dart @@ -16,11 +16,12 @@ final platformAgnosticIntegrations = [ // These should only be added to Android final androidIntegrations = [ - LoadAndroidImageListIntegration, + LoadImageListIntegration, ]; // These should be added to iOS and macOS final iOsAndMacOsIntegrations = [ + LoadImageListIntegration, LoadContextsIntegration, ]; diff --git a/flutter/test/sentry_flutter_util.dart b/flutter/test/sentry_flutter_util.dart index b8c3bcd1ae..a0fb496c02 100644 --- a/flutter/test/sentry_flutter_util.dart +++ b/flutter/test/sentry_flutter_util.dart @@ -8,8 +8,8 @@ import 'package:sentry_flutter/src/sentry_flutter_options.dart'; import 'mocks.dart'; FutureOr Function(SentryFlutterOptions) getConfigurationTester({ - required List shouldHaveIntegrations, - required List shouldNotHaveIntegrations, + required Iterable shouldHaveIntegrations, + required Iterable shouldNotHaveIntegrations, required bool hasFileSystemTransport, }) => (options) async { @@ -21,17 +21,18 @@ FutureOr Function(SentryFlutterOptions) getConfigurationTester({ reason: '$FileSystemTransport was wrongly set', ); + final integrations = {}; + for (var e in options.integrations) { + integrations[e.runtimeType] = integrations[e.runtimeType] ?? 0 + 1; + } + for (final type in shouldHaveIntegrations) { - final integrations = options.integrations - .where((element) => element.runtimeType == type) - .toList(); - expect(integrations.length, 1); + expect(integrations, containsPair(type, 1)); } + shouldNotHaveIntegrations = Set.of(shouldNotHaveIntegrations) + .difference(Set.of(shouldHaveIntegrations)); for (final type in shouldNotHaveIntegrations) { - final integrations = options.integrations - .where((element) => element.runtimeType == type) - .toList(); - expect(integrations.isEmpty, true); + expect(integrations, isNot(contains(type))); } };