From cbaa720f093e683d1f61a9b629cc794fe759e6ea Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Fri, 22 Mar 2024 15:24:15 +0100 Subject: [PATCH 01/39] wip: Add Mobile Replay --- .../io/sentry/react/RNSentryModuleImpl.java | 44 +++++++++++++- ios/RNSentry.mm | 57 ++++++++++++------- samples/react-native/src/App.tsx | 1 + src/js/integrations/mobilereplay.ts | 16 ++++++ src/js/options.ts | 13 +++++ 5 files changed, 110 insertions(+), 21 deletions(-) create mode 100644 src/js/integrations/mobilereplay.ts diff --git a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 4c69337e2..0bb837c87 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -54,6 +54,7 @@ import io.sentry.IScope; import io.sentry.ISerializer; import io.sentry.Integration; +import io.sentry.Scope; import io.sentry.Sentry; import io.sentry.SentryDate; import io.sentry.SentryDateProvider; @@ -77,11 +78,14 @@ import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.android.core.performance.AppStartMetrics; +import io.sentry.android.replay.ReplayIntegration; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryException; +import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryPackage; import io.sentry.protocol.User; import io.sentry.protocol.ViewHierarchy; +import io.sentry.transport.CurrentDateProvider; import io.sentry.util.DebugMetaPropertiesApplier; import io.sentry.util.JsonSerializationUtils; import io.sentry.vendor.Base64; @@ -252,7 +256,20 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { if (rnOptions.hasKey("enableNdk")) { options.setEnableNdk(rnOptions.getBoolean("enableNdk")); } - + // TODO: Remove for debug only + final List integrations = options.getIntegrations(); + for (final Integration integration : integrations) { + if (integration instanceof ReplayIntegration) { + integrations.remove(integration); + } + } + options.addIntegration(new ReplayIntegration(this.getReactApplicationContext(), CurrentDateProvider.getInstance())); + if (rnOptions.hasKey("replaysSessionSampleRate")) { + // TODO: Set the Android option when available + } + if (rnOptions.hasKey("replaysOnErrorSampleRate")) { + // TODO: Set the Android option when available + } options.setBeforeSend((event, hint) -> { // React native internally throws a JavascriptException // Since we catch it before that, we don't want to send this one @@ -273,7 +290,7 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { }); if (rnOptions.hasKey("enableNativeCrashHandling") && !rnOptions.getBoolean("enableNativeCrashHandling")) { - final List integrations = options.getIntegrations(); + // final List integrations = options.getIntegrations(); for (final Integration integration : integrations) { if (integration instanceof UncaughtExceptionHandlerIntegration || integration instanceof AnrIntegration || integration instanceof NdkIntegration) { @@ -410,6 +427,29 @@ public void fetchNativeFrames(Promise promise) { } } + public void captureReplay(Promise promise) { + // Buffered mode + // TODO: Call the correct replay hybrid SDKs API + //SentryAndroid.startReplay(); + promise.resolve(getCurrentReplayId()); + } + + public @Nullable String captureReplayOnCrash() { + // Sync function to block the main loop before the app closes + // Save to disk, upload on next app start + return getCurrentReplayId(); + } + + private @Nullable String getCurrentReplayId() { + final @Nullable IScope scope = InternalSentrySdk.getCurrentScope(); + if (scope == null) { + return null; + } + + final @Nullable SentryId id = scope.getReplayId(); + return id == null ? null : id.toString(); + } + public void captureEnvelope(String rawBytes, ReadableMap options, Promise promise) { byte[] bytes = Base64.decode(rawBytes, Base64.DEFAULT); diff --git a/ios/RNSentry.mm b/ios/RNSentry.mm index aa9362cc9..f884985e6 100644 --- a/ios/RNSentry.mm +++ b/ios/RNSentry.mm @@ -107,29 +107,48 @@ + (BOOL)requiresMainQueueSetup { - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options error: (NSError *_Nonnull *_Nonnull) errorPointer { - SentryBeforeSendEventCallback beforeSend = ^SentryEvent*(SentryEvent *event) { - // We don't want to send an event after startup that came from a Unhandled JS Exception of react native - // Because we sent it already before the app crashed. - if (nil != event.exceptions.firstObject.type && - [event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location != NSNotFound) { - NSLog(@"Unhandled JS Exception"); - return nil; - } + SentryBeforeSendEventCallback beforeSend = ^SentryEvent*(SentryEvent *event) { + // We don't want to send an event after startup that came from a Unhandled JS Exception of react native + // Because we sent it already before the app crashed. + if (nil != event.exceptions.firstObject.type && + [event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location != NSNotFound) { + NSLog(@"Unhandled JS Exception"); + return nil; + } - [self setEventOriginTag:event]; + [self setEventOriginTag:event]; - return event; - }; + return event; + }; + + NSMutableDictionary * mutableOptions =[options mutableCopy]; + [mutableOptions setValue:beforeSend forKey:@"beforeSend"]; - NSMutableDictionary * mutableOptions =[options mutableCopy]; - [mutableOptions setValue:beforeSend forKey:@"beforeSend"]; + // remove performance traces sample rate and traces sampler since we don't want to synchronize these configurations + // to the Native SDKs. + // The user could tho initialize the SDK manually and set themselves. + [mutableOptions removeObjectForKey:@"tracesSampleRate"]; + [mutableOptions removeObjectForKey:@"tracesSampler"]; + [mutableOptions removeObjectForKey:@"enableTracing"]; - // remove performance traces sample rate and traces sampler since we don't want to synchronize these configurations - // to the Native SDKs. - // The user could tho initialize the SDK manually and set themselves. - [mutableOptions removeObjectForKey:@"tracesSampleRate"]; - [mutableOptions removeObjectForKey:@"tracesSampler"]; - [mutableOptions removeObjectForKey:@"enableTracing"]; + if (mutableOptions[@"replaysSessionSampleRate"] != nil) { + if ([mutableOptions objectForKey:@"sessionReplayOptions"] && [[mutableOptions objectForKey:@"sessionReplayOptions"] isKindOfClass:[NSDictionary class]]) { + NSMutableDictionary *nestedDict = [mutableOptions objectForKey:@"sessionReplayOptions"]; + [nestedDict setValue:mutableOptions[@"replaysSessionSampleRate"] forKey:@"replaysSessionSampleRate"]; + } else { + NSMutableDictionary *newNestedDict = [NSMutableDictionary dictionaryWithObject:mutableOptions[@"replaysSessionSampleRate"] forKey:@"replaysSessionSampleRate"]; + [mutableOptions setObject:newNestedDict forKey:@"sessionReplayOptions"]; + } + } + if (mutableOptions[@"replaysOnErrorSampleRate"] != nil) { + if ([mutableOptions objectForKey:@"sessionReplayOptions"] && [[mutableOptions objectForKey:@"sessionReplayOptions"] isKindOfClass:[NSDictionary class]]) { + NSMutableDictionary *nestedDict = [mutableOptions objectForKey:@"sessionReplayOptions"]; + [nestedDict setValue:mutableOptions[@"replaysOnErrorSampleRate"] forKey:@"replaysSessionSampleRate"]; + } else { + NSMutableDictionary *newNestedDict = [NSMutableDictionary dictionaryWithObject:mutableOptions[@"replaysOnErrorSampleRate"] forKey:@"replaysOnErrorSampleRate"]; + [mutableOptions setObject:newNestedDict forKey:@"sessionReplayOptions"]; + } + } SentryOptions *sentryOptions = [[SentryOptions alloc] initWithDict:mutableOptions didFailWithError:errorPointer]; if (*errorPointer != nil) { diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index ee05aedd5..62dcfb9a4 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -100,6 +100,7 @@ Sentry.init({ profilesSampleRate: 1.0, }, enableSpotlight: true, + replaysSessionSampleRate: 1.0, }); const Stack = createNativeStackNavigator(); diff --git a/src/js/integrations/mobilereplay.ts b/src/js/integrations/mobilereplay.ts new file mode 100644 index 000000000..8de4aacaf --- /dev/null +++ b/src/js/integrations/mobilereplay.ts @@ -0,0 +1,16 @@ +import { IntegrationFn } from '@sentry/types'; + +const NAME = 'MobileReplay'; + +/** + * MobileReplay Integration let's you change default options. + * // TODO: + */ +export const mobileReplay: IntegrationFn = (_options: { + // TODO: Common options for Android and iOS +} = {}) => { + return { + name: NAME, + setupOnce() {}, + }; +}; diff --git a/src/js/options.ts b/src/js/options.ts index d12db7991..24f01785b 100644 --- a/src/js/options.ts +++ b/src/js/options.ts @@ -180,6 +180,19 @@ export interface BaseReactNativeOptions { * @default "http://localhost:8969/stream" */ spotlightSidecarUrl?: string; + + /** + * The sample rate for session-long replays. + * 1.0 will record all sessions and 0 will record none. + */ + replaysSessionSampleRate?: number; + + /** + * The sample rate for sessions that has had an error occur. + * This is independent of `sessionSampleRate`. + * 1.0 will record all sessions and 0 will record none. + */ + replaysOnErrorSampleRate?: number; } export interface ReactNativeTransportOptions extends BrowserTransportOptions { From 8a2580bce9dfaa5ba07fc574e9bb54da9fdc283f Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 25 Mar 2024 12:16:48 +0100 Subject: [PATCH 02/39] Add mobile replay js interface --- src/js/NativeRNSentry.ts | 2 + src/js/integrations/mobilereplay.ts | 94 +++++++++++++++++-- .../integrations/reactnativeerrorhandlers.ts | 4 + src/js/utils/environment.ts | 10 ++ src/js/wrapper.ts | 28 ++++++ 5 files changed, 132 insertions(+), 6 deletions(-) diff --git a/src/js/NativeRNSentry.ts b/src/js/NativeRNSentry.ts index b76ce2848..82330346f 100644 --- a/src/js/NativeRNSentry.ts +++ b/src/js/NativeRNSentry.ts @@ -44,6 +44,8 @@ export interface Spec extends TurboModule { fetchNativePackageName(): string | undefined | null; fetchNativeStackFramesBy(instructionsAddr: number[]): NativeStackFrames | undefined | null; initNativeReactNavigationNewFrameTracking(): Promise; + captureReplay(): Promise; + captureReplayOnCrash(): string | undefined | null; } export type NativeStackFrame = { diff --git a/src/js/integrations/mobilereplay.ts b/src/js/integrations/mobilereplay.ts index 8de4aacaf..01204a5e9 100644 --- a/src/js/integrations/mobilereplay.ts +++ b/src/js/integrations/mobilereplay.ts @@ -1,16 +1,98 @@ -import { IntegrationFn } from '@sentry/types'; +import { getClient } from '@sentry/core'; +import type { Event, IntegrationFn, IntegrationFnResult } from '@sentry/types'; +import { logger } from '@sentry/utils'; + +import { ReactNativeClient } from '../client'; +import { isExpoGo, notMobileOs } from '../utils/environment'; +import { NATIVE } from '../wrapper'; const NAME = 'MobileReplay'; /** * MobileReplay Integration let's you change default options. - * // TODO: */ -export const mobileReplay: IntegrationFn = (_options: { - // TODO: Common options for Android and iOS -} = {}) => { +export const mobileReplay: IntegrationFn = () => { + if (isExpoGo()) { + logger.warn(`[Sentry] ${NAME} is not supported in Expo Go. Use EAS Build or \`expo prebuild\` to enable it.`); + } + if (notMobileOs()) { + logger.warn(`[Sentry] ${NAME} is not supported on this platform.`); + } + + if (isExpoGo() || notMobileOs()) { + return mobileReplayNoop(); + } + return { name: NAME, - setupOnce() {}, + setupOnce() { /* Noop */ }, + processEvent, }; }; + +/** + * Capture a replay of the last user interaction before crash. + */ +export const captureReplayOnCrash = (): string | null => { + if (isExpoGo()) { + logger.warn(`[Sentry] ${NAME} is not supported in Expo Go. Use EAS Build or \`expo prebuild\` to enable it.`); + return null; + } + if (notMobileOs()) { + logger.warn(`[Sentry] ${NAME} is not supported on this platform.`); + return null; + } + + const client = getClient(); + if (!client) { + logger.warn(`[Sentry] ${NAME} no client available.`); + return null; + } + + if (!(client instanceof ReactNativeClient)) { + logger.warn(`[Sentry] ${NAME} supports only React Native clients.`); + return null; + } + + const replaySampleRate = client.getOptions().replaysOnErrorSampleRate; + if (!replaySampleRate) { + logger.debug(`[Sentry] ${NAME} disabled for this client.`); + return null; + } + + return NATIVE.captureReplayOnCrash(); +}; + +async function processEvent(event: Event): Promise { + if (!event.exception) { + return event; + } + + const replayExists = false; + if (replayExists /* TODO: Check if replay already exists */) { + return event; + } + + const replayId = await NATIVE.captureReplay(); + if (!replayId) { + logger.debug(`[Sentry] ${NAME} not sampled for event, ${event.event_id}.`); + } + + addReplayToEvent(event, replayId); + // TODO: Add replayId to DSC + return event; +} + +/** + * Attach a replay to the event. + */ +export const addReplayToEvent = (_event: Event, _replayId: string | null): void => { + // TODO: Add replayId to the current event +} + +function mobileReplayNoop(): IntegrationFnResult { + return { + name: NAME, + setupOnce() { /* Noop */ }, + }; +} diff --git a/src/js/integrations/reactnativeerrorhandlers.ts b/src/js/integrations/reactnativeerrorhandlers.ts index 934c201ad..d4d056078 100644 --- a/src/js/integrations/reactnativeerrorhandlers.ts +++ b/src/js/integrations/reactnativeerrorhandlers.ts @@ -3,6 +3,7 @@ import type { EventHint, Integration, SeverityLevel } from '@sentry/types'; import { addExceptionMechanism, logger } from '@sentry/utils'; import type { ReactNativeClient } from '../client'; +import { addReplayToEvent, captureReplayOnCrash } from '../integrations/mobilereplay'; import { createSyntheticError, isErrorLike } from '../utils/error'; import { ReactNativeLibraries } from '../utils/rnlibraries'; import { RN_GLOBAL_OBJ } from '../utils/worldwide'; @@ -223,11 +224,14 @@ export class ReactNativeErrorHandlers implements Integration { const options = client.getOptions(); + const replayId = captureReplayOnCrash(); + const hint: EventHint = { originalException: error, attachments: scope?.getAttachments(), }; const event = await client.eventFromException(error, hint); + addReplayToEvent(event, replayId); if (isFatal) { event.level = 'fatal' as SeverityLevel; diff --git a/src/js/utils/environment.ts b/src/js/utils/environment.ts index 3af94979c..d30472cb8 100644 --- a/src/js/utils/environment.ts +++ b/src/js/utils/environment.ts @@ -58,6 +58,16 @@ export function notWeb(): boolean { return Platform.OS !== 'web'; } +/** Checks if the current platform is supported mobile platform (iOS or Android) */ +export function isMobileOs(): boolean { + return Platform.OS === 'ios' || Platform.OS === 'android'; +} + +/** Checks if the current platform is not supported mobile platform (iOS or Android) */ +export function notMobileOs(): boolean { + return !isMobileOs(); +} + /** Returns Hermes Version if hermes is present in the runtime */ export function getHermesVersion(): string | undefined { return ( diff --git a/src/js/wrapper.ts b/src/js/wrapper.ts index 1d27a9d05..ade960a79 100644 --- a/src/js/wrapper.ts +++ b/src/js/wrapper.ts @@ -104,6 +104,8 @@ interface SentryNativeWrapper { */ fetchNativeStackFramesBy(instructionsAddr: number[]): NativeStackFrames | null; initNativeReactNavigationNewFrameTracking(): Promise; + captureReplay(): Promise; + captureReplayOnCrash(): string | null; } const EOL = utf8ToBytes('\n'); @@ -608,6 +610,32 @@ export const NATIVE: SentryNativeWrapper = { return RNSentry.initNativeReactNavigationNewFrameTracking(); }, + async captureReplay(): Promise { + if (!this.enableNative) { + logger.warn('[NATIVE] `captureReplay` is not available when native is disabled.'); + return Promise.resolve(null); + } + if (!this._isModuleLoaded(RNSentry)) { + logger.warn('[NATIVE] `captureReplay` is not available when native is not available.'); + return Promise.resolve(null); + } + + return (await RNSentry.captureReplay()) || null; + }, + + captureReplayOnCrash(): string | null { + if (!this.enableNative) { + logger.warn('[NATIVE] `captureReplayOnCrash` is not available when native is disabled.'); + return null; + } + if (!this._isModuleLoaded(RNSentry)) { + logger.warn('[NATIVE] `captureReplayOnCrash` is not available when native is not available.'); + return null; + } + + return RNSentry.captureReplayOnCrash() || null; + }, + /** * Gets the event from envelopeItem and applies the level filter to the selected event. * @param data An envelope item containing the event. From 29c3ad93fa9568b49c65124ed6c2e18f728bbae1 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 25 Mar 2024 17:30:29 +0100 Subject: [PATCH 03/39] Simplify integration interface, always use native replayId --- .../java/io/sentry/react/RNSentryModule.java | 10 ++ ios/RNSentry.mm | 90 +++++++++------ src/js/NativeRNSentry.ts | 4 +- src/js/integrations/default.ts | 8 +- src/js/integrations/mobilereplay.ts | 105 +++++++----------- .../integrations/reactnativeerrorhandlers.ts | 4 - src/js/utils/clientutils.ts | 5 + src/js/wrapper.ts | 21 ++-- 8 files changed, 133 insertions(+), 114 deletions(-) create mode 100644 src/js/utils/clientutils.ts diff --git a/android/src/newarch/java/io/sentry/react/RNSentryModule.java b/android/src/newarch/java/io/sentry/react/RNSentryModule.java index 78dfa4fa5..100886c27 100644 --- a/android/src/newarch/java/io/sentry/react/RNSentryModule.java +++ b/android/src/newarch/java/io/sentry/react/RNSentryModule.java @@ -158,4 +158,14 @@ public WritableMap fetchNativeStackFramesBy(ReadableArray instructionsAddr) { // Not used on Android return null; } + + @Override + public void startReplay(boolean isHardCrash, Promise promise) { + this.impl.startReplay(isHardCrash, promise); + } + + @Override + public String getCurrentReplayId() { + return this.impl.getCurrentReplayId(); + } } diff --git a/ios/RNSentry.mm b/ios/RNSentry.mm index f884985e6..1493ef44a 100644 --- a/ios/RNSentry.mm +++ b/ios/RNSentry.mm @@ -15,7 +15,7 @@ #define SENTRY_TARGET_PROFILING_SUPPORTED 0 #endif -#import +@import Sentry; #import #import #import @@ -112,43 +112,44 @@ - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) // Because we sent it already before the app crashed. if (nil != event.exceptions.firstObject.type && [event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location != NSNotFound) { - NSLog(@"Unhandled JS Exception"); - return nil; - } - - [self setEventOriginTag:event]; - - return event; - }; + NSLog(@"Unhandled JS Exception"); + return nil; + } - NSMutableDictionary * mutableOptions =[options mutableCopy]; - [mutableOptions setValue:beforeSend forKey:@"beforeSend"]; + [self setEventOriginTag:event]; - // remove performance traces sample rate and traces sampler since we don't want to synchronize these configurations - // to the Native SDKs. - // The user could tho initialize the SDK manually and set themselves. - [mutableOptions removeObjectForKey:@"tracesSampleRate"]; - [mutableOptions removeObjectForKey:@"tracesSampler"]; - [mutableOptions removeObjectForKey:@"enableTracing"]; + return event; + }; - if (mutableOptions[@"replaysSessionSampleRate"] != nil) { - if ([mutableOptions objectForKey:@"sessionReplayOptions"] && [[mutableOptions objectForKey:@"sessionReplayOptions"] isKindOfClass:[NSDictionary class]]) { - NSMutableDictionary *nestedDict = [mutableOptions objectForKey:@"sessionReplayOptions"]; - [nestedDict setValue:mutableOptions[@"replaysSessionSampleRate"] forKey:@"replaysSessionSampleRate"]; - } else { - NSMutableDictionary *newNestedDict = [NSMutableDictionary dictionaryWithObject:mutableOptions[@"replaysSessionSampleRate"] forKey:@"replaysSessionSampleRate"]; - [mutableOptions setObject:newNestedDict forKey:@"sessionReplayOptions"]; + NSMutableDictionary * mutableOptions =[options mutableCopy]; + [mutableOptions setValue:beforeSend forKey:@"beforeSend"]; + + // remove performance traces sample rate and traces sampler since we don't want to synchronize these configurations + // to the Native SDKs. + // The user could tho initialize the SDK manually and set themselves. + [mutableOptions removeObjectForKey:@"tracesSampleRate"]; + [mutableOptions removeObjectForKey:@"tracesSampler"]; + [mutableOptions removeObjectForKey:@"enableTracing"]; + + // TODO: If replays is enabled setup RTC native components for redacting + if (mutableOptions[@"replaysSessionSampleRate"] != nil) { + if ([mutableOptions objectForKey:@"sessionReplayOptions"] && [[mutableOptions objectForKey:@"sessionReplayOptions"] isKindOfClass:[NSDictionary class]]) { + NSMutableDictionary *nestedDict = [mutableOptions objectForKey:@"sessionReplayOptions"]; + [nestedDict setValue:mutableOptions[@"replaysSessionSampleRate"] forKey:@"replaysSessionSampleRate"]; + } else { + NSMutableDictionary *newNestedDict = [NSMutableDictionary dictionaryWithObject:mutableOptions[@"replaysSessionSampleRate"] forKey:@"replaysSessionSampleRate"]; + [mutableOptions setObject:newNestedDict forKey:@"sessionReplayOptions"]; + } } - } - if (mutableOptions[@"replaysOnErrorSampleRate"] != nil) { - if ([mutableOptions objectForKey:@"sessionReplayOptions"] && [[mutableOptions objectForKey:@"sessionReplayOptions"] isKindOfClass:[NSDictionary class]]) { - NSMutableDictionary *nestedDict = [mutableOptions objectForKey:@"sessionReplayOptions"]; - [nestedDict setValue:mutableOptions[@"replaysOnErrorSampleRate"] forKey:@"replaysSessionSampleRate"]; - } else { - NSMutableDictionary *newNestedDict = [NSMutableDictionary dictionaryWithObject:mutableOptions[@"replaysOnErrorSampleRate"] forKey:@"replaysOnErrorSampleRate"]; - [mutableOptions setObject:newNestedDict forKey:@"sessionReplayOptions"]; + if (mutableOptions[@"replaysOnErrorSampleRate"] != nil) { + if ([mutableOptions objectForKey:@"sessionReplayOptions"] && [[mutableOptions objectForKey:@"sessionReplayOptions"] isKindOfClass:[NSDictionary class]]) { + NSMutableDictionary *nestedDict = [mutableOptions objectForKey:@"sessionReplayOptions"]; + [nestedDict setValue:mutableOptions[@"replaysOnErrorSampleRate"] forKey:@"replaysSessionSampleRate"]; + } else { + NSMutableDictionary *newNestedDict = [NSMutableDictionary dictionaryWithObject:mutableOptions[@"replaysOnErrorSampleRate"] forKey:@"replaysOnErrorSampleRate"]; + [mutableOptions setObject:newNestedDict forKey:@"sessionReplayOptions"]; + } } - } SentryOptions *sentryOptions = [[SentryOptions alloc] initWithDict:mutableOptions didFailWithError:errorPointer]; if (*errorPointer != nil) { @@ -651,6 +652,29 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray*)instructionsAdd // the 'tracesSampleRate' or 'tracesSampler' option. } +RCT_EXPORT_METHOD(startReplay:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + // TODO: start replay + resolve([self getCurrentReplayIdFromScope]); +} + +RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, getCurrentReplayId) +{ + return [self getCurrentReplayIdFromScope]; +} + +- (NSString * __nullable) getCurrentReplayIdFromScope { + __block NSString * __nullable replayId; + + [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { + //replayId = [scope replayId]; + replayId = nil; + }]; + + return replayId; +} + static NSString* const enabledProfilingMessage = @"Enable Hermes to use Sentry Profiling."; static SentryId* nativeProfileTraceId = nil; static uint64_t nativeProfileStartTime = 0; diff --git a/src/js/NativeRNSentry.ts b/src/js/NativeRNSentry.ts index 82330346f..f90234d98 100644 --- a/src/js/NativeRNSentry.ts +++ b/src/js/NativeRNSentry.ts @@ -44,8 +44,8 @@ export interface Spec extends TurboModule { fetchNativePackageName(): string | undefined | null; fetchNativeStackFramesBy(instructionsAddr: number[]): NativeStackFrames | undefined | null; initNativeReactNavigationNewFrameTracking(): Promise; - captureReplay(): Promise; - captureReplayOnCrash(): string | undefined | null; + startReplay(isHardCrash: boolean): Promise; + getCurrentReplayId(): string | undefined | null; } export type NativeStackFrame = { diff --git a/src/js/integrations/default.ts b/src/js/integrations/default.ts index 4dc16bfae..5e4fc71c1 100644 --- a/src/js/integrations/default.ts +++ b/src/js/integrations/default.ts @@ -1,5 +1,5 @@ import { HttpClient } from '@sentry/integrations'; -import { Integrations as BrowserReactIntegrations } from '@sentry/react'; +import { Integrations as BrowserReactIntegrations , replayIntegration } from '@sentry/react'; import type { Integration } from '@sentry/types'; import type { ReactNativeClientOptions } from '../options'; @@ -10,6 +10,7 @@ import { DebugSymbolicator } from './debugsymbolicator'; import { DeviceContext } from './devicecontext'; import { EventOrigin } from './eventorigin'; import { ExpoContext } from './expocontext'; +import { mobileReplayIntegration } from './mobilereplay'; import { ModulesLoader } from './modulesloader'; import { NativeLinkedErrors } from './nativelinkederrors'; import { ReactNativeErrorHandlers } from './reactnativeerrorhandlers'; @@ -103,5 +104,10 @@ export function getDefaultIntegrations(options: ReactNativeClientOptions): Integ ); } + if (typeof options.replaysOnErrorSampleRate === 'number' || + typeof options.replaysSessionSampleRate === 'number') { + integrations.push(notWeb() ? mobileReplayIntegration() : replayIntegration()); + } + return integrations; } diff --git a/src/js/integrations/mobilereplay.ts b/src/js/integrations/mobilereplay.ts index 01204a5e9..471860aab 100644 --- a/src/js/integrations/mobilereplay.ts +++ b/src/js/integrations/mobilereplay.ts @@ -1,8 +1,8 @@ -import { getClient } from '@sentry/core'; -import type { Event, IntegrationFn, IntegrationFnResult } from '@sentry/types'; +import type { Client, DynamicSamplingContext, Event, IntegrationFn, IntegrationFnResult } from '@sentry/types'; import { logger } from '@sentry/utils'; -import { ReactNativeClient } from '../client'; +import { isHardCrash } from '../misc'; +import { hasHooks } from '../utils/clientutils'; import { isExpoGo, notMobileOs } from '../utils/environment'; import { NATIVE } from '../wrapper'; @@ -11,7 +11,7 @@ const NAME = 'MobileReplay'; /** * MobileReplay Integration let's you change default options. */ -export const mobileReplay: IntegrationFn = () => { +export const mobileReplayIntegration: IntegrationFn = () => { if (isExpoGo()) { logger.warn(`[Sentry] ${NAME} is not supported in Expo Go. Use EAS Build or \`expo prebuild\` to enable it.`); } @@ -20,77 +20,54 @@ export const mobileReplay: IntegrationFn = () => { } if (isExpoGo() || notMobileOs()) { - return mobileReplayNoop(); + return mobileReplayIntegrationNoop(); } - return { - name: NAME, - setupOnce() { /* Noop */ }, - processEvent, - }; -}; - -/** - * Capture a replay of the last user interaction before crash. - */ -export const captureReplayOnCrash = (): string | null => { - if (isExpoGo()) { - logger.warn(`[Sentry] ${NAME} is not supported in Expo Go. Use EAS Build or \`expo prebuild\` to enable it.`); - return null; - } - if (notMobileOs()) { - logger.warn(`[Sentry] ${NAME} is not supported on this platform.`); - return null; - } + async function processEvent(event: Event): Promise { + if (!event.exception) { + // Event is not an error, will not capture replay + return event; + } - const client = getClient(); - if (!client) { - logger.warn(`[Sentry] ${NAME} no client available.`); - return null; - } + const recordingReplayId = NATIVE.getCurrentReplayId(); + if (recordingReplayId) { + logger.debug(`[Sentry] ${NAME} assign already recording replay ${recordingReplayId} for event ${event.event_id}.`); + return event; + } - if (!(client instanceof ReactNativeClient)) { - logger.warn(`[Sentry] ${NAME} supports only React Native clients.`); - return null; - } + const replayId = await NATIVE.startReplay(isHardCrash(event)); + if (!replayId) { + logger.debug(`[Sentry] ${NAME} not sampled for event ${event.event_id}.`); + } - const replaySampleRate = client.getOptions().replaysOnErrorSampleRate; - if (!replaySampleRate) { - logger.debug(`[Sentry] ${NAME} disabled for this client.`); - return null; - } - - return NATIVE.captureReplayOnCrash(); -}; - -async function processEvent(event: Event): Promise { - if (!event.exception) { return event; } - const replayExists = false; - if (replayExists /* TODO: Check if replay already exists */) { - return event; + function setup(client: Client): void { + if (!hasHooks(client)) { + return; + } + + client.on('createDsc', (dsc: DynamicSamplingContext) => { + // TODO: For better performance, we should emit replayId changes on native, and hold the replayId value in JS + const currentReplayId = NATIVE.getCurrentReplayId(); + if (currentReplayId) { + dsc.replay_id = currentReplayId; + } + }); } - const replayId = await NATIVE.captureReplay(); - if (!replayId) { - logger.debug(`[Sentry] ${NAME} not sampled for event, ${event.event_id}.`); - } - - addReplayToEvent(event, replayId); - // TODO: Add replayId to DSC - return event; -} - -/** - * Attach a replay to the event. - */ -export const addReplayToEvent = (_event: Event, _replayId: string | null): void => { - // TODO: Add replayId to the current event -} + // TODO: When adding manual API, ensure overlap with the web replay so users can use the same API interchangeably + // https://github.com/getsentry/sentry-javascript/blob/develop/packages/replay-internal/src/integration.ts#L45 + return { + name: NAME, + setupOnce() { /* Noop */ }, + setup, + processEvent, + }; +}; -function mobileReplayNoop(): IntegrationFnResult { +function mobileReplayIntegrationNoop(): IntegrationFnResult { return { name: NAME, setupOnce() { /* Noop */ }, diff --git a/src/js/integrations/reactnativeerrorhandlers.ts b/src/js/integrations/reactnativeerrorhandlers.ts index d4d056078..934c201ad 100644 --- a/src/js/integrations/reactnativeerrorhandlers.ts +++ b/src/js/integrations/reactnativeerrorhandlers.ts @@ -3,7 +3,6 @@ import type { EventHint, Integration, SeverityLevel } from '@sentry/types'; import { addExceptionMechanism, logger } from '@sentry/utils'; import type { ReactNativeClient } from '../client'; -import { addReplayToEvent, captureReplayOnCrash } from '../integrations/mobilereplay'; import { createSyntheticError, isErrorLike } from '../utils/error'; import { ReactNativeLibraries } from '../utils/rnlibraries'; import { RN_GLOBAL_OBJ } from '../utils/worldwide'; @@ -224,14 +223,11 @@ export class ReactNativeErrorHandlers implements Integration { const options = client.getOptions(); - const replayId = captureReplayOnCrash(); - const hint: EventHint = { originalException: error, attachments: scope?.getAttachments(), }; const event = await client.eventFromException(error, hint); - addReplayToEvent(event, replayId); if (isFatal) { event.level = 'fatal' as SeverityLevel; diff --git a/src/js/utils/clientutils.ts b/src/js/utils/clientutils.ts new file mode 100644 index 000000000..db0d20d7f --- /dev/null +++ b/src/js/utils/clientutils.ts @@ -0,0 +1,5 @@ +import { Client } from '@sentry/types'; + +export function hasHooks(client: Client): client is Client & { on: Required['on'] } { + return client.on !== undefined; +} diff --git a/src/js/wrapper.ts b/src/js/wrapper.ts index ade960a79..ce14b5764 100644 --- a/src/js/wrapper.ts +++ b/src/js/wrapper.ts @@ -104,8 +104,9 @@ interface SentryNativeWrapper { */ fetchNativeStackFramesBy(instructionsAddr: number[]): NativeStackFrames | null; initNativeReactNavigationNewFrameTracking(): Promise; - captureReplay(): Promise; - captureReplayOnCrash(): string | null; + + startReplay(isHardCrash: boolean): Promise; + getCurrentReplayId(): string | null; } const EOL = utf8ToBytes('\n'); @@ -610,30 +611,30 @@ export const NATIVE: SentryNativeWrapper = { return RNSentry.initNativeReactNavigationNewFrameTracking(); }, - async captureReplay(): Promise { + async startReplay(isHardCrash: boolean): Promise { if (!this.enableNative) { - logger.warn('[NATIVE] `captureReplay` is not available when native is disabled.'); + logger.warn(`[NATIVE] \`${this.startReplay.name}\` is not available when native is disabled.`); return Promise.resolve(null); } if (!this._isModuleLoaded(RNSentry)) { - logger.warn('[NATIVE] `captureReplay` is not available when native is not available.'); + logger.warn(`[NATIVE] \`${this.startReplay.name}\` is not available when native is not available.`); return Promise.resolve(null); } - return (await RNSentry.captureReplay()) || null; + return (await RNSentry.startReplay(isHardCrash)) || null; }, - captureReplayOnCrash(): string | null { + getCurrentReplayId(): string | null { if (!this.enableNative) { - logger.warn('[NATIVE] `captureReplayOnCrash` is not available when native is disabled.'); + logger.warn(`[NATIVE] \`${this.getCurrentReplayId.name}\` is not available when native is disabled.`); return null; } if (!this._isModuleLoaded(RNSentry)) { - logger.warn('[NATIVE] `captureReplayOnCrash` is not available when native is not available.'); + logger.warn(`[NATIVE] \`${this.getCurrentReplayId.name}\` is not available when native is not available.`); return null; } - return RNSentry.captureReplayOnCrash() || null; + return RNSentry.getCurrentReplayId() || null; }, /** From de6669b6abde892a2965973e97db654c22abab55 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 25 Mar 2024 17:32:28 +0100 Subject: [PATCH 04/39] remove unwanted style changes --- ios/RNSentry.mm | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/ios/RNSentry.mm b/ios/RNSentry.mm index 1493ef44a..d188ca502 100644 --- a/ios/RNSentry.mm +++ b/ios/RNSentry.mm @@ -107,12 +107,11 @@ + (BOOL)requiresMainQueueSetup { - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options error: (NSError *_Nonnull *_Nonnull) errorPointer { - SentryBeforeSendEventCallback beforeSend = ^SentryEvent*(SentryEvent *event) { - // We don't want to send an event after startup that came from a Unhandled JS Exception of react native - // Because we sent it already before the app crashed. - if (nil != event.exceptions.firstObject.type && - [event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location != NSNotFound) { - NSLog(@"Unhandled JS Exception"); + SentryBeforeSendEventCallback beforeSend = ^SentryEvent*(SentryEvent *event) { + // We don't want to send an event after startup that came from a Unhandled JS Exception of react native + // Because we sent it already before the app crashed. + if (nil != event.exceptions.firstObject.type && + [event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location != NSNotFound) { return nil; } @@ -671,7 +670,7 @@ - (NSString * __nullable) getCurrentReplayIdFromScope { //replayId = [scope replayId]; replayId = nil; }]; - + return replayId; } From 4a66cdd7f74f9606a1aa35f5a4f52eee2eb0033f Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 25 Mar 2024 17:56:33 +0100 Subject: [PATCH 05/39] add pod fix for sentry-cocoa replay version --- RNSentry.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RNSentry.podspec b/RNSentry.podspec index e6e216440..43a97661d 100644 --- a/RNSentry.podspec +++ b/RNSentry.podspec @@ -14,7 +14,7 @@ is_new_arch_enabled = ENV["RCT_NEW_ARCH_ENABLED"] == "1" is_using_hermes = (ENV['USE_HERMES'] == nil && is_hermes_default) || ENV['USE_HERMES'] == '1' new_arch_enabled_flag = (is_new_arch_enabled ? folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED" : "") sentry_profiling_supported_flag = (is_profiling_supported ? " -DSENTRY_PROFILING_SUPPORTED=1" : "") -other_cflags = "$(inherited)" + new_arch_enabled_flag + sentry_profiling_supported_flag +other_cflags = "$(inherited) -fmodules -fcxx-modules " + new_arch_enabled_flag + sentry_profiling_supported_flag Pod::Spec.new do |s| s.name = 'RNSentry' From 2afdcc1156bca85a71d39eb632880efd9e680911 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 25 Mar 2024 18:40:45 +0100 Subject: [PATCH 06/39] fix java impl --- .../io/sentry/react/RNSentryModuleImpl.java | 28 +++++-------------- .../java/io/sentry/react/RNSentryModule.java | 10 +++++++ 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 0bb837c87..8f4473ce0 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -54,7 +54,6 @@ import io.sentry.IScope; import io.sentry.ISerializer; import io.sentry.Integration; -import io.sentry.Scope; import io.sentry.Sentry; import io.sentry.SentryDate; import io.sentry.SentryDateProvider; @@ -78,14 +77,12 @@ import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.android.core.performance.AppStartMetrics; -import io.sentry.android.replay.ReplayIntegration; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryException; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryPackage; import io.sentry.protocol.User; import io.sentry.protocol.ViewHierarchy; -import io.sentry.transport.CurrentDateProvider; import io.sentry.util.DebugMetaPropertiesApplier; import io.sentry.util.JsonSerializationUtils; import io.sentry.vendor.Base64; @@ -256,14 +253,6 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { if (rnOptions.hasKey("enableNdk")) { options.setEnableNdk(rnOptions.getBoolean("enableNdk")); } - // TODO: Remove for debug only - final List integrations = options.getIntegrations(); - for (final Integration integration : integrations) { - if (integration instanceof ReplayIntegration) { - integrations.remove(integration); - } - } - options.addIntegration(new ReplayIntegration(this.getReactApplicationContext(), CurrentDateProvider.getInstance())); if (rnOptions.hasKey("replaysSessionSampleRate")) { // TODO: Set the Android option when available } @@ -290,7 +279,7 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { }); if (rnOptions.hasKey("enableNativeCrashHandling") && !rnOptions.getBoolean("enableNativeCrashHandling")) { - // final List integrations = options.getIntegrations(); + final List integrations = options.getIntegrations(); for (final Integration integration : integrations) { if (integration instanceof UncaughtExceptionHandlerIntegration || integration instanceof AnrIntegration || integration instanceof NdkIntegration) { @@ -427,27 +416,24 @@ public void fetchNativeFrames(Promise promise) { } } - public void captureReplay(Promise promise) { + public void startReplay(boolean isHardCrash, Promise promise) { // Buffered mode // TODO: Call the correct replay hybrid SDKs API //SentryAndroid.startReplay(); promise.resolve(getCurrentReplayId()); } - public @Nullable String captureReplayOnCrash() { - // Sync function to block the main loop before the app closes - // Save to disk, upload on next app start - return getCurrentReplayId(); - } - - private @Nullable String getCurrentReplayId() { + public @Nullable String getCurrentReplayId() { final @Nullable IScope scope = InternalSentrySdk.getCurrentScope(); if (scope == null) { return null; } final @Nullable SentryId id = scope.getReplayId(); - return id == null ? null : id.toString(); + if (id == null) { + return null; + } + return id.toString(); } public void captureEnvelope(String rawBytes, ReadableMap options, Promise promise) { diff --git a/android/src/oldarch/java/io/sentry/react/RNSentryModule.java b/android/src/oldarch/java/io/sentry/react/RNSentryModule.java index 1a11e8571..bd0a5cfb8 100644 --- a/android/src/oldarch/java/io/sentry/react/RNSentryModule.java +++ b/android/src/oldarch/java/io/sentry/react/RNSentryModule.java @@ -158,4 +158,14 @@ public WritableMap fetchNativeStackFramesBy(ReadableArray instructionsAddr) { // Not used on Android return null; } + + @ReactMethod + public void startReplay(boolean isHardCrash, Promise promise) { + this.impl.startReplay(isHardCrash, promise); + } + + @ReactMethod(isBlockingSynchronousMethod = true) + public String getCurrentReplayId() { + return this.impl.getCurrentReplayId(); + } } From 10379f91ecf3b8bd047fd05845f3d8170f1504a6 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 25 Mar 2024 19:36:20 +0100 Subject: [PATCH 07/39] Add RN Components playground --- samples/react-native/src/App.tsx | 15 +++ .../src/Screens/PlaygroundScreen.tsx | 96 +++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 samples/react-native/src/Screens/PlaygroundScreen.tsx diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index 62dcfb9a4..9010415de 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -23,6 +23,7 @@ import GesturesTracingScreen from './Screens/GesturesTracingScreen'; import { StyleSheet } from 'react-native'; import { HttpClient } from '@sentry/integrations'; import Ionicons from 'react-native-vector-icons/Ionicons'; +import PlaygroundScreen from './Screens/PlaygroundScreen'; const reactNavigationInstrumentation = new Sentry.ReactNavigationInstrumentation({ @@ -198,6 +199,20 @@ function BottomTabs() { ), }} /> + ( + + ), + }} + /> ); diff --git a/samples/react-native/src/Screens/PlaygroundScreen.tsx b/samples/react-native/src/Screens/PlaygroundScreen.tsx new file mode 100644 index 000000000..2898e1f72 --- /dev/null +++ b/samples/react-native/src/Screens/PlaygroundScreen.tsx @@ -0,0 +1,96 @@ +import * as React from 'react'; +import { + View, + StyleSheet, + Text, + TextInput, + Image, + ImageBackground, + TouchableWithoutFeedback, + KeyboardAvoidingView, + Keyboard, + ScrollView, + SafeAreaView, + Pressable, +} from 'react-native'; + +const multilineText = `This +is +a +multiline +input +text +`; + +const PlaygroundScreen = () => { + return ( + + + + + + Text: + {"This is "} + + TextInput: + + + Image: + + + BackgroundImage: + + + This text should be over the image. + + + Pressable: + { + event.stopPropagation(); + event.preventDefault(); + console.log('Pressable pressed'); + }} + > + Press me + + + + + + + ); +}; + +export default PlaygroundScreen; + +const styles = StyleSheet.create({ + space: { + marginBottom: 50, + }, + container: { + padding: 5, + flex: 1, + }, + image: { + width: 200, + height: 200, + }, + backgroundImageContainer: { + width: 200, + height: 200, + }, + textInputStyle: { + height: 200, + borderColor: 'gray', + borderWidth: 1, + } +}); From f8eb045c81c57d984c8e8ba5c4b50b72985508c7 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 26 Mar 2024 10:34:55 +0100 Subject: [PATCH 08/39] fix missing hard crash --- ios/RNSentry.mm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ios/RNSentry.mm b/ios/RNSentry.mm index d188ca502..9182bb02b 100644 --- a/ios/RNSentry.mm +++ b/ios/RNSentry.mm @@ -651,7 +651,8 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray*)instructionsAdd // the 'tracesSampleRate' or 'tracesSampler' option. } -RCT_EXPORT_METHOD(startReplay:(RCTPromiseResolveBlock)resolve +RCT_EXPORT_METHOD(startReplay: (BOOL)isHardCrash + resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { // TODO: start replay From 1c9001b7d4b634a4a89b340ad99f485d47a69a8f Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 22 Apr 2024 11:47:17 +0200 Subject: [PATCH 09/39] revert clags changes --- RNSentry.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RNSentry.podspec b/RNSentry.podspec index acd00c107..d8413326b 100644 --- a/RNSentry.podspec +++ b/RNSentry.podspec @@ -14,7 +14,7 @@ is_new_arch_enabled = ENV["RCT_NEW_ARCH_ENABLED"] == "1" is_using_hermes = (ENV['USE_HERMES'] == nil && is_hermes_default) || ENV['USE_HERMES'] == '1' new_arch_enabled_flag = (is_new_arch_enabled ? folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED" : "") sentry_profiling_supported_flag = (is_profiling_supported ? " -DSENTRY_PROFILING_SUPPORTED=1" : "") -other_cflags = "$(inherited) -fmodules -fcxx-modules " + new_arch_enabled_flag + sentry_profiling_supported_flag +other_cflags = "$(inherited)" + new_arch_enabled_flag + sentry_profiling_supported_flag Pod::Spec.new do |s| s.name = 'RNSentry' From 3abfca87c949234e50ec63f2ce6a54b35210b142 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 22 Apr 2024 11:47:43 +0200 Subject: [PATCH 10/39] Bump native SDKs to alphas with replay and add changelog --- CHANGELOG.md | 20 ++++++++++++++++++++ RNSentry.podspec | 2 +- android/build.gradle | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a716e9f88..bc54fade7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## Unreleased + +### Features + +- Mobile Session Replay Alpha ([#3714](https://github.com/getsentry/sentry-react-native/pull/3714)) + + ```js + Sentry.init({ + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 1.0, + }); + ``` + + We are in closed alpha stage, [please let us know on the waitlist if you're interested](https://sentry.io/lp/mobile-replay-beta/). + +### Dependencies + +- Bump Cocoa SDK to [8.24.1-alpha.0](https://github.com/getsentry/sentry-cocoa/releases/tag/8.24.1-alpha.0) +- Bump Android SDK to [v7.8.0-alpha.0](https://github.com/getsentry/sentry-java/releases/tag/7.8.0-alpha.0) + ## 5.22.0 ### Features diff --git a/RNSentry.podspec b/RNSentry.podspec index d8413326b..3a762ca3c 100644 --- a/RNSentry.podspec +++ b/RNSentry.podspec @@ -33,7 +33,7 @@ Pod::Spec.new do |s| s.preserve_paths = '*.js' s.dependency 'React-Core' - s.dependency 'Sentry/HybridSDK', '8.24.0' + s.dependency 'Sentry/HybridSDK', '8.24.0-alpha.0' s.source_files = 'ios/**/*.{h,m,mm}' s.public_header_files = 'ios/RNSentry.h' diff --git a/android/build.gradle b/android/build.gradle index 704f34f91..18b8d2c8e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -54,5 +54,5 @@ android { dependencies { implementation 'com.facebook.react:react-native:+' - api 'io.sentry:sentry-android:7.8.0' + api 'io.sentry:sentry-android:7.8.0-alpha.0' } From 82fbf4c60aaae0d1f382ec279363735e3be534bc Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 23 Apr 2024 11:34:29 +0200 Subject: [PATCH 11/39] Add replay tag in JS --- src/js/integrations/mobilereplay.ts | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/js/integrations/mobilereplay.ts b/src/js/integrations/mobilereplay.ts index 471860aab..d14eae097 100644 --- a/src/js/integrations/mobilereplay.ts +++ b/src/js/integrations/mobilereplay.ts @@ -32,14 +32,19 @@ export const mobileReplayIntegration: IntegrationFn = () => { const recordingReplayId = NATIVE.getCurrentReplayId(); if (recordingReplayId) { logger.debug(`[Sentry] ${NAME} assign already recording replay ${recordingReplayId} for event ${event.event_id}.`); + addReplayIdToTraceContext(event, recordingReplayId); + addReplayIdToTags(event, recordingReplayId); return event; } const replayId = await NATIVE.startReplay(isHardCrash(event)); if (!replayId) { logger.debug(`[Sentry] ${NAME} not sampled for event ${event.event_id}.`); + return event; } + addReplayIdToTraceContext(event, replayId); + addReplayIdToTags(event, replayId); return event; } @@ -67,6 +72,31 @@ export const mobileReplayIntegration: IntegrationFn = () => { }; }; +function addReplayIdToTraceContext(event: Event, replayId: string): void { + if (!event.contexts || !event.contexts.trace) { + logger.warn(`[Sentry][${NAME}] Event ${event.event_id} is missing trace context. Won't add replay_id.`); + return; + } + + if (event.contexts.trace.replay_id) { + // No log, as this is expected behavior when replay is already recording + return; + } + + event.contexts.trace.replay_id = replayId; +} + +function addReplayIdToTags(event: Event, replayId: string): void { + event.tags = event.tags || {}; + + if (event.tags.replayId) { + logger.warn(`[Sentry][${NAME}] Event ${event.event_id} already has replayId tag. Won't overwrite.`); + return; + } + + event.tags.replayId = replayId; +} + function mobileReplayIntegrationNoop(): IntegrationFnResult { return { name: NAME, From a0727c98826aaf09eb0578c49cd4572eb1909f1d Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 23 Apr 2024 16:12:14 +0200 Subject: [PATCH 12/39] add more robust is exception event check --- src/js/integrations/mobilereplay.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/js/integrations/mobilereplay.ts b/src/js/integrations/mobilereplay.ts index d14eae097..260d31f6c 100644 --- a/src/js/integrations/mobilereplay.ts +++ b/src/js/integrations/mobilereplay.ts @@ -24,7 +24,8 @@ export const mobileReplayIntegration: IntegrationFn = () => { } async function processEvent(event: Event): Promise { - if (!event.exception) { + const hasException = event.exception && event.exception.values && event.exception.values.length > 0; + if (!hasException) { // Event is not an error, will not capture replay return event; } From 8a291ed627b8591671f10e601bea3895d21f92de Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 23 Apr 2024 16:12:30 +0200 Subject: [PATCH 13/39] Add on error sample rate to the sample app --- samples/react-native/src/App.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index 9010415de..fd321381b 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -101,7 +101,8 @@ Sentry.init({ profilesSampleRate: 1.0, }, enableSpotlight: true, - replaysSessionSampleRate: 1.0, + // replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 1.0, }); const Stack = createNativeStackNavigator(); From 7f1c85fd0c82805d126ac8103d63a634172b056a Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Tue, 23 Apr 2024 16:12:53 +0200 Subject: [PATCH 14/39] Use cocoa impl of captureReplay --- ios/RNSentry.mm | 41 ++++++++++------------------------------- 1 file changed, 10 insertions(+), 31 deletions(-) diff --git a/ios/RNSentry.mm b/ios/RNSentry.mm index 6721ba161..462f791ab 100644 --- a/ios/RNSentry.mm +++ b/ios/RNSentry.mm @@ -130,25 +130,15 @@ - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) [mutableOptions removeObjectForKey:@"tracesSampleRate"]; [mutableOptions removeObjectForKey:@"tracesSampler"]; [mutableOptions removeObjectForKey:@"enableTracing"]; - + // TODO: If replays is enabled setup RTC native components for redacting - if (mutableOptions[@"replaysSessionSampleRate"] != nil) { - if ([mutableOptions objectForKey:@"sessionReplayOptions"] && [[mutableOptions objectForKey:@"sessionReplayOptions"] isKindOfClass:[NSDictionary class]]) { - NSMutableDictionary *nestedDict = [mutableOptions objectForKey:@"sessionReplayOptions"]; - [nestedDict setValue:mutableOptions[@"replaysSessionSampleRate"] forKey:@"replaysSessionSampleRate"]; - } else { - NSMutableDictionary *newNestedDict = [NSMutableDictionary dictionaryWithObject:mutableOptions[@"replaysSessionSampleRate"] forKey:@"replaysSessionSampleRate"]; - [mutableOptions setObject:newNestedDict forKey:@"sessionReplayOptions"]; - } - } - if (mutableOptions[@"replaysOnErrorSampleRate"] != nil) { - if ([mutableOptions objectForKey:@"sessionReplayOptions"] && [[mutableOptions objectForKey:@"sessionReplayOptions"] isKindOfClass:[NSDictionary class]]) { - NSMutableDictionary *nestedDict = [mutableOptions objectForKey:@"sessionReplayOptions"]; - [nestedDict setValue:mutableOptions[@"replaysOnErrorSampleRate"] forKey:@"replaysSessionSampleRate"]; - } else { - NSMutableDictionary *newNestedDict = [NSMutableDictionary dictionaryWithObject:mutableOptions[@"replaysOnErrorSampleRate"] forKey:@"replaysOnErrorSampleRate"]; - [mutableOptions setObject:newNestedDict forKey:@"sessionReplayOptions"]; + if (mutableOptions[@"replaysSessionSampleRate"] != nil || mutableOptions[@"replaysOnErrorSampleRate"] != nil) { + [mutableOptions setValue:@{ + @"sessionReplay": @{ + @"sessionSampleRate": mutableOptions[@"replaysSessionSampleRate"] ?: [NSNull null], + @"errorSampleRate": mutableOptions[@"replaysOnErrorSampleRate"] ?: [NSNull null], } + } forKey:@"experimental"]; } SentryOptions *sentryOptions = [[SentryOptions alloc] initWithDict:mutableOptions didFailWithError:errorPointer]; @@ -656,24 +646,13 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray*)instructionsAdd resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - // TODO: start replay - resolve([self getCurrentReplayIdFromScope]); + [PrivateSentrySDKOnly captureReplay]; + resolve([PrivateSentrySDKOnly getReplayId]); } RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSString *, getCurrentReplayId) { - return [self getCurrentReplayIdFromScope]; -} - -- (NSString * __nullable) getCurrentReplayIdFromScope { - __block NSString * __nullable replayId; - - [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { - //replayId = [scope replayId]; - replayId = nil; - }]; - - return replayId; + return [PrivateSentrySDKOnly getReplayId]; } static NSString* const enabledProfilingMessage = @"Enable Hermes to use Sentry Profiling."; From 48be9e986ad6d584c0e0356586b71f724dbd54f9 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 24 Apr 2024 15:22:18 +0200 Subject: [PATCH 15/39] Add Android native replay options, capture replay and EMPTY_ID check --- .../io/sentry/react/RNSentryModuleImpl.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 8f4473ce0..d376020ff 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -61,6 +61,7 @@ import io.sentry.SentryExecutorService; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.SentryReplayOptions; import io.sentry.UncaughtExceptionHandlerIntegration; import io.sentry.android.core.AndroidLogger; import io.sentry.android.core.AndroidProfiler; @@ -253,11 +254,15 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { if (rnOptions.hasKey("enableNdk")) { options.setEnableNdk(rnOptions.getBoolean("enableNdk")); } - if (rnOptions.hasKey("replaysSessionSampleRate")) { - // TODO: Set the Android option when available - } - if (rnOptions.hasKey("replaysOnErrorSampleRate")) { - // TODO: Set the Android option when available + if (rnOptions.hasKey("replaysSessionSampleRate") || rnOptions.hasKey("replaysOnErrorSampleRate")) { + @Nullable Double replaysSessionSampleRate = rnOptions.hasKey("replaysSessionSampleRate") + ? rnOptions.getDouble("replaysSessionSampleRate") : null; + @Nullable Double replaysOnErrorSampleRate = rnOptions.hasKey("replaysOnErrorSampleRate") + ? rnOptions.getDouble("replaysOnErrorSampleRate") : null; + options.getExperimental().setSessionReplay(new SentryReplayOptions( + replaysSessionSampleRate, + replaysOnErrorSampleRate + )); } options.setBeforeSend((event, hint) -> { // React native internally throws a JavascriptException @@ -417,9 +422,7 @@ public void fetchNativeFrames(Promise promise) { } public void startReplay(boolean isHardCrash, Promise promise) { - // Buffered mode - // TODO: Call the correct replay hybrid SDKs API - //SentryAndroid.startReplay(); + Sentry.getCurrentHub().getOptions().getReplayController().sendReplay(isHardCrash, null, null); promise.resolve(getCurrentReplayId()); } @@ -429,8 +432,8 @@ public void startReplay(boolean isHardCrash, Promise promise) { return null; } - final @Nullable SentryId id = scope.getReplayId(); - if (id == null) { + final @NotNull SentryId id = scope.getReplayId(); + if (id == SentryId.EMPTY_ID) { return null; } return id.toString(); From f9acb418e65085ac88aa1b831bd8fa6d202d9c6f Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 24 Apr 2024 15:23:15 +0200 Subject: [PATCH 16/39] add redacting RCT text and images for iOS replay --- ios/RNSentry.mm | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ios/RNSentry.mm b/ios/RNSentry.mm index 462f791ab..3054b00d3 100644 --- a/ios/RNSentry.mm +++ b/ios/RNSentry.mm @@ -7,6 +7,9 @@ #import "RCTConvert.h" #endif +#import +#import + #if __has_include() && SENTRY_PROFILING_SUPPORTED #define SENTRY_PROFILING_ENABLED 1 #import @@ -139,6 +142,7 @@ - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) @"errorSampleRate": mutableOptions[@"replaysOnErrorSampleRate"] ?: [NSNull null], } } forKey:@"experimental"]; + [self addReplayRNRedactClasses]; } SentryOptions *sentryOptions = [[SentryOptions alloc] initWithDict:mutableOptions didFailWithError:errorPointer]; @@ -655,6 +659,11 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray*)instructionsAdd return [PrivateSentrySDKOnly getReplayId]; } +- (void) addReplayRNRedactClasses +{ + [PrivateSentrySDKOnly addReplayRedactClasses: @[[RCTTextView class], [RCTImageView class]]]; +} + static NSString* const enabledProfilingMessage = @"Enable Hermes to use Sentry Profiling."; static SentryId* nativeProfileTraceId = nil; static uint64_t nativeProfileStartTime = 0; From e7eb005215a7b547e32214d59912e4c0bcc7a059 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 24 Apr 2024 17:38:37 +0200 Subject: [PATCH 17/39] revert replayID tag and context functions --- src/js/integrations/mobilereplay.ts | 33 ++++------------------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/src/js/integrations/mobilereplay.ts b/src/js/integrations/mobilereplay.ts index 260d31f6c..bad7afc89 100644 --- a/src/js/integrations/mobilereplay.ts +++ b/src/js/integrations/mobilereplay.ts @@ -33,8 +33,6 @@ export const mobileReplayIntegration: IntegrationFn = () => { const recordingReplayId = NATIVE.getCurrentReplayId(); if (recordingReplayId) { logger.debug(`[Sentry] ${NAME} assign already recording replay ${recordingReplayId} for event ${event.event_id}.`); - addReplayIdToTraceContext(event, recordingReplayId); - addReplayIdToTags(event, recordingReplayId); return event; } @@ -44,8 +42,6 @@ export const mobileReplayIntegration: IntegrationFn = () => { return event; } - addReplayIdToTraceContext(event, replayId); - addReplayIdToTags(event, replayId); return event; } @@ -55,6 +51,10 @@ export const mobileReplayIntegration: IntegrationFn = () => { } client.on('createDsc', (dsc: DynamicSamplingContext) => { + if (dsc.replay_id) { + return; + } + // TODO: For better performance, we should emit replayId changes on native, and hold the replayId value in JS const currentReplayId = NATIVE.getCurrentReplayId(); if (currentReplayId) { @@ -73,31 +73,6 @@ export const mobileReplayIntegration: IntegrationFn = () => { }; }; -function addReplayIdToTraceContext(event: Event, replayId: string): void { - if (!event.contexts || !event.contexts.trace) { - logger.warn(`[Sentry][${NAME}] Event ${event.event_id} is missing trace context. Won't add replay_id.`); - return; - } - - if (event.contexts.trace.replay_id) { - // No log, as this is expected behavior when replay is already recording - return; - } - - event.contexts.trace.replay_id = replayId; -} - -function addReplayIdToTags(event: Event, replayId: string): void { - event.tags = event.tags || {}; - - if (event.tags.replayId) { - logger.warn(`[Sentry][${NAME}] Event ${event.event_id} already has replayId tag. Won't overwrite.`); - return; - } - - event.tags.replayId = replayId; -} - function mobileReplayIntegrationNoop(): IntegrationFnResult { return { name: NAME, From f792d2a4aba1f436bb88cffc7bde4446311da048 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 24 Apr 2024 17:43:00 +0200 Subject: [PATCH 18/39] fix lint --- samples/react-native/src/App.tsx | 4 +- .../src/Screens/PlaygroundScreen.tsx | 83 ++++++++++--------- src/js/integrations/default.ts | 5 +- src/js/integrations/mobilereplay.ts | 12 ++- src/js/options.ts | 2 +- src/js/utils/clientutils.ts | 5 +- 6 files changed, 62 insertions(+), 49 deletions(-) diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index fd321381b..ddafac879 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -207,7 +207,9 @@ function BottomTabs() { tabBarLabel: 'Playground', tabBarIcon: ({ focused, color, size }) => ( diff --git a/samples/react-native/src/Screens/PlaygroundScreen.tsx b/samples/react-native/src/Screens/PlaygroundScreen.tsx index 2898e1f72..690b14bbe 100644 --- a/samples/react-native/src/Screens/PlaygroundScreen.tsx +++ b/samples/react-native/src/Screens/PlaygroundScreen.tsx @@ -24,49 +24,52 @@ text const PlaygroundScreen = () => { return ( - - - - - - Text: - {"This is "} - - TextInput: - - - Image: - - - BackgroundImage: + + + + - - This text should be over the image. - - - Pressable: - { + Text: + {'This is '} + + TextInput: + + + Image: + + + BackgroundImage: + + + This text should be over the image. + + + Pressable: + { event.stopPropagation(); event.preventDefault(); console.log('Pressable pressed'); - }} - > - Press me - - - - - - + }}> + Press me + + + + + + ); }; @@ -92,5 +95,5 @@ const styles = StyleSheet.create({ height: 200, borderColor: 'gray', borderWidth: 1, - } + }, }); diff --git a/src/js/integrations/default.ts b/src/js/integrations/default.ts index 5e4fc71c1..0d4224e30 100644 --- a/src/js/integrations/default.ts +++ b/src/js/integrations/default.ts @@ -1,5 +1,5 @@ import { HttpClient } from '@sentry/integrations'; -import { Integrations as BrowserReactIntegrations , replayIntegration } from '@sentry/react'; +import { Integrations as BrowserReactIntegrations, replayIntegration } from '@sentry/react'; import type { Integration } from '@sentry/types'; import type { ReactNativeClientOptions } from '../options'; @@ -104,8 +104,7 @@ export function getDefaultIntegrations(options: ReactNativeClientOptions): Integ ); } - if (typeof options.replaysOnErrorSampleRate === 'number' || - typeof options.replaysSessionSampleRate === 'number') { + if (typeof options.replaysOnErrorSampleRate === 'number' || typeof options.replaysSessionSampleRate === 'number') { integrations.push(notWeb() ? mobileReplayIntegration() : replayIntegration()); } diff --git a/src/js/integrations/mobilereplay.ts b/src/js/integrations/mobilereplay.ts index bad7afc89..2b844ee83 100644 --- a/src/js/integrations/mobilereplay.ts +++ b/src/js/integrations/mobilereplay.ts @@ -32,7 +32,9 @@ export const mobileReplayIntegration: IntegrationFn = () => { const recordingReplayId = NATIVE.getCurrentReplayId(); if (recordingReplayId) { - logger.debug(`[Sentry] ${NAME} assign already recording replay ${recordingReplayId} for event ${event.event_id}.`); + logger.debug( + `[Sentry] ${NAME} assign already recording replay ${recordingReplayId} for event ${event.event_id}.`, + ); return event; } @@ -67,7 +69,9 @@ export const mobileReplayIntegration: IntegrationFn = () => { // https://github.com/getsentry/sentry-javascript/blob/develop/packages/replay-internal/src/integration.ts#L45 return { name: NAME, - setupOnce() { /* Noop */ }, + setupOnce() { + /* Noop */ + }, setup, processEvent, }; @@ -76,6 +80,8 @@ export const mobileReplayIntegration: IntegrationFn = () => { function mobileReplayIntegrationNoop(): IntegrationFnResult { return { name: NAME, - setupOnce() { /* Noop */ }, + setupOnce() { + /* Noop */ + }, }; } diff --git a/src/js/options.ts b/src/js/options.ts index 34a6133ee..86eacea37 100644 --- a/src/js/options.ts +++ b/src/js/options.ts @@ -186,7 +186,7 @@ export interface BaseReactNativeOptions { * relevant if `attachScreenshot` is set to true. When false is returned * from the function, no screenshot will be attached. */ - beforeScreenshot ?: (event: Event, hint: EventHint) => boolean; + beforeScreenshot?: (event: Event, hint: EventHint) => boolean; /** * The sample rate for session-long replays. diff --git a/src/js/utils/clientutils.ts b/src/js/utils/clientutils.ts index db0d20d7f..40f073a66 100644 --- a/src/js/utils/clientutils.ts +++ b/src/js/utils/clientutils.ts @@ -1,5 +1,8 @@ -import { Client } from '@sentry/types'; +import type { Client } from '@sentry/types'; +/** + * + */ export function hasHooks(client: Client): client is Client & { on: Required['on'] } { return client.on !== undefined; } From 2f8b8c84da26d3f47a49cd2382edd88de98abbed Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Thu, 25 Apr 2024 16:24:33 +0200 Subject: [PATCH 19/39] move replay options under experiments --- .../io/sentry/react/RNSentryModuleImpl.java | 13 +++--- ios/RNSentry.mm | 23 ++++++----- samples/react-native/src/App.tsx | 4 +- src/js/integrations/default.ts | 6 ++- src/js/options.ts | 41 +++++++++++++------ 5 files changed, 57 insertions(+), 30 deletions(-) diff --git a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index d376020ff..20eda74da 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -254,16 +254,19 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { if (rnOptions.hasKey("enableNdk")) { options.setEnableNdk(rnOptions.getBoolean("enableNdk")); } - if (rnOptions.hasKey("replaysSessionSampleRate") || rnOptions.hasKey("replaysOnErrorSampleRate")) { - @Nullable Double replaysSessionSampleRate = rnOptions.hasKey("replaysSessionSampleRate") - ? rnOptions.getDouble("replaysSessionSampleRate") : null; - @Nullable Double replaysOnErrorSampleRate = rnOptions.hasKey("replaysOnErrorSampleRate") - ? rnOptions.getDouble("replaysOnErrorSampleRate") : null; + if (rnOptions.hasKey("_experiments")) { + @Nullable final ReadableMap rnExperimentsOptions = rnOptions.getMap("_experiments"); + if (rnExperimentsOptions != null && (rnExperimentsOptions.hasKey("replaysSessionSampleRate") || rnExperimentsOptions.hasKey("replaysOnErrorSampleRate"))) { + @Nullable Double replaysSessionSampleRate = rnExperimentsOptions.hasKey("replaysSessionSampleRate") + ? rnExperimentsOptions.getDouble("replaysSessionSampleRate") : null; + @Nullable Double replaysOnErrorSampleRate = rnExperimentsOptions.hasKey("replaysOnErrorSampleRate") + ? rnExperimentsOptions.getDouble("replaysOnErrorSampleRate") : null; options.getExperimental().setSessionReplay(new SentryReplayOptions( replaysSessionSampleRate, replaysOnErrorSampleRate )); } + } options.setBeforeSend((event, hint) -> { // React native internally throws a JavascriptException // Since we catch it before that, we don't want to send this one diff --git a/ios/RNSentry.mm b/ios/RNSentry.mm index 3054b00d3..4c50f9420 100644 --- a/ios/RNSentry.mm +++ b/ios/RNSentry.mm @@ -133,16 +133,19 @@ - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) [mutableOptions removeObjectForKey:@"tracesSampleRate"]; [mutableOptions removeObjectForKey:@"tracesSampler"]; [mutableOptions removeObjectForKey:@"enableTracing"]; - - // TODO: If replays is enabled setup RTC native components for redacting - if (mutableOptions[@"replaysSessionSampleRate"] != nil || mutableOptions[@"replaysOnErrorSampleRate"] != nil) { - [mutableOptions setValue:@{ - @"sessionReplay": @{ - @"sessionSampleRate": mutableOptions[@"replaysSessionSampleRate"] ?: [NSNull null], - @"errorSampleRate": mutableOptions[@"replaysOnErrorSampleRate"] ?: [NSNull null], - } - } forKey:@"experimental"]; - [self addReplayRNRedactClasses]; + + 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], + } + } forKey:@"experimental"]; + [self addReplayRNRedactClasses]; + } + [mutableOptions removeObjectForKey:@"_experiments"]; } SentryOptions *sentryOptions = [[SentryOptions alloc] initWithDict:mutableOptions didFailWithError:errorPointer]; diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index ddafac879..a00ffd458 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -99,10 +99,10 @@ Sentry.init({ // dist: `1`, _experiments: { profilesSampleRate: 1.0, + // replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 1.0, }, enableSpotlight: true, - // replaysSessionSampleRate: 1.0, - replaysOnErrorSampleRate: 1.0, }); const Stack = createNativeStackNavigator(); diff --git a/src/js/integrations/default.ts b/src/js/integrations/default.ts index 0d4224e30..1dff2b155 100644 --- a/src/js/integrations/default.ts +++ b/src/js/integrations/default.ts @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ import { HttpClient } from '@sentry/integrations'; import { Integrations as BrowserReactIntegrations, replayIntegration } from '@sentry/react'; import type { Integration } from '@sentry/types'; @@ -104,7 +105,10 @@ export function getDefaultIntegrations(options: ReactNativeClientOptions): Integ ); } - if (typeof options.replaysOnErrorSampleRate === 'number' || typeof options.replaysSessionSampleRate === 'number') { + if ( + (options._experiments && typeof options._experiments.replaysOnErrorSampleRate === 'number') || + (options._experiments && typeof options._experiments.replaysSessionSampleRate === 'number') + ) { integrations.push(notWeb() ? mobileReplayIntegration() : replayIntegration()); } diff --git a/src/js/options.ts b/src/js/options.ts index 86eacea37..1892a1603 100644 --- a/src/js/options.ts +++ b/src/js/options.ts @@ -189,17 +189,30 @@ export interface BaseReactNativeOptions { beforeScreenshot?: (event: Event, hint: EventHint) => boolean; /** - * The sample rate for session-long replays. - * 1.0 will record all sessions and 0 will record none. + * Options which are in beta, or otherwise not guaranteed to be stable. */ - replaysSessionSampleRate?: number; - - /** - * The sample rate for sessions that has had an error occur. - * This is independent of `sessionSampleRate`. - * 1.0 will record all sessions and 0 will record none. - */ - replaysOnErrorSampleRate?: number; + _experiments?: { + [key: string]: unknown; + + /** + * The sample rate for profiling + * 1.0 will profile all transactions and 0 will profile none. + */ + profilesSampleRate?: number; + + /** + * The sample rate for session-long replays. + * 1.0 will record all sessions and 0 will record none. + */ + replaysSessionSampleRate?: number; + + /** + * The sample rate for sessions that has had an error occur. + * This is independent of `sessionSampleRate`. + * 1.0 will record all sessions and 0 will record none. + */ + replaysOnErrorSampleRate?: number; + }; } export interface ReactNativeTransportOptions extends BrowserTransportOptions { @@ -214,9 +227,13 @@ export interface ReactNativeTransportOptions extends BrowserTransportOptions { * @see ReactNativeFrontend for more information. */ -export interface ReactNativeOptions extends Options, BaseReactNativeOptions {} +export interface ReactNativeOptions + extends Omit, '_experiments'>, + BaseReactNativeOptions {} -export interface ReactNativeClientOptions extends ClientOptions, BaseReactNativeOptions {} +export interface ReactNativeClientOptions + extends Omit, '_experiments'>, + BaseReactNativeOptions {} export interface ReactNativeWrapperOptions { /** Props for the root React profiler */ From 615faafbfc4eb4f2598365af3972f75d153049eb Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Thu, 25 Apr 2024 16:32:09 +0200 Subject: [PATCH 20/39] enable replay in expo sample --- samples/expo/app/_layout.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/samples/expo/app/_layout.tsx b/samples/expo/app/_layout.tsx index 6fac65744..810f56797 100644 --- a/samples/expo/app/_layout.tsx +++ b/samples/expo/app/_layout.tsx @@ -78,6 +78,8 @@ process.env.EXPO_SKIP_DURING_EXPORT !== 'true' && Sentry.init({ // dist: `1`, _experiments: { profilesSampleRate: 0, + // replaysOnErrorSampleRate: 1.0, + replaysSessionSampleRate: 1.0, }, enableSpotlight: true, }); From 51649305e98f39a050e71770b30078d6328dec8b Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Thu, 25 Apr 2024 16:44:37 +0200 Subject: [PATCH 21/39] pass replay options to enabled web --- src/js/integrations/default.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/js/integrations/default.ts b/src/js/integrations/default.ts index 1dff2b155..7caaa822c 100644 --- a/src/js/integrations/default.ts +++ b/src/js/integrations/default.ts @@ -1,6 +1,7 @@ /* eslint-disable complexity */ import { HttpClient } from '@sentry/integrations'; -import { Integrations as BrowserReactIntegrations, replayIntegration } from '@sentry/react'; +import type { BrowserOptions} from '@sentry/react'; +import { Integrations as BrowserReactIntegrations, replayIntegration as browserReplayIntegration } from '@sentry/react'; import type { Integration } from '@sentry/types'; import type { ReactNativeClientOptions } from '../options'; @@ -109,7 +110,11 @@ export function getDefaultIntegrations(options: ReactNativeClientOptions): Integ (options._experiments && typeof options._experiments.replaysOnErrorSampleRate === 'number') || (options._experiments && typeof options._experiments.replaysSessionSampleRate === 'number') ) { - integrations.push(notWeb() ? mobileReplayIntegration() : replayIntegration()); + integrations.push(notWeb() ? mobileReplayIntegration() : browserReplayIntegration()); + if (!notWeb()) { + (options as BrowserOptions).replaysOnErrorSampleRate = options._experiments.replaysOnErrorSampleRate; + (options as BrowserOptions).replaysSessionSampleRate = options._experiments.replaysSessionSampleRate; + } } return integrations; From 6227788926d3c82f6a1a50e410f63038cbd644d7 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Fri, 26 Apr 2024 11:48:05 +0200 Subject: [PATCH 22/39] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc54fade7..e1094e234 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ }); ``` - We are in closed alpha stage, [please let us know on the waitlist if you're interested](https://sentry.io/lp/mobile-replay-beta/). + 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/) ### Dependencies From a46da312b63da3b511aeda9ca0944a00299f1539 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Fri, 26 Apr 2024 16:09:18 +0200 Subject: [PATCH 23/39] add mask options --- .../io/sentry/react/RNSentryModuleImpl.java | 22 ++++++++++---- .../java/io/sentry/react/RNSentryModule.java | 7 +++++ .../java/io/sentry/react/RNSentryModule.java | 5 ++++ ios/RNSentry.mm | 17 ++++++++++- samples/react-native/src/App.tsx | 4 +++ src/js/NativeRNSentry.ts | 1 + src/js/index.ts | 1 + src/js/integrations/default.ts | 2 +- src/js/integrations/index.ts | 1 + src/js/integrations/mobilereplay.ts | 30 +++++++++++++++---- src/js/wrapper.ts | 15 ++++++++++ 11 files changed, 93 insertions(+), 12 deletions(-) diff --git a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 20eda74da..0e7aecbde 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -133,6 +133,9 @@ public class RNSentryModuleImpl { /** Max trace file size in bytes. */ private long maxTraceFileSize = 5 * 1024 * 1024; + /** Set from JS before the Android SDK init */ + private @Nullable ReadableMap replayOptions = null; + public RNSentryModuleImpl(ReactApplicationContext reactApplicationContext) { packageInfo = getPackageInfo(reactApplicationContext); this.reactApplicationContext = reactApplicationContext; @@ -257,15 +260,20 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { if (rnOptions.hasKey("_experiments")) { @Nullable final ReadableMap rnExperimentsOptions = rnOptions.getMap("_experiments"); if (rnExperimentsOptions != null && (rnExperimentsOptions.hasKey("replaysSessionSampleRate") || rnExperimentsOptions.hasKey("replaysOnErrorSampleRate"))) { - @Nullable Double replaysSessionSampleRate = rnExperimentsOptions.hasKey("replaysSessionSampleRate") + final @Nullable Double replaysSessionSampleRate = rnExperimentsOptions.hasKey("replaysSessionSampleRate") ? rnExperimentsOptions.getDouble("replaysSessionSampleRate") : null; - @Nullable Double replaysOnErrorSampleRate = rnExperimentsOptions.hasKey("replaysOnErrorSampleRate") + final @Nullable Double replaysOnErrorSampleRate = rnExperimentsOptions.hasKey("replaysOnErrorSampleRate") ? rnExperimentsOptions.getDouble("replaysOnErrorSampleRate") : null; - options.getExperimental().setSessionReplay(new SentryReplayOptions( + final @NotNull SentryReplayOptions androidReplayOptions = new SentryReplayOptions( replaysSessionSampleRate, replaysOnErrorSampleRate - )); - } + ); + androidReplayOptions.setRedactAllText(replayOptions != null && replayOptions.hasKey("maskAllText") + ? replayOptions.getBoolean("maskAllText") : true); + androidReplayOptions.setRedactAllImages(replayOptions != null && replayOptions.hasKey("maskAllImages") + ? replayOptions.getBoolean("maskAllImages") : true); + options.getExperimental().setSessionReplay(androidReplayOptions); + } } options.setBeforeSend((event, hint) -> { // React native internally throws a JavascriptException @@ -442,6 +450,10 @@ public void startReplay(boolean isHardCrash, Promise promise) { return id.toString(); } + public void setReplayOptions(@NotNull ReadableMap options) { + this.replayOptions = options; + } + public void captureEnvelope(String rawBytes, ReadableMap options, Promise promise) { byte[] bytes = Base64.decode(rawBytes, Base64.DEFAULT); diff --git a/android/src/newarch/java/io/sentry/react/RNSentryModule.java b/android/src/newarch/java/io/sentry/react/RNSentryModule.java index 100886c27..d7313b616 100644 --- a/android/src/newarch/java/io/sentry/react/RNSentryModule.java +++ b/android/src/newarch/java/io/sentry/react/RNSentryModule.java @@ -8,6 +8,8 @@ import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.WritableMap; +import org.jetbrains.annotations.NotNull; + public class RNSentryModule extends NativeRNSentrySpec { private final RNSentryModuleImpl impl; @@ -168,4 +170,9 @@ public void startReplay(boolean isHardCrash, Promise promise) { public String getCurrentReplayId() { return this.impl.getCurrentReplayId(); } + + @Override + public void setReplayOptions(@NotNull ReadableMap options) { + this.impl.setReplayOptions(options); + } } diff --git a/android/src/oldarch/java/io/sentry/react/RNSentryModule.java b/android/src/oldarch/java/io/sentry/react/RNSentryModule.java index bd0a5cfb8..51e1e8eee 100644 --- a/android/src/oldarch/java/io/sentry/react/RNSentryModule.java +++ b/android/src/oldarch/java/io/sentry/react/RNSentryModule.java @@ -168,4 +168,9 @@ public void startReplay(boolean isHardCrash, Promise promise) { public String getCurrentReplayId() { return this.impl.getCurrentReplayId(); } + + @ReactMethod + public void setReplayOptions(@NotNull ReadableMap options) { + this.impl.setReplayOptions(options); + } } diff --git a/ios/RNSentry.mm b/ios/RNSentry.mm index 4c50f9420..13edfba43 100644 --- a/ios/RNSentry.mm +++ b/ios/RNSentry.mm @@ -61,6 +61,7 @@ + (void)storeEnvelope:(SentryEnvelope *)envelope; @implementation RNSentry { bool sentHybridSdkDidBecomeActive; bool hasListeners; + NSDictionary *_Nullable replayOptions; } - (dispatch_queue_t)methodQueue @@ -141,6 +142,8 @@ - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) @"sessionReplay": @{ @"sessionSampleRate": experiments[@"replaysSessionSampleRate"] ?: [NSNull null], @"errorSampleRate": experiments[@"replaysOnErrorSampleRate"] ?: [NSNull null], + @"redactAllImages": replayOptions[@"maskAllImages"] ?: [NSNull null], + @"redactAllText": replayOptions[@"maskAllText"] ?: [NSNull null], } } forKey:@"experimental"]; [self addReplayRNRedactClasses]; @@ -662,9 +665,21 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray*)instructionsAdd return [PrivateSentrySDKOnly getReplayId]; } +RCT_EXPORT_METHOD(setReplayOptions: (NSDictionary *_Nonnull)options) +{ + replayOptions = options; +} + - (void) addReplayRNRedactClasses { - [PrivateSentrySDKOnly addReplayRedactClasses: @[[RCTTextView class], [RCTImageView class]]]; + NSMutableArray *_Nonnull classesToRedact = [[NSMutableArray alloc] init]; + if ([replayOptions[@"maskAllImages"] boolValue] == YES) { + [classesToRedact addObject: [RCTImageView class]]; + } + if ([replayOptions[@"maskAllText"] boolValue] == YES) { + [classesToRedact addObject: [RCTTextView class]]; + } + [PrivateSentrySDKOnly addReplayRedactClasses: classesToRedact]; } static NSString* const enabledProfilingMessage = @"Enable Hermes to use Sentry Profiling."; diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index a00ffd458..478924b0d 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -76,6 +76,10 @@ Sentry.init({ failedRequestTargets: [/.*/], }), Sentry.metrics.metricsAggregatorIntegration(), + Sentry.mobileReplayIntegration({ + maskAllImages: false, + // maskAllText: false, + }), ); return integrations.filter(i => i.name !== 'Dedupe'); }, diff --git a/src/js/NativeRNSentry.ts b/src/js/NativeRNSentry.ts index f90234d98..580dba48f 100644 --- a/src/js/NativeRNSentry.ts +++ b/src/js/NativeRNSentry.ts @@ -46,6 +46,7 @@ export interface Spec extends TurboModule { initNativeReactNavigationNewFrameTracking(): Promise; startReplay(isHardCrash: boolean): Promise; getCurrentReplayId(): string | undefined | null; + setReplayOptions(options: UnsafeObject): void; } export type NativeStackFrame = { diff --git a/src/js/index.ts b/src/js/index.ts index d1d66f0e1..f11963b4c 100644 --- a/src/js/index.ts +++ b/src/js/index.ts @@ -64,6 +64,7 @@ export { export { lastEventId } from '@sentry/browser'; import * as Integrations from './integrations'; +export { mobileReplayIntegration } from './integrations'; import { SDK_NAME, SDK_VERSION } from './version'; export type { ReactNativeOptions } from './options'; export { ReactNativeClient } from './client'; diff --git a/src/js/integrations/default.ts b/src/js/integrations/default.ts index 7caaa822c..4d4129f7d 100644 --- a/src/js/integrations/default.ts +++ b/src/js/integrations/default.ts @@ -1,6 +1,6 @@ /* eslint-disable complexity */ import { HttpClient } from '@sentry/integrations'; -import type { BrowserOptions} from '@sentry/react'; +import type { BrowserOptions } from '@sentry/react'; import { Integrations as BrowserReactIntegrations, replayIntegration as browserReplayIntegration } from '@sentry/react'; import type { Integration } from '@sentry/types'; diff --git a/src/js/integrations/index.ts b/src/js/integrations/index.ts index 3a8ad303a..e70f953a2 100644 --- a/src/js/integrations/index.ts +++ b/src/js/integrations/index.ts @@ -8,3 +8,4 @@ export { ReactNativeInfo } from './reactnativeinfo'; export { ModulesLoader } from './modulesloader'; export { HermesProfiling } from '../profiling/integration'; export { Spotlight } from './spotlight'; +export { mobileReplayIntegration } from './mobilereplay'; diff --git a/src/js/integrations/mobilereplay.ts b/src/js/integrations/mobilereplay.ts index 2b844ee83..99351f81e 100644 --- a/src/js/integrations/mobilereplay.ts +++ b/src/js/integrations/mobilereplay.ts @@ -1,4 +1,4 @@ -import type { Client, DynamicSamplingContext, Event, IntegrationFn, IntegrationFnResult } from '@sentry/types'; +import type { Client, DynamicSamplingContext, Event, IntegrationFn } from '@sentry/types'; import { logger } from '@sentry/utils'; import { isHardCrash } from '../misc'; @@ -8,10 +8,27 @@ import { NATIVE } from '../wrapper'; const NAME = 'MobileReplay'; +export interface MobileReplayOptions { + /** + * Mask all text in recordings + */ + maskAllText?: boolean; + + /** + * Mask all text in recordings + */ + maskAllImages?: boolean; +} + +const defaultOptions: Required = { + maskAllText: true, + maskAllImages: true, +}; + /** * MobileReplay Integration let's you change default options. */ -export const mobileReplayIntegration: IntegrationFn = () => { +export const mobileReplayIntegration = ((options: MobileReplayOptions = defaultOptions) => { if (isExpoGo()) { logger.warn(`[Sentry] ${NAME} is not supported in Expo Go. Use EAS Build or \`expo prebuild\` to enable it.`); } @@ -23,6 +40,9 @@ export const mobileReplayIntegration: IntegrationFn = () => { return mobileReplayIntegrationNoop(); } + const initialOptions = { ...defaultOptions, ...options }; + NATIVE.setReplayOptions(initialOptions); + async function processEvent(event: Event): Promise { const hasException = event.exception && event.exception.values && event.exception.values.length > 0; if (!hasException) { @@ -75,13 +95,13 @@ export const mobileReplayIntegration: IntegrationFn = () => { setup, processEvent, }; -}; +}) satisfies IntegrationFn; -function mobileReplayIntegrationNoop(): IntegrationFnResult { +const mobileReplayIntegrationNoop = (() => { return { name: NAME, setupOnce() { /* Noop */ }, }; -} +}) satisfies IntegrationFn; diff --git a/src/js/wrapper.ts b/src/js/wrapper.ts index ce14b5764..ae4a074fc 100644 --- a/src/js/wrapper.ts +++ b/src/js/wrapper.ts @@ -107,6 +107,8 @@ interface SentryNativeWrapper { startReplay(isHardCrash: boolean): Promise; getCurrentReplayId(): string | null; + + setReplayOptions(options: Record): void; } const EOL = utf8ToBytes('\n'); @@ -637,6 +639,19 @@ export const NATIVE: SentryNativeWrapper = { return RNSentry.getCurrentReplayId() || null; }, + setReplayOptions(options: Record): void { + if (!this.enableNative) { + logger.warn(`[NATIVE] \`${this.setReplayOptions.name}\` is not available when native is disabled.`); + return; + } + if (!this._isModuleLoaded(RNSentry)) { + logger.warn(`[NATIVE] \`${this.setReplayOptions.name}\` is not available when native is not available.`); + return; + } + + RNSentry.setReplayOptions(options); + }, + /** * Gets the event from envelopeItem and applies the level filter to the selected event. * @param data An envelope item containing the event. From 0512490c2d34772420730f5157e3add284273d38 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Fri, 26 Apr 2024 16:14:28 +0200 Subject: [PATCH 24/39] update changelog --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1094e234..0e46d9e94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,27 @@ - Mobile Session Replay Alpha ([#3714](https://github.com/getsentry/sentry-react-native/pull/3714)) + To enable Replay for React Native on mobile and web add the following options. + + ```js + Sentry.init({ + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 1.0, + }); + ``` + + To change the default Mobile Replay options add the `mobileReplayIntegration`. + ```js Sentry.init({ replaysSessionSampleRate: 1.0, replaysOnErrorSampleRate: 1.0, + integration: [ + Sentry.mobileReplayIntegration({ + maskAllText: true, + maskAllImages: true, + }), + ], }); ``` From 9f747c56822d7d44eed4d9bd9f5dd40eae5fc829 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Fri, 26 Apr 2024 18:07:08 +0200 Subject: [PATCH 25/39] Pass replay options to native during sdk initialization --- .../io/sentry/react/RNSentryModuleImpl.java | 55 +++++++++++-------- .../java/io/sentry/react/RNSentryModule.java | 7 --- .../java/io/sentry/react/RNSentryModule.java | 5 -- ios/RNSentry.mm | 20 +++---- src/js/NativeRNSentry.ts | 1 - src/js/client.ts | 20 ++++++- src/js/integrations/mobilereplay.ts | 33 +++++++---- src/js/wrapper.ts | 26 +++------ 8 files changed, 89 insertions(+), 78 deletions(-) diff --git a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 0e7aecbde..fde282261 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -133,9 +133,6 @@ public class RNSentryModuleImpl { /** Max trace file size in bytes. */ private long maxTraceFileSize = 5 * 1024 * 1024; - /** Set from JS before the Android SDK init */ - private @Nullable ReadableMap replayOptions = null; - public RNSentryModuleImpl(ReactApplicationContext reactApplicationContext) { packageInfo = getPackageInfo(reactApplicationContext); this.reactApplicationContext = reactApplicationContext; @@ -258,22 +255,7 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { options.setEnableNdk(rnOptions.getBoolean("enableNdk")); } if (rnOptions.hasKey("_experiments")) { - @Nullable final ReadableMap rnExperimentsOptions = rnOptions.getMap("_experiments"); - if (rnExperimentsOptions != null && (rnExperimentsOptions.hasKey("replaysSessionSampleRate") || rnExperimentsOptions.hasKey("replaysOnErrorSampleRate"))) { - final @Nullable Double replaysSessionSampleRate = rnExperimentsOptions.hasKey("replaysSessionSampleRate") - ? rnExperimentsOptions.getDouble("replaysSessionSampleRate") : null; - final @Nullable Double replaysOnErrorSampleRate = rnExperimentsOptions.hasKey("replaysOnErrorSampleRate") - ? rnExperimentsOptions.getDouble("replaysOnErrorSampleRate") : null; - final @NotNull SentryReplayOptions androidReplayOptions = new SentryReplayOptions( - replaysSessionSampleRate, - replaysOnErrorSampleRate - ); - androidReplayOptions.setRedactAllText(replayOptions != null && replayOptions.hasKey("maskAllText") - ? replayOptions.getBoolean("maskAllText") : true); - androidReplayOptions.setRedactAllImages(replayOptions != null && replayOptions.hasKey("maskAllImages") - ? replayOptions.getBoolean("maskAllImages") : true); - options.getExperimental().setSessionReplay(androidReplayOptions); - } + options.getExperimental().setSessionReplay(getReplayOptions(rnOptions)); } options.setBeforeSend((event, hint) -> { // React native internally throws a JavascriptException @@ -315,6 +297,37 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { promise.resolve(true); } + private SentryReplayOptions getReplayOptions(@NotNull ReadableMap rnOptions) { + @NotNull final SentryReplayOptions androidReplayOptions = new SentryReplayOptions(); + + @Nullable final ReadableMap rnExperimentsOptions = rnOptions.getMap("_experiments"); + if (rnExperimentsOptions == null) { + return androidReplayOptions; + } + + if (!(rnExperimentsOptions.hasKey("replaysSessionSampleRate") || rnExperimentsOptions.hasKey("replaysOnErrorSampleRate"))) { + return androidReplayOptions; + } + + androidReplayOptions.setErrorSampleRate(rnExperimentsOptions.hasKey("replaysSessionSampleRate") + ? rnExperimentsOptions.getDouble("replaysSessionSampleRate") : null); + androidReplayOptions.setErrorSampleRate(rnExperimentsOptions.hasKey("replaysOnErrorSampleRate") + ? rnExperimentsOptions.getDouble("replaysOnErrorSampleRate") : null); + + if (!rnOptions.hasKey("mobileReplayOptions")) { + return androidReplayOptions; + } + @Nullable final ReadableMap rnMobileReplayOptions = rnOptions.getMap("mobileReplayOptions"); + if (rnMobileReplayOptions == null) { + return androidReplayOptions; + } + + androidReplayOptions.setRedactAllText(!rnMobileReplayOptions.hasKey("maskAllText") || rnMobileReplayOptions.getBoolean("maskAllText")); + androidReplayOptions.setRedactAllImages(!rnMobileReplayOptions.hasKey("maskAllImages") || rnMobileReplayOptions.getBoolean("maskAllImages")); + + return androidReplayOptions; + } + public void crash() { throw new RuntimeException("TEST - Sentry Client Crash (only works in release mode)"); } @@ -450,10 +463,6 @@ public void startReplay(boolean isHardCrash, Promise promise) { return id.toString(); } - public void setReplayOptions(@NotNull ReadableMap options) { - this.replayOptions = options; - } - public void captureEnvelope(String rawBytes, ReadableMap options, Promise promise) { byte[] bytes = Base64.decode(rawBytes, Base64.DEFAULT); diff --git a/android/src/newarch/java/io/sentry/react/RNSentryModule.java b/android/src/newarch/java/io/sentry/react/RNSentryModule.java index d7313b616..100886c27 100644 --- a/android/src/newarch/java/io/sentry/react/RNSentryModule.java +++ b/android/src/newarch/java/io/sentry/react/RNSentryModule.java @@ -8,8 +8,6 @@ import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.WritableMap; -import org.jetbrains.annotations.NotNull; - public class RNSentryModule extends NativeRNSentrySpec { private final RNSentryModuleImpl impl; @@ -170,9 +168,4 @@ public void startReplay(boolean isHardCrash, Promise promise) { public String getCurrentReplayId() { return this.impl.getCurrentReplayId(); } - - @Override - public void setReplayOptions(@NotNull ReadableMap options) { - this.impl.setReplayOptions(options); - } } diff --git a/android/src/oldarch/java/io/sentry/react/RNSentryModule.java b/android/src/oldarch/java/io/sentry/react/RNSentryModule.java index 51e1e8eee..bd0a5cfb8 100644 --- a/android/src/oldarch/java/io/sentry/react/RNSentryModule.java +++ b/android/src/oldarch/java/io/sentry/react/RNSentryModule.java @@ -168,9 +168,4 @@ public void startReplay(boolean isHardCrash, Promise promise) { public String getCurrentReplayId() { return this.impl.getCurrentReplayId(); } - - @ReactMethod - public void setReplayOptions(@NotNull ReadableMap options) { - this.impl.setReplayOptions(options); - } } diff --git a/ios/RNSentry.mm b/ios/RNSentry.mm index 13edfba43..78d67eae2 100644 --- a/ios/RNSentry.mm +++ b/ios/RNSentry.mm @@ -61,7 +61,6 @@ + (void)storeEnvelope:(SentryEnvelope *)envelope; @implementation RNSentry { bool sentHybridSdkDidBecomeActive; bool hasListeners; - NSDictionary *_Nullable replayOptions; } - (dispatch_queue_t)methodQueue @@ -142,11 +141,17 @@ - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) @"sessionReplay": @{ @"sessionSampleRate": experiments[@"replaysSessionSampleRate"] ?: [NSNull null], @"errorSampleRate": experiments[@"replaysOnErrorSampleRate"] ?: [NSNull null], - @"redactAllImages": replayOptions[@"maskAllImages"] ?: [NSNull null], - @"redactAllText": replayOptions[@"maskAllText"] ?: [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]; + [self addReplayRNRedactClasses: mutableOptions[@"mobileReplayOptions"]]; } [mutableOptions removeObjectForKey:@"_experiments"]; } @@ -665,12 +670,7 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray*)instructionsAdd return [PrivateSentrySDKOnly getReplayId]; } -RCT_EXPORT_METHOD(setReplayOptions: (NSDictionary *_Nonnull)options) -{ - replayOptions = options; -} - -- (void) addReplayRNRedactClasses +- (void) addReplayRNRedactClasses: (NSDictionary *_Nullable)replayOptions { NSMutableArray *_Nonnull classesToRedact = [[NSMutableArray alloc] init]; if ([replayOptions[@"maskAllImages"] boolValue] == YES) { diff --git a/src/js/NativeRNSentry.ts b/src/js/NativeRNSentry.ts index 580dba48f..f90234d98 100644 --- a/src/js/NativeRNSentry.ts +++ b/src/js/NativeRNSentry.ts @@ -46,7 +46,6 @@ export interface Spec extends TurboModule { initNativeReactNavigationNewFrameTracking(): Promise; startReplay(isHardCrash: boolean): Promise; getCurrentReplayId(): string | undefined | null; - setReplayOptions(options: UnsafeObject): void; } export type NativeStackFrame = { diff --git a/src/js/client.ts b/src/js/client.ts index 9c62b5ea3..59ad2a9a7 100644 --- a/src/js/client.ts +++ b/src/js/client.ts @@ -16,6 +16,8 @@ import { dateTimestampInSeconds, logger, SentryError } from '@sentry/utils'; import { Alert } from 'react-native'; import { createIntegration } from './integrations/factory'; +import type { mobileReplayIntegration } from './integrations/mobilereplay'; +import { MOBILE_REPLAY_INTEGRATION_NAME } from './integrations/mobilereplay'; import { defaultSdkInfo } from './integrations/sdkinfo'; import type { ReactNativeClientOptions } from './options'; import { ReactNativeTracing } from './tracing'; @@ -44,7 +46,6 @@ export class ReactNativeClient extends BaseClient { super(options); this._outcomesBuffer = []; - this._initNativeSdk(); } /** @@ -111,6 +112,13 @@ export class ReactNativeClient extends BaseClient { */ public setupIntegrations(): void { super.setupIntegrations(); + } + + /** + * @inheritDoc + */ + public init(): void { + super.init(); const tracing = this.getIntegration(ReactNativeTracing); const routingName = tracing?.options.routingInstrumentation?.name; if (routingName) { @@ -120,6 +128,7 @@ export class ReactNativeClient extends BaseClient { if (enableUserInteractionTracing) { this.addIntegration(createIntegration('ReactNativeUserInteractionTracing')); } + this._initNativeSdk(); } /** @@ -160,7 +169,14 @@ export class ReactNativeClient extends BaseClient { * Starts native client with dsn and options */ private _initNativeSdk(): void { - NATIVE.initNativeSdk(this._options) + NATIVE.initNativeSdk({ + ...this._options, + mobileReplayOptions: + this._integrations[MOBILE_REPLAY_INTEGRATION_NAME] && + 'options' in this._integrations[MOBILE_REPLAY_INTEGRATION_NAME] + ? (this._integrations[MOBILE_REPLAY_INTEGRATION_NAME] as ReturnType).options + : undefined, + }) .then( (result: boolean) => { return result; diff --git a/src/js/integrations/mobilereplay.ts b/src/js/integrations/mobilereplay.ts index 99351f81e..07b0c9e4b 100644 --- a/src/js/integrations/mobilereplay.ts +++ b/src/js/integrations/mobilereplay.ts @@ -1,4 +1,4 @@ -import type { Client, DynamicSamplingContext, Event, IntegrationFn } from '@sentry/types'; +import type { Client, DynamicSamplingContext, Event, IntegrationFn, IntegrationFnResult } from '@sentry/types'; import { logger } from '@sentry/utils'; import { isHardCrash } from '../misc'; @@ -6,7 +6,7 @@ import { hasHooks } from '../utils/clientutils'; import { isExpoGo, notMobileOs } from '../utils/environment'; import { NATIVE } from '../wrapper'; -const NAME = 'MobileReplay'; +export const MOBILE_REPLAY_INTEGRATION_NAME = 'MobileReplay'; export interface MobileReplayOptions { /** @@ -25,23 +25,30 @@ const defaultOptions: Required = { maskAllImages: true, }; +type MobileReplayIntegration = IntegrationFnResult & { + options: Required; +}; + /** * MobileReplay Integration let's you change default options. */ -export const mobileReplayIntegration = ((options: MobileReplayOptions = defaultOptions) => { +export const mobileReplayIntegration = (( + initOptions: MobileReplayOptions = defaultOptions, +): MobileReplayIntegration => { if (isExpoGo()) { - logger.warn(`[Sentry] ${NAME} is not supported in Expo Go. Use EAS Build or \`expo prebuild\` to enable it.`); + logger.warn( + `[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} is not supported in Expo Go. Use EAS Build or \`expo prebuild\` to enable it.`, + ); } if (notMobileOs()) { - logger.warn(`[Sentry] ${NAME} is not supported on this platform.`); + logger.warn(`[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} is not supported on this platform.`); } if (isExpoGo() || notMobileOs()) { return mobileReplayIntegrationNoop(); } - const initialOptions = { ...defaultOptions, ...options }; - NATIVE.setReplayOptions(initialOptions); + const options = { ...defaultOptions, ...initOptions }; async function processEvent(event: Event): Promise { const hasException = event.exception && event.exception.values && event.exception.values.length > 0; @@ -53,14 +60,14 @@ export const mobileReplayIntegration = ((options: MobileReplayOptions = defaultO const recordingReplayId = NATIVE.getCurrentReplayId(); if (recordingReplayId) { logger.debug( - `[Sentry] ${NAME} assign already recording replay ${recordingReplayId} for event ${event.event_id}.`, + `[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} assign already recording replay ${recordingReplayId} for event ${event.event_id}.`, ); return event; } const replayId = await NATIVE.startReplay(isHardCrash(event)); if (!replayId) { - logger.debug(`[Sentry] ${NAME} not sampled for event ${event.event_id}.`); + logger.debug(`[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} not sampled for event ${event.event_id}.`); return event; } @@ -88,20 +95,22 @@ export const mobileReplayIntegration = ((options: MobileReplayOptions = defaultO // TODO: When adding manual API, ensure overlap with the web replay so users can use the same API interchangeably // https://github.com/getsentry/sentry-javascript/blob/develop/packages/replay-internal/src/integration.ts#L45 return { - name: NAME, + name: MOBILE_REPLAY_INTEGRATION_NAME, setupOnce() { /* Noop */ }, setup, processEvent, + options: options, }; }) satisfies IntegrationFn; -const mobileReplayIntegrationNoop = (() => { +const mobileReplayIntegrationNoop = ((): MobileReplayIntegration => { return { - name: NAME, + name: MOBILE_REPLAY_INTEGRATION_NAME, setupOnce() { /* Noop */ }, + options: defaultOptions, }; }) satisfies IntegrationFn; diff --git a/src/js/wrapper.ts b/src/js/wrapper.ts index ae4a074fc..0f8ca3558 100644 --- a/src/js/wrapper.ts +++ b/src/js/wrapper.ts @@ -12,6 +12,7 @@ import type { import { logger, normalize, SentryError } from '@sentry/utils'; import { NativeModules, Platform } from 'react-native'; +import type { MobileReplayOptions } from './integrations/mobilereplay'; import { isHardCrash } from './misc'; import type { NativeAppStartResponse, @@ -47,6 +48,10 @@ export interface Screenshot { filename: string; } +export type NativeSdkOptions = Partial & { + mobileReplayOptions: MobileReplayOptions | undefined; +}; + interface SentryNativeWrapper { enableNative: boolean; nativeIsReady: boolean; @@ -63,7 +68,7 @@ interface SentryNativeWrapper { isNativeAvailable(): boolean; - initNativeSdk(options: Partial): PromiseLike; + initNativeSdk(options: NativeSdkOptions): PromiseLike; closeNativeSdk(): PromiseLike; sendEnvelope(envelope: Envelope): Promise; @@ -107,8 +112,6 @@ interface SentryNativeWrapper { startReplay(isHardCrash: boolean): Promise; getCurrentReplayId(): string | null; - - setReplayOptions(options: Record): void; } const EOL = utf8ToBytes('\n'); @@ -198,8 +201,8 @@ export const NATIVE: SentryNativeWrapper = { * Starts native with the provided options. * @param options ReactNativeClientOptions */ - async initNativeSdk(originalOptions: Partial): Promise { - const options: Partial = { + async initNativeSdk(originalOptions: NativeSdkOptions): Promise { + const options: NativeSdkOptions = { enableNative: true, autoInitializeNativeSdk: true, ...originalOptions, @@ -639,19 +642,6 @@ export const NATIVE: SentryNativeWrapper = { return RNSentry.getCurrentReplayId() || null; }, - setReplayOptions(options: Record): void { - if (!this.enableNative) { - logger.warn(`[NATIVE] \`${this.setReplayOptions.name}\` is not available when native is disabled.`); - return; - } - if (!this._isModuleLoaded(RNSentry)) { - logger.warn(`[NATIVE] \`${this.setReplayOptions.name}\` is not available when native is not available.`); - return; - } - - RNSentry.setReplayOptions(options); - }, - /** * Gets the event from envelopeItem and applies the level filter to the selected event. * @param data An envelope item containing the event. From 1c15afa6d000446684baa7447407b98eea34c35a Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Fri, 26 Apr 2024 18:16:59 +0200 Subject: [PATCH 26/39] satisfies breaks tests --- src/js/integrations/mobilereplay.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/js/integrations/mobilereplay.ts b/src/js/integrations/mobilereplay.ts index 07b0c9e4b..54bfbf38e 100644 --- a/src/js/integrations/mobilereplay.ts +++ b/src/js/integrations/mobilereplay.ts @@ -1,4 +1,4 @@ -import type { Client, DynamicSamplingContext, Event, IntegrationFn, IntegrationFnResult } from '@sentry/types'; +import type { Client, DynamicSamplingContext, Event, IntegrationFnResult } from '@sentry/types'; import { logger } from '@sentry/utils'; import { isHardCrash } from '../misc'; @@ -32,9 +32,7 @@ type MobileReplayIntegration = IntegrationFnResult & { /** * MobileReplay Integration let's you change default options. */ -export const mobileReplayIntegration = (( - initOptions: MobileReplayOptions = defaultOptions, -): MobileReplayIntegration => { +export const mobileReplayIntegration = (initOptions: MobileReplayOptions = defaultOptions): MobileReplayIntegration => { if (isExpoGo()) { logger.warn( `[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} is not supported in Expo Go. Use EAS Build or \`expo prebuild\` to enable it.`, @@ -103,9 +101,9 @@ export const mobileReplayIntegration = (( processEvent, options: options, }; -}) satisfies IntegrationFn; +}; -const mobileReplayIntegrationNoop = ((): MobileReplayIntegration => { +const mobileReplayIntegrationNoop = (): MobileReplayIntegration => { return { name: MOBILE_REPLAY_INTEGRATION_NAME, setupOnce() { @@ -113,4 +111,4 @@ const mobileReplayIntegrationNoop = ((): MobileReplayIntegration => { }, options: defaultOptions, }; -}) satisfies IntegrationFn; +}; From 1e651fb62a84c24f0e4ee796502b6814a1e1252e Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Fri, 26 Apr 2024 18:17:30 +0200 Subject: [PATCH 27/39] fix client init --- src/js/client.ts | 9 ++++++++- test/client.test.ts | 8 ++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/js/client.ts b/src/js/client.ts index 59ad2a9a7..ddf011055 100644 --- a/src/js/client.ts +++ b/src/js/client.ts @@ -119,6 +119,14 @@ export class ReactNativeClient extends BaseClient { */ public init(): void { super.init(); + this._initNativeSdk(); + } + + /** + * @inheritdoc + */ + protected _setupIntegrations(): void { + super._setupIntegrations(); const tracing = this.getIntegration(ReactNativeTracing); const routingName = tracing?.options.routingInstrumentation?.name; if (routingName) { @@ -128,7 +136,6 @@ export class ReactNativeClient extends BaseClient { if (enableUserInteractionTracing) { this.addIntegration(createIntegration('ReactNativeUserInteractionTracing')); } - this._initNativeSdk(); } /** diff --git a/test/client.test.ts b/test/client.test.ts index ffdefc954..2ea3a7971 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -241,7 +241,7 @@ describe('Tests ReactNativeClient', () => { }, transport: () => new NativeTransport(), }), - ); + ).init(); }); test('catches errors from onReady callback', () => { @@ -254,7 +254,7 @@ describe('Tests ReactNativeClient', () => { }, transport: () => new NativeTransport(), }), - ); + ).init(); }); test('calls onReady callback with false if Native SDK was not initialized', done => { @@ -269,7 +269,7 @@ describe('Tests ReactNativeClient', () => { }, transport: () => new NativeTransport(), }), - ); + ).init(); }); test('calls onReady callback with false if Native SDK failed to initialize', done => { @@ -290,7 +290,7 @@ describe('Tests ReactNativeClient', () => { }, transport: () => new NativeTransport(), }), - ); + ).init(); }); }); From c14d9cdd510e514cc14b968ab04aa93b1494e30a Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Fri, 26 Apr 2024 18:24:50 +0200 Subject: [PATCH 28/39] bump sentry cocoa --- CHANGELOG.md | 2 +- RNSentry.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e46d9e94..066c3280b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ ### Dependencies -- Bump Cocoa SDK to [8.24.1-alpha.0](https://github.com/getsentry/sentry-cocoa/releases/tag/8.24.1-alpha.0) +- Bump Cocoa SDK to [8.25.0-alpha.0](https://github.com/getsentry/sentry-cocoa/releases/tag/8.25.0-alpha.0) - Bump Android SDK to [v7.8.0-alpha.0](https://github.com/getsentry/sentry-java/releases/tag/7.8.0-alpha.0) ## 5.22.0 diff --git a/RNSentry.podspec b/RNSentry.podspec index 3a762ca3c..3560f8ef8 100644 --- a/RNSentry.podspec +++ b/RNSentry.podspec @@ -33,7 +33,7 @@ Pod::Spec.new do |s| s.preserve_paths = '*.js' s.dependency 'React-Core' - s.dependency 'Sentry/HybridSDK', '8.24.0-alpha.0' + s.dependency 'Sentry/HybridSDK', '8.25.0-alpha.0' s.source_files = 'ios/**/*.{h,m,mm}' s.public_header_files = 'ios/RNSentry.h' From 0a7996b638387220bacac867060ed3f12fad9541 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Fri, 26 Apr 2024 18:28:07 +0200 Subject: [PATCH 29/39] bump sentry-android --- CHANGELOG.md | 2 +- android/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 066c3280b..36ea390e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,7 @@ ### Dependencies - Bump Cocoa SDK to [8.25.0-alpha.0](https://github.com/getsentry/sentry-cocoa/releases/tag/8.25.0-alpha.0) -- Bump Android SDK to [v7.8.0-alpha.0](https://github.com/getsentry/sentry-java/releases/tag/7.8.0-alpha.0) +- Bump Android SDK to [7.9.0-alpha.1](https://github.com/getsentry/sentry-java/releases/tag/7.9.0-alpha.1) ## 5.22.0 diff --git a/android/build.gradle b/android/build.gradle index 18b8d2c8e..e0b809155 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -54,5 +54,5 @@ android { dependencies { implementation 'com.facebook.react:react-native:+' - api 'io.sentry:sentry-android:7.8.0-alpha.0' + api 'io.sentry:sentry-android:7.9.0-alpha.1' } From 982610da93fbde0a2f8b5f915e51af51a73c4d60 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 29 Apr 2024 11:34:33 +0200 Subject: [PATCH 30/39] use class from string to fix dynamically linked frameworks --- ios/RNSentry.mm | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ios/RNSentry.mm b/ios/RNSentry.mm index 78d67eae2..9a1b69775 100644 --- a/ios/RNSentry.mm +++ b/ios/RNSentry.mm @@ -7,9 +7,6 @@ #import "RCTConvert.h" #endif -#import -#import - #if __has_include() && SENTRY_PROFILING_SUPPORTED #define SENTRY_PROFILING_ENABLED 1 #import @@ -674,10 +671,10 @@ - (void) addReplayRNRedactClasses: (NSDictionary *_Nullable)replayOptions { NSMutableArray *_Nonnull classesToRedact = [[NSMutableArray alloc] init]; if ([replayOptions[@"maskAllImages"] boolValue] == YES) { - [classesToRedact addObject: [RCTImageView class]]; + [classesToRedact addObject: NSClassFromString(@"RCTImageView")]; } if ([replayOptions[@"maskAllText"] boolValue] == YES) { - [classesToRedact addObject: [RCTTextView class]]; + [classesToRedact addObject: NSClassFromString(@"RCTTextView")]; } [PrivateSentrySDKOnly addReplayRedactClasses: classesToRedact]; } From 8f61ad1a6fcbdd7ff963661c1abded4604e17793 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 29 Apr 2024 11:54:55 +0200 Subject: [PATCH 31/39] fix rn 0.65 e2e tests --- .github/workflows/e2e.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 751de524e..12aa587f1 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -178,7 +178,7 @@ jobs: device: 'iPhone 14' - platform: ios rn-version: '0.65.3' - runs-on: macos-latest + runs-on: macos-12 runtime: 'latest' device: 'iPhone 14' - platform: android @@ -224,6 +224,9 @@ jobs: echo "SENTRY_RELEASE=$SENTRY_RELEASE" echo "SENTRY_DIST=$SENTRY_DIST" + - run: sudo xcode-select -s /Applications/Xcode_14.2.app/Contents/Developer + if: ${{ matrix.rn-version == '0.65.3' }} + - uses: actions/setup-node@v4 with: node-version: 18 From d014385e6f634135d40fed6eb4eea01ba8d867cf Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 29 Apr 2024 12:21:20 +0200 Subject: [PATCH 32/39] Add tsDoc to mobile replay integration --- src/js/integrations/mobilereplay.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/js/integrations/mobilereplay.ts b/src/js/integrations/mobilereplay.ts index 54bfbf38e..fa46c57a2 100644 --- a/src/js/integrations/mobilereplay.ts +++ b/src/js/integrations/mobilereplay.ts @@ -30,7 +30,22 @@ type MobileReplayIntegration = IntegrationFnResult & { }; /** - * MobileReplay Integration let's you change default options. + * The Mobile Replay Integration, let's you adjust the default mobile replay options. + * To be passed to `Sentry.init` with `replaysOnErrorSampleRate` or `replaysSessionSampleRate`. + * + * ```javascript + * Sentry.init({ + * _experiments: { + * replaysOnErrorSampleRate: 1.0, + * replaysSessionSampleRate: 1.0, + * }, + * integrations: [mobileReplayIntegration({ + * // Adjust the default options + * })], + * }); + * ``` + * + * @experimental */ export const mobileReplayIntegration = (initOptions: MobileReplayOptions = defaultOptions): MobileReplayIntegration => { if (isExpoGo()) { From fef4a4e3de93be37dbf2da2a5923a96003aab82d Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 29 Apr 2024 12:41:46 +0200 Subject: [PATCH 33/39] fix 0.65 build --- .github/workflows/e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 12aa587f1..96c54c0e6 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -225,7 +225,7 @@ jobs: echo "SENTRY_DIST=$SENTRY_DIST" - run: sudo xcode-select -s /Applications/Xcode_14.2.app/Contents/Developer - if: ${{ matrix.rn-version == '0.65.3' }} + if: ${{ matrix.platform == 'ios' && matrix.rn-version == '0.65.3' }} - uses: actions/setup-node@v4 with: From d9997edc4efdb9a08bd9dd219c766f5f9407aaec Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 29 Apr 2024 12:04:45 +0000 Subject: [PATCH 34/39] release: 5.23.0-alpha.0 --- CHANGELOG.md | 2 +- package.json | 2 +- samples/expo/app.json | 6 +++--- samples/expo/package.json | 2 +- samples/react-native/android/app/build.gradle | 4 ++-- samples/react-native/ios/sentryreactnativesample/Info.plist | 4 ++-- .../ios/sentryreactnativesampleTests/Info.plist | 4 ++-- samples/react-native/package.json | 2 +- src/js/version.ts | 2 +- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36ea390e7..fbacca7c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 5.23.0-alpha.0 ### Features diff --git a/package.json b/package.json index 74880a147..9912dd301 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@sentry/react-native", "homepage": "https://github.com/getsentry/sentry-react-native", "repository": "https://github.com/getsentry/sentry-react-native", - "version": "5.22.0", + "version": "5.23.0-alpha.0", "description": "Official Sentry SDK for react-native", "typings": "dist/js/index.d.ts", "types": "dist/js/index.d.ts", diff --git a/samples/expo/app.json b/samples/expo/app.json index 7c04146b4..6d18b9d8a 100644 --- a/samples/expo/app.json +++ b/samples/expo/app.json @@ -4,7 +4,7 @@ "slug": "sentry-react-native-expo-sample", "jsEngine": "hermes", "scheme": "sentry-expo-sample", - "version": "5.22.0", + "version": "5.23.0-alpha.0", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "light", @@ -19,7 +19,7 @@ "ios": { "supportsTablet": true, "bundleIdentifier": "io.sentry.expo.sample", - "buildNumber": "4" + "buildNumber": "5" }, "android": { "adaptiveIcon": { @@ -27,7 +27,7 @@ "backgroundColor": "#ffffff" }, "package": "io.sentry.expo.sample", - "versionCode": 4 + "versionCode": 5 }, "web": { "bundler": "metro", diff --git a/samples/expo/package.json b/samples/expo/package.json index 13b26f4d1..436cbfd28 100644 --- a/samples/expo/package.json +++ b/samples/expo/package.json @@ -1,6 +1,6 @@ { "name": "sentry-react-native-expo-sample", - "version": "5.22.0", + "version": "5.23.0-alpha.0", "main": "expo-router/entry", "scripts": { "start": "expo start", diff --git a/samples/react-native/android/app/build.gradle b/samples/react-native/android/app/build.gradle index d976a0c8d..f4c8bbaa5 100644 --- a/samples/react-native/android/app/build.gradle +++ b/samples/react-native/android/app/build.gradle @@ -134,8 +134,8 @@ android { applicationId "io.sentry.reactnative.sample" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 11 - versionName "5.22.0" + versionCode 12 + versionName "5.23.0-alpha.0" } signingConfigs { diff --git a/samples/react-native/ios/sentryreactnativesample/Info.plist b/samples/react-native/ios/sentryreactnativesample/Info.plist index df413c2e2..488c9b6ac 100644 --- a/samples/react-native/ios/sentryreactnativesample/Info.plist +++ b/samples/react-native/ios/sentryreactnativesample/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 5.22.0 + 5.23.0 CFBundleSignature ???? CFBundleVersion - 11 + 12 LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/samples/react-native/ios/sentryreactnativesampleTests/Info.plist b/samples/react-native/ios/sentryreactnativesampleTests/Info.plist index a50944b0a..71f08fd08 100644 --- a/samples/react-native/ios/sentryreactnativesampleTests/Info.plist +++ b/samples/react-native/ios/sentryreactnativesampleTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 5.22.0 + 5.23.0 CFBundleSignature ???? CFBundleVersion - 11 + 12 diff --git a/samples/react-native/package.json b/samples/react-native/package.json index 3039624df..641c3d852 100644 --- a/samples/react-native/package.json +++ b/samples/react-native/package.json @@ -1,6 +1,6 @@ { "name": "sentry-react-native-sample", - "version": "5.22.0", + "version": "5.23.0-alpha.0", "private": true, "scripts": { "postinstall": "patch-package", diff --git a/src/js/version.ts b/src/js/version.ts index 07d4cac20..4c334aa23 100644 --- a/src/js/version.ts +++ b/src/js/version.ts @@ -1,3 +1,3 @@ export const SDK_PACKAGE_NAME = 'npm:@sentry/react-native'; export const SDK_NAME = 'sentry.javascript.react-native'; -export const SDK_VERSION = '5.22.0'; +export const SDK_VERSION = '5.23.0-alpha.0'; From a9f9f3968b2b9794b9f602eb38fb73baf30a35f5 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 29 Apr 2024 15:13:07 +0200 Subject: [PATCH 35/39] fix changelog _experiments --- CHANGELOG.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbacca7c3..9e58704f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,10 @@ ```js Sentry.init({ - replaysSessionSampleRate: 1.0, - replaysOnErrorSampleRate: 1.0, + _experiments: { + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 1.0, + }, }); ``` @@ -19,8 +21,10 @@ ```js Sentry.init({ - replaysSessionSampleRate: 1.0, - replaysOnErrorSampleRate: 1.0, + _experiments: { + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 1.0, + }, integration: [ Sentry.mobileReplayIntegration({ maskAllText: true, From 592f5138b147bc259077d7433d934440687d0076 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Mon, 6 May 2024 10:52:06 -0400 Subject: [PATCH 36/39] fix session sample rate on Android --- CHANGELOG.md | 4 ++++ android/src/main/java/io/sentry/react/RNSentryModuleImpl.java | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f9f67c10..3b737624a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- Pass `replaysSessionSampleRate` option to Android ([#3714](https://github.com/getsentry/sentry-react-native/pull/3714)) + 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/) ## 5.22.1 diff --git a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index fde282261..d027ee06e 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -309,7 +309,7 @@ private SentryReplayOptions getReplayOptions(@NotNull ReadableMap rnOptions) { return androidReplayOptions; } - androidReplayOptions.setErrorSampleRate(rnExperimentsOptions.hasKey("replaysSessionSampleRate") + androidReplayOptions.setSessionSampleRate(rnExperimentsOptions.hasKey("replaysSessionSampleRate") ? rnExperimentsOptions.getDouble("replaysSessionSampleRate") : null); androidReplayOptions.setErrorSampleRate(rnExperimentsOptions.hasKey("replaysOnErrorSampleRate") ? rnExperimentsOptions.getDouble("replaysOnErrorSampleRate") : null); From 5519309d52183fa455f761cd5eca5e273887d617 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 9 May 2024 15:57:31 +0000 Subject: [PATCH 37/39] release: 5.23.0-alpha.1 --- CHANGELOG.md | 2 +- package.json | 2 +- samples/expo/app.json | 8 ++++---- samples/expo/package.json | 2 +- samples/react-native/android/app/build.gradle | 4 ++-- .../react-native/ios/sentryreactnativesample/Info.plist | 2 +- .../ios/sentryreactnativesampleTests/Info.plist | 2 +- samples/react-native/package.json | 2 +- src/js/version.ts | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7d845df6..01199962c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 5.23.0-alpha.1 ### Fixes diff --git a/package.json b/package.json index be1b8ed0e..97dbde9f7 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@sentry/react-native", "homepage": "https://github.com/getsentry/sentry-react-native", "repository": "https://github.com/getsentry/sentry-react-native", - "version": "5.23.0-alpha.0", + "version": "5.23.0-alpha.1", "description": "Official Sentry SDK for react-native", "typings": "dist/js/index.d.ts", "types": "dist/js/index.d.ts", diff --git a/samples/expo/app.json b/samples/expo/app.json index f9f61cca5..47bfff57d 100644 --- a/samples/expo/app.json +++ b/samples/expo/app.json @@ -4,7 +4,7 @@ "slug": "sentry-react-native-expo-sample", "jsEngine": "hermes", "scheme": "sentry-expo-sample", - "version": "5.23.0-alpha.0", + "version": "5.23.0-alpha.1", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "light", @@ -19,7 +19,7 @@ "ios": { "supportsTablet": true, "bundleIdentifier": "io.sentry.expo.sample", - "buildNumber": "6" + "buildNumber": "7" }, "android": { "adaptiveIcon": { @@ -27,7 +27,7 @@ "backgroundColor": "#ffffff" }, "package": "io.sentry.expo.sample", - "versionCode": 6 + "versionCode": 7 }, "web": { "bundler": "metro", @@ -59,4 +59,4 @@ ] ] } -} +} \ No newline at end of file diff --git a/samples/expo/package.json b/samples/expo/package.json index 436cbfd28..b73fd22c9 100644 --- a/samples/expo/package.json +++ b/samples/expo/package.json @@ -1,6 +1,6 @@ { "name": "sentry-react-native-expo-sample", - "version": "5.23.0-alpha.0", + "version": "5.23.0-alpha.1", "main": "expo-router/entry", "scripts": { "start": "expo start", diff --git a/samples/react-native/android/app/build.gradle b/samples/react-native/android/app/build.gradle index 4150fa83a..43ff16d38 100644 --- a/samples/react-native/android/app/build.gradle +++ b/samples/react-native/android/app/build.gradle @@ -134,8 +134,8 @@ android { applicationId "io.sentry.reactnative.sample" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 13 - versionName "5.23.0-alpha.0" + versionCode 14 + versionName "5.23.0-alpha.1" } signingConfigs { diff --git a/samples/react-native/ios/sentryreactnativesample/Info.plist b/samples/react-native/ios/sentryreactnativesample/Info.plist index 8e114e381..e6d7979cf 100644 --- a/samples/react-native/ios/sentryreactnativesample/Info.plist +++ b/samples/react-native/ios/sentryreactnativesample/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 13 + 14 LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/samples/react-native/ios/sentryreactnativesampleTests/Info.plist b/samples/react-native/ios/sentryreactnativesampleTests/Info.plist index 816ed34cd..2ecb12ad0 100644 --- a/samples/react-native/ios/sentryreactnativesampleTests/Info.plist +++ b/samples/react-native/ios/sentryreactnativesampleTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 13 + 14 diff --git a/samples/react-native/package.json b/samples/react-native/package.json index 972126e10..365b469db 100644 --- a/samples/react-native/package.json +++ b/samples/react-native/package.json @@ -1,6 +1,6 @@ { "name": "sentry-react-native-sample", - "version": "5.23.0-alpha.0", + "version": "5.23.0-alpha.1", "private": true, "scripts": { "postinstall": "patch-package", diff --git a/src/js/version.ts b/src/js/version.ts index 4c334aa23..d01f20d30 100644 --- a/src/js/version.ts +++ b/src/js/version.ts @@ -1,3 +1,3 @@ export const SDK_PACKAGE_NAME = 'npm:@sentry/react-native'; export const SDK_NAME = 'sentry.javascript.react-native'; -export const SDK_VERSION = '5.23.0-alpha.0'; +export const SDK_VERSION = '5.23.0-alpha.1'; From 3177b5ca3ef9b185a6b0deda22ba79f1f374fa13 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich Date: Wed, 15 May 2024 18:18:15 +0200 Subject: [PATCH 38/39] rename startReplay to captureReplay --- .../main/java/io/sentry/react/RNSentryModuleImpl.java | 2 +- .../newarch/java/io/sentry/react/RNSentryModule.java | 4 ++-- .../oldarch/java/io/sentry/react/RNSentryModule.java | 4 ++-- ios/RNSentry.mm | 2 +- src/js/NativeRNSentry.ts | 2 +- src/js/integrations/mobilereplay.ts | 2 +- src/js/wrapper.ts | 10 +++++----- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index d027ee06e..7bc43cfbb 100644 --- a/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -445,7 +445,7 @@ public void fetchNativeFrames(Promise promise) { } } - public void startReplay(boolean isHardCrash, Promise promise) { + public void captureReplay(boolean isHardCrash, Promise promise) { Sentry.getCurrentHub().getOptions().getReplayController().sendReplay(isHardCrash, null, null); promise.resolve(getCurrentReplayId()); } diff --git a/android/src/newarch/java/io/sentry/react/RNSentryModule.java b/android/src/newarch/java/io/sentry/react/RNSentryModule.java index 100886c27..3d585b6b1 100644 --- a/android/src/newarch/java/io/sentry/react/RNSentryModule.java +++ b/android/src/newarch/java/io/sentry/react/RNSentryModule.java @@ -160,8 +160,8 @@ public WritableMap fetchNativeStackFramesBy(ReadableArray instructionsAddr) { } @Override - public void startReplay(boolean isHardCrash, Promise promise) { - this.impl.startReplay(isHardCrash, promise); + public void captureReplay(boolean isHardCrash, Promise promise) { + this.impl.captureReplay(isHardCrash, promise); } @Override diff --git a/android/src/oldarch/java/io/sentry/react/RNSentryModule.java b/android/src/oldarch/java/io/sentry/react/RNSentryModule.java index bd0a5cfb8..33fa7283b 100644 --- a/android/src/oldarch/java/io/sentry/react/RNSentryModule.java +++ b/android/src/oldarch/java/io/sentry/react/RNSentryModule.java @@ -160,8 +160,8 @@ public WritableMap fetchNativeStackFramesBy(ReadableArray instructionsAddr) { } @ReactMethod - public void startReplay(boolean isHardCrash, Promise promise) { - this.impl.startReplay(isHardCrash, promise); + public void captureReplay(boolean isHardCrash, Promise promise) { + this.impl.captureReplay(isHardCrash, promise); } @ReactMethod(isBlockingSynchronousMethod = true) diff --git a/ios/RNSentry.mm b/ios/RNSentry.mm index a4ee636a8..79da19826 100644 --- a/ios/RNSentry.mm +++ b/ios/RNSentry.mm @@ -665,7 +665,7 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray*)instructionsAdd // the 'tracesSampleRate' or 'tracesSampler' option. } -RCT_EXPORT_METHOD(startReplay: (BOOL)isHardCrash +RCT_EXPORT_METHOD(captureReplay: (BOOL)isHardCrash resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { diff --git a/src/js/NativeRNSentry.ts b/src/js/NativeRNSentry.ts index f90234d98..0c06d9033 100644 --- a/src/js/NativeRNSentry.ts +++ b/src/js/NativeRNSentry.ts @@ -44,7 +44,7 @@ export interface Spec extends TurboModule { fetchNativePackageName(): string | undefined | null; fetchNativeStackFramesBy(instructionsAddr: number[]): NativeStackFrames | undefined | null; initNativeReactNavigationNewFrameTracking(): Promise; - startReplay(isHardCrash: boolean): Promise; + captureReplay(isHardCrash: boolean): Promise; getCurrentReplayId(): string | undefined | null; } diff --git a/src/js/integrations/mobilereplay.ts b/src/js/integrations/mobilereplay.ts index fa46c57a2..9353ab8ee 100644 --- a/src/js/integrations/mobilereplay.ts +++ b/src/js/integrations/mobilereplay.ts @@ -78,7 +78,7 @@ export const mobileReplayIntegration = (initOptions: MobileReplayOptions = defau return event; } - const replayId = await NATIVE.startReplay(isHardCrash(event)); + const replayId = await NATIVE.captureReplay(isHardCrash(event)); if (!replayId) { logger.debug(`[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} not sampled for event ${event.event_id}.`); return event; diff --git a/src/js/wrapper.ts b/src/js/wrapper.ts index 0f8ca3558..f3950d5fa 100644 --- a/src/js/wrapper.ts +++ b/src/js/wrapper.ts @@ -110,7 +110,7 @@ interface SentryNativeWrapper { fetchNativeStackFramesBy(instructionsAddr: number[]): NativeStackFrames | null; initNativeReactNavigationNewFrameTracking(): Promise; - startReplay(isHardCrash: boolean): Promise; + captureReplay(isHardCrash: boolean): Promise; getCurrentReplayId(): string | null; } @@ -616,17 +616,17 @@ export const NATIVE: SentryNativeWrapper = { return RNSentry.initNativeReactNavigationNewFrameTracking(); }, - async startReplay(isHardCrash: boolean): Promise { + async captureReplay(isHardCrash: boolean): Promise { if (!this.enableNative) { - logger.warn(`[NATIVE] \`${this.startReplay.name}\` is not available when native is disabled.`); + logger.warn(`[NATIVE] \`${this.captureReplay.name}\` is not available when native is disabled.`); return Promise.resolve(null); } if (!this._isModuleLoaded(RNSentry)) { - logger.warn(`[NATIVE] \`${this.startReplay.name}\` is not available when native is not available.`); + logger.warn(`[NATIVE] \`${this.captureReplay.name}\` is not available when native is not available.`); return Promise.resolve(null); } - return (await RNSentry.startReplay(isHardCrash)) || null; + return (await RNSentry.captureReplay(isHardCrash)) || null; }, getCurrentReplayId(): string | null { From 50cbcdafd2c2bf579ea16e0c8f8b06442658c2a3 Mon Sep 17 00:00:00 2001 From: Krystof Woldrich <31292499+krystofwoldrich@users.noreply.github.com> Date: Tue, 21 May 2024 13:00:57 +0200 Subject: [PATCH 39/39] Update src/js/utils/clientutils.ts Co-authored-by: LucasZF --- src/js/utils/clientutils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/js/utils/clientutils.ts b/src/js/utils/clientutils.ts index 40f073a66..95047fa00 100644 --- a/src/js/utils/clientutils.ts +++ b/src/js/utils/clientutils.ts @@ -1,7 +1,9 @@ import type { Client } from '@sentry/types'; /** - * + * Checks if the provided Sentry client has hooks implemented. + * @param client The Sentry client object to check. + * @returns True if the client has hooks, false otherwise. */ export function hasHooks(client: Client): client is Client & { on: Required['on'] } { return client.on !== undefined;