diff --git a/CHANGELOG.md b/CHANGELOG.md index f267c228c..40a0233ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ ### Features +- Add support for React Native mixed stacktraces ([#3201](https://github.com/getsentry/sentry-react-native/pull/3201)) + + In the current `react-native@nightly` (`0.73.0-nightly-20230809-cb60e5c67`) JS errors from native modules can + contain native JVM or Objective-C exception stack trace. Both JS and native stack trace + are processed by default no configuration needed. + - Add `tracePropagationTargets` option ([#3230](https://github.com/getsentry/sentry-react-native/pull/3230)) This release adds support for [distributed tracing](https://docs.sentry.io/platforms/react-native/usage/distributed-tracing/) diff --git a/RNSentryCocoaTester/Podfile b/RNSentryCocoaTester/Podfile index b2bd69527..f3d4c1b95 100644 --- a/RNSentryCocoaTester/Podfile +++ b/RNSentryCocoaTester/Podfile @@ -5,4 +5,5 @@ platform :ios, '12.4' target 'RNSentryCocoaTesterTests' do use_react_native!() pod 'RNSentry', :path => '../RNSentry.podspec' + pod 'OCMock', '3.9.1' end diff --git a/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj b/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj index a3cda2587..36e409870 100644 --- a/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj +++ b/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ /* Begin PBXFileReference section */ 1482D5685A340AB93348A43D /* Pods-RNSentryCocoaTesterTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNSentryCocoaTesterTests.release.xcconfig"; path = "Target Support Files/Pods-RNSentryCocoaTesterTests/Pods-RNSentryCocoaTesterTests.release.xcconfig"; sourceTree = ""; }; 3360898D29524164007C7730 /* RNSentryCocoaTesterTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RNSentryCocoaTesterTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 338739072A7D7D2800950DDD /* RNSentry+initNativeSdk.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RNSentry+initNativeSdk.h"; sourceTree = ""; }; 33F58ACF2977037D008F60EA /* RNSentry+initNativeSdk.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "RNSentry+initNativeSdk.mm"; sourceTree = ""; }; 650CB718ACFBD05609BF2126 /* libPods-RNSentryCocoaTesterTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RNSentryCocoaTesterTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; E2321E7CFA55AB617247098E /* Pods-RNSentryCocoaTesterTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNSentryCocoaTesterTests.debug.xcconfig"; path = "Target Support Files/Pods-RNSentryCocoaTesterTests/Pods-RNSentryCocoaTesterTests.debug.xcconfig"; sourceTree = ""; }; @@ -62,6 +63,7 @@ isa = PBXGroup; children = ( 33F58ACF2977037D008F60EA /* RNSentry+initNativeSdk.mm */, + 338739072A7D7D2800950DDD /* RNSentry+initNativeSdk.h */, ); path = RNSentryCocoaTesterTests; sourceTree = ""; @@ -249,6 +251,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = "\"${PODS_ROOT}/Sentry/Sources/Sentry/include\""; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -301,6 +304,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = "\"${PODS_ROOT}/Sentry/Sources/Sentry/include\""; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -316,6 +320,49 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/Headers/Public\"", + "\"${PODS_ROOT}/Headers/Public/DoubleConversion\"", + "\"${PODS_ROOT}/Headers/Public/FBLazyVector\"", + "\"${PODS_ROOT}/Headers/Public/OCMock\"", + "\"${PODS_ROOT}/Headers/Public/RCT-Folly\"", + "\"${PODS_ROOT}/Headers/Public/RCTRequired\"", + "\"${PODS_ROOT}/Headers/Public/RCTTypeSafety\"", + "\"${PODS_ROOT}/Headers/Public/RNSentry\"", + "\"${PODS_ROOT}/Headers/Public/React-Codegen\"", + "\"${PODS_ROOT}/Headers/Public/React-Core\"", + "\"${PODS_ROOT}/Headers/Public/React-NativeModulesApple\"", + "\"${PODS_ROOT}/Headers/Public/React-RCTAnimation\"", + "\"${PODS_ROOT}/Headers/Public/React-RCTAppDelegate\"", + "\"${PODS_ROOT}/Headers/Public/React-RCTBlob\"", + "\"${PODS_ROOT}/Headers/Public/React-RCTText\"", + "\"${PODS_ROOT}/Headers/Public/React-callinvoker\"", + "\"${PODS_ROOT}/Headers/Public/React-cxxreact\"", + "\"${PODS_ROOT}/Headers/Public/React-debug\"", + "\"${PODS_ROOT}/Headers/Public/React-hermes\"", + "\"${PODS_ROOT}/Headers/Public/React-jsi\"", + "\"${PODS_ROOT}/Headers/Public/React-jsiexecutor\"", + "\"${PODS_ROOT}/Headers/Public/React-jsinspector\"", + "\"${PODS_ROOT}/Headers/Public/React-logger\"", + "\"${PODS_ROOT}/Headers/Public/React-perflogger\"", + "\"${PODS_ROOT}/Headers/Public/React-runtimeexecutor\"", + "\"${PODS_ROOT}/Headers/Public/React-runtimescheduler\"", + "\"${PODS_ROOT}/Headers/Public/React-utils\"", + "\"${PODS_ROOT}/Headers/Public/ReactCommon\"", + "\"${PODS_ROOT}/Headers/Public/Sentry\"", + "\"${PODS_ROOT}/Headers/Public/SocketRocket\"", + "\"${PODS_ROOT}/Headers/Public/Yoga\"", + "\"${PODS_ROOT}/Headers/Public/fmt\"", + "\"${PODS_ROOT}/Headers/Public/glog\"", + "\"${PODS_ROOT}/Headers/Public/hermes-engine\"", + "\"${PODS_ROOT}/Headers/Public/libevent\"", + "\"$(PODS_ROOT)/DoubleConversion\"", + "\"$(PODS_ROOT)/boost\"", + "\"$(PODS_ROOT)/Headers/Private/React-Core\"", + "\"$(PODS_TARGET_SRCROOT)/include/\"", + "\"${PODS_ROOT}/Sentry/Sources/Sentry/include\"", + ); IPHONEOS_DEPLOYMENT_TARGET = 12.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.RNSentryCocoaTesterTests; @@ -335,6 +382,49 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/Headers/Public\"", + "\"${PODS_ROOT}/Headers/Public/DoubleConversion\"", + "\"${PODS_ROOT}/Headers/Public/FBLazyVector\"", + "\"${PODS_ROOT}/Headers/Public/OCMock\"", + "\"${PODS_ROOT}/Headers/Public/RCT-Folly\"", + "\"${PODS_ROOT}/Headers/Public/RCTRequired\"", + "\"${PODS_ROOT}/Headers/Public/RCTTypeSafety\"", + "\"${PODS_ROOT}/Headers/Public/RNSentry\"", + "\"${PODS_ROOT}/Headers/Public/React-Codegen\"", + "\"${PODS_ROOT}/Headers/Public/React-Core\"", + "\"${PODS_ROOT}/Headers/Public/React-NativeModulesApple\"", + "\"${PODS_ROOT}/Headers/Public/React-RCTAnimation\"", + "\"${PODS_ROOT}/Headers/Public/React-RCTAppDelegate\"", + "\"${PODS_ROOT}/Headers/Public/React-RCTBlob\"", + "\"${PODS_ROOT}/Headers/Public/React-RCTText\"", + "\"${PODS_ROOT}/Headers/Public/React-callinvoker\"", + "\"${PODS_ROOT}/Headers/Public/React-cxxreact\"", + "\"${PODS_ROOT}/Headers/Public/React-debug\"", + "\"${PODS_ROOT}/Headers/Public/React-hermes\"", + "\"${PODS_ROOT}/Headers/Public/React-jsi\"", + "\"${PODS_ROOT}/Headers/Public/React-jsiexecutor\"", + "\"${PODS_ROOT}/Headers/Public/React-jsinspector\"", + "\"${PODS_ROOT}/Headers/Public/React-logger\"", + "\"${PODS_ROOT}/Headers/Public/React-perflogger\"", + "\"${PODS_ROOT}/Headers/Public/React-runtimeexecutor\"", + "\"${PODS_ROOT}/Headers/Public/React-runtimescheduler\"", + "\"${PODS_ROOT}/Headers/Public/React-utils\"", + "\"${PODS_ROOT}/Headers/Public/ReactCommon\"", + "\"${PODS_ROOT}/Headers/Public/Sentry\"", + "\"${PODS_ROOT}/Headers/Public/SocketRocket\"", + "\"${PODS_ROOT}/Headers/Public/Yoga\"", + "\"${PODS_ROOT}/Headers/Public/fmt\"", + "\"${PODS_ROOT}/Headers/Public/glog\"", + "\"${PODS_ROOT}/Headers/Public/hermes-engine\"", + "\"${PODS_ROOT}/Headers/Public/libevent\"", + "\"$(PODS_ROOT)/DoubleConversion\"", + "\"$(PODS_ROOT)/boost\"", + "\"$(PODS_ROOT)/Headers/Private/React-Core\"", + "\"$(PODS_TARGET_SRCROOT)/include/\"", + "\"${PODS_ROOT}/Sentry/Sources/Sentry/include\"", + ); IPHONEOS_DEPLOYMENT_TARGET = 12.4; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.sentry.RNSentryCocoaTesterTests; diff --git a/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentry+initNativeSdk.h b/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentry+initNativeSdk.h new file mode 100644 index 000000000..2690a7d73 --- /dev/null +++ b/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentry+initNativeSdk.h @@ -0,0 +1,25 @@ +#import +#import + +@interface +SentrySDK (PrivateTests) +- (nullable SentryOptions *) options; +@end + +@interface SentryBinaryImageInfo : NSObject +@property (nonatomic, strong) NSString *name; +@property (nonatomic) uint64_t address; +@property (nonatomic) uint64_t size; +@end + +@interface SentryBinaryImageCache : NSObject +@property (nonatomic, readonly, class) SentryBinaryImageCache *shared; +- (void)start; +- (void)stop; +- (nullable SentryBinaryImageInfo *)imageByAddress:(const uint64_t)address; +@end + +@interface SentryDependencyContainer : NSObject ++ (instancetype)sharedInstance; +@property (nonatomic, strong) SentryDebugImageProvider *debugImageProvider; +@end diff --git a/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentry+initNativeSdk.mm b/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentry+initNativeSdk.mm index a6acb6429..01d5a5200 100644 --- a/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentry+initNativeSdk.mm +++ b/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentry+initNativeSdk.mm @@ -1,8 +1,8 @@ +#import "RNSentry+initNativeSdk.h" +#import #import #import -#import -#import -#import "RNSentry.h" +#import @interface RNSentryInitNativeSdkTests : XCTestCase @@ -168,4 +168,123 @@ - (void)testEventFromSentryReactNativeOriginAndEnvironmentTagsAreOverwritten XCTAssertEqual(testEvent.tags[@"event.environment"], @"native"); } +void (^expectRejecterNotCalled)(NSString*, NSString*, NSError*) = ^(NSString *code, NSString *message, NSError *error) { + @throw [NSException exceptionWithName:@"Promise Rejector should not be called." reason:nil userInfo:nil]; +}; + +uint64_t MOCKED_SYMBOL_ADDRESS = 123; +char const* MOCKED_SYMBOL_NAME = "symbolicatedname"; + +int sucessfulSymbolicate(const void *, Dl_info *info){ + info->dli_saddr = (void *) MOCKED_SYMBOL_ADDRESS; + info->dli_sname = MOCKED_SYMBOL_NAME; + return 1; +} + +- (void)prepareNativeFrameMocksWithLocalSymbolication: (BOOL) debug +{ + SentryOptions* sentryOptions = [[SentryOptions alloc] init]; + sentryOptions.debug = debug; //no local symbolication + + id sentrySDKMock = OCMClassMock([SentrySDK class]); + OCMStub([(SentrySDK*) sentrySDKMock options]).andReturn(sentryOptions); + + id sentryBinaryImageInfoMockOne = OCMClassMock([SentryBinaryImageInfo class]); + OCMStub([(SentryBinaryImageInfo*) sentryBinaryImageInfoMockOne address]).andReturn([@112233 unsignedLongLongValue]); + OCMStub([sentryBinaryImageInfoMockOne name]).andReturn(@"testnameone"); + + id sentryBinaryImageInfoMockTwo = OCMClassMock([SentryBinaryImageInfo class]); + OCMStub([(SentryBinaryImageInfo*) sentryBinaryImageInfoMockTwo address]).andReturn([@112233 unsignedLongLongValue]); + OCMStub([sentryBinaryImageInfoMockTwo name]).andReturn(@"testnametwo"); + + id sentryBinaryImageCacheMock = OCMClassMock([SentryBinaryImageCache class]); + OCMStub(ClassMethod([sentryBinaryImageCacheMock shared])).andReturn(sentryBinaryImageCacheMock); + OCMStub([sentryBinaryImageCacheMock imageByAddress:[@123 unsignedLongLongValue]]).andReturn(sentryBinaryImageInfoMockOne); + OCMStub([sentryBinaryImageCacheMock imageByAddress:[@456 unsignedLongLongValue]]).andReturn(sentryBinaryImageInfoMockTwo); + + NSDictionary* serializedDebugImage = @{ + @"uuid": @"mockuuid", + @"debug_id": @"mockdebugid", + @"type": @"macho", + @"image_addr": @"0x000000000001b669", + }; + id sentryDebugImageMock = OCMClassMock([SentryDebugMeta class]); + OCMStub([sentryDebugImageMock serialize]).andReturn(serializedDebugImage); + + id sentryDebugImageProviderMock = OCMClassMock([SentryDebugImageProvider class]); + OCMStub([sentryDebugImageProviderMock getDebugImagesForAddresses:[NSSet setWithObject:@"0x000000000001b669"] isCrash:false]).andReturn(@[sentryDebugImageMock]); + + id sentryDependencyContainerMock = OCMClassMock([SentryDependencyContainer class]); + OCMStub(ClassMethod([sentryDependencyContainerMock sharedInstance])).andReturn(sentryDependencyContainerMock); + OCMStub([sentryDependencyContainerMock debugImageProvider]).andReturn(sentryDebugImageProviderMock); +} + +- (void)testFetchNativeStackFramesByInstructionsServerSymbolication +{ + [self prepareNativeFrameMocksWithLocalSymbolication:NO]; + RNSentry* rnSentry = [[RNSentry alloc] init]; + NSDictionary* actual = [rnSentry fetchNativeStackFramesBy: @[@123, @456] + symbolicate: sucessfulSymbolicate]; + + NSDictionary* expected = @{ + @"debugMetaImages": @[ + @{ + @"uuid": @"mockuuid", + @"debug_id": @"mockdebugid", + @"type": @"macho", + @"image_addr": @"0x000000000001b669", + }, + ], + @"frames": @[ + @{ + @"package": @"testnameone", + @"in_app": @NO, + @"platform": @"cocoa", + @"instruction_addr": @"0x000000000000007b", //123 + @"image_addr": @"0x000000000001b669", //112233 + }, + @{ + @"package": @"testnametwo", + @"in_app": @NO, + @"platform": @"cocoa", + @"instruction_addr": @"0x00000000000001c8", //456 + @"image_addr": @"0x000000000001b669", //445566 + }, + ], + }; + XCTAssertTrue([actual isEqualToDictionary:expected]); +} + +- (void)testFetchNativeStackFramesByInstructionsOnDeviceSymbolication +{ + [self prepareNativeFrameMocksWithLocalSymbolication:YES]; + RNSentry* rnSentry = [[RNSentry alloc] init]; + NSDictionary* actual = [rnSentry fetchNativeStackFramesBy: @[@123, @456] + symbolicate: sucessfulSymbolicate]; + + NSDictionary* expected = @{ + @"frames": @[ + @{ + @"function": @"symbolicatedname", + @"package": @"testnameone", + @"in_app": @NO, + @"platform": @"cocoa", + @"symbol_addr": @"0x000000000000007b", //123 + @"instruction_addr": @"0x000000000000007b", //123 + @"image_addr": @"0x000000000001b669", //112233 + }, + @{ + @"function": @"symbolicatedname", + @"package": @"testnametwo", + @"in_app": @NO, + @"platform": @"cocoa", + @"symbol_addr": @"0x000000000000007b", //123 + @"instruction_addr": @"0x00000000000001c8", //456 + @"image_addr": @"0x000000000001b669", //445566 + }, + ], + }; + XCTAssertTrue([actual isEqualToDictionary:expected]); +} + @end diff --git a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 845fc76ec..158847b0c 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -704,6 +704,10 @@ public void fetchNativeSdkInfo(Promise promise) { } } + public void fetchNativePackageName(Promise promise) { + promise.resolve(packageInfo.packageName); + } + private void setEventOriginTag(SentryEvent event) { SdkVersion sdk = event.getSdk(); if (sdk != null) { diff --git a/android/src/newarch/java/io/sentry/react/RNSentryModule.java b/android/src/newarch/java/io/sentry/react/RNSentryModule.java index 0310f537a..33bfeec44 100644 --- a/android/src/newarch/java/io/sentry/react/RNSentryModule.java +++ b/android/src/newarch/java/io/sentry/react/RNSentryModule.java @@ -133,4 +133,14 @@ public WritableMap startProfiling() { public WritableMap stopProfiling() { return this.impl.stopProfiling(); } + + @Override + public void fetchNativePackageName(Promise promise) { + this.impl.fetchNativePackageName(promise); + } + + @Override + public void fetchNativeStackFramesBy(Promise promise) { + // Not used on Android + } } diff --git a/android/src/oldarch/java/io/sentry/react/RNSentryModule.java b/android/src/oldarch/java/io/sentry/react/RNSentryModule.java index 3f0d70863..7a5a5f0db 100644 --- a/android/src/oldarch/java/io/sentry/react/RNSentryModule.java +++ b/android/src/oldarch/java/io/sentry/react/RNSentryModule.java @@ -132,4 +132,14 @@ public WritableMap startProfiling() { public WritableMap stopProfiling() { return this.impl.stopProfiling(); } + + @ReactMethod + public void fetchNativePackageName(Promise promise) { + this.impl.fetchNativePackageName(promise); + } + + @ReactMethod + public void fetchNativeStackFramesBy(Promise promise) { + // Not used on Android + } } diff --git a/ios/RNSentry.h b/ios/RNSentry.h index df9be9293..5a125edbd 100644 --- a/ios/RNSentry.h +++ b/ios/RNSentry.h @@ -4,13 +4,31 @@ #import "RCTBridge.h" #endif +#import + +#import #import +#import + +typedef int (*SymbolicateCallbackType)(const void *, Dl_info *); + +@interface SentryDebugImageProvider () +- (NSArray * _Nonnull)getDebugImagesForAddresses:(NSSet * _Nonnull)addresses isCrash:(BOOL)isCrash; +@end + +@interface +SentrySDK (Private) +@property (nonatomic, nullable, readonly, class) SentryOptions *options; +@end @interface RNSentry : NSObject - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options error:(NSError *_Nullable*_Nonnull)errorPointer; -- (void)setEventOriginTag:(SentryEvent *)event; +- (void) setEventOriginTag: (SentryEvent*) event; + +- (NSDictionary*_Nonnull) fetchNativeStackFramesBy: (NSArray*)instructionsAddr + symbolicate: (SymbolicateCallbackType) symbolicate; @end diff --git a/ios/RNSentry.mm b/ios/RNSentry.mm index 8afe998f4..5b361e26d 100644 --- a/ios/RNSentry.mm +++ b/ios/RNSentry.mm @@ -1,3 +1,4 @@ +#import #import "RNSentry.h" #if __has_include() @@ -10,6 +11,9 @@ #import #import #import +#import +#import +#import #if __has_include() #define SENTRY_PROFILING_ENABLED 1 @@ -195,6 +199,86 @@ - (void)setEventEnvironmentTag:(SentryEvent *)event resolve(modulesString); } +RCT_EXPORT_METHOD(fetchNativePackageName:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSString *packageName = [[NSBundle mainBundle] executablePath]; + resolve(packageName); +} + +- (NSDictionary*) fetchNativeStackFramesBy: (NSArray*)instructionsAddr + symbolicate: (SymbolicateCallbackType) symbolicate +{ + BOOL shouldSymbolicateLocally = [SentrySDK.options debug]; + NSString *appPackageName = [[NSBundle mainBundle] executablePath]; + + NSMutableSet * _Nonnull imagesAddrToRetrieveDebugMetaImages = [[NSMutableSet alloc] init]; + NSMutableArray *> * _Nonnull serializedFrames = [[NSMutableArray alloc] init]; + + for (NSNumber *addr in instructionsAddr) { + SentryBinaryImageInfo * _Nullable image = [[SentryBinaryImageCache shared] imageByAddress:[addr unsignedLongLongValue]]; + if (image != nil) { + NSString * imageAddr = sentry_formatHexAddressUInt64([image address]); + [imagesAddrToRetrieveDebugMetaImages addObject: imageAddr]; + + NSDictionary * _Nonnull nativeFrame = @{ + @"platform": @"cocoa", + @"instruction_addr": sentry_formatHexAddress(addr), + @"package": [image name], + @"image_addr": imageAddr, + @"in_app": [NSNumber numberWithBool:[appPackageName isEqualToString:[image name]]], + }; + + if (shouldSymbolicateLocally) { + Dl_info symbolsBuffer; + bool symbols_succeed = false; + symbols_succeed = symbolicate((void *) [addr unsignedLongLongValue], &symbolsBuffer) != 0; + if (symbols_succeed) { + NSMutableDictionary * _Nonnull symbolicated = nativeFrame.mutableCopy; + symbolicated[@"symbol_addr"] = sentry_formatHexAddressUInt64((uintptr_t)symbolsBuffer.dli_saddr); + symbolicated[@"function"] = [NSString stringWithCString:symbolsBuffer.dli_sname encoding:NSUTF8StringEncoding]; + + nativeFrame = symbolicated; + } + } + + [serializedFrames addObject:nativeFrame]; + } else { + [serializedFrames addObject: @{ + @"platform": @"cocoa", + @"instruction_addr": sentry_formatHexAddress(addr), + }]; + } + } + + if (shouldSymbolicateLocally) { + return @{ + @"frames": serializedFrames, + }; + } else { + NSMutableArray *> * _Nonnull serializedDebugMetaImages = [[NSMutableArray alloc] init]; + + NSArray *debugMetaImages = [[[SentryDependencyContainer sharedInstance] debugImageProvider] getDebugImagesForAddresses:imagesAddrToRetrieveDebugMetaImages isCrash:false]; + + for (SentryDebugMeta *debugImage in debugMetaImages) { + [serializedDebugMetaImages addObject:[debugImage serialize]]; + } + + return @{ + @"frames": serializedFrames, + @"debugMetaImages": serializedDebugMetaImages, + }; + } +} + +RCT_EXPORT_METHOD(fetchNativeStackFramesBy:(NSArray *)instructionsAddr + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + resolve([self fetchNativeStackFramesBy:instructionsAddr + symbolicate:dladdr]); +} + RCT_EXPORT_METHOD(fetchNativeDeviceContexts:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { diff --git a/src/js/NativeRNSentry.ts b/src/js/NativeRNSentry.ts index be3c11c12..582c32fdf 100644 --- a/src/js/NativeRNSentry.ts +++ b/src/js/NativeRNSentry.ts @@ -33,8 +33,53 @@ export interface Spec extends TurboModule { fetchViewHierarchy(): Promise; startProfiling(): { started?: boolean; error?: string }; stopProfiling(): { profile?: string; error?: string }; + fetchNativePackageName(): Promise; + fetchNativeStackFramesBy(instructionsAddr: number[]): Promise; } +export type NativeStackFrame = { + platform: string; + /** + * The instruction address of this frame. + * Formatted as hex with 0x prefix. + */ + instruction_addr: string; + package?: string; + /** + * The debug image address of this frame. + * Formatted as hex with 0x prefix. + */ + image_addr?: string; + in_app?: boolean; + /** + * The symbol name of this frame. + * If symbolicated locally. + */ + function?: string; + /** + * The symbol address of this frame. + * If symbolicated locally. + * Formatted as hex with 0x prefix. + */ + symbol_addr?: string; +}; + +export type NativeDebugImage = { + name?: string; + type?: string; + uuid?: string; + debug_id?: string; + image_addr?: string; + image_size?: number; + code_file?: string; + image_vmaddr?: string; +}; + +export type NativeStackFrames = { + frames: NativeStackFrame[]; + debugMetaImages?: NativeDebugImage[]; +}; + export type NativeAppStartResponse = { isColdStart: boolean; appStartTime: number; diff --git a/src/js/integrations/nativelinkederrors.ts b/src/js/integrations/nativelinkederrors.ts new file mode 100644 index 000000000..6c0d8d394 --- /dev/null +++ b/src/js/integrations/nativelinkederrors.ts @@ -0,0 +1,231 @@ +import { exceptionFromError } from '@sentry/browser'; +import type { + DebugImage, + Event, + EventHint, + EventProcessor, + Exception, + ExtendedError, + Hub, + Integration, + StackFrame, + StackParser, +} from '@sentry/types'; +import { isInstanceOf, isPlainObject } from '@sentry/utils'; + +import type { NativeStackFrames } from '../NativeRNSentry'; +import { NATIVE } from '../wrapper'; + +const DEFAULT_KEY = 'cause'; +const DEFAULT_LIMIT = 5; + +interface LinkedErrorsOptions { + key: string; + limit: number; +} + +/** + * Processes JS and RN native linked errors. + */ +export class NativeLinkedErrors implements Integration { + /** + * @inheritDoc + */ + public static id: string = 'NativeLinkedErrors'; + + /** + * @inheritDoc + */ + public name: string = NativeLinkedErrors.id; + + private readonly _key: LinkedErrorsOptions['key']; + private readonly _limit: LinkedErrorsOptions['limit']; + private _nativePackage: string | null = null; + + /** + * @inheritDoc + */ + public constructor(options: Partial = {}) { + this._key = options.key || DEFAULT_KEY; + this._limit = options.limit || DEFAULT_LIMIT; + } + + /** + * @inheritDoc + */ + public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { + const client = getCurrentHub().getClient(); + if (!client) { + return; + } + + addGlobalEventProcessor(async (event: Event, hint?: EventHint) => { + if (this._nativePackage === null) { + this._nativePackage = await this._fetchNativePackage(); + } + const self = getCurrentHub().getIntegration(NativeLinkedErrors); + return self ? this._handler(client.getOptions().stackParser, self._key, self._limit, event, hint) : event; + }); + } + + /** + * Enriches passed event with linked exceptions and native debug meta images. + */ + private async _handler( + parser: StackParser, + key: string, + limit: number, + event: Event, + hint?: EventHint, + ): Promise { + if (!event.exception || !event.exception.values || !hint || !isInstanceOf(hint.originalException, Error)) { + return event; + } + const { exceptions: linkedErrors, debugImages } = await this._walkErrorTree( + parser, + limit, + hint.originalException as ExtendedError, + key, + ); + event.exception.values = [...event.exception.values, ...linkedErrors]; + + event.debug_meta = event.debug_meta || {}; + event.debug_meta.images = event.debug_meta.images || []; + event.debug_meta.images.push(...(debugImages || [])); + + return event; + } + + /** + * Walks linked errors and created Sentry exceptions chain. + * Collects debug images from native errors stack frames. + */ + private async _walkErrorTree( + parser: StackParser, + limit: number, + error: ExtendedError, + key: string, + exceptions: Exception[] = [], + debugImages: DebugImage[] = [], + ): Promise<{ + exceptions: Exception[]; + debugImages?: DebugImage[]; + }> { + const linkedError = error[key]; + if (!linkedError || exceptions.length + 1 >= limit) { + return { + exceptions, + debugImages, + }; + } + + let exception: Exception; + let exceptionDebugImages: DebugImage[] | undefined; + if ('stackElements' in linkedError) { + // isJavaException + exception = this._exceptionFromJavaStackElements(linkedError); + } else if ('stackReturnAddresses' in linkedError) { + // isObjCException + const { appleException, appleDebugImages } = await this._exceptionFromAppleStackReturnAddresses(linkedError); + exception = appleException; + exceptionDebugImages = appleDebugImages; + } else if (isInstanceOf(linkedError, Error)) { + exception = exceptionFromError(parser, error[key]); + } else if (isPlainObject(linkedError)) { + exception = { + type: typeof linkedError.name === 'string' ? linkedError.name : undefined, + value: typeof linkedError.message === 'string' ? linkedError.message : undefined, + }; + } else { + return { + exceptions, + debugImages, + }; + } + + return this._walkErrorTree( + parser, + limit, + linkedError, + key, + [...exceptions, exception], + [...debugImages, ...(exceptionDebugImages || [])], + ); + } + + /** + * Converts a Java Throwable to an SentryException + */ + private _exceptionFromJavaStackElements(javaThrowable: { + name: string; + message: string; + stackElements: { + className: string; + fileName: string; + methodName: string; + lineNumber: number; + }[]; + }): Exception { + return { + type: javaThrowable.name, + value: javaThrowable.message, + stacktrace: { + frames: javaThrowable.stackElements + .map( + stackElement => + { + platform: 'java', + module: stackElement.className, + filename: stackElement.fileName, + lineno: stackElement.lineNumber >= 0 ? stackElement.lineNumber : undefined, + function: stackElement.methodName, + in_app: + this._nativePackage !== null && stackElement.className.startsWith(this._nativePackage) + ? true + : undefined, + }, + ) + .reverse(), + }, + }; + } + + /** + * Converts StackAddresses to a SentryException with DebugMetaImages + */ + private async _exceptionFromAppleStackReturnAddresses(objCException: { + name: string; + message: string; + stackReturnAddresses: number[]; + }): Promise<{ + appleException: Exception; + appleDebugImages: DebugImage[]; + }> { + const nativeStackFrames = await this._fetchNativeStackFrames(objCException.stackReturnAddresses); + + return { + appleException: { + type: objCException.name, + value: objCException.message, + stacktrace: { + frames: (nativeStackFrames && nativeStackFrames.frames.reverse()) || [], + }, + }, + appleDebugImages: (nativeStackFrames && (nativeStackFrames.debugMetaImages as DebugImage[])) || [], + }; + } + + /** + * Fetches the native package/image name from the native layer + */ + private _fetchNativePackage(): Promise { + return NATIVE.fetchNativePackageName(); + } + + /** + * Fetches native debug image information on iOS + */ + private _fetchNativeStackFrames(instructionsAddr: number[]): Promise { + return NATIVE.fetchNativeStackFramesBy(instructionsAddr); + } +} diff --git a/src/js/integrations/rewriteframes.ts b/src/js/integrations/rewriteframes.ts index 2c9ad2a9d..38773400f 100644 --- a/src/js/integrations/rewriteframes.ts +++ b/src/js/integrations/rewriteframes.ts @@ -16,6 +16,12 @@ export const IOS_DEFAULT_BUNDLE_NAME = 'app:///main.jsbundle'; export function createReactNativeRewriteFrames(): RewriteFrames { return new RewriteFrames({ iteratee: (frame: StackFrame) => { + if (frame.platform === 'java' || frame.platform === 'cocoa') { + // Because platform is not required in StackFrame type + // we assume that if not set it's javascript + return frame; + } + if (!frame.filename) { return frame; } diff --git a/src/js/sdk.tsx b/src/js/sdk.tsx index 07db97e9a..7893f0dff 100644 --- a/src/js/sdk.tsx +++ b/src/js/sdk.tsx @@ -23,6 +23,7 @@ import { Release, SdkInfo, } from './integrations'; +import { NativeLinkedErrors } from './integrations/nativelinkederrors'; import { createReactNativeRewriteFrames } from './integrations/rewriteframes'; import { Screenshot } from './integrations/screenshot'; import { ViewHierarchy } from './integrations/viewhierarchy'; @@ -38,6 +39,7 @@ import { NATIVE } from './wrapper'; const IGNORED_DEFAULT_INTEGRATIONS = [ 'GlobalHandlers', // We will use the react-native internal handlers 'TryCatch', // We don't need this + 'LinkedErrors', // We replace this with `NativeLinkedError` ]; const DEFAULT_OPTIONS: ReactNativeOptions = { enableNativeCrashHandling: true, @@ -106,6 +108,7 @@ export function init(passedOptions: ReactNativeOptions): void { ), ]); + defaultIntegrations.push(new NativeLinkedErrors()); defaultIntegrations.push(new EventOrigin()); defaultIntegrations.push(new SdkInfo()); defaultIntegrations.push(new ReactNativeInfo()); diff --git a/src/js/wrapper.ts b/src/js/wrapper.ts index 0d37406d6..44b350762 100644 --- a/src/js/wrapper.ts +++ b/src/js/wrapper.ts @@ -19,6 +19,7 @@ import type { NativeFramesResponse, NativeReleaseResponse, NativeScreenshot, + NativeStackFrames, Spec, } from './NativeRNSentry'; import type { ReactNativeClientOptions } from './options'; @@ -82,6 +83,13 @@ interface SentryNativeWrapper { startProfiling(): boolean; stopProfiling(): Hermes.Profile | null; + + fetchNativePackageName(): Promise; + + /** + * Fetches native stack frames and debug images for the instructions addresses. + */ + fetchNativeStackFramesBy(instructionsAddr: number[]): Promise; } /** @@ -531,6 +539,28 @@ export const NATIVE: SentryNativeWrapper = { } }, + async fetchNativePackageName(): Promise { + if (!this.enableNative) { + return null; + } + if (!this._isModuleLoaded(RNSentry)) { + return null; + } + + return (await RNSentry.fetchNativePackageName()) || null; + }, + + async fetchNativeStackFramesBy(instructionsAddr: number[]): Promise { + if (!this.enableNative) { + return null; + } + if (!this._isModuleLoaded(RNSentry)) { + return null; + } + + return (await RNSentry.fetchNativeStackFramesBy(instructionsAddr)) || null; + }, + /** * Gets the event from envelopeItem and applies the level filter to the selected event. * @param data An envelope item containing the event. diff --git a/test/integrations/nativelinkederrors.test.ts b/test/integrations/nativelinkederrors.test.ts new file mode 100644 index 000000000..b27258fe3 --- /dev/null +++ b/test/integrations/nativelinkederrors.test.ts @@ -0,0 +1,374 @@ +import { defaultStackParser } from '@sentry/browser'; +import type { DebugImage, Event, EventHint, ExtendedError, Hub } from '@sentry/types'; + +import { NativeLinkedErrors } from '../../src/js/integrations/nativelinkederrors'; +import type { NativeStackFrames } from '../../src/js/NativeRNSentry'; +import { NATIVE } from '../../src/js/wrapper'; + +jest.mock('../../src/js/wrapper'); + +(NATIVE.fetchNativePackageName as jest.Mock).mockImplementation(() => Promise.resolve('mock.native.bundle.id')); + +(NATIVE.fetchNativeStackFramesBy as jest.Mock).mockImplementation(() => Promise.resolve(null)); + +describe('NativeLinkedErrors', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('keeps event without cause as is', async () => { + const actualEvent = await executeIntegrationFor( + { + exception: { + values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + in_app: false, + platform: 'node', + }, + ], + }, + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + }, + {}, + ); + + expect(actualEvent).toEqual({ + exception: { + values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + in_app: false, + platform: 'node', + }, + ], + }, + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + }); + }); + + it('adds android java cause from the original error to the event', async () => { + const actualEvent = await executeIntegrationFor( + { + exception: { + values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + }, + ], + }, + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + }, + { + originalException: createNewError({ + message: 'JavaScript error message', + name: 'JavaScriptError', + stack: + 'JavaScriptError: JavaScript error message\n' + + 'at onPress (index.bundle:75:33)\n' + + 'at _performTransitionSideEffects (index.bundle:65919:22)', + cause: { + name: 'java.lang.RuntimeException', + message: 'Java error message.', + stackElements: [ + { + className: 'mock.native.bundle.id.Crash', + fileName: 'Crash.kt', + lineNumber: 10, + methodName: 'getDataCrash', + }, + { + className: 'com.facebook.jni.NativeRunnable', + fileName: 'NativeRunnable.java', + lineNumber: 2, + methodName: 'run', + }, + ], + }, + }), + }, + ); + + expect(NATIVE.fetchNativePackageName).toBeCalledTimes(1); + expect(NATIVE.fetchNativeStackFramesBy).not.toBeCalled(); + expect(actualEvent).toEqual( + expect.objectContaining(>{ + exception: { + values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + }, + ], + }, + mechanism: { + type: 'generic', + handled: true, + }, + }, + { + type: 'java.lang.RuntimeException', + value: 'Java error message.', + stacktrace: { + frames: [ + expect.objectContaining({ + platform: 'java', + module: 'com.facebook.jni.NativeRunnable', + filename: 'NativeRunnable.java', + lineno: 2, + function: 'run', + }), + expect.objectContaining({ + platform: 'java', + module: 'mock.native.bundle.id.Crash', + filename: 'Crash.kt', + lineno: 10, + function: 'getDataCrash', + in_app: true, + }), + ], + }, + }, + ], + }, + }), + ); + }); + + it('adds ios objective-c cause from the original error to the event', async () => { + (NATIVE.fetchNativeStackFramesBy as jest.Mock).mockImplementation(() => + Promise.resolve({ + frames: [ + // Locally symbolicated frame + { + platform: 'cocoa', + package: 'CoreFoundation', + function: '__exceptionPreprocess', + symbol_addr: '0x0000000180437330', + instruction_addr: '0x0000000180437330', + image_addr: '0x7fffe668e000', + }, + { + platform: 'cocoa', + function: 'objc_exception_throw', + instruction_addr: '0x0000000180051274', + image_addr: '0x7fffe668e000', + }, + { + platform: 'cocoa', + package: 'mock.native.bundle.id', + instruction_addr: '0x0000000103535900', + image_addr: '0x7fffe668e000', + in_app: true, + }, + ], + debugMetaImages: [ + { + type: 'macho', + debug_id: '84a04d24-0e60-3810-a8c0-90a65e2df61a', + code_file: '/usr/lib/libDiagnosticMessagesClient.dylib', + image_addr: '0x7fffe668e000', + image_size: 8192, + image_vmaddr: '0x40000', + }, + ], + }), + ); + + const actualEvent = await executeIntegrationFor( + { + exception: { + values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + }, + ], + }, + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + }, + { + originalException: createNewError({ + message: 'JavaScript error message', + name: 'JavaScriptError', + stack: + 'JavaScriptError: JavaScript error message\n' + + 'at onPress (index.bundle:75:33)\n' + + 'at _performTransitionSideEffects (index.bundle:65919:22)', + cause: { + name: 'Error', + message: 'Objective-c error message.', + stackSymbols: [ + '0 CoreFoundation 0x0000000180437330 __exceptionPreprocess + 172', + '1 libobjc.A.dylib 0x0000000180051274 objc_exception_throw + 56', + '2 mock.native.bundle.id 0x0000000103535900 -[RCTSampleTurboModule getObjectThrows:] + 120', + ], + stackReturnAddresses: [6446871344, 6442783348, 4350761216], + }, + }), + }, + ); + + expect(NATIVE.fetchNativePackageName).toBeCalledTimes(1); + expect(NATIVE.fetchNativeStackFramesBy).toBeCalledTimes(1); + expect(NATIVE.fetchNativeStackFramesBy).toBeCalledWith([6446871344, 6442783348, 4350761216]); + expect(actualEvent).toEqual( + expect.objectContaining(>{ + debug_meta: { + images: [ + { + type: 'macho', + debug_id: '84a04d24-0e60-3810-a8c0-90a65e2df61a', + code_file: '/usr/lib/libDiagnosticMessagesClient.dylib', + image_addr: '0x7fffe668e000', + image_size: 8192, + image_vmaddr: '0x40000', + } as unknown as DebugImage, + ], + }, + exception: { + values: [ + { + type: 'Error', + value: 'Captured exception', + stacktrace: { + frames: [ + { + colno: 17, + filename: 'app:///Pressability.js', + function: '_performTransitionSideEffects', + }, + ], + }, + mechanism: { + type: 'generic', + handled: true, + }, + }, + { + type: 'Error', + value: 'Objective-c error message.', + stacktrace: { + frames: [ + expect.objectContaining({ + platform: 'cocoa', + package: 'mock.native.bundle.id', + instruction_addr: '0x0000000103535900', + image_addr: '0x7fffe668e000', + in_app: true, + }), + expect.objectContaining({ + platform: 'cocoa', + function: 'objc_exception_throw', + instruction_addr: '0x0000000180051274', + image_addr: '0x7fffe668e000', + }), + expect.objectContaining({ + platform: 'cocoa', + package: 'CoreFoundation', + function: '__exceptionPreprocess', + symbol_addr: '0x0000000180437330', + instruction_addr: '0x0000000180437330', + image_addr: '0x7fffe668e000', + }), + ], + }, + }, + ], + }, + }), + ); + }); +}); + +function executeIntegrationFor(mockedEvent: Event, mockedHint: EventHint): Promise { + const integration = new NativeLinkedErrors(); + return new Promise((resolve, reject) => { + integration.setupOnce( + async eventProcessor => { + try { + const processedEvent = await eventProcessor(mockedEvent, mockedHint); + resolve(processedEvent); + } catch (e) { + reject(e); + } + }, + () => + ({ + getClient: () => ({ + getOptions: () => ({ + stackParser: defaultStackParser, + }), + }), + getIntegration: () => integration, + } as unknown as Hub), + ); + }); +} + +function createNewError(from: { message: string; name?: string; stack?: string; cause?: unknown }): ExtendedError { + const error: ExtendedError = new Error(from.message); + if (from.name) { + error.name = from.name; + } + error.stack = from.stack; + error.cause = from.cause; + return error; +} diff --git a/test/integrations/rewriteframes.test.ts b/test/integrations/rewriteframes.test.ts index 45f7ddace..58867ef34 100644 --- a/test/integrations/rewriteframes.test.ts +++ b/test/integrations/rewriteframes.test.ts @@ -1,5 +1,6 @@ import type { Exception } from '@sentry/browser'; import { defaultStackParser, eventFromException } from '@sentry/browser'; +import type { Event } from '@sentry/types'; import { Platform } from 'react-native'; import { createReactNativeRewriteFrames } from '../../src/js/integrations/rewriteframes'; @@ -31,6 +32,78 @@ describe('RewriteFrames', () => { jest.resetAllMocks(); }); + it('should not change cocoa frames', async () => { + const EXPECTED_SENTRY_COCOA_EXCEPTION = { + type: 'Error', + value: 'Objective-c error message.', + stacktrace: { + frames: [ + { + platform: 'cocoa', + package: 'CoreFoundation', + function: '__exceptionPreprocess', + instruction_addr: '0000000180437330', + }, + { + platform: 'cocoa', + package: 'libobjc.A.dylib', + function: 'objc_exception_throw', + instruction_addr: '0000000180051274', + }, + { + platform: 'cocoa', + package: 'RNTester', + function: '-[RCTSampleTurboModule getObjectThrows:]', + instruction_addr: '0000000103535900', + }, + ], + }, + }; + + const SENTRY_COCOA_EXCEPTION_EVENT: Event = { + exception: { + values: [JSON.parse(JSON.stringify(EXPECTED_SENTRY_COCOA_EXCEPTION))], + }, + }; + + const event = createReactNativeRewriteFrames().process(SENTRY_COCOA_EXCEPTION_EVENT); + expect(event.exception?.values?.[0]).toEqual(EXPECTED_SENTRY_COCOA_EXCEPTION); + }); + + it('should not change jvm frames', async () => { + const EXPECTED_SENTRY_JVM_EXCEPTION = { + type: 'java.lang.RuntimeException', + value: 'Java error message.', + stacktrace: { + frames: [ + { + platform: 'java', + module: 'com.example.modules.Crash', + filename: 'Crash.kt', + lineno: 10, + function: 'getDataCrash', + }, + { + platform: 'java', + module: 'com.facebook.jni.NativeRunnable', + filename: 'NativeRunnable.java', + lineno: 2, + function: 'run', + }, + ], + }, + }; + + const SENTRY_JVM_EXCEPTION_EVENT: Event = { + exception: { + values: [JSON.parse(JSON.stringify(EXPECTED_SENTRY_JVM_EXCEPTION))], + }, + }; + + const event = createReactNativeRewriteFrames().process(SENTRY_JVM_EXCEPTION_EVENT); + expect(event.exception?.values?.[0]).toEqual(EXPECTED_SENTRY_JVM_EXCEPTION); + }); + it('should parse exceptions for react-native-v8', async () => { const REACT_NATIVE_V8_EXCEPTION = { message: 'Manually triggered crash to test Sentry reporting', diff --git a/test/mockWrapper.ts b/test/mockWrapper.ts index 0d2c6da08..d904b0c87 100644 --- a/test/mockWrapper.ts +++ b/test/mockWrapper.ts @@ -16,7 +16,6 @@ const NATIVE: MockInterface = { _processLevel: jest.fn(), _serializeObject: jest.fn(), _isModuleLoaded: jest.fn(), - _getBreadcrumbs: jest.fn(), isNativeAvailable: jest.fn(), @@ -49,6 +48,9 @@ const NATIVE: MockInterface = { startProfiling: jest.fn(), stopProfiling: jest.fn(), + + fetchNativePackageName: jest.fn(), + fetchNativeStackFramesBy: jest.fn(), }; NATIVE.isNativeAvailable.mockReturnValue(true); @@ -67,5 +69,7 @@ NATIVE.fetchModules.mockResolvedValue(null); NATIVE.fetchViewHierarchy.mockResolvedValue(null); NATIVE.startProfiling.mockReturnValue(false); NATIVE.stopProfiling.mockReturnValue(null); +NATIVE.fetchNativePackageName.mockResolvedValue('mock-native-package-name'); +NATIVE.fetchNativeStackFramesBy.mockResolvedValue(null); export { NATIVE };