From 432829833125e783e058648c8bd5c22178e5a740 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 14 Jan 2025 13:47:44 +0100 Subject: [PATCH 01/36] chore: Initialize `RNSentryTimeToDisplay` during native module `init` on iOS --- CHANGELOG.md | 4 ++++ packages/core/ios/RNSentry.mm | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dd716d64a..f46face18c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ - Rename `navigation.processing` span to more expressive `Navigation dispatch to screen A mounted/navigation cancelled` ([#4423](https://github.com/getsentry/sentry-react-native/pull/4423)) - Add RN SDK package to `sdk.packages` for Cocoa ([#4381](https://github.com/getsentry/sentry-react-native/pull/4381)) +### Internal + +- Initialize `RNSentryTimeToDisplay` during native module `init` on iOS ([#4443](https://github.com/getsentry/sentry-react-native/pull/4443)) + ### Dependencies - Bump CLI from v2.39.1 to v2.40.0 ([#4412](https://github.com/getsentry/sentry-react-native/pull/4412)) diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index d22852df6a..79ff76d0ae 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -78,6 +78,14 @@ + (BOOL)requiresMainQueueSetup return YES; } +- (instancetype)init +{ + if (self = [super init]) { + _timeToDisplay = [[RNSentryTimeToDisplay alloc] init]; + } + return self; +} + RCT_EXPORT_MODULE() RCT_EXPORT_METHOD(initNativeSdk @@ -152,8 +160,6 @@ - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) [mutableOptions removeObjectForKey:@"tracesSampler"]; [mutableOptions removeObjectForKey:@"enableTracing"]; - _timeToDisplay = [[RNSentryTimeToDisplay alloc] init]; - #if SENTRY_TARGET_REPLAY_SUPPORTED [RNSentryReplay updateOptions:mutableOptions]; #endif From 27d76dec32ef85ebde6e8724de7e5c23c6db53e5 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 14 Jan 2025 13:48:59 +0100 Subject: [PATCH 02/36] ref(ios): Extract Cocoa SDK init into standalone file --- packages/core/ios/RNSentry.h | 5 - packages/core/ios/RNSentry.mm | 152 +----------------------------- packages/core/ios/RNSentrySDK.h | 20 ++++ packages/core/ios/RNSentrySDK.m | 159 ++++++++++++++++++++++++++++++++ 4 files changed, 182 insertions(+), 154 deletions(-) create mode 100644 packages/core/ios/RNSentrySDK.h create mode 100644 packages/core/ios/RNSentrySDK.m diff --git a/packages/core/ios/RNSentry.h b/packages/core/ios/RNSentry.h index cfd0b74b28..66dc7219ac 100644 --- a/packages/core/ios/RNSentry.h +++ b/packages/core/ios/RNSentry.h @@ -20,11 +20,6 @@ SentrySDK (Private) @interface RNSentry : RCTEventEmitter -- (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options - error:(NSError *_Nullable *_Nonnull)errorPointer; - -- (void)setEventOriginTag:(SentryEvent *)event; - - (NSDictionary *_Nonnull)fetchNativeStackFramesBy:(NSArray *)instructionsAddr symbolicate:(SymbolicateCallbackType)symbolicate; diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index 79ff76d0ae..4fa8c4dde8 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -49,7 +49,7 @@ # import "RNSentryRNSScreen.h" #endif -#import "RNSentryVersion.h" +#import "RNSentrySDK.h" @interface SentrySDK (RNSentry) @@ -94,18 +94,13 @@ - (instancetype)init : (RCTPromiseRejectBlock)reject) { NSError *error = nil; - SentryOptions *sentryOptions = [self createOptionsWithDictionary:options error:&error]; + SentryOptions *sentryOptions = [RNSentrySDK createOptionsWithDictionary:options error:&error]; if (error != nil) { reject(@"SentryReactNative", error.localizedDescription, error); return; } - NSString *sdkVersion = [PrivateSentrySDKOnly getSdkVersionString]; - [PrivateSentrySDKOnly setSdkName:NATIVE_SDK_NAME andVersionString:sdkVersion]; - [PrivateSentrySDKOnly addSdkPackage:REACT_NATIVE_SDK_PACKAGE_NAME - version:REACT_NATIVE_SDK_PACKAGE_VERSION]; - - [SentrySDK startWithOptions:sentryOptions]; + [RNSentrySDK startWithOptions:sentryOptions]; #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST BOOL appIsActive = @@ -125,150 +120,9 @@ - (instancetype)init sentHybridSdkDidBecomeActive = true; } -#if SENTRY_TARGET_REPLAY_SUPPORTED - [RNSentryReplay postInit]; -#endif - resolve(@YES); } -- (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options - error:(NSError *_Nonnull *_Nonnull)errorPointer -{ - SentryBeforeSendEventCallback beforeSend = ^SentryEvent *(SentryEvent *event) - { - // We don't want to send an event after startup that came from a Unhandled JS Exception of - // react native Because we sent it already before the app crashed. - if (nil != event.exceptions.firstObject.type && - [event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location - != NSNotFound) { - return nil; - } - - [self setEventOriginTag:event]; - - return event; - }; - - NSMutableDictionary *mutableOptions = [options mutableCopy]; - [mutableOptions setValue:beforeSend forKey:@"beforeSend"]; - - // remove performance traces sample rate and traces sampler since we don't want to synchronize - // these configurations to the Native SDKs. The user could tho initialize the SDK manually and - // set themselves. - [mutableOptions removeObjectForKey:@"tracesSampleRate"]; - [mutableOptions removeObjectForKey:@"tracesSampler"]; - [mutableOptions removeObjectForKey:@"enableTracing"]; - -#if SENTRY_TARGET_REPLAY_SUPPORTED - [RNSentryReplay updateOptions:mutableOptions]; -#endif - - SentryOptions *sentryOptions = [[SentryOptions alloc] initWithDict:mutableOptions - didFailWithError:errorPointer]; - if (*errorPointer != nil) { - return nil; - } - - // Exclude Dev Server and Sentry Dsn request from Breadcrumbs - NSString *dsn = [self getURLFromDSN:[mutableOptions valueForKey:@"dsn"]]; - NSString *devServerUrl = [mutableOptions valueForKey:@"devServerUrl"]; - sentryOptions.beforeBreadcrumb - = ^SentryBreadcrumb *_Nullable(SentryBreadcrumb *_Nonnull breadcrumb) - { - NSString *url = breadcrumb.data[@"url"] ?: @""; - - if ([@"http" isEqualToString:breadcrumb.type] - && ((dsn != nil && [url hasPrefix:dsn]) - || (devServerUrl != nil && [url hasPrefix:devServerUrl]))) { - return nil; - } - return breadcrumb; - }; - - if ([mutableOptions valueForKey:@"enableNativeCrashHandling"] != nil) { - BOOL enableNativeCrashHandling = [mutableOptions[@"enableNativeCrashHandling"] boolValue]; - - if (!enableNativeCrashHandling) { - NSMutableArray *integrations = sentryOptions.integrations.mutableCopy; - [integrations removeObject:@"SentryCrashIntegration"]; - sentryOptions.integrations = integrations; - } - } - - // Set spotlight option - if ([mutableOptions valueForKey:@"spotlight"] != nil) { - id spotlightValue = [mutableOptions valueForKey:@"spotlight"]; - if ([spotlightValue isKindOfClass:[NSString class]]) { - NSLog(@"Using Spotlight on address: %@", spotlightValue); - sentryOptions.enableSpotlight = true; - sentryOptions.spotlightUrl = spotlightValue; - } else if ([spotlightValue isKindOfClass:[NSNumber class]]) { - sentryOptions.enableSpotlight = [spotlightValue boolValue]; - id defaultSpotlightUrl = [mutableOptions valueForKey:@"defaultSidecarUrl"]; - if (defaultSpotlightUrl != nil) { - sentryOptions.spotlightUrl = defaultSpotlightUrl; - } - } - } - - // Enable the App start and Frames tracking measurements - if ([mutableOptions valueForKey:@"enableAutoPerformanceTracing"] != nil) { - BOOL enableAutoPerformanceTracing = - [mutableOptions[@"enableAutoPerformanceTracing"] boolValue]; - PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode = enableAutoPerformanceTracing; -#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST - PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode = enableAutoPerformanceTracing; -#endif - } - - // Failed requests can only be enabled in one SDK to avoid duplicates - sentryOptions.enableCaptureFailedRequests = NO; - - return sentryOptions; -} - -- (NSString *_Nullable)getURLFromDSN:(NSString *)dsn -{ - NSURL *url = [NSURL URLWithString:dsn]; - if (!url) { - return nil; - } - return [NSString stringWithFormat:@"%@://%@", url.scheme, url.host]; -} - -- (void)setEventOriginTag:(SentryEvent *)event -{ - if (event.sdk != nil) { - NSString *sdkName = event.sdk[@"name"]; - - // If the event is from react native, it gets set - // there and we do not handle it here. - if ([sdkName isEqual:NATIVE_SDK_NAME]) { - [self setEventEnvironmentTag:event origin:@"ios" environment:@"native"]; - } - } -} - -- (void)setEventEnvironmentTag:(SentryEvent *)event - origin:(NSString *)origin - environment:(NSString *)environment -{ - NSMutableDictionary *newTags = [NSMutableDictionary new]; - - if (nil != event.tags && [event.tags count] > 0) { - [newTags addEntriesFromDictionary:event.tags]; - } - if (nil != origin) { - [newTags setValue:origin forKey:@"event.origin"]; - } - if (nil != environment) { - [newTags setValue:environment forKey:@"event.environment"]; - } - - event.tags = newTags; -} - RCT_EXPORT_METHOD(initNativeReactNavigationNewFrameTracking : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) diff --git a/packages/core/ios/RNSentrySDK.h b/packages/core/ios/RNSentrySDK.h new file mode 100644 index 0000000000..af6444b34d --- /dev/null +++ b/packages/core/ios/RNSentrySDK.h @@ -0,0 +1,20 @@ +#import + +@interface RNSentrySDK : NSObject +SENTRY_NO_INIT + ++ (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options +error:(NSError *_Nonnull *_Nonnull)errorPointer; + +/** + * @experimental + * Inits and configures Sentry for React Native applications. Make sure to + * set a valid DSN. + * + * @discussion Call this method on the main thread. When calling it from a background thread, the + * SDK starts on the main thread async. + */ ++ (void)startWithOptions:(SentryOptions *)options NS_SWIFT_NAME(start(options:)); + +@end + diff --git a/packages/core/ios/RNSentrySDK.m b/packages/core/ios/RNSentrySDK.m new file mode 100644 index 0000000000..cd012fc607 --- /dev/null +++ b/packages/core/ios/RNSentrySDK.m @@ -0,0 +1,159 @@ +#import "RNSentrySDK.h" +#import "RNSentryReplay.h" +#import "RNSentryVersion.h" + +#import +#import + +@implementation RNSentrySDK + ++ (void)startWithOptions:(SentryOptions *)options NS_SWIFT_NAME(start(options:)) +{ + NSString *sdkVersion = [PrivateSentrySDKOnly getSdkVersionString]; + [PrivateSentrySDKOnly setSdkName:nativeSdkName andVersionString:sdkVersion]; + + [SentrySDK startWithOptions:options]; + +#if SENTRY_TARGET_REPLAY_SUPPORTED + [RNSentryReplay postInit]; +#endif +} + ++ (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options + error:(NSError *_Nonnull *_Nonnull)errorPointer +{ + SentryBeforeSendEventCallback beforeSend = ^SentryEvent *(SentryEvent *event) + { + // We don't want to send an event after startup that came from a Unhandled JS Exception of + // react native Because we sent it already before the app crashed. + if (nil != event.exceptions.firstObject.type && + [event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location + != NSNotFound) { + return nil; + } + + [self setEventOriginTag:event]; + + return event; + }; + + NSMutableDictionary *mutableOptions = [options mutableCopy]; + [mutableOptions setValue:beforeSend forKey:@"beforeSend"]; + + // remove performance traces sample rate and traces sampler since we don't want to synchronize + // these configurations to the Native SDKs. The user could tho initialize the SDK manually and + // set themselves. + [mutableOptions removeObjectForKey:@"tracesSampleRate"]; + [mutableOptions removeObjectForKey:@"tracesSampler"]; + [mutableOptions removeObjectForKey:@"enableTracing"]; + +#if SENTRY_TARGET_REPLAY_SUPPORTED + [RNSentryReplay updateOptions:mutableOptions]; +#endif + + SentryOptions *sentryOptions = [[SentryOptions alloc] initWithDict:mutableOptions + didFailWithError:errorPointer]; + if (*errorPointer != nil) { + return nil; + } + + // Exclude Dev Server and Sentry Dsn request from Breadcrumbs + NSString *dsn = [self getURLFromDSN:[mutableOptions valueForKey:@"dsn"]]; + NSString *devServerUrl = [mutableOptions valueForKey:@"devServerUrl"]; + sentryOptions.beforeBreadcrumb + = ^SentryBreadcrumb *_Nullable(SentryBreadcrumb *_Nonnull breadcrumb) + { + NSString *url = breadcrumb.data[@"url"] ?: @""; + + if ([@"http" isEqualToString:breadcrumb.type] + && ((dsn != nil && [url hasPrefix:dsn]) + || (devServerUrl != nil && [url hasPrefix:devServerUrl]))) { + return nil; + } + return breadcrumb; + }; + + if ([mutableOptions valueForKey:@"enableNativeCrashHandling"] != nil) { + BOOL enableNativeCrashHandling = [mutableOptions[@"enableNativeCrashHandling"] boolValue]; + + if (!enableNativeCrashHandling) { + NSMutableArray *integrations = sentryOptions.integrations.mutableCopy; + [integrations removeObject:@"SentryCrashIntegration"]; + sentryOptions.integrations = integrations; + } + } + + // Set spotlight option + if ([mutableOptions valueForKey:@"spotlight"] != nil) { + id spotlightValue = [mutableOptions valueForKey:@"spotlight"]; + if ([spotlightValue isKindOfClass:[NSString class]]) { + NSLog(@"Using Spotlight on address: %@", spotlightValue); + sentryOptions.enableSpotlight = true; + sentryOptions.spotlightUrl = spotlightValue; + } else if ([spotlightValue isKindOfClass:[NSNumber class]]) { + sentryOptions.enableSpotlight = [spotlightValue boolValue]; + id defaultSpotlightUrl = [mutableOptions valueForKey:@"defaultSidecarUrl"]; + if (defaultSpotlightUrl != nil) { + sentryOptions.spotlightUrl = defaultSpotlightUrl; + } + } + } + + // Enable the App start and Frames tracking measurements + if ([mutableOptions valueForKey:@"enableAutoPerformanceTracing"] != nil) { + BOOL enableAutoPerformanceTracing = + [mutableOptions[@"enableAutoPerformanceTracing"] boolValue]; + PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode = enableAutoPerformanceTracing; +#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST + PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode = enableAutoPerformanceTracing; +#endif + } + + // Failed requests can only be enabled in one SDK to avoid duplicates + sentryOptions.enableCaptureFailedRequests = NO; + + return sentryOptions; +} + ++ (void)setEventOriginTag:(SentryEvent *)event +{ + if (event.sdk != nil) { + NSString *sdkName = event.sdk[@"name"]; + + // If the event is from react native, it gets set + // there and we do not handle it here. + if ([sdkName isEqual:nativeSdkName]) { + [self setEventEnvironmentTag:event origin:@"ios" environment:@"native"]; + } + } +} + ++ (void)setEventEnvironmentTag:(SentryEvent *)event + origin:(NSString *)origin + environment:(NSString *)environment +{ + NSMutableDictionary *newTags = [NSMutableDictionary new]; + + if (nil != event.tags && [event.tags count] > 0) { + [newTags addEntriesFromDictionary:event.tags]; + } + if (nil != origin) { + [newTags setValue:origin forKey:@"event.origin"]; + } + if (nil != environment) { + [newTags setValue:environment forKey:@"event.environment"]; + } + + event.tags = newTags; +} + ++ (NSString *_Nullable)getURLFromDSN:(NSString *)dsn +{ + NSURL *url = [NSURL URLWithString:dsn]; + if (!url) { + return nil; + } + return [NSString stringWithFormat:@"%@://%@", url.scheme, url.host]; +} + +@end From 5a28c0308fe815ea5c6984e8252287df9f1b967d Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 14 Jan 2025 13:49:00 +0100 Subject: [PATCH 03/36] fix lint --- packages/core/ios/RNSentry.mm | 1 + packages/core/ios/RNSentrySDK.h | 3 +-- packages/core/ios/RNSentrySDK.m | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index 4fa8c4dde8..88efb2d42f 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -50,6 +50,7 @@ #endif #import "RNSentrySDK.h" +#import "RNSentryVersion.h" @interface SentrySDK (RNSentry) diff --git a/packages/core/ios/RNSentrySDK.h b/packages/core/ios/RNSentrySDK.h index af6444b34d..c9b4d1c53c 100644 --- a/packages/core/ios/RNSentrySDK.h +++ b/packages/core/ios/RNSentrySDK.h @@ -4,7 +4,7 @@ SENTRY_NO_INIT + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options -error:(NSError *_Nonnull *_Nonnull)errorPointer; + error:(NSError *_Nonnull *_Nonnull)errorPointer; /** * @experimental @@ -17,4 +17,3 @@ error:(NSError *_Nonnull *_Nonnull)errorPointer; + (void)startWithOptions:(SentryOptions *)options NS_SWIFT_NAME(start(options:)); @end - diff --git a/packages/core/ios/RNSentrySDK.m b/packages/core/ios/RNSentrySDK.m index cd012fc607..10227f8194 100644 --- a/packages/core/ios/RNSentrySDK.m +++ b/packages/core/ios/RNSentrySDK.m @@ -28,7 +28,7 @@ + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) // react native Because we sent it already before the app crashed. if (nil != event.exceptions.firstObject.type && [event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location - != NSNotFound) { + != NSNotFound) { return nil; } @@ -61,7 +61,7 @@ + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) NSString *dsn = [self getURLFromDSN:[mutableOptions valueForKey:@"dsn"]]; NSString *devServerUrl = [mutableOptions valueForKey:@"devServerUrl"]; sentryOptions.beforeBreadcrumb - = ^SentryBreadcrumb *_Nullable(SentryBreadcrumb *_Nonnull breadcrumb) + = ^SentryBreadcrumb *_Nullable(SentryBreadcrumb *_Nonnull breadcrumb) { NSString *url = breadcrumb.data[@"url"] ?: @""; @@ -102,7 +102,7 @@ + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) // Enable the App start and Frames tracking measurements if ([mutableOptions valueForKey:@"enableAutoPerformanceTracing"] != nil) { BOOL enableAutoPerformanceTracing = - [mutableOptions[@"enableAutoPerformanceTracing"] boolValue]; + [mutableOptions[@"enableAutoPerformanceTracing"] boolValue]; PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode = enableAutoPerformanceTracing; #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode = enableAutoPerformanceTracing; From 8b67bf1d672857a5e0b04597bc8a760c69a07efb Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 14 Jan 2025 13:49:00 +0100 Subject: [PATCH 04/36] rename rnsentrysdk to rnsentrystart --- packages/core/ios/RNSentry.mm | 2 +- packages/core/ios/{RNSentrySDK.h => RNSentryStart.h} | 0 packages/core/ios/{RNSentrySDK.m => RNSentryStart.m} | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/core/ios/{RNSentrySDK.h => RNSentryStart.h} (100%) rename packages/core/ios/{RNSentrySDK.m => RNSentryStart.m} (99%) diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index 88efb2d42f..f85d5053d0 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -49,7 +49,7 @@ # import "RNSentryRNSScreen.h" #endif -#import "RNSentrySDK.h" +#import "RNSentryStart.h" #import "RNSentryVersion.h" @interface diff --git a/packages/core/ios/RNSentrySDK.h b/packages/core/ios/RNSentryStart.h similarity index 100% rename from packages/core/ios/RNSentrySDK.h rename to packages/core/ios/RNSentryStart.h diff --git a/packages/core/ios/RNSentrySDK.m b/packages/core/ios/RNSentryStart.m similarity index 99% rename from packages/core/ios/RNSentrySDK.m rename to packages/core/ios/RNSentryStart.m index 10227f8194..9d25ad6118 100644 --- a/packages/core/ios/RNSentrySDK.m +++ b/packages/core/ios/RNSentryStart.m @@ -1,4 +1,4 @@ -#import "RNSentrySDK.h" +#import "RNSentryStart.h" #import "RNSentryReplay.h" #import "RNSentryVersion.h" From 05cef298344f90150ff6b5d221e9e80fc62e7f95 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 14 Jan 2025 13:49:28 +0100 Subject: [PATCH 05/36] rename class name also to rnsentrystart, fix tests --- .../project.pbxproj | 4 + .../RNSentryStart+Test.h | 8 ++ .../RNSentryCocoaTesterTests/RNSentryTests.mm | 73 ++++++++++--------- packages/core/ios/RNSentry.mm | 4 +- packages/core/ios/RNSentryStart.h | 2 +- packages/core/ios/RNSentryStart.m | 4 +- 6 files changed, 56 insertions(+), 39 deletions(-) create mode 100644 packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStart+Test.h diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj index f78b1be0e0..112c485d6f 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ 332D33482CDBDC7300547D76 /* RNSentry.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentry.h; path = ../ios/RNSentry.h; sourceTree = SOURCE_ROOT; }; 332D33492CDCC8E100547D76 /* RNSentryTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSentryTests.h; sourceTree = ""; }; 332D334A2CDCC8EB00547D76 /* RNSentryCocoaTesterTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RNSentryCocoaTesterTests-Bridging-Header.h"; sourceTree = ""; }; + 333B58A82D35BA93000F8D04 /* RNSentryStart.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryStart.h; path = ../ios/RNSentryStart.h; sourceTree = SOURCE_ROOT; }; + 333B58A92D35BB2D000F8D04 /* RNSentryStart+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "RNSentryStart+Test.h"; path = "RNSentryCocoaTesterTests/RNSentryStart+Test.h"; sourceTree = SOURCE_ROOT; }; 336084382C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RNSentryReplayBreadcrumbConverterTests.swift; sourceTree = ""; }; 3360843A2C32E3A8008CC412 /* RNSentryReplayBreadcrumbConverter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryReplayBreadcrumbConverter.h; path = ../ios/RNSentryReplayBreadcrumbConverter.h; sourceTree = ""; }; 3360843C2C340C76008CC412 /* RNSentryBreadcrumbTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryBreadcrumbTests.swift; sourceTree = ""; }; @@ -116,6 +118,8 @@ 33AFE0122B8F319000AAB120 /* RNSentry */ = { isa = PBXGroup; children = ( + 333B58A92D35BB2D000F8D04 /* RNSentryStart+Test.h */, + 333B58A82D35BA93000F8D04 /* RNSentryStart.h */, 3380C6C02CDEC56B0018B9B6 /* Replay */, 332D33482CDBDC7300547D76 /* RNSentry.h */, 3360843A2C32E3A8008CC412 /* RNSentryReplayBreadcrumbConverter.h */, diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStart+Test.h b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStart+Test.h new file mode 100644 index 0000000000..fcdfe7872b --- /dev/null +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStart+Test.h @@ -0,0 +1,8 @@ +#import "RNSentryStart.h" + +@interface +RNSentryStart (Test) + ++ (void)setEventOriginTag:(SentryEvent *)event; + +@end diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm index 6e63793b85..1cbc57caf6 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm @@ -1,4 +1,5 @@ #import "RNSentryTests.h" +#import "RNSentryStart+Test.h" #import #import #import @@ -25,8 +26,8 @@ - (void)testCreateOptionsWithDictionaryRemovesPerformanceProperties , @"enableTracing" : @YES, } ; -SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; +SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); @@ -46,8 +47,8 @@ - (void)testCaptureFailedRequestsIsDisabled NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); @@ -62,8 +63,8 @@ - (void)testCreateOptionsWithDictionaryNativeCrashHandlingDefault NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertEqual([actualOptions.integrations containsObject:@"SentryCrashIntegration"], true, @@ -78,8 +79,8 @@ - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingDefault NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertEqual( @@ -95,8 +96,8 @@ - (void)testCreateOptionsWithDictionaryNativeCrashHandlingEnabled @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"enableNativeCrashHandling" : @YES, }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertEqual([actualOptions.integrations containsObject:@"SentryCrashIntegration"], true, @@ -112,8 +113,8 @@ - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingEnabled @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"enableAutoPerformanceTracing" : @YES, }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertEqual( @@ -129,8 +130,8 @@ - (void)testCreateOptionsWithDictionaryNativeCrashHandlingDisabled @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"enableNativeCrashHandling" : @NO, }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertEqual([actualOptions.integrations containsObject:@"SentryCrashIntegration"], false, @@ -146,8 +147,8 @@ - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingDisabled @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"enableAutoPerformanceTracing" : @NO, }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertEqual(actualOptions.enableAutoPerformanceTracing, false, @@ -164,8 +165,8 @@ - (void)testCreateOptionsWithDictionarySpotlightEnabled @"spotlight" : @YES, @"defaultSidecarUrl" : @"http://localhost:8969/teststream", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertTrue(actualOptions.enableSpotlight, @"Did not enable spotlight"); @@ -182,8 +183,8 @@ - (void)testCreateOptionsWithDictionarySpotlightOne @"spotlight" : @1, @"defaultSidecarUrl" : @"http://localhost:8969/teststream", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertTrue(actualOptions.enableSpotlight, @"Did not enable spotlight"); @@ -199,8 +200,8 @@ - (void)testCreateOptionsWithDictionarySpotlightUrl @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"spotlight" : @"http://localhost:8969/teststream", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertTrue(actualOptions.enableSpotlight, @"Did not enable spotlight"); @@ -216,8 +217,8 @@ - (void)testCreateOptionsWithDictionarySpotlightDisabled @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"spotlight" : @NO, }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertFalse(actualOptions.enableSpotlight, @"Did not disable spotlight"); @@ -232,8 +233,8 @@ - (void)testCreateOptionsWithDictionarySpotlightZero @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", @"spotlight" : @0, }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertFalse(actualOptions.enableSpotlight, @"Did not disable spotlight"); @@ -247,8 +248,8 @@ - (void)testPassesErrorOnWrongDsn NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"not_a_valid_dsn", }; - SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary - error:&error]; + SentryOptions *actualOptions = + [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; XCTAssertNil(actualOptions, @"Created invalid sentry options"); XCTAssertNotNil(error, @"Did not created error on invalid dsn"); @@ -263,7 +264,8 @@ - (void)testBeforeBreadcrumbsCallbackFiltersOutSentryDsnRequestBreadcrumbs @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", @"devServerUrl" : @"http://localhost:8081" }; - SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedDictionary error:&error]; + SentryOptions *options = [RNSentryStart createOptionsWithDictionary:mockedDictionary + error:&error]; SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init]; breadcrumb.type = @"http"; @@ -283,7 +285,8 @@ - (void)testBeforeBreadcrumbsCallbackFiltersOutDevServerRequestBreadcrumbs NSDictionary *_Nonnull mockedDictionary = @{ @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", @"devServerUrl" : mockDevServer }; - SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedDictionary error:&error]; + SentryOptions *options = [RNSentryStart createOptionsWithDictionary:mockedDictionary + error:&error]; SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init]; breadcrumb.type = @"http"; @@ -303,7 +306,8 @@ - (void)testBeforeBreadcrumbsCallbackDoesNotFiltersOutNonDevServerOrDsnRequestBr @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", @"devServerUrl" : @"http://localhost:8081" }; - SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedDictionary error:&error]; + SentryOptions *options = [RNSentryStart createOptionsWithDictionary:mockedDictionary + error:&error]; SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init]; breadcrumb.type = @"http"; @@ -322,7 +326,8 @@ - (void)testBeforeBreadcrumbsCallbackKeepsBreadcrumbWhenDevServerUrlIsNotPassedA NSDictionary *_Nonnull mockedDictionary = @{ // dsn is always validated in SentryOptions initialization @"dsn" : @"https://abc@def.ingest.sentry.io/1234567" }; - SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedDictionary error:&error]; + SentryOptions *options = [RNSentryStart createOptionsWithDictionary:mockedDictionary + error:&error]; SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] init]; breadcrumb.type = @"http"; @@ -341,7 +346,7 @@ - (void)testEventFromSentryCocoaReactNativeHasOriginAndEnvironmentTags @"name" : @"sentry.cocoa.react-native", }; - [rnSentry setEventOriginTag:testEvent]; + [RNSentryStart setEventOriginTag:testEvent]; XCTAssertEqual(testEvent.tags[@"event.origin"], @"ios"); XCTAssertEqual(testEvent.tags[@"event.environment"], @"native"); @@ -359,7 +364,7 @@ - (void)testEventFromSentryReactNativeOriginAndEnvironmentTagsAreOverwritten @"event.environment" : @"testEventEnvironmentTag", }; - [rnSentry setEventOriginTag:testEvent]; + [RNSentryStart setEventOriginTag:testEvent]; XCTAssertEqual(testEvent.tags[@"event.origin"], @"ios"); XCTAssertEqual(testEvent.tags[@"event.environment"], @"native"); diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index f85d5053d0..eb6588b361 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -95,13 +95,13 @@ - (instancetype)init : (RCTPromiseRejectBlock)reject) { NSError *error = nil; - SentryOptions *sentryOptions = [RNSentrySDK createOptionsWithDictionary:options error:&error]; + SentryOptions *sentryOptions = [RNSentryStart createOptionsWithDictionary:options error:&error]; if (error != nil) { reject(@"SentryReactNative", error.localizedDescription, error); return; } - [RNSentrySDK startWithOptions:sentryOptions]; + [RNSentryStart startWithOptions:sentryOptions]; #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST BOOL appIsActive = diff --git a/packages/core/ios/RNSentryStart.h b/packages/core/ios/RNSentryStart.h index c9b4d1c53c..5b52871251 100644 --- a/packages/core/ios/RNSentryStart.h +++ b/packages/core/ios/RNSentryStart.h @@ -1,6 +1,6 @@ #import -@interface RNSentrySDK : NSObject +@interface RNSentryStart : NSObject SENTRY_NO_INIT + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index 9d25ad6118..a5f6e8f1b3 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -5,14 +5,14 @@ #import #import -@implementation RNSentrySDK +@implementation RNSentryStart + (void)startWithOptions:(SentryOptions *)options NS_SWIFT_NAME(start(options:)) { NSString *sdkVersion = [PrivateSentrySDKOnly getSdkVersionString]; [PrivateSentrySDKOnly setSdkName:nativeSdkName andVersionString:sdkVersion]; - [SentrySDK startWithOptions:options]; + [self startWithOptions:options]; #if SENTRY_TARGET_REPLAY_SUPPORTED [RNSentryReplay postInit]; From 778478fe67425ff9c2a5f645ea2ea04e3d95005a Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 14 Jan 2025 13:49:28 +0100 Subject: [PATCH 06/36] explicitly import only used classes --- packages/core/ios/RNSentryStart.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/ios/RNSentryStart.h b/packages/core/ios/RNSentryStart.h index 5b52871251..bc5adf35af 100644 --- a/packages/core/ios/RNSentryStart.h +++ b/packages/core/ios/RNSentryStart.h @@ -1,4 +1,5 @@ -#import +#import +#import @interface RNSentryStart : NSObject SENTRY_NO_INIT From 3d3d0b4b7c7f3d02d56ccb0df10bb4ca4b5e1ebd Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 14 Jan 2025 13:49:28 +0100 Subject: [PATCH 07/36] fix call cocoa start not itself --- packages/core/ios/RNSentryStart.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index a5f6e8f1b3..11edc14f02 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -2,6 +2,7 @@ #import "RNSentryReplay.h" #import "RNSentryVersion.h" +#import #import #import @@ -12,7 +13,7 @@ + (void)startWithOptions:(SentryOptions *)options NS_SWIFT_NAME(start(options:)) NSString *sdkVersion = [PrivateSentrySDKOnly getSdkVersionString]; [PrivateSentrySDKOnly setSdkName:nativeSdkName andVersionString:sdkVersion]; - [self startWithOptions:options]; + [SentrySDK startWithOptions:options]; #if SENTRY_TARGET_REPLAY_SUPPORTED [RNSentryReplay postInit]; From e2e40fa0ef13ccc2404583d74d9db5c71c2371a8 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 14 Jan 2025 13:49:28 +0100 Subject: [PATCH 08/36] also extract didBecomeActive notification --- packages/core/ios/RNSentry.mm | 20 -------------------- packages/core/ios/RNSentryStart.m | 25 +++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index eb6588b361..6907513da5 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -64,7 +64,6 @@ + (void)storeEnvelope:(SentryEnvelope *)envelope; static bool hasFetchedAppStart; @implementation RNSentry { - bool sentHybridSdkDidBecomeActive; bool hasListeners; RNSentryTimeToDisplay *_timeToDisplay; } @@ -102,25 +101,6 @@ - (instancetype)init } [RNSentryStart startWithOptions:sentryOptions]; - -#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST - BOOL appIsActive = - [[UIApplication sharedApplication] applicationState] == UIApplicationStateActive; -#else - BOOL appIsActive = [[NSApplication sharedApplication] isActive]; -#endif - - // If the app is active/in foreground, and we have not sent the SentryHybridSdkDidBecomeActive - // notification, send it. - if (appIsActive && !sentHybridSdkDidBecomeActive - && (PrivateSentrySDKOnly.options.enableAutoSessionTracking - || PrivateSentrySDKOnly.options.enableWatchdogTerminationTracking)) { - [[NSNotificationCenter defaultCenter] postNotificationName:@"SentryHybridSdkDidBecomeActive" - object:nil]; - - sentHybridSdkDidBecomeActive = true; - } - resolve(@YES); } diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index 11edc14f02..708cdc3218 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -18,6 +18,8 @@ + (void)startWithOptions:(SentryOptions *)options NS_SWIFT_NAME(start(options:)) #if SENTRY_TARGET_REPLAY_SUPPORTED [RNSentryReplay postInit]; #endif + + [self postDidBecomeActiveNotification]; } + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options @@ -157,4 +159,27 @@ + (NSString *_Nullable)getURLFromDSN:(NSString *)dsn return [NSString stringWithFormat:@"%@://%@", url.scheme, url.host]; } +static bool sentHybridSdkDidBecomeActive = NO; + ++ (void)postDidBecomeActiveNotification +{ +#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST + BOOL appIsActive = + [[UIApplication sharedApplication] applicationState] == UIApplicationStateActive; +#else + BOOL appIsActive = [[NSApplication sharedApplication] isActive]; +#endif + + // If the app is active/in foreground, and we have not sent the SentryHybridSdkDidBecomeActive + // notification, send it. + if (appIsActive && !sentHybridSdkDidBecomeActive + && (PrivateSentrySDKOnly.options.enableAutoSessionTracking + || PrivateSentrySDKOnly.options.enableWatchdogTerminationTracking)) { + [[NSNotificationCenter defaultCenter] postNotificationName:@"SentryHybridSdkDidBecomeActive" + object:nil]; + + sentHybridSdkDidBecomeActive = true; + } +} + @end From 83106a72a33236930917f64dbdf9767a012624ee Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 14 Jan 2025 13:53:44 +0100 Subject: [PATCH 09/36] add explanation comment to native crash handling --- packages/core/ios/RNSentryStart.m | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index 708cdc3218..59382c0d3b 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -76,6 +76,7 @@ + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) return breadcrumb; }; + // JS options.enableNativeCrashHandling equals to native options.enableCrashHandler if ([mutableOptions valueForKey:@"enableNativeCrashHandling"] != nil) { BOOL enableNativeCrashHandling = [mutableOptions[@"enableNativeCrashHandling"] boolValue]; From 742f28cf58d6668d6c06dadc81ada9c50e3897b2 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 14 Jan 2025 14:03:56 +0100 Subject: [PATCH 10/36] explain notification purpose --- packages/core/ios/RNSentryStart.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index 59382c0d3b..c5f3b81a47 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -176,6 +176,8 @@ + (void)postDidBecomeActiveNotification if (appIsActive && !sentHybridSdkDidBecomeActive && (PrivateSentrySDKOnly.options.enableAutoSessionTracking || PrivateSentrySDKOnly.options.enableWatchdogTerminationTracking)) { + // Updates Native App State Manager https://github.com/getsentry/sentry-cocoa/blob/888a145b144b8077e03151a886520f332e47e297/Sources/Sentry/SentryAppStateManager.m#L136 + // Triggers Session Tracker https://github.com/getsentry/sentry-cocoa/blob/888a145b144b8077e03151a886520f332e47e297/Sources/Sentry/SentrySessionTracker.m#L144 [[NSNotificationCenter defaultCenter] postNotificationName:@"SentryHybridSdkDidBecomeActive" object:nil]; From 4897aa344e9466b36c44fa14bee464b1534e48d7 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 14 Jan 2025 14:04:38 +0100 Subject: [PATCH 11/36] fix sdk name --- packages/core/ios/RNSentryStart.m | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index c5f3b81a47..e2790bcc09 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -11,7 +11,9 @@ @implementation RNSentryStart + (void)startWithOptions:(SentryOptions *)options NS_SWIFT_NAME(start(options:)) { NSString *sdkVersion = [PrivateSentrySDKOnly getSdkVersionString]; - [PrivateSentrySDKOnly setSdkName:nativeSdkName andVersionString:sdkVersion]; + [PrivateSentrySDKOnly setSdkName:NATIVE_SDK_NAME andVersionString:sdkVersion]; + [PrivateSentrySDKOnly addSdkPackage:REACT_NATIVE_SDK_PACKAGE_NAME + version:REACT_NATIVE_SDK_PACKAGE_VERSION]; [SentrySDK startWithOptions:options]; @@ -126,7 +128,7 @@ + (void)setEventOriginTag:(SentryEvent *)event // If the event is from react native, it gets set // there and we do not handle it here. - if ([sdkName isEqual:nativeSdkName]) { + if ([sdkName isEqual:REACT_NATIVE_SDK_PACKAGE_NAME]) { [self setEventEnvironmentTag:event origin:@"ios" environment:@"native"]; } } From d37ebdd36bf6937ea601eeeb39760b7b1e9faca9 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 14 Jan 2025 15:23:56 +0100 Subject: [PATCH 12/36] fix native sdk not react native package --- packages/core/ios/RNSentryStart.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index e2790bcc09..0cca458672 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -128,7 +128,7 @@ + (void)setEventOriginTag:(SentryEvent *)event // If the event is from react native, it gets set // there and we do not handle it here. - if ([sdkName isEqual:REACT_NATIVE_SDK_PACKAGE_NAME]) { + if ([sdkName isEqual:NATIVE_SDK_NAME]) { [self setEventEnvironmentTag:event origin:@"ios" environment:@"native"]; } } From 13a0b89a14fec9e32ce8266a6153d9cac2a8bb40 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 14 Jan 2025 15:30:40 +0100 Subject: [PATCH 13/36] fix lint sentry start --- packages/core/ios/RNSentryStart.m | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index 0cca458672..e4ce65c314 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -168,7 +168,7 @@ + (void)postDidBecomeActiveNotification { #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST BOOL appIsActive = - [[UIApplication sharedApplication] applicationState] == UIApplicationStateActive; + [[UIApplication sharedApplication] applicationState] == UIApplicationStateActive; #else BOOL appIsActive = [[NSApplication sharedApplication] isActive]; #endif @@ -178,8 +178,10 @@ + (void)postDidBecomeActiveNotification if (appIsActive && !sentHybridSdkDidBecomeActive && (PrivateSentrySDKOnly.options.enableAutoSessionTracking || PrivateSentrySDKOnly.options.enableWatchdogTerminationTracking)) { - // Updates Native App State Manager https://github.com/getsentry/sentry-cocoa/blob/888a145b144b8077e03151a886520f332e47e297/Sources/Sentry/SentryAppStateManager.m#L136 - // Triggers Session Tracker https://github.com/getsentry/sentry-cocoa/blob/888a145b144b8077e03151a886520f332e47e297/Sources/Sentry/SentrySessionTracker.m#L144 + // Updates Native App State Manager + // https://github.com/getsentry/sentry-cocoa/blob/888a145b144b8077e03151a886520f332e47e297/Sources/Sentry/SentryAppStateManager.m#L136 + // Triggers Session Tracker + // https://github.com/getsentry/sentry-cocoa/blob/888a145b144b8077e03151a886520f332e47e297/Sources/Sentry/SentrySessionTracker.m#L144 [[NSNotificationCenter defaultCenter] postNotificationName:@"SentryHybridSdkDidBecomeActive" object:nil]; From d7270b1c656103c2d522a6af9360df1fe3ced74b Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 14 Jan 2025 15:33:24 +0100 Subject: [PATCH 14/36] add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f46face18c..f1f48bfc55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ ### Internal - Initialize `RNSentryTimeToDisplay` during native module `init` on iOS ([#4443](https://github.com/getsentry/sentry-react-native/pull/4443)) +- Extract iOS native initialization to standalone structures ([#4444](https://github.com/getsentry/sentry-react-native/pull/4444)) ### Dependencies From 580590ba8b7d9a91922a7e089b24237fd5254fcb Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 14 Jan 2025 16:25:22 +0100 Subject: [PATCH 15/36] feat(experimental): Add native `startWithConfigureOptions` for Apple platforms` --- CHANGELOG.md | 1 + packages/core/RNSentry.podspec | 2 +- .../project.pbxproj | 2 + .../RNSentryCocoaTesterTests/RNSentryTests.mm | 32 ++---- packages/core/ios/RNSentry.h | 3 + packages/core/ios/RNSentry.mm | 4 +- packages/core/ios/RNSentrySDK.h | 17 +++ packages/core/ios/RNSentrySDK.m | 15 +++ packages/core/ios/RNSentryStart.h | 6 ++ packages/core/ios/RNSentryStart.m | 101 ++++++++++++------ .../sentryreactnativesample/AppDelegate.mm | 6 ++ 11 files changed, 124 insertions(+), 65 deletions(-) create mode 100644 packages/core/ios/RNSentrySDK.h create mode 100644 packages/core/ios/RNSentrySDK.m diff --git a/CHANGELOG.md b/CHANGELOG.md index f1f48bfc55..512824779b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Rename `navigation.processing` span to more expressive `Navigation dispatch to screen A mounted/navigation cancelled` ([#4423](https://github.com/getsentry/sentry-react-native/pull/4423)) - Add RN SDK package to `sdk.packages` for Cocoa ([#4381](https://github.com/getsentry/sentry-react-native/pull/4381)) +- Add experimental version of `startWithConfigureOptions` for Apple platforms ([#4444](https://github.com/getsentry/sentry-react-native/pull/4444)) ### Internal diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 97c4fd315c..3c595c08eb 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -33,7 +33,7 @@ Pod::Spec.new do |s| s.preserve_paths = '*.js' s.source_files = 'ios/**/*.{h,m,mm}' - s.public_header_files = 'ios/RNSentry.h' + s.public_header_files = 'ios/RNSentry.h', 'ios/RNSentrySDK.h' s.compiler_flags = other_cflags diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj index 112c485d6f..b19dfc43fc 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 332D334A2CDCC8EB00547D76 /* RNSentryCocoaTesterTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RNSentryCocoaTesterTests-Bridging-Header.h"; sourceTree = ""; }; 333B58A82D35BA93000F8D04 /* RNSentryStart.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryStart.h; path = ../ios/RNSentryStart.h; sourceTree = SOURCE_ROOT; }; 333B58A92D35BB2D000F8D04 /* RNSentryStart+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "RNSentryStart+Test.h"; path = "RNSentryCocoaTesterTests/RNSentryStart+Test.h"; sourceTree = SOURCE_ROOT; }; + 333B58AF2D36A7FD000F8D04 /* RNSentrySDK.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentrySDK.h; path = ../ios/RNSentrySDK.h; sourceTree = SOURCE_ROOT; }; 336084382C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RNSentryReplayBreadcrumbConverterTests.swift; sourceTree = ""; }; 3360843A2C32E3A8008CC412 /* RNSentryReplayBreadcrumbConverter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryReplayBreadcrumbConverter.h; path = ../ios/RNSentryReplayBreadcrumbConverter.h; sourceTree = ""; }; 3360843C2C340C76008CC412 /* RNSentryBreadcrumbTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryBreadcrumbTests.swift; sourceTree = ""; }; @@ -118,6 +119,7 @@ 33AFE0122B8F319000AAB120 /* RNSentry */ = { isa = PBXGroup; children = ( + 333B58AF2D36A7FD000F8D04 /* RNSentrySDK.h */, 333B58A92D35BB2D000F8D04 /* RNSentryStart+Test.h */, 333B58A82D35BA93000F8D04 /* RNSentryStart.h */, 3380C6C02CDEC56B0018B9B6 /* Replay */, diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm index 1cbc57caf6..abe2ae70ce 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm @@ -2,6 +2,7 @@ #import "RNSentryStart+Test.h" #import #import +#import #import #import #import @@ -12,9 +13,8 @@ @interface RNSentryInitNativeSdkTests : XCTestCase @implementation RNSentryInitNativeSdkTests -- (void)testCreateOptionsWithDictionaryRemovesPerformanceProperties +- (void)testStartWithDictionaryRemovesPerformanceProperties { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @@ -26,9 +26,8 @@ - (void)testCreateOptionsWithDictionaryRemovesPerformanceProperties , @"enableTracing" : @YES, } ; -SentryOptions *actualOptions = - [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; - +[RNSentryStart startWithOptions:mockedReactNativeDictionary error:&error]; +SentryOptions *actualOptions = PrivateSentrySDKOnly.options; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); XCTAssertNotNil( @@ -41,14 +40,13 @@ - (void)testCreateOptionsWithDictionaryRemovesPerformanceProperties - (void)testCaptureFailedRequestsIsDisabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", }; - SentryOptions *actualOptions = - [RNSentryStart createOptionsWithDictionary:mockedReactNativeDictionary error:&error]; + [RNSentryStart startWithOptions:mockedReactNativeDictionary error:&error]; + SentryOptions *actualOptions = PrivateSentrySDKOnly.options; XCTAssertNotNil(actualOptions, @"Did not create sentry options"); XCTAssertNil(error, @"Should not pass no error"); @@ -57,7 +55,6 @@ - (void)testCaptureFailedRequestsIsDisabled - (void)testCreateOptionsWithDictionaryNativeCrashHandlingDefault { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -73,7 +70,6 @@ - (void)testCreateOptionsWithDictionaryNativeCrashHandlingDefault - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingDefault { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -89,7 +85,6 @@ - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingDefault - (void)testCreateOptionsWithDictionaryNativeCrashHandlingEnabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -106,7 +101,6 @@ - (void)testCreateOptionsWithDictionaryNativeCrashHandlingEnabled - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingEnabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -123,7 +117,6 @@ - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingEnabled - (void)testCreateOptionsWithDictionaryNativeCrashHandlingDisabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -140,7 +133,6 @@ - (void)testCreateOptionsWithDictionaryNativeCrashHandlingDisabled - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingDisabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -157,7 +149,6 @@ - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingDisabled - (void)testCreateOptionsWithDictionarySpotlightEnabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -175,7 +166,6 @@ - (void)testCreateOptionsWithDictionarySpotlightEnabled - (void)testCreateOptionsWithDictionarySpotlightOne { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -193,7 +183,6 @@ - (void)testCreateOptionsWithDictionarySpotlightOne - (void)testCreateOptionsWithDictionarySpotlightUrl { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -210,7 +199,6 @@ - (void)testCreateOptionsWithDictionarySpotlightUrl - (void)testCreateOptionsWithDictionarySpotlightDisabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -226,7 +214,6 @@ - (void)testCreateOptionsWithDictionarySpotlightDisabled - (void)testCreateOptionsWithDictionarySpotlightZero { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -242,7 +229,6 @@ - (void)testCreateOptionsWithDictionarySpotlightZero - (void)testPassesErrorOnWrongDsn { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -257,7 +243,6 @@ - (void)testPassesErrorOnWrongDsn - (void)testBeforeBreadcrumbsCallbackFiltersOutSentryDsnRequestBreadcrumbs { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedDictionary = @{ @@ -278,7 +263,6 @@ - (void)testBeforeBreadcrumbsCallbackFiltersOutSentryDsnRequestBreadcrumbs - (void)testBeforeBreadcrumbsCallbackFiltersOutDevServerRequestBreadcrumbs { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSString *mockDevServer = @"http://localhost:8081"; @@ -299,7 +283,6 @@ - (void)testBeforeBreadcrumbsCallbackFiltersOutDevServerRequestBreadcrumbs - (void)testBeforeBreadcrumbsCallbackDoesNotFiltersOutNonDevServerOrDsnRequestBreadcrumbs { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedDictionary = @{ @@ -320,7 +303,6 @@ - (void)testBeforeBreadcrumbsCallbackDoesNotFiltersOutNonDevServerOrDsnRequestBr - (void)testBeforeBreadcrumbsCallbackKeepsBreadcrumbWhenDevServerUrlIsNotPassedAndDsnDoesNotMatch { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedDictionary = @{ // dsn is always validated in SentryOptions initialization @@ -340,7 +322,6 @@ - (void)testBeforeBreadcrumbsCallbackKeepsBreadcrumbWhenDevServerUrlIsNotPassedA - (void)testEventFromSentryCocoaReactNativeHasOriginAndEnvironmentTags { - RNSentry *rnSentry = [[RNSentry alloc] init]; SentryEvent *testEvent = [[SentryEvent alloc] init]; testEvent.sdk = @{ @"name" : @"sentry.cocoa.react-native", @@ -354,7 +335,6 @@ - (void)testEventFromSentryCocoaReactNativeHasOriginAndEnvironmentTags - (void)testEventFromSentryReactNativeOriginAndEnvironmentTagsAreOverwritten { - RNSentry *rnSentry = [[RNSentry alloc] init]; SentryEvent *testEvent = [[SentryEvent alloc] init]; testEvent.sdk = @{ @"name" : @"sentry.cocoa.react-native", diff --git a/packages/core/ios/RNSentry.h b/packages/core/ios/RNSentry.h index 66dc7219ac..c7fb93e0ea 100644 --- a/packages/core/ios/RNSentry.h +++ b/packages/core/ios/RNSentry.h @@ -11,6 +11,9 @@ #import #import +// This import exposes public RNSentrySDK start +#import "RNSentrySDK.h" + typedef int (*SymbolicateCallbackType)(const void *, Dl_info *); @interface diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index 6907513da5..69fd287403 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -94,13 +94,11 @@ - (instancetype)init : (RCTPromiseRejectBlock)reject) { NSError *error = nil; - SentryOptions *sentryOptions = [RNSentryStart createOptionsWithDictionary:options error:&error]; + [RNSentryStart startWithOptions:options error:&error]; if (error != nil) { reject(@"SentryReactNative", error.localizedDescription, error); return; } - - [RNSentryStart startWithOptions:sentryOptions]; resolve(@YES); } diff --git a/packages/core/ios/RNSentrySDK.h b/packages/core/ios/RNSentrySDK.h new file mode 100644 index 0000000000..28fca1d6d5 --- /dev/null +++ b/packages/core/ios/RNSentrySDK.h @@ -0,0 +1,17 @@ +#import + +@interface RNSentrySDK : NSObject +SENTRY_NO_INIT + +/** + * @experimental + * Inits and configures Sentry for React Native applications. Make sure to + * set a valid DSN. + * + * @discussion Call this method on the main thread. When calling it from a background thread, the + * SDK starts on the main thread async. + */ ++ (void)startWithConfigureOptions:(void (^)(SentryOptions *options))configureOptions + NS_SWIFT_NAME(start(configureOptions:)); + +@end diff --git a/packages/core/ios/RNSentrySDK.m b/packages/core/ios/RNSentrySDK.m new file mode 100644 index 0000000000..f4031dc06b --- /dev/null +++ b/packages/core/ios/RNSentrySDK.m @@ -0,0 +1,15 @@ +#import "RNSentrySDK.h" +#import "RNSentryStart.h" + +@implementation RNSentrySDK + ++ (void)startWithConfigureOptions:(void (^)(SentryOptions *options))configureOptions +{ + SentryOptions *options = [[SentryOptions alloc] init]; + [RNSentryStart updateWithReactDefaults:options]; + configureOptions(options); + [RNSentryStart updateWithReactFinals:options]; + [RNSentryStart startWithOptions:options]; +} + +@end diff --git a/packages/core/ios/RNSentryStart.h b/packages/core/ios/RNSentryStart.h index bc5adf35af..a45ddea679 100644 --- a/packages/core/ios/RNSentryStart.h +++ b/packages/core/ios/RNSentryStart.h @@ -4,9 +4,15 @@ @interface RNSentryStart : NSObject SENTRY_NO_INIT ++ (void)startWithOptions:(NSDictionary *_Nonnull)javascriptOptions + error:(NSError *_Nonnull *_Nonnull)errorPointer; + + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options error:(NSError *_Nonnull *_Nonnull)errorPointer; ++ (void)updateWithReactDefaults:(SentryOptions *)options; ++ (void)updateWithReactFinals:(SentryOptions *)options; + /** * @experimental * Inits and configures Sentry for React Native applications. Make sure to diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index e4ce65c314..5cf0d430e4 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -2,12 +2,23 @@ #import "RNSentryReplay.h" #import "RNSentryVersion.h" -#import #import +#import +#import #import @implementation RNSentryStart ++ (void)startWithOptions:(NSDictionary *_Nonnull)javascriptOptions + error:(NSError *_Nonnull *_Nonnull)errorPointer +{ + SentryOptions *options = [self createOptionsWithDictionary:javascriptOptions + error:errorPointer]; + [self updateWithReactDefaults:options]; + [self updateWithReactFinals:options]; + [self startWithOptions:options]; +} + + (void)startWithOptions:(SentryOptions *)options NS_SWIFT_NAME(start(options:)) { NSString *sdkVersion = [PrivateSentrySDKOnly getSdkVersionString]; @@ -27,30 +38,7 @@ + (void)startWithOptions:(SentryOptions *)options NS_SWIFT_NAME(start(options:)) + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options error:(NSError *_Nonnull *_Nonnull)errorPointer { - SentryBeforeSendEventCallback beforeSend = ^SentryEvent *(SentryEvent *event) - { - // We don't want to send an event after startup that came from a Unhandled JS Exception of - // react native Because we sent it already before the app crashed. - if (nil != event.exceptions.firstObject.type && - [event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location - != NSNotFound) { - return nil; - } - - [self setEventOriginTag:event]; - - return event; - }; - NSMutableDictionary *mutableOptions = [options mutableCopy]; - [mutableOptions setValue:beforeSend forKey:@"beforeSend"]; - - // remove performance traces sample rate and traces sampler since we don't want to synchronize - // these configurations to the Native SDKs. The user could tho initialize the SDK manually and - // set themselves. - [mutableOptions removeObjectForKey:@"tracesSampleRate"]; - [mutableOptions removeObjectForKey:@"tracesSampler"]; - [mutableOptions removeObjectForKey:@"enableTracing"]; #if SENTRY_TARGET_REPLAY_SUPPORTED [RNSentryReplay updateOptions:mutableOptions]; @@ -63,6 +51,7 @@ + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) } // Exclude Dev Server and Sentry Dsn request from Breadcrumbs + // TODO: Migrate for manual init NSString *dsn = [self getURLFromDSN:[mutableOptions valueForKey:@"dsn"]]; NSString *devServerUrl = [mutableOptions valueForKey:@"devServerUrl"]; sentryOptions.beforeBreadcrumb @@ -105,20 +94,62 @@ + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) } } - // Enable the App start and Frames tracking measurements - if ([mutableOptions valueForKey:@"enableAutoPerformanceTracing"] != nil) { - BOOL enableAutoPerformanceTracing = - [mutableOptions[@"enableAutoPerformanceTracing"] boolValue]; - PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode = enableAutoPerformanceTracing; + return sentryOptions; +} + +/** + * This function updates the options with RNSentry defaults. These default can be + * overwritten by users during manual native initialization. + */ ++ (void)updateWithReactDefaults:(SentryOptions *)options +{ + // Failed requests are captured only in JS to avoid duplicates + options.enableCaptureFailedRequests = NO; + + // Tracing is only enabled in JS to avoid duplicate navigation spans + options.tracesSampleRate = nil; + options.tracesSampler = nil; + options.enableTracing = NO; +} + +/** + * This function updates options with changes RNSentry users should not change + * and so this is applied after the configureOptions callback during manual native initialization. + */ ++ (void)updateWithReactFinals:(SentryOptions *)options +{ + SentryBeforeSendEventCallback userBeforeSend = options.beforeSend; + options.beforeSend = ^SentryEvent *(SentryEvent *event) + { + // Unhandled JS Exception are processed by the SDK on JS layer + // To avoid duplicates we drop them in the native SDKs + if (nil != event.exceptions.firstObject.type && + [event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location + != NSNotFound) { + return nil; + } + + [self setEventOriginTag:event]; + if (userBeforeSend == nil) { + return event; + } else { + return userBeforeSend(event); + } + }; + + if (options.enableAutoPerformanceTracing != nil) { + // App Start Hybrid mode doesn't wait for didFinishLaunchNotification and the + // didBecomeVisibleNotification as they will be missed when auto initializing from JS + // App Start measurements are created right after the tracking starts + PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode + = options.enableAutoPerformanceTracing; #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST - PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode = enableAutoPerformanceTracing; + // Frames Tracking Hybrid Mode ensures tracking + // is enabled without tracing enabled in the native SDK + PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode + = options.enableAutoPerformanceTracing; #endif } - - // Failed requests can only be enabled in one SDK to avoid duplicates - sentryOptions.enableCaptureFailedRequests = NO; - - return sentryOptions; } + (void)setEventOriginTag:(SentryEvent *)event diff --git a/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm b/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm index 71a62884ac..2a6a0a0956 100644 --- a/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm +++ b/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm @@ -9,6 +9,7 @@ # import #endif +#import #import #import @@ -57,6 +58,11 @@ - (BOOL)application:(UIApplication *)application // When the native init is enabled the `autoInitializeNativeSdk` // in JS has to be set to `false` // [self initializeSentry]; + // [RNSentrySDK startWithConfigureOptions:^(SentryOptions *options) { + // options.dsn = + // @"https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561"; + // options.debug = YES; + // }]; self.moduleName = @"sentry-react-native-sample"; // You can add your custom initial props in the dictionary below. From 2cc92f21007476e5b68f319a6d4ea6d01fb19dda Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 14 Jan 2025 23:16:00 +0100 Subject: [PATCH 16/36] feat: Read `sentry.options.json` during cocoa init --- CHANGELOG.md | 1 + packages/core/ios/RNSentrySDK.h | 10 ++++ packages/core/ios/RNSentrySDK.m | 49 ++++++++++++++++++- packages/core/scripts/sentry-xcode.sh | 21 ++++++++ .../sentryreactnativesample/AppDelegate.mm | 6 +-- samples/react-native/sentry.options.json | 20 ++++++++ 6 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 samples/react-native/sentry.options.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 512824779b..83b74a508e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Rename `navigation.processing` span to more expressive `Navigation dispatch to screen A mounted/navigation cancelled` ([#4423](https://github.com/getsentry/sentry-react-native/pull/4423)) - Add RN SDK package to `sdk.packages` for Cocoa ([#4381](https://github.com/getsentry/sentry-react-native/pull/4381)) - Add experimental version of `startWithConfigureOptions` for Apple platforms ([#4444](https://github.com/getsentry/sentry-react-native/pull/4444)) +- Add initialization using `sentry.options.json` for Apple platforms ([#4446](https://github.com/getsentry/sentry-react-native/pull/4446)) ### Internal diff --git a/packages/core/ios/RNSentrySDK.h b/packages/core/ios/RNSentrySDK.h index 28fca1d6d5..1790905e2b 100644 --- a/packages/core/ios/RNSentrySDK.h +++ b/packages/core/ios/RNSentrySDK.h @@ -3,6 +3,16 @@ @interface RNSentrySDK : NSObject SENTRY_NO_INIT +/** + * @experimental + * Inits and configures Sentry for React Native applications. Make sure to + * set a valid DSN. + * + * @discussion Call this method on the main thread. When calling it from a background thread, the + * SDK starts on the main thread async. + */ ++ (void)start; + /** * @experimental * Inits and configures Sentry for React Native applications. Make sure to diff --git a/packages/core/ios/RNSentrySDK.m b/packages/core/ios/RNSentrySDK.m index f4031dc06b..12ce4f3a95 100644 --- a/packages/core/ios/RNSentrySDK.m +++ b/packages/core/ios/RNSentrySDK.m @@ -1,13 +1,58 @@ #import "RNSentrySDK.h" #import "RNSentryStart.h" +static NSString *SENTRY_OPTIONS_RESOURCE_NAME = @"sentry.options"; +static NSString *SENTRY_OPTIONS_RESOURCE_TYPE = @"json"; + @implementation RNSentrySDK ++ (void)start +{ + [self startWithConfigureOptions:nil]; +} + + (void)startWithConfigureOptions:(void (^)(SentryOptions *options))configureOptions { - SentryOptions *options = [[SentryOptions alloc] init]; + NSError *readError = nil; + NSError *parseError = nil; + NSError *optionsError = nil; + + NSString *path = [[NSBundle mainBundle] pathForResource:SENTRY_OPTIONS_RESOURCE_NAME + ofType:SENTRY_OPTIONS_RESOURCE_TYPE]; + NSData *content = [NSData dataWithContentsOfFile:path options:0 error:&readError]; + NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:content + options:0 + error:&parseError]; + + if (readError != nil) { + NSLog(@"[RNSentry] Failed to load options from %@, with error: %@", path, + readError.localizedDescription); + } + + if (parseError != nil) { + NSLog(@"[RNSentry] Failed to parse JSON from %@, with error: %@", path, + parseError.localizedDescription); + } + + SentryOptions *options = nil; + if (dict != nil) { + options = [RNSentryStart createOptionsWithDictionary:dict error:&optionsError]; + } + + if (optionsError != nil) { + NSLog(@"[RNSentry] Failed to parse options from %@, with error: %@", path, + optionsError.localizedDescription); + } + + if (options == nil) { + // Fallback in case that options file could not be parsed. + options = [[SentryOptions alloc] init]; + } + [RNSentryStart updateWithReactDefaults:options]; - configureOptions(options); + if (configureOptions != nil) { + configureOptions(options); + } [RNSentryStart updateWithReactFinals:options]; [RNSentryStart startWithOptions:options]; } diff --git a/packages/core/scripts/sentry-xcode.sh b/packages/core/scripts/sentry-xcode.sh index 78970c4c60..b812150cb0 100755 --- a/packages/core/scripts/sentry-xcode.sh +++ b/packages/core/scripts/sentry-xcode.sh @@ -51,3 +51,24 @@ fi if [ -f "$SENTRY_COLLECT_MODULES" ]; then /bin/sh "$SENTRY_COLLECT_MODULES" fi + +SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX="Skipping options file copy. To disable this behavior, set SENTRY_COPY_OPTIONS_FILE=false in your environment variables." +SENTRY_OPTIONS_FILE_NAME="sentry.options.json" +SENTRY_OPTIONS_FILE_DESTINATION_PATH="$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/$SENTRY_OPTIONS_FILE_NAME" +[ -z "$SENTRY_OPTIONS_FILE_PATH" ] && SENTRY_OPTIONS_FILE_PATH="$RN_PROJECT_ROOT/$SENTRY_OPTIONS_FILE_NAME" +[ -z "$SENTRY_COPY_OPTIONS_FILE" ] && SENTRY_COPY_OPTIONS_FILE=true + +if [ "$SENTRY_COPY_OPTIONS_FILE" = true ]; then + if [[ -z "$CONFIGURATION_BUILD_DIR" ]]; then + echo "[Sentry] CONFIGURATION_BUILD_DIR is not set. $SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX" 1>&2 + elif [[ -z "$UNLOCALIZED_RESOURCES_FOLDER_PATH" ]]; then + echo "[Sentry] UNLOCALIZED_RESOURCES_FOLDER_PATH is not set. $SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX" 1>&2 + fi + + if [ -f "$SENTRY_OPTIONS_FILE_PATH" ]; then + cp "$SENTRY_OPTIONS_FILE_PATH" "$SENTRY_OPTIONS_FILE_DESTINATION_PATH" + echo "[Sentry] Copied $SENTRY_OPTIONS_FILE_PATH to $SENTRY_OPTIONS_FILE_DESTINATION_PATH" + else + echo "[Sentry] $SENTRY_OPTIONS_FILE_PATH not found. $SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX" 1>&2 + fi +fi diff --git a/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm b/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm index 2a6a0a0956..0073ecfa28 100644 --- a/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm +++ b/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm @@ -58,11 +58,7 @@ - (BOOL)application:(UIApplication *)application // When the native init is enabled the `autoInitializeNativeSdk` // in JS has to be set to `false` // [self initializeSentry]; - // [RNSentrySDK startWithConfigureOptions:^(SentryOptions *options) { - // options.dsn = - // @"https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561"; - // options.debug = YES; - // }]; + // [RNSentrySDK start]; self.moduleName = @"sentry-react-native-sample"; // You can add your custom initial props in the dictionary below. diff --git a/samples/react-native/sentry.options.json b/samples/react-native/sentry.options.json new file mode 100644 index 0000000000..f6465b7923 --- /dev/null +++ b/samples/react-native/sentry.options.json @@ -0,0 +1,20 @@ +{ + "dsn": "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561", + "debug": true, + "environment": "dev", + "enableUserInteractionTracing": true, + "enableAutoSessionTracking": true, + "sessionTrackingIntervalMillis": 30000, + "enableTracing": true, + "tracesSampleRate": 1.0, + "attachStacktrace": true, + "attachScreenshot": true, + "attachViewHierarchy": true, + "enableCaptureFailedRequests": true, + "_release": "myapp@1.2.3+1", + "_dist": 1, + "profilesSampleRate": 1.0, + "replaysSessionSampleRate": 1.0, + "replaysOnErrorSampleRate": 1.0, + "spotlight": true +} From 592b90373bb98db5f5569508f2d6ad894921cb0d Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Wed, 15 Jan 2025 11:27:44 +0100 Subject: [PATCH 17/36] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83b74a508e..54a5fe7f93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ - Rename `navigation.processing` span to more expressive `Navigation dispatch to screen A mounted/navigation cancelled` ([#4423](https://github.com/getsentry/sentry-react-native/pull/4423)) - Add RN SDK package to `sdk.packages` for Cocoa ([#4381](https://github.com/getsentry/sentry-react-native/pull/4381)) - Add experimental version of `startWithConfigureOptions` for Apple platforms ([#4444](https://github.com/getsentry/sentry-react-native/pull/4444)) -- Add initialization using `sentry.options.json` for Apple platforms ([#4446](https://github.com/getsentry/sentry-react-native/pull/4446)) +- Add initialization using `sentry.options.json` for Apple platforms ([#4447](https://github.com/getsentry/sentry-react-native/pull/4447)) ### Internal From a2717bd38894d0a69720b7683bc2d5a63916ca87 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 15 Jan 2025 14:33:10 +0100 Subject: [PATCH 18/36] remove global processor --- packages/core/ios/RNSentryStart.m | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index 5cf0d430e4..09fcddf6e0 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -4,7 +4,6 @@ #import #import -#import #import @implementation RNSentryStart From 890133a86bf4f71cc6dce056b8aecde26feaa00f Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 15 Jan 2025 14:47:32 +0100 Subject: [PATCH 19/36] Update code docs for start methods --- packages/core/ios/RNSentrySDK.h | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/core/ios/RNSentrySDK.h b/packages/core/ios/RNSentrySDK.h index 1790905e2b..6b79df54f0 100644 --- a/packages/core/ios/RNSentrySDK.h +++ b/packages/core/ios/RNSentrySDK.h @@ -5,8 +5,8 @@ SENTRY_NO_INIT /** * @experimental - * Inits and configures Sentry for React Native applications. Make sure to - * set a valid DSN. + * Inits and configures Sentry for React Native applications using `sentry.options.json` + * configuration file. * * @discussion Call this method on the main thread. When calling it from a background thread, the * SDK starts on the main thread async. @@ -15,8 +15,11 @@ SENTRY_NO_INIT /** * @experimental - * Inits and configures Sentry for React Native applications. Make sure to - * set a valid DSN. + * Inits and configures Sentry for React Native applicationsusing `sentry.options.json` + * configuration file and `configureOptions` callback. + * + * The `configureOptions` callback can overwrite the config file options + * and add non-serializable items to the options object. * * @discussion Call this method on the main thread. When calling it from a background thread, the * SDK starts on the main thread async. From cfc6293f0d16e5368f8f9dca97de937fc2c1a799 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 20 Jan 2025 15:48:12 +0100 Subject: [PATCH 20/36] fix lint --- packages/core/ios/RNSentryStart.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index e4ce65c314..c364b6c630 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -2,8 +2,8 @@ #import "RNSentryReplay.h" #import "RNSentryVersion.h" -#import #import +#import #import @implementation RNSentryStart From f2a041e8e0867451e09a11a4c2398a867a855e14 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 20 Jan 2025 16:44:33 +0100 Subject: [PATCH 21/36] remove unused rnSentry --- .../RNSentryCocoaTesterTests/RNSentryTests.mm | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm index 1cbc57caf6..9cefc4747a 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.mm @@ -14,7 +14,6 @@ @implementation RNSentryInitNativeSdkTests - (void)testCreateOptionsWithDictionaryRemovesPerformanceProperties { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @@ -41,7 +40,6 @@ - (void)testCreateOptionsWithDictionaryRemovesPerformanceProperties - (void)testCaptureFailedRequestsIsDisabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -57,7 +55,6 @@ - (void)testCaptureFailedRequestsIsDisabled - (void)testCreateOptionsWithDictionaryNativeCrashHandlingDefault { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -73,7 +70,6 @@ - (void)testCreateOptionsWithDictionaryNativeCrashHandlingDefault - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingDefault { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -89,7 +85,6 @@ - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingDefault - (void)testCreateOptionsWithDictionaryNativeCrashHandlingEnabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -106,7 +101,6 @@ - (void)testCreateOptionsWithDictionaryNativeCrashHandlingEnabled - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingEnabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -123,7 +117,6 @@ - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingEnabled - (void)testCreateOptionsWithDictionaryNativeCrashHandlingDisabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -140,7 +133,6 @@ - (void)testCreateOptionsWithDictionaryNativeCrashHandlingDisabled - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingDisabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -157,7 +149,6 @@ - (void)testCreateOptionsWithDictionaryAutoPerformanceTracingDisabled - (void)testCreateOptionsWithDictionarySpotlightEnabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -175,7 +166,6 @@ - (void)testCreateOptionsWithDictionarySpotlightEnabled - (void)testCreateOptionsWithDictionarySpotlightOne { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -193,7 +183,6 @@ - (void)testCreateOptionsWithDictionarySpotlightOne - (void)testCreateOptionsWithDictionarySpotlightUrl { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -210,7 +199,6 @@ - (void)testCreateOptionsWithDictionarySpotlightUrl - (void)testCreateOptionsWithDictionarySpotlightDisabled { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -226,7 +214,6 @@ - (void)testCreateOptionsWithDictionarySpotlightDisabled - (void)testCreateOptionsWithDictionarySpotlightZero { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -242,7 +229,6 @@ - (void)testCreateOptionsWithDictionarySpotlightZero - (void)testPassesErrorOnWrongDsn { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedReactNativeDictionary = @{ @@ -257,7 +243,6 @@ - (void)testPassesErrorOnWrongDsn - (void)testBeforeBreadcrumbsCallbackFiltersOutSentryDsnRequestBreadcrumbs { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedDictionary = @{ @@ -278,7 +263,6 @@ - (void)testBeforeBreadcrumbsCallbackFiltersOutSentryDsnRequestBreadcrumbs - (void)testBeforeBreadcrumbsCallbackFiltersOutDevServerRequestBreadcrumbs { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSString *mockDevServer = @"http://localhost:8081"; @@ -299,7 +283,6 @@ - (void)testBeforeBreadcrumbsCallbackFiltersOutDevServerRequestBreadcrumbs - (void)testBeforeBreadcrumbsCallbackDoesNotFiltersOutNonDevServerOrDsnRequestBreadcrumbs { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedDictionary = @{ @@ -320,7 +303,6 @@ - (void)testBeforeBreadcrumbsCallbackDoesNotFiltersOutNonDevServerOrDsnRequestBr - (void)testBeforeBreadcrumbsCallbackKeepsBreadcrumbWhenDevServerUrlIsNotPassedAndDsnDoesNotMatch { - RNSentry *rnSentry = [[RNSentry alloc] init]; NSError *error = nil; NSDictionary *_Nonnull mockedDictionary = @{ // dsn is always validated in SentryOptions initialization @@ -340,7 +322,6 @@ - (void)testBeforeBreadcrumbsCallbackKeepsBreadcrumbWhenDevServerUrlIsNotPassedA - (void)testEventFromSentryCocoaReactNativeHasOriginAndEnvironmentTags { - RNSentry *rnSentry = [[RNSentry alloc] init]; SentryEvent *testEvent = [[SentryEvent alloc] init]; testEvent.sdk = @{ @"name" : @"sentry.cocoa.react-native", @@ -354,7 +335,6 @@ - (void)testEventFromSentryCocoaReactNativeHasOriginAndEnvironmentTags - (void)testEventFromSentryReactNativeOriginAndEnvironmentTagsAreOverwritten { - RNSentry *rnSentry = [[RNSentry alloc] init]; SentryEvent *testEvent = [[SentryEvent alloc] init]; testEvent.sdk = @{ @"name" : @"sentry.cocoa.react-native", From fc4ff7efb558e2a9eb59b1db5aedcdc47bcc5a8b Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 20 Jan 2025 16:44:40 +0100 Subject: [PATCH 22/36] fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b3ed24744..dc596b2e6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ ### Internal - Initialize `RNSentryTimeToDisplay` during native module `init` on iOS ([#4443](https://github.com/getsentry/sentry-react-native/pull/4443)) -- Extract iOS native initialization to standalone structures ([#4444](https://github.com/getsentry/sentry-react-native/pull/4444)) +- Extract iOS native initialization to standalone structures ([#4442](https://github.com/getsentry/sentry-react-native/pull/4442)) ### Dependencies From dec9869775c7167f1b192c5069280b0b9e791164 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 20 Jan 2025 18:57:40 +0100 Subject: [PATCH 23/36] wip! add manual init tests --- .../project.pbxproj | 4 + ...RNSentryCocoaTesterTests-Bridging-Header.h | 2 + .../RNSentryStartTests.swift | 112 ++++++++++++++++++ packages/core/ios/RNSentrySDK.h | 2 +- packages/core/ios/RNSentryStart.h | 2 +- packages/core/ios/RNSentryStart.m | 2 +- 6 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj index b19dfc43fc..74701dfe9e 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 336084392C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336084382C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift */; }; 3380C6C42CE25ECA0018B9B6 /* RNSentryReplayPostInitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3380C6C32CE25ECA0018B9B6 /* RNSentryReplayPostInitTests.swift */; }; 33958C692BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33958C682BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m */; }; + 339C6C3C2D3EB25100CA72ED /* RNSentryStartTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */; }; 33AFDFED2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */; }; 33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */; }; 33F58AD02977037D008F60EA /* RNSentryTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 33F58ACF2977037D008F60EA /* RNSentryTests.mm */; }; @@ -38,6 +39,7 @@ 338739072A7D7D2800950DDD /* RNSentryReplay.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryReplay.h; path = ../ios/RNSentryReplay.h; sourceTree = ""; }; 33958C672BFCEF5A00AD1FB6 /* RNSentryOnDrawReporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryOnDrawReporter.h; path = ../ios/RNSentryOnDrawReporter.h; sourceTree = ""; }; 33958C682BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryOnDrawReporterTests.m; sourceTree = ""; }; + 339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryStartTests.swift; sourceTree = ""; }; 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryFramesTrackerListenerTests.m; sourceTree = ""; }; 33AFDFEE2B8D14C200AAB120 /* RNSentryFramesTrackerListenerTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSentryFramesTrackerListenerTests.h; sourceTree = ""; }; 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryDependencyContainerTests.m; sourceTree = ""; }; @@ -91,6 +93,7 @@ 3360899029524164007C7730 /* RNSentryCocoaTesterTests */ = { isa = PBXGroup; children = ( + 339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */, 332D334A2CDCC8EB00547D76 /* RNSentryCocoaTesterTests-Bridging-Header.h */, 332D33492CDCC8E100547D76 /* RNSentryTests.h */, 336084382C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift */, @@ -245,6 +248,7 @@ files = ( AEFB00422CC90C4B00EC8A9A /* RNSentryBreadcrumbTests.swift in Sources */, 332D33472CDBDBB600547D76 /* RNSentryReplayOptionsTests.swift in Sources */, + 339C6C3C2D3EB25100CA72ED /* RNSentryStartTests.swift in Sources */, 33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */, 336084392C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift in Sources */, 33F58AD02977037D008F60EA /* RNSentryTests.mm in Sources */, diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h index bc2bdd0304..f7aa88e193 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h @@ -7,3 +7,5 @@ #import "RNSentryReplayBreadcrumbConverter.h" #import "RNSentryReplayMask.h" #import "RNSentryReplayUnmask.h" +#import "RNSentrySDK.h" +#import "RNSentryStart.h" diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift new file mode 100644 index 0000000000..15d56bc26a --- /dev/null +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift @@ -0,0 +1,112 @@ +import XCTest + +final class RNSentryStartTests: XCTestCase { + + func testStartDoesNotThrowWithoutConfigure() { + RNSentrySDK.start(configureOptions: nil) + } + + func assertReactDefaults(_ actualOptions: Options?) { + XCTAssertFalse(actualOptions!.enableCaptureFailedRequests) + XCTAssertNil(actualOptions!.tracesSampleRate) + XCTAssertNil(actualOptions!.tracesSampler) + XCTAssertFalse(actualOptions!.enableTracing) + } + + func testStartSetsReactDeafults() { + var actualOptions: Options? = nil + + RNSentrySDK.start { options in + actualOptions = options + } + + XCTAssertNotNil(actualOptions, "start have not provided default options or have not executed configure callback") + assertReactDefaults(actualOptions) + } + + func testAutoStartSetsReactDefaults() throws { + try startFromRN(options: [ + "dsn" : "https://abcd@efgh.ingest.sentry.io/123456" + ]) + + let actualOptions = PrivateSentrySDKOnly.options; + assertReactDefaults(actualOptions) + } + + func testAutoStartWithEmptyOptionsThrows() { + XCTAssertThrowsError(try startFromRN(options: [:])) + } + + func testStartEnablesHybridTracing() throws { + let testCases: [() throws -> Void] = [ + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + } + }, + { + try self.startFromRN(options: [ + "dsn" : "https://abcd@efgh.ingest.sentry.io/123456", + ]) + }, + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + options.enableAutoPerformanceTracing = true + } + }, + { + try self.startFromRN(options: [ + "dsn" : "https://abcd@efgh.ingest.sentry.io/123456", + "enableAutoPerformanceTracing": true, + ]) + } + ] + + // Test each implementation + for startMethod in testCases { + try startMethod() + } + + let actualOptions = PrivateSentrySDKOnly.options + + XCTAssertTrue(PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode) + XCTAssertTrue(PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode) + } + + func testStartDisablesHybridTracing() throws { + let testCases: [() throws -> Void] = [ + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + options.enableAutoPerformanceTracing = false + } + }, + { + try self.startFromRN(options: [ + "dsn" : "https://abcd@efgh.ingest.sentry.io/123456", + "enableAutoPerformanceTracing": false, + ]) + } + ] + + // Test each implementation + for startMethod in testCases { + try startMethod() + } + + let actualOptions = PrivateSentrySDKOnly.options + + XCTAssertFalse(PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode) + XCTAssertFalse(PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode) + } + + func startFromRN(options: [AnyHashable: Any]) throws { + var error: NSError? + RNSentryStart.start(options: options, error: &error) + + if let error = error { + throw error + } + } +} diff --git a/packages/core/ios/RNSentrySDK.h b/packages/core/ios/RNSentrySDK.h index 28fca1d6d5..71b5be3498 100644 --- a/packages/core/ios/RNSentrySDK.h +++ b/packages/core/ios/RNSentrySDK.h @@ -11,7 +11,7 @@ SENTRY_NO_INIT * @discussion Call this method on the main thread. When calling it from a background thread, the * SDK starts on the main thread async. */ -+ (void)startWithConfigureOptions:(void (^)(SentryOptions *options))configureOptions ++ (void)startWithConfigureOptions:(void (^_Nullable)(SentryOptions *_Nonnull options))configureOptions NS_SWIFT_NAME(start(configureOptions:)); @end diff --git a/packages/core/ios/RNSentryStart.h b/packages/core/ios/RNSentryStart.h index a45ddea679..01a0617148 100644 --- a/packages/core/ios/RNSentryStart.h +++ b/packages/core/ios/RNSentryStart.h @@ -5,7 +5,7 @@ SENTRY_NO_INIT + (void)startWithOptions:(NSDictionary *_Nonnull)javascriptOptions - error:(NSError *_Nonnull *_Nonnull)errorPointer; + error:(NSError *_Nullable *_Nullable)errorPointer; + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options error:(NSError *_Nonnull *_Nonnull)errorPointer; diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index 5cf0d430e4..cfbc6f8d64 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -10,7 +10,7 @@ @implementation RNSentryStart + (void)startWithOptions:(NSDictionary *_Nonnull)javascriptOptions - error:(NSError *_Nonnull *_Nonnull)errorPointer + error:(NSError *_Nullable*_Nullable)errorPointer { SentryOptions *options = [self createOptionsWithDictionary:javascriptOptions error:errorPointer]; From 22e88c7d8d2e44b7ebceb0da2dc5b094af121893 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 21 Jan 2025 14:01:14 +0100 Subject: [PATCH 24/36] add tests --- .../project.pbxproj | 2 + ...RNSentryCocoaTesterTests-Bridging-Header.h | 1 + .../RNSentryStartTests.swift | 178 +++++++++++++++--- packages/core/ios/RNSentrySDK.h | 3 +- packages/core/ios/RNSentrySDK.m | 4 +- packages/core/ios/RNSentryStart.m | 21 +-- 6 files changed, 174 insertions(+), 35 deletions(-) diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj index 74701dfe9e..1621383063 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ 33958C672BFCEF5A00AD1FB6 /* RNSentryOnDrawReporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSentryOnDrawReporter.h; path = ../ios/RNSentryOnDrawReporter.h; sourceTree = ""; }; 33958C682BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryOnDrawReporterTests.m; sourceTree = ""; }; 339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryStartTests.swift; sourceTree = ""; }; + 339C6C3D2D3FA04D00CA72ED /* RNSentryVersion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryVersion.h; path = ../ios/RNSentryVersion.h; sourceTree = SOURCE_ROOT; }; 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryFramesTrackerListenerTests.m; sourceTree = ""; }; 33AFDFEE2B8D14C200AAB120 /* RNSentryFramesTrackerListenerTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSentryFramesTrackerListenerTests.h; sourceTree = ""; }; 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryDependencyContainerTests.m; sourceTree = ""; }; @@ -122,6 +123,7 @@ 33AFE0122B8F319000AAB120 /* RNSentry */ = { isa = PBXGroup; children = ( + 339C6C3D2D3FA04D00CA72ED /* RNSentryVersion.h */, 333B58AF2D36A7FD000F8D04 /* RNSentrySDK.h */, 333B58A92D35BB2D000F8D04 /* RNSentryStart+Test.h */, 333B58A82D35BA93000F8D04 /* RNSentryStart.h */, diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h index f7aa88e193..ba8d8f703d 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h @@ -9,3 +9,4 @@ #import "RNSentryReplayUnmask.h" #import "RNSentrySDK.h" #import "RNSentryStart.h" +#import "RNSentryVersion.h" diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift index 15d56bc26a..b9d12200cf 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartTests.swift @@ -14,7 +14,7 @@ final class RNSentryStartTests: XCTestCase { } func testStartSetsReactDeafults() { - var actualOptions: Options? = nil + var actualOptions: Options? RNSentrySDK.start { options in actualOptions = options @@ -26,17 +26,13 @@ final class RNSentryStartTests: XCTestCase { func testAutoStartSetsReactDefaults() throws { try startFromRN(options: [ - "dsn" : "https://abcd@efgh.ingest.sentry.io/123456" + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" ]) - let actualOptions = PrivateSentrySDKOnly.options; + let actualOptions = PrivateSentrySDKOnly.options assertReactDefaults(actualOptions) } - func testAutoStartWithEmptyOptionsThrows() { - XCTAssertThrowsError(try startFromRN(options: [:])) - } - func testStartEnablesHybridTracing() throws { let testCases: [() throws -> Void] = [ { @@ -46,7 +42,7 @@ final class RNSentryStartTests: XCTestCase { }, { try self.startFromRN(options: [ - "dsn" : "https://abcd@efgh.ingest.sentry.io/123456", + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" ]) }, { @@ -57,8 +53,8 @@ final class RNSentryStartTests: XCTestCase { }, { try self.startFromRN(options: [ - "dsn" : "https://abcd@efgh.ingest.sentry.io/123456", - "enableAutoPerformanceTracing": true, + "dsn": "https://abcd@efgh.ingest.sentry.io/123456", + "enableAutoPerformanceTracing": true ]) } ] @@ -66,12 +62,12 @@ final class RNSentryStartTests: XCTestCase { // Test each implementation for startMethod in testCases { try startMethod() - } - let actualOptions = PrivateSentrySDKOnly.options + let actualOptions = PrivateSentrySDKOnly.options - XCTAssertTrue(PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode) - XCTAssertTrue(PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode) + XCTAssertTrue(PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode) + XCTAssertTrue(PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode) + } } func testStartDisablesHybridTracing() throws { @@ -84,24 +80,124 @@ final class RNSentryStartTests: XCTestCase { }, { try self.startFromRN(options: [ - "dsn" : "https://abcd@efgh.ingest.sentry.io/123456", - "enableAutoPerformanceTracing": false, + "dsn": "https://abcd@efgh.ingest.sentry.io/123456", + "enableAutoPerformanceTracing": false ]) } ] - // Test each implementation for startMethod in testCases { try startMethod() + + let actualOptions = PrivateSentrySDKOnly.options + + XCTAssertFalse(PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode) + XCTAssertFalse(PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode) } + } - let actualOptions = PrivateSentrySDKOnly.options + func testStartIgnoresUnhandledJsExceptions() throws { + let testCases: [() throws -> Void] = [ + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" + ]) + } + ] + + for startMethod in testCases { + try startMethod() + + let actualOptions = PrivateSentrySDKOnly.options - XCTAssertFalse(PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode) - XCTAssertFalse(PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode) + let actualEvent = actualOptions.beforeSend!(createUnhandledJsExceptionEvent()) + + XCTAssertNil(actualEvent) + } } - func startFromRN(options: [AnyHashable: Any]) throws { + func testStartSetsNativeEventOrigin() throws { + let testCases: [() throws -> Void] = [ + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" + ]) + } + ] + + for startMethod in testCases { + try startMethod() + + let actualOptions = PrivateSentrySDKOnly.options + + let actualEvent = actualOptions.beforeSend!(createNativeEvent()) + + XCTAssertNotNil(actualEvent) + XCTAssertNotNil(actualEvent!.tags) + XCTAssertEqual(actualEvent!.tags!["event.origin"], "ios") + XCTAssertEqual(actualEvent!.tags!["event.environment"], "native") + } + } + + func testStartDoesNotOverwriteUserBeforeSend() { + var executed = false + + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + options.beforeSend = { event in + executed = true + return event + } + } + + PrivateSentrySDKOnly.options.beforeSend!(genericEvent()) + + XCTAssertTrue(executed) + } + + func testStartSetsHybridSdkName() throws { + let testCases: [() throws -> Void] = [ + { + RNSentrySDK.start { options in + options.dsn = "https://abcd@efgh.ingest.sentry.io/123456" + } + }, + { + try self.startFromRN(options: [ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456" + ]) + } + ] + + for startMethod in testCases { + try startMethod() + + let actualEvent = captuteTestEvent() + + XCTAssertNotNil(actualEvent) + XCTAssertNotNil(actualEvent!.sdk) + XCTAssertEqual(actualEvent!.sdk!["name"] as! String, NATIVE_SDK_NAME) + + let packages = actualEvent!.sdk!["packages"] as! [[String: String]] + let reactPackage = packages.first { $0["name"] == REACT_NATIVE_SDK_PACKAGE_NAME } + + XCTAssertNotNil(reactPackage) + XCTAssertEqual(reactPackage!["name"], REACT_NATIVE_SDK_PACKAGE_NAME) + XCTAssertEqual(reactPackage!["version"], REACT_NATIVE_SDK_PACKAGE_VERSION) + } + } + + func startFromRN(options: [String: Any]) throws { var error: NSError? RNSentryStart.start(options: options, error: &error) @@ -109,4 +205,44 @@ final class RNSentryStartTests: XCTestCase { throw error } } + + func createUnhandledJsExceptionEvent() -> Event { + let event = Event() + event.exceptions = [] + event.exceptions!.append(Exception(value: "Test", type: "Unhandled JS Exception: undefined is not a function")) + return event + } + + func createNativeEvent() -> Event { + let event = Event() + event.sdk = [ + "name": NATIVE_SDK_NAME, + "version": "1.2.3" + ] + return event + } + + func genericEvent() -> Event { + return Event() + } + + func captuteTestEvent() -> Event? { + var actualEvent: Event? + + // This is the closest to the sent event we can get using the actual Sentry start method + let originalBeforeSend = PrivateSentrySDKOnly.options.beforeSend + PrivateSentrySDKOnly.options.beforeSend = { event in + if let originalBeforeSend = originalBeforeSend { + let processedEvent = originalBeforeSend(event) + actualEvent = processedEvent + return processedEvent + } + actualEvent = event + return event + } + + SentrySDK.capture(message: "Test") + + return actualEvent + } } diff --git a/packages/core/ios/RNSentrySDK.h b/packages/core/ios/RNSentrySDK.h index 71b5be3498..7d3512bb5d 100644 --- a/packages/core/ios/RNSentrySDK.h +++ b/packages/core/ios/RNSentrySDK.h @@ -11,7 +11,8 @@ SENTRY_NO_INIT * @discussion Call this method on the main thread. When calling it from a background thread, the * SDK starts on the main thread async. */ -+ (void)startWithConfigureOptions:(void (^_Nullable)(SentryOptions *_Nonnull options))configureOptions ++ (void)startWithConfigureOptions: + (void (^_Nullable)(SentryOptions *_Nonnull options))configureOptions NS_SWIFT_NAME(start(configureOptions:)); @end diff --git a/packages/core/ios/RNSentrySDK.m b/packages/core/ios/RNSentrySDK.m index f4031dc06b..b7ed6f4a7b 100644 --- a/packages/core/ios/RNSentrySDK.m +++ b/packages/core/ios/RNSentrySDK.m @@ -7,7 +7,9 @@ + (void)startWithConfigureOptions:(void (^)(SentryOptions *options))configureOpt { SentryOptions *options = [[SentryOptions alloc] init]; [RNSentryStart updateWithReactDefaults:options]; - configureOptions(options); + if (configureOptions != nil) { + configureOptions(options); + } [RNSentryStart updateWithReactFinals:options]; [RNSentryStart startWithOptions:options]; } diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index cfbc6f8d64..5b8398e06c 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -10,7 +10,7 @@ @implementation RNSentryStart + (void)startWithOptions:(NSDictionary *_Nonnull)javascriptOptions - error:(NSError *_Nullable*_Nullable)errorPointer + error:(NSError *_Nullable *_Nullable)errorPointer { SentryOptions *options = [self createOptionsWithDictionary:javascriptOptions error:errorPointer]; @@ -137,19 +137,16 @@ + (void)updateWithReactFinals:(SentryOptions *)options } }; - if (options.enableAutoPerformanceTracing != nil) { - // App Start Hybrid mode doesn't wait for didFinishLaunchNotification and the - // didBecomeVisibleNotification as they will be missed when auto initializing from JS - // App Start measurements are created right after the tracking starts - PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode - = options.enableAutoPerformanceTracing; + // App Start Hybrid mode doesn't wait for didFinishLaunchNotification and the + // didBecomeVisibleNotification as they will be missed when auto initializing from JS + // App Start measurements are created right after the tracking starts + PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode = options.enableAutoPerformanceTracing; #if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST - // Frames Tracking Hybrid Mode ensures tracking - // is enabled without tracing enabled in the native SDK - PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode - = options.enableAutoPerformanceTracing; + // Frames Tracking Hybrid Mode ensures tracking + // is enabled without tracing enabled in the native SDK + PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode + = options.enableAutoPerformanceTracing; #endif - } } + (void)setEventOriginTag:(SentryEvent *)event From 1baac187e1ad74782bb96020608abc365ed33f5d Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 21 Jan 2025 16:28:46 +0100 Subject: [PATCH 25/36] add tests --- .../project.pbxproj | 37 ++++++ ...RNSentryCocoaTesterTests-Bridging-Header.h | 2 +- .../RNSentryStartFromFileTests.swift | 115 ++++++++++++++++++ .../RNSentryCocoaTester/RNSentrySDK+Test.h | 9 ++ .../TestAssets/invalid.options.json | 5 + .../TestAssets/invalid.options.txt | 1 + .../TestAssets/valid.options.json | 4 + packages/core/ios/RNSentrySDK.m | 23 +++- packages/core/ios/RNSentryStart.m | 3 +- 9 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift create mode 100644 packages/core/RNSentryCocoaTester/RNSentrySDK+Test.h create mode 100644 packages/core/RNSentryCocoaTester/TestAssets/invalid.options.json create mode 100644 packages/core/RNSentryCocoaTester/TestAssets/invalid.options.txt create mode 100644 packages/core/RNSentryCocoaTester/TestAssets/valid.options.json diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj index 1621383063..0d82e39ef1 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTester.xcodeproj/project.pbxproj @@ -12,6 +12,10 @@ 3380C6C42CE25ECA0018B9B6 /* RNSentryReplayPostInitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3380C6C32CE25ECA0018B9B6 /* RNSentryReplayPostInitTests.swift */; }; 33958C692BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33958C682BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m */; }; 339C6C3C2D3EB25100CA72ED /* RNSentryStartTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */; }; + 339C6C422D3FD3AE00CA72ED /* RNSentryStartFromFileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 339C6C412D3FD39500CA72ED /* RNSentryStartFromFileTests.swift */; }; + 339C6C482D3FD9A700CA72ED /* invalid.options.json in Resources */ = {isa = PBXBuildFile; fileRef = 339C6C462D3FD91900CA72ED /* invalid.options.json */; }; + 339C6C492D3FD9A700CA72ED /* invalid.options.txt in Resources */ = {isa = PBXBuildFile; fileRef = 339C6C452D3FD90200CA72ED /* invalid.options.txt */; }; + 339C6C4B2D3FD9B200CA72ED /* valid.options.json in Resources */ = {isa = PBXBuildFile; fileRef = 339C6C4A2D3FD9AB00CA72ED /* valid.options.json */; }; 33AFDFED2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */; }; 33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */; }; 33F58AD02977037D008F60EA /* RNSentryTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 33F58ACF2977037D008F60EA /* RNSentryTests.mm */; }; @@ -41,6 +45,11 @@ 33958C682BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryOnDrawReporterTests.m; sourceTree = ""; }; 339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryStartTests.swift; sourceTree = ""; }; 339C6C3D2D3FA04D00CA72ED /* RNSentryVersion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNSentryVersion.h; path = ../ios/RNSentryVersion.h; sourceTree = SOURCE_ROOT; }; + 339C6C412D3FD39500CA72ED /* RNSentryStartFromFileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNSentryStartFromFileTests.swift; sourceTree = ""; }; + 339C6C442D3FD62D00CA72ED /* RNSentrySDK+Test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RNSentrySDK+Test.h"; sourceTree = ""; }; + 339C6C452D3FD90200CA72ED /* invalid.options.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = invalid.options.txt; sourceTree = ""; }; + 339C6C462D3FD91900CA72ED /* invalid.options.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = invalid.options.json; sourceTree = ""; }; + 339C6C4A2D3FD9AB00CA72ED /* valid.options.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = valid.options.json; sourceTree = ""; }; 33AFDFEC2B8D14B300AAB120 /* RNSentryFramesTrackerListenerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryFramesTrackerListenerTests.m; sourceTree = ""; }; 33AFDFEE2B8D14C200AAB120 /* RNSentryFramesTrackerListenerTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNSentryFramesTrackerListenerTests.h; sourceTree = ""; }; 33AFDFF02B8D15E500AAB120 /* RNSentryDependencyContainerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSentryDependencyContainerTests.m; sourceTree = ""; }; @@ -75,6 +84,7 @@ 3360896929524163007C7730 = { isa = PBXGroup; children = ( + 339C6C432D3FD41C00CA72ED /* TestAssets */, 33AFE0122B8F319000AAB120 /* RNSentry */, 3360899029524164007C7730 /* RNSentryCocoaTesterTests */, 3360897329524163007C7730 /* Products */, @@ -94,6 +104,7 @@ 3360899029524164007C7730 /* RNSentryCocoaTesterTests */ = { isa = PBXGroup; children = ( + 339C6C412D3FD39500CA72ED /* RNSentryStartFromFileTests.swift */, 339C6C3B2D3EB23B00CA72ED /* RNSentryStartTests.swift */, 332D334A2CDCC8EB00547D76 /* RNSentryCocoaTesterTests-Bridging-Header.h */, 332D33492CDCC8E100547D76 /* RNSentryTests.h */, @@ -120,9 +131,20 @@ path = Replay; sourceTree = ""; }; + 339C6C432D3FD41C00CA72ED /* TestAssets */ = { + isa = PBXGroup; + children = ( + 339C6C4A2D3FD9AB00CA72ED /* valid.options.json */, + 339C6C462D3FD91900CA72ED /* invalid.options.json */, + 339C6C452D3FD90200CA72ED /* invalid.options.txt */, + ); + path = TestAssets; + sourceTree = ""; + }; 33AFE0122B8F319000AAB120 /* RNSentry */ = { isa = PBXGroup; children = ( + 339C6C442D3FD62D00CA72ED /* RNSentrySDK+Test.h */, 339C6C3D2D3FA04D00CA72ED /* RNSentryVersion.h */, 333B58AF2D36A7FD000F8D04 /* RNSentrySDK.h */, 333B58A92D35BB2D000F8D04 /* RNSentryStart+Test.h */, @@ -157,6 +179,7 @@ 3360898929524164007C7730 /* Sources */, BB7D14838753E6599863899B /* Frameworks */, CC7959F3721CB3AD7CB6A047 /* [CP] Copy Pods Resources */, + 339C6C472D3FD99900CA72ED /* Resources */, ); buildRules = ( ); @@ -201,6 +224,19 @@ }; /* End PBXProject section */ +/* Begin PBXResourcesBuildPhase section */ + 339C6C472D3FD99900CA72ED /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 339C6C482D3FD9A700CA72ED /* invalid.options.json in Resources */, + 339C6C4B2D3FD9B200CA72ED /* valid.options.json in Resources */, + 339C6C492D3FD9A700CA72ED /* invalid.options.txt in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + /* Begin PBXShellScriptBuildPhase section */ 30F19D4E16BEEFEC68733838 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -252,6 +288,7 @@ 332D33472CDBDBB600547D76 /* RNSentryReplayOptionsTests.swift in Sources */, 339C6C3C2D3EB25100CA72ED /* RNSentryStartTests.swift in Sources */, 33AFDFF12B8D15E500AAB120 /* RNSentryDependencyContainerTests.m in Sources */, + 339C6C422D3FD3AE00CA72ED /* RNSentryStartFromFileTests.swift in Sources */, 336084392C32E382008CC412 /* RNSentryReplayBreadcrumbConverterTests.swift in Sources */, 33F58AD02977037D008F60EA /* RNSentryTests.mm in Sources */, 33958C692BFCF12600AD1FB6 /* RNSentryOnDrawReporterTests.m in Sources */, diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h index ba8d8f703d..08fddcbf8e 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryCocoaTesterTests-Bridging-Header.h @@ -7,6 +7,6 @@ #import "RNSentryReplayBreadcrumbConverter.h" #import "RNSentryReplayMask.h" #import "RNSentryReplayUnmask.h" -#import "RNSentrySDK.h" +#import "RNSentrySDK+Test.h" #import "RNSentryStart.h" #import "RNSentryVersion.h" diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift new file mode 100644 index 0000000000..e0269a5961 --- /dev/null +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryStartFromFileTests.swift @@ -0,0 +1,115 @@ +import XCTest + +final class RNSentryStartFromFileTests: XCTestCase { + + func testNoThrowOnMissingOptionsFile() { + var wasConfigurationCalled = false + + RNSentrySDK.start(getNonExistingOptionsPath(), configureOptions: { _ in + wasConfigurationCalled = true + }) + + XCTAssertTrue(wasConfigurationCalled) + + let actualOptions = PrivateSentrySDKOnly.options + XCTAssertNil(actualOptions.dsn) + XCTAssertNil(actualOptions.parsedDsn) + } + + func testNoThrowOnInvalidFileType() { + var wasConfigurationCalled = false + + RNSentrySDK.start(getInvalidOptionsTypePath(), configureOptions: { _ in + wasConfigurationCalled = true + }) + + XCTAssertTrue(wasConfigurationCalled) + + let actualOptions = PrivateSentrySDKOnly.options + XCTAssertNil(actualOptions.dsn) + XCTAssertNil(actualOptions.parsedDsn) + } + + func testNoThrowOnInvalidOptions() { + var wasConfigurationCalled = false + + RNSentrySDK.start(getInvalidOptionsPath(), configureOptions: { _ in + wasConfigurationCalled = true + }) + + XCTAssertTrue(wasConfigurationCalled) + + let actualOptions = PrivateSentrySDKOnly.options + XCTAssertNil(actualOptions.dsn) + XCTAssertNotNil(actualOptions.parsedDsn) + XCTAssertEqual(actualOptions.environment, "environment-from-invalid-file") + } + + func testLoadValidOptions() { + var wasConfigurationCalled = false + + RNSentrySDK.start(getValidOptionsPath(), configureOptions: { _ in + wasConfigurationCalled = true + }) + + XCTAssertTrue(wasConfigurationCalled) + + let actualOptions = PrivateSentrySDKOnly.options + XCTAssertNil(actualOptions.dsn) + XCTAssertNotNil(actualOptions.parsedDsn) + XCTAssertEqual(actualOptions.environment, "environment-from-valid-file") + } + + func testOptionsFromFileInConfigureOptions() { + var wasConfigurationCalled = false + + RNSentrySDK.start(getValidOptionsPath()) { options in + wasConfigurationCalled = true + XCTAssertEqual(options.environment, "environment-from-valid-file") + } + + XCTAssertTrue(wasConfigurationCalled) + } + + func testOptionsOverwrittenInConfigureOptions() { + RNSentrySDK.start(getValidOptionsPath()) { options in + options.environment = "new-environment" + } + + let actualOptions = PrivateSentrySDKOnly.options + XCTAssertEqual(actualOptions.environment, "new-environment") + } + + func getNonExistingOptionsPath() -> String { + return "/non-existing.options.json" + } + + func getInvalidOptionsTypePath() -> String { + guard let path = getTestBundle().path(forResource: "invalid.options", ofType: "txt") else { + fatalError("Could not get invalid type options path") + } + return path + } + + func getInvalidOptionsPath() -> String { + guard let path = getTestBundle().path(forResource: "invalid.options", ofType: "json") else { + fatalError("Could not get invalid options path") + } + return path + } + + func getValidOptionsPath() -> String { + guard let path = getTestBundle().path(forResource: "valid.options", ofType: "json") else { + fatalError("Could not get invalid options path") + } + return path + } + + func getTestBundle() -> Bundle { + let maybeBundle = Bundle.allBundles.first(where: { $0.bundlePath.hasSuffix(".xctest") }) + guard let bundle = maybeBundle else { + fatalError("Could not find test bundle") + } + return bundle + } +} diff --git a/packages/core/RNSentryCocoaTester/RNSentrySDK+Test.h b/packages/core/RNSentryCocoaTester/RNSentrySDK+Test.h new file mode 100644 index 0000000000..06da31b42d --- /dev/null +++ b/packages/core/RNSentryCocoaTester/RNSentrySDK+Test.h @@ -0,0 +1,9 @@ +#import "RNSentrySDK.h" + +@interface +RNSentrySDK (Test) + ++ (void)start:(NSString *)path + configureOptions:(void (^)(SentryOptions *_Nonnull options))configureOptions; + +@end diff --git a/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.json b/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.json new file mode 100644 index 0000000000..bf8f2be64c --- /dev/null +++ b/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.json @@ -0,0 +1,5 @@ +{ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456", + "environment": "environment-from-invalid-file", + "invalid-option": 123 +} diff --git a/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.txt b/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.txt new file mode 100644 index 0000000000..601553b507 --- /dev/null +++ b/packages/core/RNSentryCocoaTester/TestAssets/invalid.options.txt @@ -0,0 +1 @@ +invalid-options diff --git a/packages/core/RNSentryCocoaTester/TestAssets/valid.options.json b/packages/core/RNSentryCocoaTester/TestAssets/valid.options.json new file mode 100644 index 0000000000..641087d5e8 --- /dev/null +++ b/packages/core/RNSentryCocoaTester/TestAssets/valid.options.json @@ -0,0 +1,4 @@ +{ + "dsn": "https://abcd@efgh.ingest.sentry.io/123456", + "environment": "environment-from-valid-file" +} diff --git a/packages/core/ios/RNSentrySDK.m b/packages/core/ios/RNSentrySDK.m index 12ce4f3a95..7d7f4cf9b3 100644 --- a/packages/core/ios/RNSentrySDK.m +++ b/packages/core/ios/RNSentrySDK.m @@ -12,17 +12,28 @@ + (void)start } + (void)startWithConfigureOptions:(void (^)(SentryOptions *options))configureOptions +{ + NSString *path = [[NSBundle mainBundle] pathForResource:SENTRY_OPTIONS_RESOURCE_NAME + ofType:SENTRY_OPTIONS_RESOURCE_TYPE]; + + [self start:path configureOptions:configureOptions]; +} + ++ (void)start:(NSString *)path configureOptions:(void (^)(SentryOptions *options))configureOptions { NSError *readError = nil; NSError *parseError = nil; NSError *optionsError = nil; - NSString *path = [[NSBundle mainBundle] pathForResource:SENTRY_OPTIONS_RESOURCE_NAME - ofType:SENTRY_OPTIONS_RESOURCE_TYPE]; - NSData *content = [NSData dataWithContentsOfFile:path options:0 error:&readError]; - NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:content - options:0 - error:&parseError]; + NSData *_Nullable content = nil; + if (path != nil) { + content = [NSData dataWithContentsOfFile:path options:0 error:&readError]; + } + + NSDictionary *dict = nil; + if (content != nil) { + dict = [NSJSONSerialization JSONObjectWithData:content options:0 error:&parseError]; + } if (readError != nil) { NSLog(@"[RNSentry] Failed to load options from %@, with error: %@", path, diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index b3d4d5d77e..1313ac01b4 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -50,8 +50,8 @@ + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) } // Exclude Dev Server and Sentry Dsn request from Breadcrumbs - // TODO: Migrate for manual init NSString *dsn = [self getURLFromDSN:[mutableOptions valueForKey:@"dsn"]]; + // TODO: For Auto Init from JS dev server is resolved automatically, for init from options file dev server has to be specified manually NSString *devServerUrl = [mutableOptions valueForKey:@"devServerUrl"]; sentryOptions.beforeBreadcrumb = ^SentryBreadcrumb *_Nullable(SentryBreadcrumb *_Nonnull breadcrumb) @@ -86,6 +86,7 @@ + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) sentryOptions.spotlightUrl = spotlightValue; } else if ([spotlightValue isKindOfClass:[NSNumber class]]) { sentryOptions.enableSpotlight = [spotlightValue boolValue]; + // TODO: For Auto init from JS set automatically for init from options file have to be set manually id defaultSpotlightUrl = [mutableOptions valueForKey:@"defaultSidecarUrl"]; if (defaultSpotlightUrl != nil) { sentryOptions.spotlightUrl = defaultSpotlightUrl; From f92edd920b95a2680ab4fc707ef2fdd62153384c Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 21 Jan 2025 17:09:00 +0100 Subject: [PATCH 26/36] remove unavailable and unused global events processors import --- packages/core/ios/RNSentryStart.m | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index 5b8398e06c..b3d4d5d77e 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -4,7 +4,6 @@ #import #import -#import #import @implementation RNSentryStart From 7baad41c0d14268a641c0a70f512f6c766d8ca23 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 21 Jan 2025 17:12:53 +0100 Subject: [PATCH 27/36] fix c-format --- packages/core/ios/RNSentryStart.m | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/ios/RNSentryStart.m b/packages/core/ios/RNSentryStart.m index 1313ac01b4..84e2d83b02 100644 --- a/packages/core/ios/RNSentryStart.m +++ b/packages/core/ios/RNSentryStart.m @@ -51,7 +51,8 @@ + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) // Exclude Dev Server and Sentry Dsn request from Breadcrumbs NSString *dsn = [self getURLFromDSN:[mutableOptions valueForKey:@"dsn"]]; - // TODO: For Auto Init from JS dev server is resolved automatically, for init from options file dev server has to be specified manually + // TODO: For Auto Init from JS dev server is resolved automatically, for init from options file + // dev server has to be specified manually NSString *devServerUrl = [mutableOptions valueForKey:@"devServerUrl"]; sentryOptions.beforeBreadcrumb = ^SentryBreadcrumb *_Nullable(SentryBreadcrumb *_Nonnull breadcrumb) @@ -86,7 +87,8 @@ + (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) sentryOptions.spotlightUrl = spotlightValue; } else if ([spotlightValue isKindOfClass:[NSNumber class]]) { sentryOptions.enableSpotlight = [spotlightValue boolValue]; - // TODO: For Auto init from JS set automatically for init from options file have to be set manually + // TODO: For Auto init from JS set automatically for init from options file have to be + // set manually id defaultSpotlightUrl = [mutableOptions valueForKey:@"defaultSidecarUrl"]; if (defaultSpotlightUrl != nil) { sentryOptions.spotlightUrl = defaultSpotlightUrl; From 1a2aca82cfecdae2a2fb63799604faf444f1965a Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 21 Jan 2025 18:28:02 +0100 Subject: [PATCH 28/36] feat: Automatically load `sentry.options.json` file --- packages/core/src/js/tools/metroconfig.ts | 16 ++++ .../src/js/tools/sentryMetroSerializer.ts | 1 + .../src/js/tools/sentryOptionsSerializer.ts | 84 +++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 packages/core/src/js/tools/sentryOptionsSerializer.ts diff --git a/packages/core/src/js/tools/metroconfig.ts b/packages/core/src/js/tools/metroconfig.ts index 71c43389a1..f7090f21db 100644 --- a/packages/core/src/js/tools/metroconfig.ts +++ b/packages/core/src/js/tools/metroconfig.ts @@ -10,6 +10,7 @@ import { createSentryMetroSerializer, unstable_beforeAssetSerializationPlugin } import type { DefaultConfigOptions } from './vendor/expo/expoconfig'; export * from './sentryMetroSerializer'; import { withSentryMiddleware } from './metroMiddleware'; +import { withSentryOptionsFromFile } from './sentryOptionsSerializer'; enableLogger(); @@ -30,6 +31,14 @@ export interface SentryMetroConfigOptions { * @default true */ enableSourceContextInDevelopment?: boolean; + /** + * Load Sentry Options from a file. If `true` it will use the default path. + * If `false` it will not load any options from a file. Only options provided in the code will be used. + * If `string` it will use the provided path. + * + * @default '{projectRoot}/sentry.options.json' + */ + optionsFile?: string | boolean; } export interface SentryExpoConfigOptions { @@ -51,8 +60,12 @@ export function withSentryConfig( annotateReactComponents = false, includeWebReplay = true, enableSourceContextInDevelopment = true, + optionsFile = true, }: SentryMetroConfigOptions = {}, ): MetroConfig { + // eslint-disable-next-line no-console + console.log('withSentryConfig', 'projectRoot', config.projectRoot); + setSentryMetroDevServerEnvFlag(); let newConfig = config; @@ -68,6 +81,9 @@ export function withSentryConfig( if (enableSourceContextInDevelopment) { newConfig = withSentryMiddleware(newConfig); } + if (optionsFile) { + newConfig = withSentryOptionsFromFile(newConfig, optionsFile); + } return newConfig; } diff --git a/packages/core/src/js/tools/sentryMetroSerializer.ts b/packages/core/src/js/tools/sentryMetroSerializer.ts index fca0979440..feb1e65621 100644 --- a/packages/core/src/js/tools/sentryMetroSerializer.ts +++ b/packages/core/src/js/tools/sentryMetroSerializer.ts @@ -42,6 +42,7 @@ export function unstable_beforeAssetSerializationPlugin({ return [...addDebugIdModule(premodules, debugIdModule)]; } +// TODO: deprecate this and afterwards rename to createSentryDebugIdSerializer /** * Creates a Metro serializer that adds Debug ID module to the plain bundle. * The Debug ID module is a virtual module that provides a debug ID in runtime. diff --git a/packages/core/src/js/tools/sentryOptionsSerializer.ts b/packages/core/src/js/tools/sentryOptionsSerializer.ts new file mode 100644 index 0000000000..847481b24a --- /dev/null +++ b/packages/core/src/js/tools/sentryOptionsSerializer.ts @@ -0,0 +1,84 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import { MetroConfig, Module } from 'metro'; +import { createSet, VirtualJSOutput } from './utils'; +// eslint-disable-next-line import/no-extraneous-dependencies +import * as countLines from 'metro/src/lib/countLines'; + +// TODO: move to utils +type MetroCustomSerializer = Required['serializer']>['customSerializer'] | undefined; + +const DEFAULT_OPTIONS_FILE_NAME = 'sentry.options.json'; + +/** + * Loads Sentry options from a file in + */ +export function withSentryOptionsFromFile(config: MetroConfig, optionsFile: string | boolean): MetroConfig { + if (optionsFile === false) { + return config; + } + + const { projectRoot } = config; + if (!projectRoot) { + // eslint-disable-next-line no-console + console.error('[@sentry/react-native/metro] Project root is required to load Sentry options from a file'); + return config; + } + + const optionsPath = + typeof optionsFile === 'string' + ? path.join(projectRoot, optionsFile) + : path.join(projectRoot, DEFAULT_OPTIONS_FILE_NAME); + + const originalSerializer = config.serializer?.customSerializer; + if (!originalSerializer) { + // TODO: this works because we set Debug ID serializer in `withSentryDebugId` + // We should use the default serializer if non is provided + // eslint-disable-next-line no-console + console.error( + '[@sentry/react-native/metro] `config.serializer.customSerializer` is required to load Sentry options from a file', + ); + return config; + } + + const sentryOptionsSerializer: MetroCustomSerializer = (entryPoint, preModules, graph, options) => { + (preModules as Module[]).push(createSentryOptionsModule(optionsPath)); + return originalSerializer(entryPoint, preModules, graph, options); + }; + + return { + ...config, + serializer: { + ...config.serializer, + customSerializer: sentryOptionsSerializer, + }, + }; +} + +function createSentryOptionsModule(filePath: string): Module & { setSource: (code: string) => void } { + // TODO: handle errors + const content = fs.readFileSync(filePath, 'utf8'); + const parsedContent = JSON.parse(content); + const minifiedContent = JSON.stringify(parsedContent); + let optionsCode = `var __SENTRY_OPTIONS__=${minifiedContent};`; + + return { + setSource: (code: string) => { + optionsCode = code; + }, + dependencies: new Map(), + getSource: () => Buffer.from(optionsCode), + inverseDependencies: createSet(), + path: '__SENTRY_OPTIONS__', + output: [ + { + type: 'js/script/virtual', + data: { + code: optionsCode, + lineCount: countLines(optionsCode), + map: [], + }, + }, + ], + }; +} From 6d81a10bc8070aa9f6772a531fdc99a2a066130e Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Wed, 22 Jan 2025 11:39:38 +0100 Subject: [PATCH 29/36] remove old sentry-cocoa init code --- .../sentryreactnativesample/AppDelegate.mm | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm b/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm index 0073ecfa28..fe0893c5b6 100644 --- a/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm +++ b/samples/react-native/ios/sentryreactnativesample/AppDelegate.mm @@ -20,44 +20,11 @@ @implementation AppDelegate -- (void)initializeSentry -{ - [SentrySDK startWithConfigureOptions:^(SentryOptions *options) { - // Only options set here will apply to the iOS SDK - // Options from JS are not passed to the iOS SDK when initialized manually - options.dsn = @"https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561"; - options.debug = YES; // Enabled debug when first installing is always helpful - - options.beforeSend = ^SentryEvent *(SentryEvent *event) - { - // We don't want to send an event after startup that came from a Unhandled JS Exception - // of react native Because we sent it already before the app crashed. - if (nil != event.exceptions.firstObject.type && - [event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location - != NSNotFound) { - NSLog(@"Unhandled JS Exception"); - return nil; - } - - return event; - }; - - // Enable the App start and Frames tracking measurements - // If this is disabled the app start and frames tracking - // won't be passed from native to JS transactions - PrivateSentrySDKOnly.appStartMeasurementHybridSDKMode = true; -#if TARGET_OS_IPHONE || TARGET_OS_MACCATALYST - PrivateSentrySDKOnly.framesTrackingMeasurementHybridSDKMode = true; -#endif - }]; -} - - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // When the native init is enabled the `autoInitializeNativeSdk` // in JS has to be set to `false` - // [self initializeSentry]; // [RNSentrySDK start]; self.moduleName = @"sentry-react-native-sample"; From 7eb1786f283b35835b02a37e56d8bb5e988ed676 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 22 Jan 2025 16:56:23 +0100 Subject: [PATCH 30/36] fix copy options json only if destination set and file exists --- packages/core/scripts/sentry-xcode.sh | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/core/scripts/sentry-xcode.sh b/packages/core/scripts/sentry-xcode.sh index b812150cb0..336d393220 100755 --- a/packages/core/scripts/sentry-xcode.sh +++ b/packages/core/scripts/sentry-xcode.sh @@ -63,12 +63,10 @@ if [ "$SENTRY_COPY_OPTIONS_FILE" = true ]; then echo "[Sentry] CONFIGURATION_BUILD_DIR is not set. $SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX" 1>&2 elif [[ -z "$UNLOCALIZED_RESOURCES_FOLDER_PATH" ]]; then echo "[Sentry] UNLOCALIZED_RESOURCES_FOLDER_PATH is not set. $SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX" 1>&2 - fi - - if [ -f "$SENTRY_OPTIONS_FILE_PATH" ]; then + elif [ ! -f "$SENTRY_OPTIONS_FILE_PATH" ]; then + echo "[Sentry] $SENTRY_OPTIONS_FILE_PATH not found. $SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX" 1>&2 + else cp "$SENTRY_OPTIONS_FILE_PATH" "$SENTRY_OPTIONS_FILE_DESTINATION_PATH" echo "[Sentry] Copied $SENTRY_OPTIONS_FILE_PATH to $SENTRY_OPTIONS_FILE_DESTINATION_PATH" - else - echo "[Sentry] $SENTRY_OPTIONS_FILE_PATH not found. $SENTRY_OPTIONS_FILE_ERROR_MESSAGE_POSTFIX" 1>&2 fi fi From 090914871aa2ee6503fa6aefffcdcaaed20274e7 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Thu, 23 Jan 2025 14:09:58 +0100 Subject: [PATCH 31/36] fix types, handle errors, add logs, add support for expo projects --- packages/core/src/js/tools/metroconfig.ts | 10 ++--- .../src/js/tools/sentryOptionsSerializer.ts | 44 +++++++++++++------ packages/core/src/js/tools/utils.ts | 4 +- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/packages/core/src/js/tools/metroconfig.ts b/packages/core/src/js/tools/metroconfig.ts index f7090f21db..df6389b77e 100644 --- a/packages/core/src/js/tools/metroconfig.ts +++ b/packages/core/src/js/tools/metroconfig.ts @@ -11,6 +11,7 @@ import type { DefaultConfigOptions } from './vendor/expo/expoconfig'; export * from './sentryMetroSerializer'; import { withSentryMiddleware } from './metroMiddleware'; import { withSentryOptionsFromFile } from './sentryOptionsSerializer'; +import { MetroCustomSerializer } from './utils'; enableLogger(); @@ -63,9 +64,6 @@ export function withSentryConfig( optionsFile = true, }: SentryMetroConfigOptions = {}, ): MetroConfig { - // eslint-disable-next-line no-console - console.log('withSentryConfig', 'projectRoot', config.projectRoot); - setSentryMetroDevServerEnvFlag(); let newConfig = config; @@ -119,6 +117,10 @@ export function getSentryExpoConfig( newConfig = withSentryMiddleware(newConfig); } + if (options.optionsFile ?? true) { + newConfig = withSentryOptionsFromFile(newConfig, options.optionsFile ?? true); + } + return newConfig; } @@ -171,8 +173,6 @@ export function withSentryBabelTransformer(config: MetroConfig): MetroConfig { }; } -type MetroCustomSerializer = Required['serializer']>['customSerializer'] | undefined; - function withSentryDebugId(config: MetroConfig): MetroConfig { const customSerializer = createSentryMetroSerializer( config.serializer?.customSerializer || undefined, diff --git a/packages/core/src/js/tools/sentryOptionsSerializer.ts b/packages/core/src/js/tools/sentryOptionsSerializer.ts index 847481b24a..13f7ed034e 100644 --- a/packages/core/src/js/tools/sentryOptionsSerializer.ts +++ b/packages/core/src/js/tools/sentryOptionsSerializer.ts @@ -1,12 +1,10 @@ import * as path from 'path'; import * as fs from 'fs'; import { MetroConfig, Module } from 'metro'; -import { createSet, VirtualJSOutput } from './utils'; +import { createSet, MetroCustomSerializer, VirtualJSOutput } from './utils'; // eslint-disable-next-line import/no-extraneous-dependencies import * as countLines from 'metro/src/lib/countLines'; - -// TODO: move to utils -type MetroCustomSerializer = Required['serializer']>['customSerializer'] | undefined; +import { logger } from '@sentry/core'; const DEFAULT_OPTIONS_FILE_NAME = 'sentry.options.json'; @@ -33,7 +31,7 @@ export function withSentryOptionsFromFile(config: MetroConfig, optionsFile: stri const originalSerializer = config.serializer?.customSerializer; if (!originalSerializer) { // TODO: this works because we set Debug ID serializer in `withSentryDebugId` - // We should use the default serializer if non is provided + // We should use the default serializer if non is provided, for expo we know there always be a default custom serializer // eslint-disable-next-line no-console console.error( '[@sentry/react-native/metro] `config.serializer.customSerializer` is required to load Sentry options from a file', @@ -42,7 +40,10 @@ export function withSentryOptionsFromFile(config: MetroConfig, optionsFile: stri } const sentryOptionsSerializer: MetroCustomSerializer = (entryPoint, preModules, graph, options) => { - (preModules as Module[]).push(createSentryOptionsModule(optionsPath)); + const sentryOptionsModule = createSentryOptionsModule(optionsPath); + if (sentryOptionsModule) { + (preModules as Module[]).push(sentryOptionsModule); + } return originalSerializer(entryPoint, preModules, graph, options); }; @@ -55,21 +56,36 @@ export function withSentryOptionsFromFile(config: MetroConfig, optionsFile: stri }; } -function createSentryOptionsModule(filePath: string): Module & { setSource: (code: string) => void } { - // TODO: handle errors - const content = fs.readFileSync(filePath, 'utf8'); - const parsedContent = JSON.parse(content); +function createSentryOptionsModule(filePath: string): Module | null { + let content: string; + try { + content = fs.readFileSync(filePath, 'utf8'); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + logger.debug(`[@sentry/react-native/metro] Sentry options file does not exist at ${filePath}`); + } else { + logger.error(`[@sentry/react-native/metro] Failed to read Sentry options file at ${filePath}`); + } + return null; + } + + let parsedContent: Record; + try { + parsedContent = JSON.parse(content); + } catch (error) { + logger.error(`[@sentry/react-native/metro] Failed to parse Sentry options file at ${filePath}`); + return null; + } + const minifiedContent = JSON.stringify(parsedContent); let optionsCode = `var __SENTRY_OPTIONS__=${minifiedContent};`; + logger.debug(`[@sentry/react-native/metro] Sentry options added to the bundle from file at ${filePath}`); return { - setSource: (code: string) => { - optionsCode = code; - }, dependencies: new Map(), getSource: () => Buffer.from(optionsCode), inverseDependencies: createSet(), - path: '__SENTRY_OPTIONS__', + path: '__sentry-options__', output: [ { type: 'js/script/virtual', diff --git a/packages/core/src/js/tools/utils.ts b/packages/core/src/js/tools/utils.ts index 769dc9abd4..82ff4075e2 100644 --- a/packages/core/src/js/tools/utils.ts +++ b/packages/core/src/js/tools/utils.ts @@ -1,9 +1,11 @@ import * as crypto from 'crypto'; // eslint-disable-next-line import/no-extraneous-dependencies -import type { Module, ReadOnlyGraph, SerializerOptions } from 'metro'; +import type { MetroConfig, Module, ReadOnlyGraph, SerializerOptions } from 'metro'; // eslint-disable-next-line import/no-extraneous-dependencies import type CountingSet from 'metro/src/lib/CountingSet'; +export type MetroCustomSerializer = Required['serializer']>['customSerializer'] | undefined; + // Variant of MixedOutput // https://github.com/facebook/metro/blob/9b85f83c9cc837d8cd897aa7723be7da5b296067/packages/metro/src/DeltaBundler/types.flow.js#L21 export type VirtualJSOutput = { From 82f2a87b9a2b2b13e412da771944960696c9e2bb Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Thu, 23 Jan 2025 14:12:16 +0100 Subject: [PATCH 32/36] add json options for expo project --- samples/expo/sentry.options.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 samples/expo/sentry.options.json diff --git a/samples/expo/sentry.options.json b/samples/expo/sentry.options.json new file mode 100644 index 0000000000..53ae525bc0 --- /dev/null +++ b/samples/expo/sentry.options.json @@ -0,0 +1,18 @@ +{ + "dsn": "https://1df17bd4e543fdb31351dee1768bb679@o447951.ingest.sentry.io/5428561", + "debug": true, + "environment": "dev", + "enableUserInteractionTracing": true, + "enableAutoSessionTracking": true, + "sessionTrackingIntervalMillis": 30000, + "enableTracing": true, + "tracesSampleRate": 1.0, + "attachStacktrace": true, + "attachScreenshot": true, + "attachViewHierarchy": true, + "enableCaptureFailedRequests": true, + "profilesSampleRate": 1.0, + "replaysSessionSampleRate": 1.0, + "replaysOnErrorSampleRate": 1.0, + "spotlight": true +} From 21d2880877883e00e1d857f0503f77cb1f39af62 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Thu, 23 Jan 2025 14:36:48 +0100 Subject: [PATCH 33/36] fix bail todo --- packages/core/src/js/tools/sentryOptionsSerializer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/js/tools/sentryOptionsSerializer.ts b/packages/core/src/js/tools/sentryOptionsSerializer.ts index 13f7ed034e..c2dd33b0ea 100644 --- a/packages/core/src/js/tools/sentryOptionsSerializer.ts +++ b/packages/core/src/js/tools/sentryOptionsSerializer.ts @@ -30,8 +30,8 @@ export function withSentryOptionsFromFile(config: MetroConfig, optionsFile: stri const originalSerializer = config.serializer?.customSerializer; if (!originalSerializer) { - // TODO: this works because we set Debug ID serializer in `withSentryDebugId` - // We should use the default serializer if non is provided, for expo we know there always be a default custom serializer + // It's okay to bail here because we don't expose this for direct usage, but as part of `withSentryConfig` + // If used directly in RN, the user is responsible for providing a custom serializer first, Expo provides serializer in default config // eslint-disable-next-line no-console console.error( '[@sentry/react-native/metro] `config.serializer.customSerializer` is required to load Sentry options from a file', From 539892c9dd1a21babb4f572395d7467a68731deb Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Thu, 23 Jan 2025 14:36:54 +0100 Subject: [PATCH 34/36] fix lint --- packages/core/src/js/tools/metroconfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/js/tools/metroconfig.ts b/packages/core/src/js/tools/metroconfig.ts index df6389b77e..e0bd57c178 100644 --- a/packages/core/src/js/tools/metroconfig.ts +++ b/packages/core/src/js/tools/metroconfig.ts @@ -11,7 +11,7 @@ import type { DefaultConfigOptions } from './vendor/expo/expoconfig'; export * from './sentryMetroSerializer'; import { withSentryMiddleware } from './metroMiddleware'; import { withSentryOptionsFromFile } from './sentryOptionsSerializer'; -import { MetroCustomSerializer } from './utils'; +import type { MetroCustomSerializer } from './utils'; enableLogger(); From c4b2bdd1a1a6bd39fcd221de9156cd7fa73d416f Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Thu, 23 Jan 2025 18:14:56 +0100 Subject: [PATCH 35/36] add tests --- packages/core/jest.config.tools.js | 2 +- .../src/js/tools/sentryOptionsSerializer.ts | 22 +- .../tools/sentryOptionsSerializer.test.ts | 209 ++++++++++++++++++ 3 files changed, 223 insertions(+), 10 deletions(-) create mode 100644 packages/core/test/tools/sentryOptionsSerializer.test.ts diff --git a/packages/core/jest.config.tools.js b/packages/core/jest.config.tools.js index 5c5902d8a7..996ad05625 100644 --- a/packages/core/jest.config.tools.js +++ b/packages/core/jest.config.tools.js @@ -1,7 +1,7 @@ module.exports = { collectCoverage: true, preset: 'ts-jest', - setupFilesAfterEnv: ['/test/mockConsole.ts'], + setupFilesAfterEnv: ['jest-extended/all', '/test/mockConsole.ts'], globals: { __DEV__: true, }, diff --git a/packages/core/src/js/tools/sentryOptionsSerializer.ts b/packages/core/src/js/tools/sentryOptionsSerializer.ts index c2dd33b0ea..f2ab93b383 100644 --- a/packages/core/src/js/tools/sentryOptionsSerializer.ts +++ b/packages/core/src/js/tools/sentryOptionsSerializer.ts @@ -1,10 +1,12 @@ -import * as path from 'path'; +import { logger } from '@sentry/core'; import * as fs from 'fs'; -import { MetroConfig, Module } from 'metro'; -import { createSet, MetroCustomSerializer, VirtualJSOutput } from './utils'; +import type { MetroConfig, Module } from 'metro'; // eslint-disable-next-line import/no-extraneous-dependencies import * as countLines from 'metro/src/lib/countLines'; -import { logger } from '@sentry/core'; +import * as path from 'path'; + +import type { MetroCustomSerializer, VirtualJSOutput } from './utils'; +import { createSet } from './utils'; const DEFAULT_OPTIONS_FILE_NAME = 'sentry.options.json'; @@ -23,10 +25,12 @@ export function withSentryOptionsFromFile(config: MetroConfig, optionsFile: stri return config; } - const optionsPath = - typeof optionsFile === 'string' - ? path.join(projectRoot, optionsFile) - : path.join(projectRoot, DEFAULT_OPTIONS_FILE_NAME); + let optionsPath = path.join(projectRoot, DEFAULT_OPTIONS_FILE_NAME); + if (typeof optionsFile === 'string' && path.isAbsolute(optionsFile)) { + optionsPath = optionsFile; + } else if (typeof optionsFile === 'string') { + optionsPath = path.join(projectRoot, optionsFile); + } const originalSerializer = config.serializer?.customSerializer; if (!originalSerializer) { @@ -78,7 +82,7 @@ function createSentryOptionsModule(filePath: string): Module | } const minifiedContent = JSON.stringify(parsedContent); - let optionsCode = `var __SENTRY_OPTIONS__=${minifiedContent};`; + const optionsCode = `var __SENTRY_OPTIONS__=${minifiedContent};`; logger.debug(`[@sentry/react-native/metro] Sentry options added to the bundle from file at ${filePath}`); return { diff --git a/packages/core/test/tools/sentryOptionsSerializer.test.ts b/packages/core/test/tools/sentryOptionsSerializer.test.ts new file mode 100644 index 0000000000..ed946d098a --- /dev/null +++ b/packages/core/test/tools/sentryOptionsSerializer.test.ts @@ -0,0 +1,209 @@ +import { logger } from '@sentry/core'; +import * as fs from 'fs'; +import type { Graph, Module, SerializerOptions } from 'metro'; + +import { withSentryOptionsFromFile } from '../../src/js/tools/sentryOptionsSerializer'; +import { createSet } from '../../src/js/tools/utils'; + +jest.mock('fs', () => ({ + readFileSync: jest.fn(), +})); + +const consoleErrorSpy = jest.spyOn(console, 'error'); +const loggerDebugSpy = jest.spyOn(logger, 'debug'); +const loggerErrorSpy = jest.spyOn(logger, 'error'); + +const customSerializerMock = jest.fn(); +let mockedPreModules: Module[] = []; + +describe('Sentry Options Serializer', () => { + beforeEach(() => { + jest.resetAllMocks(); + mockedPreModules = createMockedPreModules(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + test('returns original config if optionsFile is false', () => { + const config = () => ({ + projectRoot: '/test', + serializer: { + customSerializer: customSerializerMock, + }, + }); + + const result = withSentryOptionsFromFile(config(), false); + expect(result).toEqual(config()); + }); + + test('logs error and returns original config if projectRoot is missing', () => { + const config = () => ({ + serializer: { + customSerializer: customSerializerMock, + }, + }); + + const result = withSentryOptionsFromFile(config(), true); + + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Project root is required')); + expect(result).toEqual(config()); + }); + + test('logs error and returns original config if customSerializer is missing', () => { + const config = () => ({ + projectRoot: '/test', + serializer: {}, + }); + const consoleErrorSpy = jest.spyOn(console, 'error'); + + const result = withSentryOptionsFromFile(config(), true); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining('`config.serializer.customSerializer` is required'), + ); + expect(result).toEqual(config()); + }); + + test('adds sentry options module when file exists and is valid JSON', () => { + const config = () => ({ + projectRoot: '/test', + serializer: { + customSerializer: customSerializerMock, + }, + }); + + const mockOptions = { test: 'value' }; + (fs.readFileSync as jest.Mock).mockReturnValue(JSON.stringify(mockOptions)); + + const actualConfig = withSentryOptionsFromFile(config(), true); + actualConfig.serializer?.customSerializer(null, mockedPreModules, null, null); + + expect(mockedPreModules).toHaveLength(2); + expect(mockedPreModules.at(-1)).toEqual( + expect.objectContaining({ + getSource: expect.any(Function), + path: '__sentry-options__', + output: [ + { + type: 'js/script/virtual', + data: { + code: 'var __SENTRY_OPTIONS__={"test":"value"};', + lineCount: 1, + map: [], + }, + }, + ], + }), + ); + expect(mockedPreModules.at(-1).getSource().toString()).toEqual(mockedPreModules.at(-1).output[0].data.code); + expect(loggerDebugSpy).toHaveBeenCalledWith(expect.stringContaining('options added to the bundle')); + }); + + test('logs error and does not add module when file does not exist', () => { + const config = () => ({ + projectRoot: '/test', + serializer: { + customSerializer: customSerializerMock, + }, + }); + + (fs.readFileSync as jest.Mock).mockImplementation(() => { + throw { code: 'ENOENT' }; + }); + + const actualConfig = withSentryOptionsFromFile(config(), true); + actualConfig.serializer?.customSerializer(null, mockedPreModules, null, null); + + expect(loggerDebugSpy).toHaveBeenCalledWith(expect.stringContaining('options file does not exist')); + expect(mockedPreModules).toMatchObject(createMockedPreModules()); + }); + + test('logs error and does not add module when file contains invalid JSON', () => { + const config = () => ({ + projectRoot: '/test', + serializer: { + customSerializer: customSerializerMock, + }, + }); + + (fs.readFileSync as jest.Mock).mockReturnValue('invalid json'); + + const actualConfig = withSentryOptionsFromFile(config(), true); + actualConfig.serializer?.customSerializer(null, mockedPreModules, null, null); + + expect(loggerErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to parse Sentry options file')); + expect(mockedPreModules).toMatchObject(createMockedPreModules()); + }); + + test('calls original serializer with correct arguments and returns its result', () => { + const mockedEntryPoint = 'entryPoint'; + const mockedGraph: Graph = jest.fn() as unknown as Graph; + const mockedOptions: SerializerOptions = jest.fn() as unknown as SerializerOptions; + const mockedResult = {}; + const originalSerializer = jest.fn().mockReturnValue(mockedResult); + + const actualConfig = withSentryOptionsFromFile( + { + projectRoot: '/test', + serializer: { + customSerializer: originalSerializer, + }, + }, + true, + ); + const actualResult = actualConfig.serializer?.customSerializer( + mockedEntryPoint, + mockedPreModules, + mockedGraph, + mockedOptions, + ); + + expect(originalSerializer).toHaveBeenCalledWith(mockedEntryPoint, mockedPreModules, mockedGraph, mockedOptions); + expect(actualResult).toEqual(mockedResult); + }); + + test('uses custom file path when optionsFile is a string', () => { + const config = () => ({ + projectRoot: '/test', + serializer: { + customSerializer: customSerializerMock, + }, + }); + + withSentryOptionsFromFile(config(), 'custom/path.json').serializer?.customSerializer( + null, + mockedPreModules, + null, + null, + ); + withSentryOptionsFromFile(config(), '/absolute/path.json').serializer?.customSerializer( + null, + mockedPreModules, + null, + null, + ); + + expect(fs.readFileSync).toHaveBeenCalledWith('/test/custom/path.json', expect.anything()); + expect(fs.readFileSync).toHaveBeenCalledWith('/absolute/path.json', expect.anything()); + }); +}); + +function createMockedPreModules(): Module[] { + return [createMinimalModule()]; +} + +function createMinimalModule(): Module { + return { + dependencies: new Map(), + getSource: getEmptySource, + inverseDependencies: createSet(), + path: '__sentry-options__', + output: [], + }; +} + +function getEmptySource(): Buffer { + return Buffer.from(''); +} From 2cdcd603aacca69adf77362d817ebc0cd7d4dc81 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Mon, 3 Feb 2025 11:54:58 +0100 Subject: [PATCH 36/36] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef2e9ed714..6a73f7f4ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ ## Unreleased +### Features + +- Load `optionsFile` into the JS bundle during Metro bundle process ([#4476](https://github.com/getsentry/sentry-react-native/pull/4476)) + ### Fixes - Add mechanism field to unhandled rejection errors ([#4457](https://github.com/getsentry/sentry-react-native/pull/4457))