diff --git a/CHANGELOG.md b/CHANGELOG.md index 127b921ea2..2e19b0d0b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ - Fix compatibility with `react-native-legal` ([#5253](https://github.com/getsentry/sentry-react-native/pull/5253)) - The licenses json file is correctly generated and placed into the `res/` folder now - Handle missing shouldAddToIgnoreList callback in Metro ([#5260](https://github.com/getsentry/sentry-react-native/pull/5260)) +- Overrides the default Cocoa SDK behavior that disables Session Replay on iOS 26.0 ([#5268](https://github.com/getsentry/sentry-react-native/pull/5268)) + - If you are using Apple's Liquid Glass we recommend that you disable Session Replay on iOS to prevent potential PII leaks (see [sentry-cocoa 8.57.0 release note warning](https://github.com/getsentry/sentry-cocoa/releases/tag/8.57.0)) ### Dependencies diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m index 4c9a9d1e04..1929c12b7b 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m @@ -736,4 +736,129 @@ - (void)testIgnoreErrorsRegexAndStringBothWork XCTAssertNotNil(result3, @"Event with non-matching error should not be dropped"); } +- (void)testCreateOptionsWithDictionaryEnableSessionReplayInUnreliableEnvironmentDefault +{ + RNSentry *rnSentry = [[RNSentry alloc] init]; + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + }; + SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary + error:&error]; + + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + + id experimentalOptions = [actualOptions valueForKey:@"experimental"]; + XCTAssertNotNil(experimentalOptions, @"Experimental options should not be nil"); + + BOOL enableUnhandledCPPExceptions = + [[experimentalOptions valueForKey:@"enableSessionReplayInUnreliableEnvironment"] boolValue]; + XCTAssertFalse(enableUnhandledCPPExceptions, + @"enableSessionReplayInUnreliableEnvironment should be disabled"); +} + +- (void)testCreateOptionsWithDictionaryEnableSessionReplayInUnreliableEnvironmentWithErrorSampleRate +{ + RNSentry *rnSentry = [[RNSentry alloc] init]; + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + @"replaysOnErrorSampleRate" : @1.0, + @"replaysSessionSampleRate" : @0 + }; + SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary + error:&error]; + + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + + id experimentalOptions = [actualOptions valueForKey:@"experimental"]; + XCTAssertNotNil(experimentalOptions, @"Experimental options should not be nil"); + + BOOL enableUnhandledCPPExceptions = + [[experimentalOptions valueForKey:@"enableSessionReplayInUnreliableEnvironment"] boolValue]; + XCTAssertTrue(enableUnhandledCPPExceptions, + @"enableSessionReplayInUnreliableEnvironment should be enabled"); +} + +- (void) + testCreateOptionsWithDictionaryEnableSessionReplayInUnreliableEnvironmentWithSessionSampleRate +{ + RNSentry *rnSentry = [[RNSentry alloc] init]; + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + @"replaysOnErrorSampleRate" : @0.0, + @"replaysSessionSampleRate" : @0.1 + }; + SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary + error:&error]; + + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + + id experimentalOptions = [actualOptions valueForKey:@"experimental"]; + XCTAssertNotNil(experimentalOptions, @"Experimental options should not be nil"); + + BOOL enableUnhandledCPPExceptions = + [[experimentalOptions valueForKey:@"enableSessionReplayInUnreliableEnvironment"] boolValue]; + XCTAssertTrue(enableUnhandledCPPExceptions, + @"enableSessionReplayInUnreliableEnvironment should be enabled"); +} + +- (void) + testCreateOptionsWithDictionaryEnableSessionReplayInUnreliableEnvironmentWithSessionSampleRates +{ + RNSentry *rnSentry = [[RNSentry alloc] init]; + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + @"replaysOnErrorSampleRate" : @1.0, + @"replaysSessionSampleRate" : @0.1 + }; + SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary + error:&error]; + + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + + id experimentalOptions = [actualOptions valueForKey:@"experimental"]; + XCTAssertNotNil(experimentalOptions, @"Experimental options should not be nil"); + + BOOL enableUnhandledCPPExceptions = + [[experimentalOptions valueForKey:@"enableSessionReplayInUnreliableEnvironment"] boolValue]; + XCTAssertTrue(enableUnhandledCPPExceptions, + @"enableSessionReplayInUnreliableEnvironment should be enabled"); +} + +- (void)testCreateOptionsWithDictionaryEnableSessionReplayInUnreliableEnvironmentDisabled +{ + RNSentry *rnSentry = [[RNSentry alloc] init]; + NSError *error = nil; + + NSDictionary *_Nonnull mockedReactNativeDictionary = @{ + @"dsn" : @"https://abcd@efgh.ingest.sentry.io/123456", + @"replaysOnErrorSampleRate" : @0, + @"replaysSessionSampleRate" : @0 + }; + SentryOptions *actualOptions = [rnSentry createOptionsWithDictionary:mockedReactNativeDictionary + error:&error]; + + XCTAssertNotNil(actualOptions, @"Did not create sentry options"); + XCTAssertNil(error, @"Should not pass no error"); + + id experimentalOptions = [actualOptions valueForKey:@"experimental"]; + XCTAssertNotNil(experimentalOptions, @"Experimental options should not be nil"); + + BOOL enableUnhandledCPPExceptions = + [[experimentalOptions valueForKey:@"enableSessionReplayInUnreliableEnvironment"] boolValue]; + XCTAssertFalse(enableUnhandledCPPExceptions, + @"enableSessionReplayInUnreliableEnvironment should be disabled"); +} + @end diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index 2f0f3cb529..c47cf779c0 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -234,7 +234,10 @@ - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) [mutableOptions removeObjectForKey:@"enableTracing"]; #if SENTRY_TARGET_REPLAY_SUPPORTED - [RNSentryReplay updateOptions:mutableOptions]; + BOOL isSessionReplayEnabled = [RNSentryReplay updateOptions:mutableOptions]; +#else + // Defaulting to false for unsupported targets + BOOL isSessionReplayEnabled = NO; #endif SentryOptions *sentryOptions = [SentryOptionsInternal initWithDict:mutableOptions @@ -315,6 +318,11 @@ - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) sentryOptions:sentryOptions]; } + if (isSessionReplayEnabled) { + [RNSentryExperimentalOptions setEnableSessionReplayInUnreliableEnvironment:YES + sentryOptions:sentryOptions]; + } + return sentryOptions; } diff --git a/packages/core/ios/RNSentryExperimentalOptions.h b/packages/core/ios/RNSentryExperimentalOptions.h index 05fab2ddef..ec0501cb05 100644 --- a/packages/core/ios/RNSentryExperimentalOptions.h +++ b/packages/core/ios/RNSentryExperimentalOptions.h @@ -28,6 +28,15 @@ NS_ASSUME_NONNULL_BEGIN */ + (void)setEnableLogs:(BOOL)enabled sentryOptions:(SentryOptions *)sentryOptions; +/** + * Sets the enableSessionReplayInUnreliableEnvironment experimental option on SentryOptions + * @param sentryOptions The SentryOptions instance to configure + * @param enabled Whether enableSessionReplayInUnreliableEnvironment from sentry Cocoa should be + * enabled + */ ++ (void)setEnableSessionReplayInUnreliableEnvironment:(BOOL)enabled + sentryOptions:(SentryOptions *)sentryOptions; + @end NS_ASSUME_NONNULL_END diff --git a/packages/core/ios/RNSentryExperimentalOptions.m b/packages/core/ios/RNSentryExperimentalOptions.m index 2e7db99a71..7e0974e527 100644 --- a/packages/core/ios/RNSentryExperimentalOptions.m +++ b/packages/core/ios/RNSentryExperimentalOptions.m @@ -27,4 +27,13 @@ + (void)setEnableLogs:(BOOL)enabled sentryOptions:(SentryOptions *)sentryOptions sentryOptions.experimental.enableLogs = enabled; } ++ (void)setEnableSessionReplayInUnreliableEnvironment:(BOOL)enabled + sentryOptions:(SentryOptions *)sentryOptions +{ + if (sentryOptions == nil) { + return; + } + sentryOptions.experimental.enableSessionReplayInUnreliableEnvironment = enabled; +} + @end diff --git a/packages/core/ios/RNSentryReplay.h b/packages/core/ios/RNSentryReplay.h index 452914af15..cda7035550 100644 --- a/packages/core/ios/RNSentryReplay.h +++ b/packages/core/ios/RNSentryReplay.h @@ -1,7 +1,11 @@ @interface RNSentryReplay : NSObject -+ (void)updateOptions:(NSMutableDictionary *)options; +/** + * Updates the session replay options + * @return true when session replay is enabled + */ ++ (BOOL)updateOptions:(NSMutableDictionary *)options; + (void)postInit; diff --git a/packages/core/ios/RNSentryReplay.mm b/packages/core/ios/RNSentryReplay.mm index 263ce6f6cb..94fa30b4e4 100644 --- a/packages/core/ios/RNSentryReplay.mm +++ b/packages/core/ios/RNSentryReplay.mm @@ -12,12 +12,14 @@ @implementation RNSentryReplay { } -+ (void)updateOptions:(NSMutableDictionary *)options ++ (BOOL)updateOptions:(NSMutableDictionary *)options { - if (options[@"replaysSessionSampleRate"] == nil - && options[@"replaysOnErrorSampleRate"] == nil) { + NSNumber *sessionSampleRate = options[@"replaysSessionSampleRate"]; + NSNumber *errorSampleRate = options[@"replaysOnErrorSampleRate"]; + + if (sessionSampleRate == nil && errorSampleRate == nil) { NSLog(@"Session replay disabled via configuration"); - return; + return NO; } NSLog(@"Setting up session replay"); @@ -26,8 +28,8 @@ + (void)updateOptions:(NSMutableDictionary *)options NSString *qualityString = options[@"replaysSessionQuality"]; [options setValue:@{ - @"sessionSampleRate" : options[@"replaysSessionSampleRate"] ?: [NSNull null], - @"errorSampleRate" : options[@"replaysOnErrorSampleRate"] ?: [NSNull null], + @"sessionSampleRate" : sessionSampleRate ?: [NSNull null], + @"errorSampleRate" : errorSampleRate ?: [NSNull null], @"quality" : @([RNSentryReplayQuality parseReplayQuality:qualityString]), @"maskAllImages" : replayOptions[@"maskAllImages"] ?: [NSNull null], @"maskAllText" : replayOptions[@"maskAllText"] ?: [NSNull null], @@ -38,6 +40,8 @@ + (void)updateOptions:(NSMutableDictionary *)options @ { @"name" : REACT_NATIVE_SDK_NAME, @"version" : REACT_NATIVE_SDK_PACKAGE_VERSION } } forKey:@"sessionReplay"]; + return (errorSampleRate != nil && [errorSampleRate doubleValue] > 0) + || (sessionSampleRate != nil && [sessionSampleRate doubleValue] > 0); } + (NSArray *_Nonnull)getReplayRNRedactClasses:(NSDictionary *_Nullable)replayOptions