Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Session Replay is GA #4384

Merged
merged 12 commits into from
Jan 3, 2025
33 changes: 27 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion packages/core/RNSentry.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,55 +10,39 @@ 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://[email protected]/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)
}

func testReplayOptionsDictContainsAllOptionsKeysWhenErrorSampleRateUsed() {
let optionsDict = ([
"dsn": "https://[email protected]/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)
}

func testReplayOptionsDictContainsAllOptionsKeysWhenErrorAndSessionSampleRatesUsed() {
let optionsDict = ([
"dsn": "https://[email protected]/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)
}
Expand All @@ -75,38 +59,37 @@ final class RNSentryReplayOptions: XCTestCase {
func testSessionSampleRate() {
let optionsDict = ([
"dsn": "https://[email protected]/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://[email protected]/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://[email protected]/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"))
Expand All @@ -115,47 +98,47 @@ final class RNSentryReplayOptions: XCTestCase {
func testMaskAllImages() {
let optionsDict = ([
"dsn": "https://[email protected]/1234567",
"_experiments": [ "replaysOnErrorSampleRate": 0.75 ],
"replaysOnErrorSampleRate": 0.75,
"mobileReplayOptions": [ "maskAllImages": true ]
] as NSDictionary).mutableCopy() as! NSMutableDictionary

RNSentryReplay.updateOptions(optionsDict)

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://[email protected]/1234567",
"_experiments": [ "replaysOnErrorSampleRate": 0.75 ],
"replaysOnErrorSampleRate": 0.75,
"mobileReplayOptions": [ "maskAllImages": false ]
] as NSDictionary).mutableCopy() as! NSMutableDictionary

RNSentryReplay.updateOptions(optionsDict)

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://[email protected]/1234567",
"_experiments": [ "replaysOnErrorSampleRate": 0.75 ],
"replaysOnErrorSampleRate": 0.75,
"mobileReplayOptions": [ "maskAllText": true ]
] as NSDictionary).mutableCopy() as! NSMutableDictionary

RNSentryReplay.updateOptions(optionsDict)

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) {
Expand All @@ -169,16 +152,16 @@ final class RNSentryReplayOptions: XCTestCase {
func testMaskAllTextFalse() {
let optionsDict = ([
"dsn": "https://[email protected]/1234567",
"_experiments": [ "replaysOnErrorSampleRate": 0.75 ],
"replaysOnErrorSampleRate": 0.75,
"mobileReplayOptions": [ "maskAllText": false ]
] as NSDictionary).mutableCopy() as! NSMutableDictionary

RNSentryReplay.updateOptions(optionsDict)

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)
}

}
2 changes: 1 addition & 1 deletion packages/core/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down Expand Up @@ -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")) {
Expand Down
25 changes: 8 additions & 17 deletions packages/core/ios/RNSentryReplay.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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
Expand Down
Loading
Loading