Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
66b14d8
touch events on android
vaind May 31, 2024
a9139c6
fix touchevent tests
vaind Jun 3, 2024
fb1bb90
Update android/src/main/java/io/sentry/react/RNSentryReplayBreadcrumb…
vaind Jun 4, 2024
e725aa9
ios breadcrumb converter
vaind Jun 5, 2024
d5c8e67
delegate to default breadcrumb converter
vaind Jun 10, 2024
3d76fae
fix: module access
vaind Jun 11, 2024
9092169
Merge branch 'feat/replay' into feat/replay-breadcrumbs
vaind Jun 18, 2024
b4a1d8f
move replay stuff to RNSentrySessionReplay
vaind Jun 19, 2024
2ebf638
move setBreadcrumbConverter to init
vaind Jun 19, 2024
0f17b72
try to fix CI
vaind Jun 19, 2024
ae558fe
navigation breadcrumbs
vaind Jun 19, 2024
04c5c9d
try to fix CI
vaind Jun 19, 2024
6211bbf
pin xcode to 15.4 for e2e and sample app builds
krystofwoldrich Jun 20, 2024
96c7aab
chore: update cocoa breadcrumb converter to match latest changes in t…
vaind Jun 24, 2024
1a13827
more updates based on upstream changes
vaind Jun 24, 2024
5564c88
Revert "pin xcode to 15.4 for e2e and sample app builds"
krystofwoldrich Jun 25, 2024
8d4f937
review changes
vaind Jun 25, 2024
272b4bb
fix changelog
vaind Jun 25, 2024
0a95f72
Merge branch 'feat/replay' into feat/replay-breadcrumbs
vaind Jun 26, 2024
30ed290
cleanup TODOs
vaind Jun 26, 2024
f71fb66
touch event path for replay breadcrumbs
vaind Jun 26, 2024
2f2ff79
remove native navigation breadcrumbs on iOS
vaind Jun 26, 2024
dcc9753
renames
vaind Jun 26, 2024
a138db1
ignore native navigation breadcrumbs on android
vaind Jun 26, 2024
e3aaee8
fix android
vaind Jun 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- 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))
- Add replay breadcrumbs for touch & navigation events ([#3846](https://github.com/getsentry/sentry-react-native/pull/3846))

### Dependencies

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) {

options.setSentryClientName(sdkVersion.getName() + "/" + sdkVersion.getVersion());
options.setNativeSdkName(NATIVE_SDK_NAME);
options.setSdkVersion(sdkVersion);
options.setSdkVersion(sdkVersion);

if (rnOptions.hasKey("debug") && rnOptions.getBoolean("debug")) {
options.setDebug(true);
Expand Down Expand Up @@ -256,6 +256,7 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) {
}
if (rnOptions.hasKey("_experiments")) {
options.getExperimental().setSessionReplay(getReplayOptions(rnOptions));
options.getReplayController().setBreadcrumbConverter(new RNSentryReplayBreadcrumbConverter());
}
options.setBeforeSend((event, hint) -> {
// React native internally throws a JavascriptException
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package io.sentry.react;

import io.sentry.Breadcrumb;
import io.sentry.android.replay.DefaultReplayBreadcrumbConverter;
import io.sentry.rrweb.RRWebEvent;
import io.sentry.rrweb.RRWebBreadcrumbEvent;
import java.util.ArrayList;
import java.util.HashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public final class RNSentryReplayBreadcrumbConverter extends DefaultReplayBreadcrumbConverter {
public RNSentryReplayBreadcrumbConverter() {
}

@Override
public @Nullable RRWebEvent convert(final @NotNull Breadcrumb breadcrumb) {
RRWebBreadcrumbEvent rrwebBreadcrumb = new RRWebBreadcrumbEvent();
assert rrwebBreadcrumb.getCategory() == null;

if (breadcrumb.getCategory().equals("touch")) {
rrwebBreadcrumb.setCategory("ui.tap");
ArrayList path = (ArrayList) breadcrumb.getData("path");
if (path != null) {
StringBuilder message = new StringBuilder();
for (int i = Math.min(3, path.size()); i >= 0; i--) {
HashMap item = (HashMap) path.get(i);
message.append(item.get("name"));
if (item.containsKey("element") || item.containsKey("file")) {
message.append('(');
if (item.containsKey("element")) {
message.append(item.get("element"));
if (item.containsKey("file")) {
message.append(", ");
message.append(item.get("file"));
}
} else if (item.containsKey("file")) {
message.append(item.get("file"));
}
message.append(')');
}
if (i > 0) {
message.append(" > ");
}
}
Comment on lines +23 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks good in the UI, let's leave it like this for now, in the Replay alpha branch.

We should discuss this with the front end as they can assemble the string from the structured data.

rrwebBreadcrumb.setMessage(message.toString());
}
rrwebBreadcrumb.setData(breadcrumb.getData());
} else if (breadcrumb.getCategory().equals("navigation")) {
rrwebBreadcrumb.setCategory(breadcrumb.getCategory());
rrwebBreadcrumb.setData(breadcrumb.getData());
}

if (rrwebBreadcrumb.getCategory() != null && !rrwebBreadcrumb.getCategory().isEmpty()) {
rrwebBreadcrumb.setLevel(breadcrumb.getLevel());
rrwebBreadcrumb.setTimestamp(breadcrumb.getTimestamp().getTime());
rrwebBreadcrumb.setBreadcrumbTimestamp(breadcrumb.getTimestamp().getTime() / 1000.0);
rrwebBreadcrumb.setBreadcrumbType("default");
return rrwebBreadcrumb;
}

RRWebEvent nativeBreadcrumb = super.convert(breadcrumb);

// ignore native navigation breadcrumbs
if (nativeBreadcrumb instanceof RRWebBreadcrumbEvent) {
rrwebBreadcrumb = (RRWebBreadcrumbEvent) nativeBreadcrumb;
if (rrwebBreadcrumb.getCategory() != null && rrwebBreadcrumb.getCategory().equals("navigation")) {
return null;
}
}

return nativeBreadcrumb;
}
}
52 changes: 19 additions & 33 deletions ios/RNSentry.mm
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
#import "RNSentryEvents.h"
#import "RNSentryDependencyContainer.h"

#if SENTRY_TARGET_REPLAY_SUPPORTED
#import "RNSentryReplay.h"
#endif

#if SENTRY_HAS_UIKIT
#import "RNSentryRNSScreen.h"
#import "RNSentryFramesTrackerListener.h"
Expand Down Expand Up @@ -106,6 +110,10 @@ + (BOOL)requiresMainQueueSetup {
sentHybridSdkDidBecomeActive = true;
}

#if SENTRY_TARGET_REPLAY_SUPPORTED
[RNSentryReplay postInit];
#endif

resolve(@YES);
}

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

if ([mutableOptions valueForKey:@"_experiments"] != nil) {
NSDictionary *experiments = mutableOptions[@"_experiments"];
if (experiments[@"replaysSessionSampleRate"] != nil || experiments[@"replaysOnErrorSampleRate"] != nil) {
[mutableOptions setValue:@{
@"sessionReplay": @{
@"sessionSampleRate": experiments[@"replaysSessionSampleRate"] ?: [NSNull null],
@"errorSampleRate": experiments[@"replaysOnErrorSampleRate"] ?: [NSNull null],
@"redactAllImages": mutableOptions[@"mobileReplayOptions"] != nil &&
mutableOptions[@"mobileReplayOptions"][@"maskAllImages"] != nil
? mutableOptions[@"mobileReplayOptions"][@"maskAllImages"]
: [NSNull null],
@"redactAllText": mutableOptions[@"mobileReplayOptions"] != nil &&
mutableOptions[@"mobileReplayOptions"][@"maskAllText"] != nil
? mutableOptions[@"mobileReplayOptions"][@"maskAllText"]
: [NSNull null],
}
} forKey:@"experimental"];
[self addReplayRNRedactClasses: mutableOptions[@"mobileReplayOptions"]];
}
[mutableOptions removeObjectForKey:@"_experiments"];
}
#if SENTRY_TARGET_REPLAY_SUPPORTED
[RNSentryReplay updateOptions:mutableOptions];
#endif

SentryOptions *sentryOptions = [[SentryOptions alloc] initWithDict:mutableOptions didFailWithError:errorPointer];
if (*errorPointer != nil) {
Expand Down Expand Up @@ -635,25 +625,21 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray<NSNumber*>*)instructionsAdd
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
#if SENTRY_TARGET_REPLAY_SUPPORTED
[PrivateSentrySDKOnly captureReplay];
resolve([PrivateSentrySDKOnly getReplayId]);
#else
resolve(nil);
#endif
}

RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, getCurrentReplayId)
{
#if SENTRY_TARGET_REPLAY_SUPPORTED
return [PrivateSentrySDKOnly getReplayId];
}

- (void) addReplayRNRedactClasses: (NSDictionary *_Nullable)replayOptions
{
NSMutableArray *_Nonnull classesToRedact = [[NSMutableArray alloc] init];
if ([replayOptions[@"maskAllImages"] boolValue] == YES) {
[classesToRedact addObject: NSClassFromString(@"RCTImageView")];
}
if ([replayOptions[@"maskAllText"] boolValue] == YES) {
[classesToRedact addObject: NSClassFromString(@"RCTTextView")];
}
[PrivateSentrySDKOnly addReplayRedactClasses: classesToRedact];
#else
return nil;
#endif
}

static NSString* const enabledProfilingMessage = @"Enable Hermes to use Sentry Profiling.";
Expand Down
8 changes: 8 additions & 0 deletions ios/RNSentryReplay.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

@interface RNSentryReplay : NSObject

+ (void)updateOptions:(NSMutableDictionary *)options;

+ (void)postInit;

@end
60 changes: 60 additions & 0 deletions ios/RNSentryReplay.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#import "RNSentryReplay.h"
#import "RNSentryReplayBreadcrumbConverter.h"

#if SENTRY_TARGET_REPLAY_SUPPORTED

@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) {
NSLog(@"Session replay disabled via configuration");
return;
}

NSLog(@"Setting up session replay");
NSDictionary *replayOptions = options[@"mobileReplayOptions"] ?: @{};

[options setValue:@{
@"sessionReplay" : @{
@"sessionSampleRate" : experiments[@"replaysSessionSampleRate"]
?: [NSNull null],
@"errorSampleRate" : experiments[@"replaysOnErrorSampleRate"]
?: [NSNull null],
@"redactAllImages" : replayOptions[@"maskAllImages"] ?: [NSNull null],
@"redactAllText" : replayOptions[@"maskAllText"] ?: [NSNull null],
}
}
forKey:@"experimental"];

[RNSentryReplay addReplayRNRedactClasses:replayOptions];
}

+ (void)addReplayRNRedactClasses:(NSDictionary *_Nullable)replayOptions {
NSMutableArray *_Nonnull classesToRedact = [[NSMutableArray alloc] init];
if ([replayOptions[@"maskAllImages"] boolValue] == YES) {
[classesToRedact addObject:NSClassFromString(@"RCTImageView")];
}
if ([replayOptions[@"maskAllText"] boolValue] == YES) {
[classesToRedact addObject:NSClassFromString(@"RCTTextView")];
}
[PrivateSentrySDKOnly addReplayRedactClasses:classesToRedact];
}

+ (void)postInit {
RNSentryReplayBreadcrumbConverter *breadcrumbConverter =
[[RNSentryReplayBreadcrumbConverter alloc] init];
[PrivateSentrySDKOnly configureSessionReplayWith:breadcrumbConverter
screenshotProvider:nil];
}

@end
#endif
12 changes: 12 additions & 0 deletions ios/RNSentryReplayBreadcrumbConverter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@import Sentry;

#if SENTRY_TARGET_REPLAY_SUPPORTED
@class SentryRRWebEvent;

@interface RNSentryReplayBreadcrumbConverter
: NSObject <SentryReplayBreadcrumbConverter>

- (instancetype _Nonnull)init;

@end
#endif
81 changes: 81 additions & 0 deletions ios/RNSentryReplayBreadcrumbConverter.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#import "RNSentryReplayBreadcrumbConverter.h"

@import Sentry;

#if SENTRY_TARGET_REPLAY_SUPPORTED

@implementation RNSentryReplayBreadcrumbConverter {
SentrySRDefaultBreadcrumbConverter *defaultConverter;
}

- (instancetype _Nonnull)init {
if (self = [super init]) {
self->defaultConverter =
[SentrySessionReplayIntegration createDefaultBreadcrumbConverter];
}
return self;
}

- (id<SentryRRWebEvent> _Nullable)convertFrom:
(SentryBreadcrumb *_Nonnull)breadcrumb {
assert(breadcrumb.timestamp != nil);

if ([breadcrumb.category isEqualToString:@"touch"]) {
NSMutableString *message;
if (breadcrumb.data) {
NSMutableArray *path = [breadcrumb.data valueForKey:@"path"];
if (path != nil) {
message = [[NSMutableString alloc] init];
for (NSInteger i = MIN(3, [path count] - 1); i >= 0; i--) {
NSDictionary *item = [path objectAtIndex:i];
[message appendString:[item objectForKey:@"name"]];
if ([item objectForKey:@"element"] || [item objectForKey:@"file"]) {
[message appendString:@"("];
if ([item objectForKey:@"element"]) {
[message appendString:[item objectForKey:@"element"]];
if ([item objectForKey:@"file"]) {
[message appendString:@", "];
[message appendString:[item objectForKey:@"file"]];
}
} else if ([item objectForKey:@"file"]) {
[message appendString:[item objectForKey:@"file"]];
}
[message appendString:@")"];
}
if (i > 0) {
[message appendString:@" > "];
}
}
}
}
return [SentrySessionReplayIntegration
createBreadcrumbwithTimestamp:breadcrumb.timestamp
category:@"ui.tap"
message:message
level:breadcrumb.level
data:breadcrumb.data];
} else if ([breadcrumb.category isEqualToString:@"navigation"]) {
return [SentrySessionReplayIntegration
createBreadcrumbwithTimestamp:breadcrumb.timestamp
category:breadcrumb.category
message:nil
level:breadcrumb.level
data:breadcrumb.data];
} else {
SentryRRWebEvent *nativeBreadcrumb =
[self->defaultConverter convertFrom:breadcrumb];

// ignore native navigation breadcrumbs
if (nativeBreadcrumb && nativeBreadcrumb.data &&
nativeBreadcrumb.data[@"payload"] &&
nativeBreadcrumb.data[@"payload"][@"category"] &&
[nativeBreadcrumb.data[@"payload"][@"category"]
isEqualToString:@"navigation"]) {
return nil;
}
return nativeBreadcrumb;
}
}

@end
#endif
18 changes: 15 additions & 3 deletions src/js/touchevents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,13 +193,25 @@ class TouchEventBoundary extends React.Component<TouchEventBoundaryProps> {
const info: TouchedComponentInfo = {};

// provided by @sentry/babel-plugin-component-annotate
if (typeof props[SENTRY_COMPONENT_PROP_KEY] === 'string' && props[SENTRY_COMPONENT_PROP_KEY].length > 0 && props[SENTRY_COMPONENT_PROP_KEY] !== 'unknown') {
if (
typeof props[SENTRY_COMPONENT_PROP_KEY] === 'string' &&
props[SENTRY_COMPONENT_PROP_KEY].length > 0 &&
props[SENTRY_COMPONENT_PROP_KEY] !== 'unknown'
) {
info.name = props[SENTRY_COMPONENT_PROP_KEY];
}
if (typeof props[SENTRY_ELEMENT_PROP_KEY] === 'string' && props[SENTRY_ELEMENT_PROP_KEY].length > 0 && props[SENTRY_ELEMENT_PROP_KEY] !== 'unknown') {
if (
typeof props[SENTRY_ELEMENT_PROP_KEY] === 'string' &&
props[SENTRY_ELEMENT_PROP_KEY].length > 0 &&
props[SENTRY_ELEMENT_PROP_KEY] !== 'unknown'
) {
info.element = props[SENTRY_ELEMENT_PROP_KEY];
}
if (typeof props[SENTRY_FILE_PROP_KEY] === 'string' && props[SENTRY_FILE_PROP_KEY].length > 0 && props[SENTRY_FILE_PROP_KEY] !== 'unknown') {
if (
typeof props[SENTRY_FILE_PROP_KEY] === 'string' &&
props[SENTRY_FILE_PROP_KEY].length > 0 &&
props[SENTRY_FILE_PROP_KEY] !== 'unknown'
) {
info.file = props[SENTRY_FILE_PROP_KEY];
}

Expand Down