diff --git a/CHANGELOG.md b/CHANGELOG.md index 50b001fa5..733381a95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,26 @@ ### Features +- Mobile Session Replay is now generally available and ready for production use ([#4384](https://github.com/getsentry/sentry-react-native/pull/4384)) + + To learn about privacy, custom masking or performance overhead visit [the documentation](https://docs.sentry.io/platforms/react-native/session-replay/). + + ```js + import * as Sentry from '@sentry/react-native'; + + Sentry.init({ + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 1.0, + integrations: [ + Sentry.mobileReplayIntegration({ + maskAllImages: true, + maskAllVectors: true, + maskAllText: true, + }), + ], + }); + ``` + - Adds new `captureFeedback` and deprecates the `captureUserFeedback` API ([#4320](https://github.com/getsentry/sentry-react-native/pull/4320)) ```jsx @@ -40,21 +60,22 @@ ### Changes - Falsy values of `options.environment` (empty string, undefined...) default to `production` +- Deprecated `_experiments.replaysSessionSampleRate` and `_experiments.replaysOnErrorSampleRate` use `replaysSessionSampleRate` and `replaysOnErrorSampleRate` ([#4384](https://github.com/getsentry/sentry-react-native/pull/4384)) ### Dependencies - Bump CLI from v2.38.2 to v2.39.1 ([#4305](https://github.com/getsentry/sentry-react-native/pull/4305), [#4316](https://github.com/getsentry/sentry-react-native/pull/4316)) - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2391) - [diff](https://github.com/getsentry/sentry-cli/compare/2.38.2...2.39.1) -- Bump Android SDK from v7.18.0 to v7.19.1 ([#4329](https://github.com/getsentry/sentry-react-native/pull/4329), [#4365](https://github.com/getsentry/sentry-react-native/pull/4365), [#4405](https://github.com/getsentry/sentry-react-native/pull/4405)) - - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#7191) - - [diff](https://github.com/getsentry/sentry-java/compare/7.18.0...7.19.1) +- Bump Android SDK from v7.18.0 to v7.20.0 ([#4329](https://github.com/getsentry/sentry-react-native/pull/4329), [#4365](https://github.com/getsentry/sentry-react-native/pull/4365), [#4405](https://github.com/getsentry/sentry-react-native/pull/4405), [#4411](https://github.com/getsentry/sentry-react-native/pull/4411)) + - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#7200) + - [diff](https://github.com/getsentry/sentry-java/compare/7.18.0...7.20.0) - Bump JavaScript SDK from v8.40.0 to v8.47.0 ([#4351](https://github.com/getsentry/sentry-react-native/pull/4351), [#4325](https://github.com/getsentry/sentry-react-native/pull/4325), [#4371](https://github.com/getsentry/sentry-react-native/pull/4371), [#4382](https://github.com/getsentry/sentry-react-native/pull/4382), [#4388](https://github.com/getsentry/sentry-react-native/pull/4388), [#4393](https://github.com/getsentry/sentry-react-native/pull/4393)) - [changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md#8470) - [diff](https://github.com/getsentry/sentry-javascript/compare/8.40.0...8.47.0) -- Bump Cocoa SDK from v8.41.0 to v8.42.1 ([#4387](https://github.com/getsentry/sentry-react-native/pull/4387), [#4399](https://github.com/getsentry/sentry-react-native/pull/4399)) - - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8421) - - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.41.0...8.42.1) +- Bump Cocoa SDK from v8.41.0 to v8.43.0 ([#4387](https://github.com/getsentry/sentry-react-native/pull/4387), [#4399](https://github.com/getsentry/sentry-react-native/pull/4399), [#4410](https://github.com/getsentry/sentry-react-native/pull/4410)) + - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8430) + - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.41.0...8.43.0) ## 6.4.0 diff --git a/packages/core/RNSentry.podspec b/packages/core/RNSentry.podspec index 449d19587..97c4fd315 100644 --- a/packages/core/RNSentry.podspec +++ b/packages/core/RNSentry.podspec @@ -37,7 +37,7 @@ Pod::Spec.new do |s| s.compiler_flags = other_cflags - s.dependency 'Sentry/HybridSDK', '8.42.1' + s.dependency 'Sentry/HybridSDK', '8.43.0' if defined? install_modules_dependencies # Default React Native dependencies for 0.71 and above (new and legacy architecture) diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift index 4e8b35c47..936d55bb1 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift @@ -10,24 +10,14 @@ final class RNSentryReplayOptions: XCTestCase { XCTAssertEqual(optionsDict.count, 0) } - func testExperimentalOptionsWithoutReplaySampleRatesAreRemoved() { - let optionsDict = (["_experiments": [:]] as NSDictionary).mutableCopy() as! NSMutableDictionary - RNSentryReplay.updateOptions(optionsDict) - - XCTAssertEqual(optionsDict.count, 0) - } - func testReplayOptionsDictContainsAllOptionsKeysWhenSessionSampleRateUsed() { let optionsDict = ([ "dsn": "https://abc@def.ingest.sentry.io/1234567", - "_experiments": [ - "replaysSessionSampleRate": 0.75 - ] + "replaysSessionSampleRate": 0.75 ] as NSDictionary).mutableCopy() as! NSMutableDictionary RNSentryReplay.updateOptions(optionsDict) - let experimental = optionsDict["experimental"] as! [String: Any] - let sessionReplay = experimental["sessionReplay"] as! [String: Any] + let sessionReplay = optionsDict["sessionReplay"] as! [String: Any] assertAllDefaultReplayOptionsAreNotNil(replayOptions: sessionReplay) } @@ -35,14 +25,11 @@ final class RNSentryReplayOptions: XCTestCase { func testReplayOptionsDictContainsAllOptionsKeysWhenErrorSampleRateUsed() { let optionsDict = ([ "dsn": "https://abc@def.ingest.sentry.io/1234567", - "_experiments": [ - "replaysOnErrorSampleRate": 0.75 - ] + "replaysOnErrorSampleRate": 0.75 ] as NSDictionary).mutableCopy() as! NSMutableDictionary RNSentryReplay.updateOptions(optionsDict) - let experimental = optionsDict["experimental"] as! [String: Any] - let sessionReplay = experimental["sessionReplay"] as! [String: Any] + let sessionReplay = optionsDict["sessionReplay"] as! [String: Any] assertAllDefaultReplayOptionsAreNotNil(replayOptions: sessionReplay) } @@ -50,15 +37,12 @@ final class RNSentryReplayOptions: XCTestCase { func testReplayOptionsDictContainsAllOptionsKeysWhenErrorAndSessionSampleRatesUsed() { let optionsDict = ([ "dsn": "https://abc@def.ingest.sentry.io/1234567", - "_experiments": [ - "replaysOnErrorSampleRate": 0.75, - "replaysSessionSampleRate": 0.75 - ] + "replaysOnErrorSampleRate": 0.75, + "replaysSessionSampleRate": 0.75 ] as NSDictionary).mutableCopy() as! NSMutableDictionary RNSentryReplay.updateOptions(optionsDict) - let experimental = optionsDict["experimental"] as! [String: Any] - let sessionReplay = experimental["sessionReplay"] as! [String: Any] + let sessionReplay = optionsDict["sessionReplay"] as! [String: Any] assertAllDefaultReplayOptionsAreNotNil(replayOptions: sessionReplay) } @@ -75,38 +59,37 @@ final class RNSentryReplayOptions: XCTestCase { func testSessionSampleRate() { let optionsDict = ([ "dsn": "https://abc@def.ingest.sentry.io/1234567", - "_experiments": [ "replaysSessionSampleRate": 0.75 ] + "replaysSessionSampleRate": 0.75 ] as NSDictionary).mutableCopy() as! NSMutableDictionary RNSentryReplay.updateOptions(optionsDict) let actualOptions = try! Options(dict: optionsDict as! [String: Any]) - XCTAssertEqual(actualOptions.experimental.sessionReplay.sessionSampleRate, 0.75) + XCTAssertEqual(actualOptions.sessionReplay.sessionSampleRate, 0.75) } func testOnErrorSampleRate() { let optionsDict = ([ "dsn": "https://abc@def.ingest.sentry.io/1234567", - "_experiments": [ "replaysOnErrorSampleRate": 0.75 ] + "replaysOnErrorSampleRate": 0.75 ] as NSDictionary).mutableCopy() as! NSMutableDictionary RNSentryReplay.updateOptions(optionsDict) let actualOptions = try! Options(dict: optionsDict as! [String: Any]) - XCTAssertEqual(actualOptions.experimental.sessionReplay.onErrorSampleRate, 0.75) + XCTAssertEqual(actualOptions.sessionReplay.onErrorSampleRate, 0.75) } func testMaskAllVectors() { let optionsDict = ([ "dsn": "https://abc@def.ingest.sentry.io/1234567", - "_experiments": [ "replaysOnErrorSampleRate": 0.75 ], + "replaysOnErrorSampleRate": 0.75, "mobileReplayOptions": [ "maskAllVectors": true ] ] as NSDictionary).mutableCopy() as! NSMutableDictionary RNSentryReplay.updateOptions(optionsDict) - XCTAssertEqual(optionsDict.count, 3) + XCTAssertEqual(optionsDict.count, 4) - let experimental = optionsDict["experimental"] as! [String: Any] - let sessionReplay = experimental["sessionReplay"] as! [String: Any] + let sessionReplay = optionsDict["sessionReplay"] as! [String: Any] let maskedViewClasses = sessionReplay["maskedViewClasses"] as! [String] XCTAssertTrue(maskedViewClasses.contains("RNSVGSvgView")) @@ -115,7 +98,7 @@ final class RNSentryReplayOptions: XCTestCase { func testMaskAllImages() { let optionsDict = ([ "dsn": "https://abc@def.ingest.sentry.io/1234567", - "_experiments": [ "replaysOnErrorSampleRate": 0.75 ], + "replaysOnErrorSampleRate": 0.75, "mobileReplayOptions": [ "maskAllImages": true ] ] as NSDictionary).mutableCopy() as! NSMutableDictionary @@ -123,14 +106,14 @@ final class RNSentryReplayOptions: XCTestCase { let actualOptions = try! Options(dict: optionsDict as! [String: Any]) - XCTAssertEqual(actualOptions.experimental.sessionReplay.maskAllImages, true) - assertContainsClass(classArray: actualOptions.experimental.sessionReplay.maskedViewClasses, stringClass: "RCTImageView") + XCTAssertEqual(actualOptions.sessionReplay.maskAllImages, true) + assertContainsClass(classArray: actualOptions.sessionReplay.maskedViewClasses, stringClass: "RCTImageView") } func testMaskAllImagesFalse() { let optionsDict = ([ "dsn": "https://abc@def.ingest.sentry.io/1234567", - "_experiments": [ "replaysOnErrorSampleRate": 0.75 ], + "replaysOnErrorSampleRate": 0.75, "mobileReplayOptions": [ "maskAllImages": false ] ] as NSDictionary).mutableCopy() as! NSMutableDictionary @@ -138,14 +121,14 @@ final class RNSentryReplayOptions: XCTestCase { let actualOptions = try! Options(dict: optionsDict as! [String: Any]) - XCTAssertEqual(actualOptions.experimental.sessionReplay.maskAllImages, false) - XCTAssertEqual(actualOptions.experimental.sessionReplay.maskedViewClasses.count, 0) + XCTAssertEqual(actualOptions.sessionReplay.maskAllImages, false) + XCTAssertEqual(actualOptions.sessionReplay.maskedViewClasses.count, 0) } func testMaskAllText() { let optionsDict = ([ "dsn": "https://abc@def.ingest.sentry.io/1234567", - "_experiments": [ "replaysOnErrorSampleRate": 0.75 ], + "replaysOnErrorSampleRate": 0.75, "mobileReplayOptions": [ "maskAllText": true ] ] as NSDictionary).mutableCopy() as! NSMutableDictionary @@ -153,9 +136,9 @@ final class RNSentryReplayOptions: XCTestCase { let actualOptions = try! Options(dict: optionsDict as! [String: Any]) - XCTAssertEqual(actualOptions.experimental.sessionReplay.maskAllText, true) - assertContainsClass(classArray: actualOptions.experimental.sessionReplay.maskedViewClasses, stringClass: "RCTTextView") - assertContainsClass(classArray: actualOptions.experimental.sessionReplay.maskedViewClasses, stringClass: "RCTParagraphComponentView") + XCTAssertEqual(actualOptions.sessionReplay.maskAllText, true) + assertContainsClass(classArray: actualOptions.sessionReplay.maskedViewClasses, stringClass: "RCTTextView") + assertContainsClass(classArray: actualOptions.sessionReplay.maskedViewClasses, stringClass: "RCTParagraphComponentView") } func assertContainsClass(classArray: [AnyClass], stringClass: String) { @@ -169,7 +152,7 @@ final class RNSentryReplayOptions: XCTestCase { func testMaskAllTextFalse() { let optionsDict = ([ "dsn": "https://abc@def.ingest.sentry.io/1234567", - "_experiments": [ "replaysOnErrorSampleRate": 0.75 ], + "replaysOnErrorSampleRate": 0.75, "mobileReplayOptions": [ "maskAllText": false ] ] as NSDictionary).mutableCopy() as! NSMutableDictionary @@ -177,8 +160,8 @@ final class RNSentryReplayOptions: XCTestCase { let actualOptions = try! Options(dict: optionsDict as! [String: Any]) - XCTAssertEqual(actualOptions.experimental.sessionReplay.maskAllText, false) - XCTAssertEqual(actualOptions.experimental.sessionReplay.maskedViewClasses.count, 0) + XCTAssertEqual(actualOptions.sessionReplay.maskAllText, false) + XCTAssertEqual(actualOptions.sessionReplay.maskedViewClasses.count, 0) } } diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index 0349c9f18..d352df859 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -54,5 +54,5 @@ android { dependencies { implementation 'com.facebook.react:react-native:+' - api 'io.sentry:sentry-android:7.19.1' + api 'io.sentry:sentry-android:7.20.0' } diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index b39261aca..b3be280d8 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -277,8 +277,10 @@ protected void getSentryAndroidOptions( options.setSpotlightConnectionUrl(rnOptions.getString("spotlight")); } } - if (rnOptions.hasKey("_experiments")) { - options.getExperimental().setSessionReplay(getReplayOptions(rnOptions)); + + SentryReplayOptions replayOptions = getReplayOptions(rnOptions); + options.setSessionReplay(replayOptions); + if (isReplayEnabled(replayOptions)) { options.getReplayController().setBreadcrumbConverter(new RNSentryReplayBreadcrumbConverter()); } @@ -330,26 +332,32 @@ protected void getSentryAndroidOptions( } } - private SentryReplayOptions getReplayOptions(@NotNull ReadableMap rnOptions) { - @NotNull final SentryReplayOptions androidReplayOptions = new SentryReplayOptions(false); - - @Nullable final ReadableMap rnExperimentsOptions = rnOptions.getMap("_experiments"); - if (rnExperimentsOptions == null) { - return androidReplayOptions; - } + private boolean isReplayEnabled(SentryReplayOptions replayOptions) { + return replayOptions.getSessionSampleRate() != null + || replayOptions.getOnErrorSampleRate() != null; + } - if (!(rnExperimentsOptions.hasKey("replaysSessionSampleRate") - || rnExperimentsOptions.hasKey("replaysOnErrorSampleRate"))) { + private SentryReplayOptions getReplayOptions(@NotNull ReadableMap rnOptions) { + final SdkVersion replaySdkVersion = + new SdkVersion( + RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_NAME, + RNSentryVersion.REACT_NATIVE_SDK_PACKAGE_VERSION); + @NotNull + final SentryReplayOptions androidReplayOptions = + new SentryReplayOptions(false, replaySdkVersion); + + if (!(rnOptions.hasKey("replaysSessionSampleRate") + || rnOptions.hasKey("replaysOnErrorSampleRate"))) { return androidReplayOptions; } androidReplayOptions.setSessionSampleRate( - rnExperimentsOptions.hasKey("replaysSessionSampleRate") - ? rnExperimentsOptions.getDouble("replaysSessionSampleRate") + rnOptions.hasKey("replaysSessionSampleRate") + ? rnOptions.getDouble("replaysSessionSampleRate") : null); androidReplayOptions.setOnErrorSampleRate( - rnExperimentsOptions.hasKey("replaysOnErrorSampleRate") - ? rnExperimentsOptions.getDouble("replaysOnErrorSampleRate") + rnOptions.hasKey("replaysOnErrorSampleRate") + ? rnOptions.getDouble("replaysOnErrorSampleRate") : null); if (!rnOptions.hasKey("mobileReplayOptions")) { diff --git a/packages/core/ios/RNSentryReplay.mm b/packages/core/ios/RNSentryReplay.mm index 8c941741c..2b7a992a3 100644 --- a/packages/core/ios/RNSentryReplay.mm +++ b/packages/core/ios/RNSentryReplay.mm @@ -12,15 +12,8 @@ @implementation RNSentryReplay { + (void)updateOptions:(NSMutableDictionary *)options { - NSDictionary *experiments = options[@"_experiments"]; - [options removeObjectForKey:@"_experiments"]; - if (experiments == nil) { - NSLog(@"Session replay disabled via configuration"); - return; - } - - if (experiments[@"replaysSessionSampleRate"] == nil - && experiments[@"replaysOnErrorSampleRate"] == nil) { + if (options[@"replaysSessionSampleRate"] == nil + && options[@"replaysOnErrorSampleRate"] == nil) { NSLog(@"Session replay disabled via configuration"); return; } @@ -29,15 +22,13 @@ + (void)updateOptions:(NSMutableDictionary *)options NSDictionary *replayOptions = options[@"mobileReplayOptions"] ?: @{}; [options setValue:@{ - @"sessionReplay" : @ { - @"sessionSampleRate" : experiments[@"replaysSessionSampleRate"] ?: [NSNull null], - @"errorSampleRate" : experiments[@"replaysOnErrorSampleRate"] ?: [NSNull null], - @"maskAllImages" : replayOptions[@"maskAllImages"] ?: [NSNull null], - @"maskAllText" : replayOptions[@"maskAllText"] ?: [NSNull null], - @"maskedViewClasses" : [RNSentryReplay getReplayRNRedactClasses:replayOptions], - } + @"sessionSampleRate" : options[@"replaysSessionSampleRate"] ?: [NSNull null], + @"errorSampleRate" : options[@"replaysOnErrorSampleRate"] ?: [NSNull null], + @"maskAllImages" : replayOptions[@"maskAllImages"] ?: [NSNull null], + @"maskAllText" : replayOptions[@"maskAllText"] ?: [NSNull null], + @"maskedViewClasses" : [RNSentryReplay getReplayRNRedactClasses:replayOptions], } - forKey:@"experimental"]; + forKey:@"sessionReplay"]; } + (NSArray *_Nonnull)getReplayRNRedactClasses:(NSDictionary *_Nullable)replayOptions diff --git a/packages/core/src/js/integrations/default.ts b/packages/core/src/js/integrations/default.ts index 8d48c4b2f..f41f68c05 100644 --- a/packages/core/src/js/integrations/default.ts +++ b/packages/core/src/js/integrations/default.ts @@ -1,10 +1,9 @@ /* eslint-disable complexity */ import type { Integration } from '@sentry/core'; -import type { BrowserOptions } from '@sentry/react'; import type { ReactNativeClientOptions } from '../options'; import { reactNativeTracingIntegration } from '../tracing'; -import { isExpoGo, isWeb, notWeb } from '../utils/environment'; +import { isExpoGo, notWeb } from '../utils/environment'; import { appStartIntegration, breadcrumbsIntegration, @@ -127,18 +126,22 @@ export function getDefaultIntegrations(options: ReactNativeClientOptions): Integ integrations.push(spotlightIntegration({ sidecarUrl })); } - if ( + const hasReplayOptions = + typeof options.replaysOnErrorSampleRate === 'number' || typeof options.replaysSessionSampleRate === 'number'; + const hasExperimentsReplayOptions = (options._experiments && typeof options._experiments.replaysOnErrorSampleRate === 'number') || - (options._experiments && typeof options._experiments.replaysSessionSampleRate === 'number') - ) { - if (isWeb()) { - // We can't create and add browserReplayIntegration as it overrides the users supplied one - // The browser replay integration works differently than the rest of default integrations - (options as BrowserOptions).replaysOnErrorSampleRate = options._experiments.replaysOnErrorSampleRate; - (options as BrowserOptions).replaysSessionSampleRate = options._experiments.replaysSessionSampleRate; - } else { - integrations.push(mobileReplayIntegration()); - } + (options._experiments && typeof options._experiments.replaysSessionSampleRate === 'number'); + + if (!hasReplayOptions && hasExperimentsReplayOptions) { + // Remove in the next major version (v7) + options.replaysOnErrorSampleRate = options._experiments.replaysOnErrorSampleRate; + options.replaysSessionSampleRate = options._experiments.replaysSessionSampleRate; + } + + if ((hasReplayOptions || hasExperimentsReplayOptions) && notWeb()) { + // We can't create and add browserReplayIntegration as it overrides the users supplied one + // The browser replay integration works differently than the rest of default integrations + integrations.push(mobileReplayIntegration()); } if (__DEV__ && notWeb()) { diff --git a/packages/core/src/js/options.ts b/packages/core/src/js/options.ts index b984af7a0..a95de6df4 100644 --- a/packages/core/src/js/options.ts +++ b/packages/core/src/js/options.ts @@ -221,6 +221,19 @@ export interface BaseReactNativeOptions { */ profilesSampleRate?: number; + /** + * The sample rate for session-long replays. + * 1.0 will record all sessions and 0 will record none. + */ + replaysSessionSampleRate?: number; + + /** + * The sample rate for sessions that has had an error occur. + * This is independent of `sessionSampleRate`. + * 1.0 will record all sessions and 0 will record none. + */ + replaysOnErrorSampleRate?: number; + /** * Options which are in beta, or otherwise not guaranteed to be stable. */ @@ -228,15 +241,16 @@ export interface BaseReactNativeOptions { [key: string]: unknown; /** - * The sample rate for session-long replays. - * 1.0 will record all sessions and 0 will record none. + * @deprecated Use `replaysSessionSampleRate` in the options root instead. + * + * This will be removed in the next major version. */ replaysSessionSampleRate?: number; /** - * The sample rate for sessions that has had an error occur. - * This is independent of `sessionSampleRate`. - * 1.0 will record all sessions and 0 will record none. + * @deprecated Use `replaysOnErrorSampleRate` in the options root instead. + * + * This will be removed in the next major version. */ replaysOnErrorSampleRate?: number; }; diff --git a/packages/core/src/js/replay/mobilereplay.ts b/packages/core/src/js/replay/mobilereplay.ts index de27fcb24..4f4501b13 100644 --- a/packages/core/src/js/replay/mobilereplay.ts +++ b/packages/core/src/js/replay/mobilereplay.ts @@ -49,10 +49,8 @@ type MobileReplayIntegration = Integration & { * * ```javascript * Sentry.init({ - * _experiments: { - * replaysOnErrorSampleRate: 1.0, - * replaysSessionSampleRate: 1.0, - * }, + * replaysOnErrorSampleRate: 1.0, + * replaysSessionSampleRate: 1.0, * integrations: [mobileReplayIntegration({ * // Adjust the default options * })], diff --git a/packages/core/test/sdk.test.ts b/packages/core/test/sdk.test.ts index 931d7fd6d..386bb66db 100644 --- a/packages/core/test/sdk.test.ts +++ b/packages/core/test/sdk.test.ts @@ -7,7 +7,7 @@ import { init, withScope } from '../src/js/sdk'; import type { ReactNativeTracingIntegration } from '../src/js/tracing'; import { REACT_NATIVE_TRACING_INTEGRATION_NAME, reactNativeTracingIntegration } from '../src/js/tracing'; import { makeNativeTransport } from '../src/js/transports/native'; -import { getDefaultEnvironment, isExpoGo, isWeb, notWeb } from '../src/js/utils/environment'; +import { getDefaultEnvironment, isExpoGo, notWeb } from '../src/js/utils/environment'; import { NATIVE } from './mockWrapper'; import { firstArg, secondArg } from './testutils'; @@ -857,6 +857,14 @@ describe('Tests the SDK functionality', () => { expectIntegration('MobileReplay'); }); + it('adds mobile replay integration when replaysOnErrorSampleRate is set', () => { + init({ + replaysOnErrorSampleRate: 1.0, + }); + + expectIntegration('MobileReplay'); + }); + it('adds mobile replay integration when _experiments.replaysSessionSampleRate is set', () => { init({ _experiments: { @@ -867,6 +875,14 @@ describe('Tests the SDK functionality', () => { expectIntegration('MobileReplay'); }); + it('adds mobile replay integration when replaysSessionSampleRate is set', () => { + init({ + replaysSessionSampleRate: 1.0, + }); + + expectIntegration('MobileReplay'); + }); + it('does not add mobile replay integration when no replay sample rates are set', () => { init({ _experiments: {}, @@ -875,8 +891,8 @@ describe('Tests the SDK functionality', () => { expectNotIntegration('MobileReplay'); }); - it('does not add any replay integration when on web even with on error sample rate', () => { - (isWeb as jest.Mock).mockImplementation(() => true); + it('does not add any replay integration when on web even with on experimental error sample rate', () => { + (notWeb as jest.Mock).mockImplementation(() => false); init({ _experiments: { replaysOnErrorSampleRate: 1.0, @@ -887,8 +903,8 @@ describe('Tests the SDK functionality', () => { expectNotIntegration('MobileReplay'); }); - it('does not add any replay integration when on web even with session sample rate', () => { - (isWeb as jest.Mock).mockImplementation(() => true); + it('does not add any replay integration when on web even with experimental session sample rate', () => { + (notWeb as jest.Mock).mockImplementation(() => false); init({ _experiments: { replaysSessionSampleRate: 1.0, @@ -899,16 +915,74 @@ describe('Tests the SDK functionality', () => { expectNotIntegration('MobileReplay'); }); + it('does not add any replay integration when on web even with on error sample rate', () => { + (notWeb as jest.Mock).mockImplementation(() => false); + init({ + replaysOnErrorSampleRate: 1.0, + }); + + expectNotIntegration('Replay'); + expectNotIntegration('MobileReplay'); + }); + + it('does not add any replay integration when on web even with session sample rate', () => { + (notWeb as jest.Mock).mockImplementation(() => false); + init({ + replaysSessionSampleRate: 1.0, + }); + + expectNotIntegration('Replay'); + expectNotIntegration('MobileReplay'); + }); + it('does not add any replay integration when on web', () => { - (isWeb as jest.Mock).mockImplementation(() => true); + (notWeb as jest.Mock).mockImplementation(() => false); init({}); expectNotIntegration('Replay'); expectNotIntegration('MobileReplay'); }); + it('ignores experimental replay options when ga options are set', () => { + (notWeb as jest.Mock).mockImplementation(() => false); + init({ + replaysOnErrorSampleRate: 0.1, + replaysSessionSampleRate: 0.2, + _experiments: { + replaysOnErrorSampleRate: 0.3, + replaysSessionSampleRate: 0.4, + }, + }); + + const actualOptions = usedOptions(); + expect(actualOptions).toEqual( + expect.objectContaining({ + replaysOnErrorSampleRate: 0.1, + replaysSessionSampleRate: 0.2, + }), + ); + }); + it('converts experimental replay options to standard web options when on web', () => { - (isWeb as jest.Mock).mockImplementation(() => true); + (notWeb as jest.Mock).mockImplementation(() => false); + init({ + _experiments: { + replaysOnErrorSampleRate: 0.5, + replaysSessionSampleRate: 0.1, + }, + }); + + const actualOptions = usedOptions(); + expect(actualOptions).toEqual( + expect.objectContaining({ + replaysOnErrorSampleRate: 0.5, + replaysSessionSampleRate: 0.1, + }), + ); + }); + + it('converts experimental replay options to standard web options when on mobile', () => { + (notWeb as jest.Mock).mockImplementation(() => true); init({ _experiments: { replaysOnErrorSampleRate: 0.5, diff --git a/samples/expo/app/_layout.tsx b/samples/expo/app/_layout.tsx index 1dd559efe..e950ff4a6 100644 --- a/samples/expo/app/_layout.tsx +++ b/samples/expo/app/_layout.tsx @@ -79,10 +79,8 @@ Sentry.init({ // release: 'myapp@1.2.3+1', // dist: `1`, profilesSampleRate: 1.0, - _experiments: { - // replaysOnErrorSampleRate: 1.0, - replaysSessionSampleRate: 1.0, - }, + // replaysOnErrorSampleRate: 1.0, + replaysSessionSampleRate: 1.0, spotlight: true, }); diff --git a/samples/react-native-macos/src/App.tsx b/samples/react-native-macos/src/App.tsx index bf6a3c4be..08873a81d 100644 --- a/samples/react-native-macos/src/App.tsx +++ b/samples/react-native-macos/src/App.tsx @@ -110,10 +110,8 @@ Sentry.init({ // release: 'myapp@1.2.3+1', // dist: `1`, profilesSampleRate: 1.0, - _experiments: { - // replaysSessionSampleRate: 1.0, - replaysOnErrorSampleRate: 1.0, - }, + // replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 1.0, spotlight: true, // This should be disabled when manually initializing the native SDK // Note that options from JS are not passed to the native SDKs when initialized manually diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index 34e0680f5..f6d106373 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -125,10 +125,8 @@ Sentry.init({ // release: 'myapp@1.2.3+1', // dist: `1`, profilesSampleRate: 1.0, - _experiments: { - replaysSessionSampleRate: 1.0, - replaysOnErrorSampleRate: 1.0, - }, + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 1.0, spotlight: true, // This should be disabled when manually initializing the native SDK // Note that options from JS are not passed to the native SDKs when initialized manually