Skip to content

Commit 60d07df

Browse files
feat(replay): Add Mobile Replay Alpha (#3714)
1 parent 8df8f60 commit 60d07df

File tree

29 files changed

+573
-29
lines changed

29 files changed

+573
-29
lines changed

CHANGELOG.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# Changelog
22

3-
## Unreleased
3+
## 5.23.0-alpha.1
4+
5+
### Fixes
6+
7+
- Pass `replaysSessionSampleRate` option to Android ([#3714](https://github.com/getsentry/sentry-react-native/pull/3714))
8+
9+
Access to Mobile Replay is limited to early access orgs on Sentry. If you're interested, [sign up for the waitlist](https://sentry.io/lp/mobile-replay-beta/)
410

511
### Features
612

@@ -69,6 +75,47 @@
6975
- [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8250)
7076
- [diff](https://github.com/getsentry/sentry-cocoa/compare/8.24.0...8.25.0)
7177

78+
## 5.23.0-alpha.0
79+
80+
### Features
81+
82+
- Mobile Session Replay Alpha ([#3714](https://github.com/getsentry/sentry-react-native/pull/3714))
83+
84+
To enable Replay for React Native on mobile and web add the following options.
85+
86+
```js
87+
Sentry.init({
88+
_experiments: {
89+
replaysSessionSampleRate: 1.0,
90+
replaysOnErrorSampleRate: 1.0,
91+
},
92+
});
93+
```
94+
95+
To change the default Mobile Replay options add the `mobileReplayIntegration`.
96+
97+
```js
98+
Sentry.init({
99+
_experiments: {
100+
replaysSessionSampleRate: 1.0,
101+
replaysOnErrorSampleRate: 1.0,
102+
},
103+
integration: [
104+
Sentry.mobileReplayIntegration({
105+
maskAllText: true,
106+
maskAllImages: true,
107+
}),
108+
],
109+
});
110+
```
111+
112+
Access is limited to early access orgs on Sentry. If you're interested, [sign up for the waitlist](https://sentry.io/lp/mobile-replay-beta/)
113+
114+
### Dependencies
115+
116+
- Bump Cocoa SDK to [8.25.0-alpha.0](https://github.com/getsentry/sentry-cocoa/releases/tag/8.25.0-alpha.0)
117+
- Bump Android SDK to [7.9.0-alpha.1](https://github.com/getsentry/sentry-java/releases/tag/7.9.0-alpha.1)
118+
72119
## 5.22.0
73120

74121
### Features

RNSentry.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Pod::Spec.new do |s|
3333
s.preserve_paths = '*.js'
3434

3535
s.dependency 'React-Core'
36-
s.dependency 'Sentry/HybridSDK', '8.25.2'
36+
s.dependency 'Sentry/HybridSDK', '8.25.0-alpha.0'
3737

3838
s.source_files = 'ios/**/*.{h,m,mm}'
3939
s.public_header_files = 'ios/RNSentry.h'

android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,5 @@ android {
5454

5555
dependencies {
5656
implementation 'com.facebook.react:react-native:+'
57-
api 'io.sentry:sentry-android:7.8.0'
57+
api 'io.sentry:sentry-android:7.9.0-alpha.1'
5858
}

android/src/main/java/io/sentry/react/RNSentryModuleImpl.java

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import io.sentry.SentryExecutorService;
6262
import io.sentry.SentryLevel;
6363
import io.sentry.SentryOptions;
64+
import io.sentry.SentryReplayOptions;
6465
import io.sentry.UncaughtExceptionHandlerIntegration;
6566
import io.sentry.android.core.AndroidLogger;
6667
import io.sentry.android.core.AndroidProfiler;
@@ -79,6 +80,7 @@
7980
import io.sentry.android.core.performance.AppStartMetrics;
8081
import io.sentry.protocol.SdkVersion;
8182
import io.sentry.protocol.SentryException;
83+
import io.sentry.protocol.SentryId;
8284
import io.sentry.protocol.SentryPackage;
8385
import io.sentry.protocol.User;
8486
import io.sentry.protocol.ViewHierarchy;
@@ -252,7 +254,9 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) {
252254
if (rnOptions.hasKey("enableNdk")) {
253255
options.setEnableNdk(rnOptions.getBoolean("enableNdk"));
254256
}
255-
257+
if (rnOptions.hasKey("_experiments")) {
258+
options.getExperimental().setSessionReplay(getReplayOptions(rnOptions));
259+
}
256260
options.setBeforeSend((event, hint) -> {
257261
// React native internally throws a JavascriptException
258262
// Since we catch it before that, we don't want to send this one
@@ -293,6 +297,37 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) {
293297
promise.resolve(true);
294298
}
295299

300+
private SentryReplayOptions getReplayOptions(@NotNull ReadableMap rnOptions) {
301+
@NotNull final SentryReplayOptions androidReplayOptions = new SentryReplayOptions();
302+
303+
@Nullable final ReadableMap rnExperimentsOptions = rnOptions.getMap("_experiments");
304+
if (rnExperimentsOptions == null) {
305+
return androidReplayOptions;
306+
}
307+
308+
if (!(rnExperimentsOptions.hasKey("replaysSessionSampleRate") || rnExperimentsOptions.hasKey("replaysOnErrorSampleRate"))) {
309+
return androidReplayOptions;
310+
}
311+
312+
androidReplayOptions.setSessionSampleRate(rnExperimentsOptions.hasKey("replaysSessionSampleRate")
313+
? rnExperimentsOptions.getDouble("replaysSessionSampleRate") : null);
314+
androidReplayOptions.setErrorSampleRate(rnExperimentsOptions.hasKey("replaysOnErrorSampleRate")
315+
? rnExperimentsOptions.getDouble("replaysOnErrorSampleRate") : null);
316+
317+
if (!rnOptions.hasKey("mobileReplayOptions")) {
318+
return androidReplayOptions;
319+
}
320+
@Nullable final ReadableMap rnMobileReplayOptions = rnOptions.getMap("mobileReplayOptions");
321+
if (rnMobileReplayOptions == null) {
322+
return androidReplayOptions;
323+
}
324+
325+
androidReplayOptions.setRedactAllText(!rnMobileReplayOptions.hasKey("maskAllText") || rnMobileReplayOptions.getBoolean("maskAllText"));
326+
androidReplayOptions.setRedactAllImages(!rnMobileReplayOptions.hasKey("maskAllImages") || rnMobileReplayOptions.getBoolean("maskAllImages"));
327+
328+
return androidReplayOptions;
329+
}
330+
296331
public void crash() {
297332
throw new RuntimeException("TEST - Sentry Client Crash (only works in release mode)");
298333
}
@@ -410,6 +445,24 @@ public void fetchNativeFrames(Promise promise) {
410445
}
411446
}
412447

448+
public void captureReplay(boolean isHardCrash, Promise promise) {
449+
Sentry.getCurrentHub().getOptions().getReplayController().sendReplay(isHardCrash, null, null);
450+
promise.resolve(getCurrentReplayId());
451+
}
452+
453+
public @Nullable String getCurrentReplayId() {
454+
final @Nullable IScope scope = InternalSentrySdk.getCurrentScope();
455+
if (scope == null) {
456+
return null;
457+
}
458+
459+
final @NotNull SentryId id = scope.getReplayId();
460+
if (id == SentryId.EMPTY_ID) {
461+
return null;
462+
}
463+
return id.toString();
464+
}
465+
413466
public void captureEnvelope(String rawBytes, ReadableMap options, Promise promise) {
414467
byte[] bytes = Base64.decode(rawBytes, Base64.DEFAULT);
415468

android/src/newarch/java/io/sentry/react/RNSentryModule.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,14 @@ public WritableMap fetchNativeStackFramesBy(ReadableArray instructionsAddr) {
158158
// Not used on Android
159159
return null;
160160
}
161+
162+
@Override
163+
public void captureReplay(boolean isHardCrash, Promise promise) {
164+
this.impl.captureReplay(isHardCrash, promise);
165+
}
166+
167+
@Override
168+
public String getCurrentReplayId() {
169+
return this.impl.getCurrentReplayId();
170+
}
161171
}

android/src/oldarch/java/io/sentry/react/RNSentryModule.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,14 @@ public WritableMap fetchNativeStackFramesBy(ReadableArray instructionsAddr) {
158158
// Not used on Android
159159
return null;
160160
}
161+
162+
@ReactMethod
163+
public void captureReplay(boolean isHardCrash, Promise promise) {
164+
this.impl.captureReplay(isHardCrash, promise);
165+
}
166+
167+
@ReactMethod(isBlockingSynchronousMethod = true)
168+
public String getCurrentReplayId() {
169+
return this.impl.getCurrentReplayId();
170+
}
161171
}

ios/RNSentry.mm

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@ - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)
116116
// Because we sent it already before the app crashed.
117117
if (nil != event.exceptions.firstObject.type &&
118118
[event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location != NSNotFound) {
119-
NSLog(@"Unhandled JS Exception");
120119
return nil;
121120
}
122121

@@ -135,6 +134,28 @@ - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)
135134
[mutableOptions removeObjectForKey:@"tracesSampler"];
136135
[mutableOptions removeObjectForKey:@"enableTracing"];
137136

137+
if ([mutableOptions valueForKey:@"_experiments"] != nil) {
138+
NSDictionary *experiments = mutableOptions[@"_experiments"];
139+
if (experiments[@"replaysSessionSampleRate"] != nil || experiments[@"replaysOnErrorSampleRate"] != nil) {
140+
[mutableOptions setValue:@{
141+
@"sessionReplay": @{
142+
@"sessionSampleRate": experiments[@"replaysSessionSampleRate"] ?: [NSNull null],
143+
@"errorSampleRate": experiments[@"replaysOnErrorSampleRate"] ?: [NSNull null],
144+
@"redactAllImages": mutableOptions[@"mobileReplayOptions"] != nil &&
145+
mutableOptions[@"mobileReplayOptions"][@"maskAllImages"] != nil
146+
? mutableOptions[@"mobileReplayOptions"][@"maskAllImages"]
147+
: [NSNull null],
148+
@"redactAllText": mutableOptions[@"mobileReplayOptions"] != nil &&
149+
mutableOptions[@"mobileReplayOptions"][@"maskAllText"] != nil
150+
? mutableOptions[@"mobileReplayOptions"][@"maskAllText"]
151+
: [NSNull null],
152+
}
153+
} forKey:@"experimental"];
154+
[self addReplayRNRedactClasses: mutableOptions[@"mobileReplayOptions"]];
155+
}
156+
[mutableOptions removeObjectForKey:@"_experiments"];
157+
}
158+
138159
SentryOptions *sentryOptions = [[SentryOptions alloc] initWithDict:mutableOptions didFailWithError:errorPointer];
139160
if (*errorPointer != nil) {
140161
return nil;
@@ -644,6 +665,31 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray<NSNumber*>*)instructionsAdd
644665
// the 'tracesSampleRate' or 'tracesSampler' option.
645666
}
646667

668+
RCT_EXPORT_METHOD(captureReplay: (BOOL)isHardCrash
669+
resolver:(RCTPromiseResolveBlock)resolve
670+
rejecter:(RCTPromiseRejectBlock)reject)
671+
{
672+
[PrivateSentrySDKOnly captureReplay];
673+
resolve([PrivateSentrySDKOnly getReplayId]);
674+
}
675+
676+
RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, getCurrentReplayId)
677+
{
678+
return [PrivateSentrySDKOnly getReplayId];
679+
}
680+
681+
- (void) addReplayRNRedactClasses: (NSDictionary *_Nullable)replayOptions
682+
{
683+
NSMutableArray *_Nonnull classesToRedact = [[NSMutableArray alloc] init];
684+
if ([replayOptions[@"maskAllImages"] boolValue] == YES) {
685+
[classesToRedact addObject: NSClassFromString(@"RCTImageView")];
686+
}
687+
if ([replayOptions[@"maskAllText"] boolValue] == YES) {
688+
[classesToRedact addObject: NSClassFromString(@"RCTTextView")];
689+
}
690+
[PrivateSentrySDKOnly addReplayRedactClasses: classesToRedact];
691+
}
692+
647693
static NSString* const enabledProfilingMessage = @"Enable Hermes to use Sentry Profiling.";
648694
static SentryId* nativeProfileTraceId = nil;
649695
static uint64_t nativeProfileStartTime = 0;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@sentry/react-native",
33
"homepage": "https://github.com/getsentry/sentry-react-native",
44
"repository": "https://github.com/getsentry/sentry-react-native",
5-
"version": "5.22.2",
5+
"version": "5.23.0-alpha.1",
66
"description": "Official Sentry SDK for react-native",
77
"typings": "dist/js/index.d.ts",
88
"types": "dist/js/index.d.ts",

samples/expo/app.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"slug": "sentry-react-native-expo-sample",
55
"jsEngine": "hermes",
66
"scheme": "sentry-expo-sample",
7-
"version": "5.22.2",
7+
"version": "5.23.0-alpha.1",
88
"orientation": "portrait",
99
"icon": "./assets/icon.png",
1010
"userInterfaceStyle": "light",
@@ -19,15 +19,15 @@
1919
"ios": {
2020
"supportsTablet": true,
2121
"bundleIdentifier": "io.sentry.expo.sample",
22-
"buildNumber": "6"
22+
"buildNumber": "7"
2323
},
2424
"android": {
2525
"adaptiveIcon": {
2626
"foregroundImage": "./assets/adaptive-icon.png",
2727
"backgroundColor": "#ffffff"
2828
},
2929
"package": "io.sentry.expo.sample",
30-
"versionCode": 6
30+
"versionCode": 7
3131
},
3232
"web": {
3333
"bundler": "metro",

samples/expo/app/_layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ process.env.EXPO_SKIP_DURING_EXPORT !== 'true' && Sentry.init({
7878
// dist: `1`,
7979
_experiments: {
8080
profilesSampleRate: 0,
81+
// replaysOnErrorSampleRate: 1.0,
82+
replaysSessionSampleRate: 1.0,
8183
},
8284
enableSpotlight: true,
8385
});

0 commit comments

Comments
 (0)