Skip to content

Commit

Permalink
feat: Session Replay is GA (#4384)
Browse files Browse the repository at this point in the history
* feat: Session Replay is GA

* update cocoa tests

* fix changelog pr num

* add js tests

* Update packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift

Co-authored-by: Antonis Lilis <[email protected]>

* chore(deps): update Android SDK to v7.20.0 (#4411)

Co-authored-by: GitHub <[email protected]>
Co-authored-by: Roman Zavarnitsyn <[email protected]>

* chore(deps): update Cocoa SDK to v8.43.0 (#4410)

Co-authored-by: GitHub <[email protected]>
Co-authored-by: Roman Zavarnitsyn <[email protected]>

* Set SdkVersion to react native for replay events on Android

* Use new options in samples

* Fixes testMaskAllVectors failing test

---------

Co-authored-by: Bruno Garcia <[email protected]>
Co-authored-by: Antonis Lilis <[email protected]>
Co-authored-by: Roman Zavarnitsyn <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: GitHub <[email protected]>
  • Loading branch information
6 people authored Jan 3, 2025
1 parent 8fe7c9d commit 5dff5ee
Show file tree
Hide file tree
Showing 13 changed files with 211 additions and 125 deletions.
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

0 comments on commit 5dff5ee

Please sign in to comment.