Skip to content

Commit 9afd1d1

Browse files
authored
Merge 30ed290 into 1f1e41e
2 parents 1f1e41e + 30ed290 commit 9afd1d1

File tree

10 files changed

+194
-35
lines changed

10 files changed

+194
-35
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Features
66

77
- Improve touch event component info if annotated with [`@sentry/babel-plugin-component-annotate`](https://www.npmjs.com/package/@sentry/babel-plugin-component-annotate) ([#3899](https://github.com/getsentry/sentry-react-native/pull/3899))
8+
- Add replay breadcrumbs for touch & navigation events ([#3846](https://github.com/getsentry/sentry-react-native/pull/3846))
89

910
### Dependencies
1011

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) {
188188

189189
options.setSentryClientName(sdkVersion.getName() + "/" + sdkVersion.getVersion());
190190
options.setNativeSdkName(NATIVE_SDK_NAME);
191-
options.setSdkVersion(sdkVersion);
191+
options.setSdkVersion(sdkVersion);
192192

193193
if (rnOptions.hasKey("debug") && rnOptions.getBoolean("debug")) {
194194
options.setDebug(true);
@@ -256,6 +256,7 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) {
256256
}
257257
if (rnOptions.hasKey("_experiments")) {
258258
options.getExperimental().setSessionReplay(getReplayOptions(rnOptions));
259+
options.getReplayController().setBreadcrumbConverter(new RNSentryReplayBreadcrumbConverter());
259260
}
260261
options.setBeforeSend((event, hint) -> {
261262
// React native internally throws a JavascriptException
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.sentry.react;
2+
3+
import io.sentry.Breadcrumb;
4+
import io.sentry.android.replay.DefaultReplayBreadcrumbConverter;
5+
import io.sentry.rrweb.RRWebEvent;
6+
import io.sentry.rrweb.RRWebBreadcrumbEvent;
7+
import org.jetbrains.annotations.NotNull;
8+
import org.jetbrains.annotations.Nullable;
9+
10+
public final class RNSentryReplayBreadcrumbConverter extends DefaultReplayBreadcrumbConverter {
11+
public RNSentryReplayBreadcrumbConverter() {
12+
}
13+
14+
@Override
15+
public @Nullable RRWebEvent convert(final @NotNull Breadcrumb breadcrumb) {
16+
RRWebBreadcrumbEvent rrwebBreadcrumb = new RRWebBreadcrumbEvent();
17+
assert rrwebBreadcrumb.getCategory() == null;
18+
19+
if (breadcrumb.getCategory().equals("touch")) {
20+
rrwebBreadcrumb.setCategory("ui.tap");
21+
Object target = breadcrumb.getData("target");
22+
if (target != null) {
23+
rrwebBreadcrumb.setMessage(target.toString());
24+
}
25+
rrwebBreadcrumb.setData(breadcrumb.getData());
26+
}
27+
28+
if (rrwebBreadcrumb.getCategory() != null && !rrwebBreadcrumb.getCategory().isEmpty()) {
29+
rrwebBreadcrumb.setTimestamp(breadcrumb.getTimestamp().getTime());
30+
rrwebBreadcrumb.setBreadcrumbTimestamp(breadcrumb.getTimestamp().getTime() / 1000.0);
31+
rrwebBreadcrumb.setBreadcrumbType("default");
32+
return rrwebBreadcrumb;
33+
}
34+
35+
return super.convert(breadcrumb);
36+
}
37+
}

ios/RNSentry.mm

Lines changed: 19 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
#import "RNSentryEvents.h"
3939
#import "RNSentryDependencyContainer.h"
4040

41+
#if SENTRY_TARGET_REPLAY_SUPPORTED
42+
#import "RNSentrySessionReplay.h"
43+
#endif
44+
4145
#if SENTRY_HAS_UIKIT
4246
#import "RNSentryRNSScreen.h"
4347
#import "RNSentryFramesTrackerListener.h"
@@ -106,6 +110,10 @@ + (BOOL)requiresMainQueueSetup {
106110
sentHybridSdkDidBecomeActive = true;
107111
}
108112

113+
#if SENTRY_TARGET_REPLAY_SUPPORTED
114+
[RNSentrySessionReplay postInit];
115+
#endif
116+
109117
resolve(@YES);
110118
}
111119

@@ -135,27 +143,9 @@ - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)
135143
[mutableOptions removeObjectForKey:@"tracesSampler"];
136144
[mutableOptions removeObjectForKey:@"enableTracing"];
137145

138-
if ([mutableOptions valueForKey:@"_experiments"] != nil) {
139-
NSDictionary *experiments = mutableOptions[@"_experiments"];
140-
if (experiments[@"replaysSessionSampleRate"] != nil || experiments[@"replaysOnErrorSampleRate"] != nil) {
141-
[mutableOptions setValue:@{
142-
@"sessionReplay": @{
143-
@"sessionSampleRate": experiments[@"replaysSessionSampleRate"] ?: [NSNull null],
144-
@"errorSampleRate": experiments[@"replaysOnErrorSampleRate"] ?: [NSNull null],
145-
@"redactAllImages": mutableOptions[@"mobileReplayOptions"] != nil &&
146-
mutableOptions[@"mobileReplayOptions"][@"maskAllImages"] != nil
147-
? mutableOptions[@"mobileReplayOptions"][@"maskAllImages"]
148-
: [NSNull null],
149-
@"redactAllText": mutableOptions[@"mobileReplayOptions"] != nil &&
150-
mutableOptions[@"mobileReplayOptions"][@"maskAllText"] != nil
151-
? mutableOptions[@"mobileReplayOptions"][@"maskAllText"]
152-
: [NSNull null],
153-
}
154-
} forKey:@"experimental"];
155-
[self addReplayRNRedactClasses: mutableOptions[@"mobileReplayOptions"]];
156-
}
157-
[mutableOptions removeObjectForKey:@"_experiments"];
158-
}
146+
#if SENTRY_TARGET_REPLAY_SUPPORTED
147+
[RNSentrySessionReplay updateOptions:mutableOptions];
148+
#endif
159149

160150
SentryOptions *sentryOptions = [[SentryOptions alloc] initWithDict:mutableOptions didFailWithError:errorPointer];
161151
if (*errorPointer != nil) {
@@ -635,25 +625,21 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray<NSNumber*>*)instructionsAdd
635625
resolver:(RCTPromiseResolveBlock)resolve
636626
rejecter:(RCTPromiseRejectBlock)reject)
637627
{
628+
#if SENTRY_TARGET_REPLAY_SUPPORTED
638629
[PrivateSentrySDKOnly captureReplay];
639630
resolve([PrivateSentrySDKOnly getReplayId]);
631+
#else
632+
resolve(nil);
633+
#endif
640634
}
641635

642636
RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, getCurrentReplayId)
643637
{
638+
#if SENTRY_TARGET_REPLAY_SUPPORTED
644639
return [PrivateSentrySDKOnly getReplayId];
645-
}
646-
647-
- (void) addReplayRNRedactClasses: (NSDictionary *_Nullable)replayOptions
648-
{
649-
NSMutableArray *_Nonnull classesToRedact = [[NSMutableArray alloc] init];
650-
if ([replayOptions[@"maskAllImages"] boolValue] == YES) {
651-
[classesToRedact addObject: NSClassFromString(@"RCTImageView")];
652-
}
653-
if ([replayOptions[@"maskAllText"] boolValue] == YES) {
654-
[classesToRedact addObject: NSClassFromString(@"RCTTextView")];
655-
}
656-
[PrivateSentrySDKOnly addReplayRedactClasses: classesToRedact];
640+
#else
641+
return nil;
642+
#endif
657643
}
658644

659645
static NSString* const enabledProfilingMessage = @"Enable Hermes to use Sentry Profiling.";

ios/RNSentryBreadcrumbConverter.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
@import Sentry;
2+
3+
#if SENTRY_TARGET_REPLAY_SUPPORTED
4+
@class SentryRRWebEvent;
5+
6+
@interface RNSentryBreadcrumbConverter
7+
: NSObject <SentryReplayBreadcrumbConverter>
8+
9+
- (instancetype _Nonnull)init;
10+
11+
@end
12+
#endif

ios/RNSentryBreadcrumbConverter.m

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#import "RNSentryBreadcrumbConverter.h"
2+
3+
@import Sentry;
4+
5+
#if SENTRY_TARGET_REPLAY_SUPPORTED
6+
7+
@implementation RNSentryBreadcrumbConverter {
8+
SentrySRDefaultBreadcrumbConverter *defaultConverter;
9+
}
10+
11+
- (instancetype _Nonnull)init {
12+
if (self = [super init]) {
13+
self->defaultConverter =
14+
[SentrySessionReplayIntegration createDefaultBreadcrumbConverter];
15+
}
16+
return self;
17+
}
18+
19+
- (id<SentryRRWebEvent> _Nullable)convertFrom:
20+
(SentryBreadcrumb *_Nonnull)breadcrumb {
21+
assert(breadcrumb.timestamp != nil);
22+
23+
if ([breadcrumb.category isEqualToString:@"touch"]) {
24+
return [SentrySessionReplayIntegration
25+
createBreadcrumbwithTimestamp:breadcrumb.timestamp
26+
category:@"ui.tap"
27+
message:breadcrumb.data
28+
? [breadcrumb.data
29+
valueForKey:@"target"]
30+
: nil
31+
level:breadcrumb.level
32+
data:breadcrumb.data];
33+
} else if ([breadcrumb.category isEqualToString:@"navigation"]) {
34+
return [SentrySessionReplayIntegration
35+
createBreadcrumbwithTimestamp:breadcrumb.timestamp ?: 0
36+
category:breadcrumb.category
37+
message:nil
38+
level:breadcrumb.level
39+
data:breadcrumb.data];
40+
} else {
41+
return [self->defaultConverter convertFrom:breadcrumb];
42+
}
43+
}
44+
45+
@end
46+
#endif

ios/RNSentrySessionReplay.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
@interface RNSentrySessionReplay : NSObject
3+
4+
+ (void)updateOptions:(NSMutableDictionary *)options;
5+
6+
+ (void)postInit;
7+
8+
@end

ios/RNSentrySessionReplay.m

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#import "RNSentrySessionReplay.h"
2+
#import "RNSentryBreadcrumbConverter.h"
3+
4+
#if SENTRY_TARGET_REPLAY_SUPPORTED
5+
6+
@implementation RNSentrySessionReplay {
7+
}
8+
9+
+ (void)updateOptions:(NSMutableDictionary *)options {
10+
NSDictionary *experiments = options[@"_experiments"];
11+
[options removeObjectForKey:@"_experiments"];
12+
if (experiments == nil) {
13+
NSLog(@"Session replay disabled via configuration");
14+
return;
15+
}
16+
17+
if (experiments[@"replaysSessionSampleRate"] == nil &&
18+
experiments[@"replaysOnErrorSampleRate"] == nil) {
19+
NSLog(@"Session replay disabled via configuration");
20+
return;
21+
}
22+
23+
NSLog(@"Setting up session replay");
24+
NSDictionary *replayOptions = options[@"mobileReplayOptions"] ?: @{};
25+
26+
[options setValue:@{
27+
@"sessionReplay" : @{
28+
@"sessionSampleRate" : experiments[@"replaysSessionSampleRate"]
29+
?: [NSNull null],
30+
@"errorSampleRate" : experiments[@"replaysOnErrorSampleRate"]
31+
?: [NSNull null],
32+
@"redactAllImages" : replayOptions[@"maskAllImages"] ?: [NSNull null],
33+
@"redactAllText" : replayOptions[@"maskAllText"] ?: [NSNull null],
34+
}
35+
}
36+
forKey:@"experimental"];
37+
38+
[RNSentrySessionReplay addReplayRNRedactClasses:replayOptions];
39+
}
40+
41+
+ (void)addReplayRNRedactClasses:(NSDictionary *_Nullable)replayOptions {
42+
NSMutableArray *_Nonnull classesToRedact = [[NSMutableArray alloc] init];
43+
if ([replayOptions[@"maskAllImages"] boolValue] == YES) {
44+
[classesToRedact addObject:NSClassFromString(@"RCTImageView")];
45+
}
46+
if ([replayOptions[@"maskAllText"] boolValue] == YES) {
47+
[classesToRedact addObject:NSClassFromString(@"RCTTextView")];
48+
}
49+
[PrivateSentrySDKOnly addReplayRedactClasses:classesToRedact];
50+
}
51+
52+
+ (void)postInit {
53+
RNSentryBreadcrumbConverter *breadcrumbConverter =
54+
[[RNSentryBreadcrumbConverter alloc] init];
55+
[PrivateSentrySDKOnly configureSessionReplayWith:breadcrumbConverter
56+
screenshotProvider:nil];
57+
}
58+
59+
@end
60+
#endif

src/js/touchevents.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,10 @@ class TouchEventBoundary extends React.Component<TouchEventBoundaryProps> {
130130

131131
const crumb = {
132132
category: this.props.breadcrumbCategory,
133-
data: { path: touchPath },
133+
data: {
134+
path: touchPath,
135+
target: detail,
136+
},
134137
level: level,
135138
message: `Touch event within element: ${detail}`,
136139
type: this.props.breadcrumbType,

test/touchevents.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ describe('TouchEventBoundary._onTouchStart', () => {
102102
category: defaultProps.breadcrumbCategory,
103103
data: {
104104
path: [{ name: 'View' }, { name: 'Connect(View)' }, { label: 'LABEL!' }],
105+
target: "LABEL!",
105106
},
106107
level: 'info' as SeverityLevel,
107108
message: 'Touch event within element: LABEL!',
@@ -161,6 +162,7 @@ describe('TouchEventBoundary._onTouchStart', () => {
161162
category: defaultProps.breadcrumbCategory,
162163
data: {
163164
path: [{ name: 'Styled(View)' }],
165+
target: "Styled(View)",
164166
},
165167
level: 'info' as SeverityLevel,
166168
message: 'Touch event within element: Styled(View)',
@@ -211,6 +213,7 @@ describe('TouchEventBoundary._onTouchStart', () => {
211213
category: defaultProps.breadcrumbCategory,
212214
data: {
213215
path: [{ label: 'Connect(View)' }, { name: 'Styled(View)' }],
216+
target: "Connect(View)",
214217
},
215218
level: 'info' as SeverityLevel,
216219
message: 'Touch event within element: Connect(View)',
@@ -274,6 +277,7 @@ describe('TouchEventBoundary._onTouchStart', () => {
274277
{ name: 'Styled(View)' },
275278
{ element: 'View', file: 'happyview.js', name: 'Happy' },
276279
],
280+
target: "Screen (screen.tsx)",
277281
},
278282
level: 'info' as SeverityLevel,
279283
message: 'Touch event within element: Screen (screen.tsx)',
@@ -305,6 +309,7 @@ describe('TouchEventBoundary._onTouchStart', () => {
305309
category: defaultProps.breadcrumbCategory,
306310
data: {
307311
path: [{ name: 'Text' }],
312+
target: "Text",
308313
},
309314
level: 'info' as SeverityLevel,
310315
message: 'Touch event within element: Text',

0 commit comments

Comments
 (0)