From 3b300d9149833f9786dfbaa35dd5dd87e4c48c75 Mon Sep 17 00:00:00 2001 From: lucas Date: Thu, 17 Oct 2024 14:50:58 -0300 Subject: [PATCH 01/13] merge main --- CHANGELOG.md | 6 + .../java/io/sentry/RNSentryTimeToDisplay.java | 39 +++ .../io/sentry/react/RNSentryModuleImpl.java | 168 ++++++++++--- .../java/io/sentry/react/RNSentryModule.java | 5 + .../java/io/sentry/react/RNSentryModule.java | 5 + packages/core/ios/RNSentry.mm | 9 + packages/core/ios/RNSentryTimeToDisplay.h | 7 + packages/core/ios/RNSentryTimeToDisplay.m | 43 ++++ packages/core/src/js/NativeRNSentry.ts | 1 + .../core/src/js/tracing/reactnavigation.ts | 15 +- .../js/utils/sentryeventemitterfallback.ts | 98 ++++++++ packages/core/src/js/wrapper.ts | 9 + packages/core/test/mockWrapper.ts | 1 + .../tracing/reactnavigation.ttid.test.tsx | 14 +- .../test/utils/mockedSentryeventemitter.ts | 39 --- .../utils/mockedSentryeventemitterfallback.ts | 26 ++ .../utils/sentryeventemitterfallback.test.ts | 236 ++++++++++++++++++ samples/react-native/src/App.tsx | 12 +- 18 files changed, 649 insertions(+), 84 deletions(-) create mode 100644 packages/core/android/src/main/java/io/sentry/RNSentryTimeToDisplay.java create mode 100644 packages/core/ios/RNSentryTimeToDisplay.h create mode 100644 packages/core/ios/RNSentryTimeToDisplay.m create mode 100644 packages/core/src/js/utils/sentryeventemitterfallback.ts delete mode 100644 packages/core/test/utils/mockedSentryeventemitter.ts create mode 100644 packages/core/test/utils/mockedSentryeventemitterfallback.ts create mode 100644 packages/core/test/utils/sentryeventemitterfallback.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e73032479..bd943a736a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ > make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first. +## Unreleased + +### Fixes + +- Enhanced accuracy of time-to-display spans. ([#4042](https://github.com/getsentry/sentry-react-native/pull/4042)) + ## 6.0.0 This is a new major version 6.0.0 of the Sentry React Native SDK. diff --git a/packages/core/android/src/main/java/io/sentry/RNSentryTimeToDisplay.java b/packages/core/android/src/main/java/io/sentry/RNSentryTimeToDisplay.java new file mode 100644 index 0000000000..264048c287 --- /dev/null +++ b/packages/core/android/src/main/java/io/sentry/RNSentryTimeToDisplay.java @@ -0,0 +1,39 @@ +package io.sentry.react; + +import com.facebook.react.bridge.Promise; + +import android.os.Handler; +import android.os.Looper; +import android.view.Choreographer; + +import org.jetbrains.annotations.NotNull; +import io.sentry.SentryDate; +import io.sentry.SentryDateProvider; +import io.sentry.android.core.SentryAndroidDateProvider; + +public class RNSentryTimeToDisplay { + public static void GetTimeToDisplay(Promise promise, SentryDateProvider dateProvider) { + Looper mainLooper = Looper.getMainLooper(); + + if (mainLooper == null) { + promise.reject("GetTimeToDisplay is not able to measure the time to display: Main looper not available."); + return; + } + + // Ensure the code runs on the main thread + new Handler(mainLooper) + .post(() -> { + try { + Choreographer choreographer = Choreographer.getInstance(); + + // Invoke the callback after the frame is rendered + choreographer.postFrameCallback(frameTimeNanos -> { + final SentryDate endDate = dateProvider.now(); + promise.resolve(endDate.nanoTimestamp() / 1e9); + }); + } catch (Exception exception) { + promise.reject("Failed to receive the instance of Choreographer", exception); + } + }); + } +} diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index ee24c634c2..71ee344dfd 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -126,10 +126,13 @@ public class RNSentryModuleImpl { /** Max trace file size in bytes. */ private long maxTraceFileSize = 5 * 1024 * 1024; + private final @NotNull SentryDateProvider dateProvider; + public RNSentryModuleImpl(ReactApplicationContext reactApplicationContext) { packageInfo = getPackageInfo(reactApplicationContext); this.reactApplicationContext = reactApplicationContext; this.emitNewFrameEvent = createEmitNewFrameEvent(); + this.dateProvider = new SentryAndroidDateProvider(); } private ReactApplicationContext getReactApplicationContext() { @@ -141,8 +144,6 @@ private ReactApplicationContext getReactApplicationContext() { } private @NotNull Runnable createEmitNewFrameEvent() { - final @NotNull SentryDateProvider dateProvider = new SentryAndroidDateProvider(); - return () -> { final SentryDate endDate = dateProvider.now(); WritableMap event = Arguments.createMap(); @@ -172,6 +173,7 @@ public void initNativeReactNavigationNewFrameTracking(Promise promise) { } public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { +<<<<<<< HEAD SentryAndroid.init( this.getReactApplicationContext(), options -> { @@ -272,36 +274,142 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { } } catch (Throwable ignored) { // NOPMD - We don't want to crash in any case // We do nothing +======= + try { + SentryAndroid.init( + this.getReactApplicationContext(), + options -> { + @Nullable SdkVersion sdkVersion = options.getSdkVersion(); + if (sdkVersion == null) { + sdkVersion = new SdkVersion(ANDROID_SDK_NAME, BuildConfig.VERSION_NAME); + } else { + sdkVersion.setName(ANDROID_SDK_NAME); +>>>>>>> 4ef0e98c (port navigation tracker #4042 to V6 / Todo: Fix tests) } - setEventOriginTag(event); - addPackages(event, options.getSdkVersion()); + options.setSentryClientName(sdkVersion.getName() + "/" + sdkVersion.getVersion()); + options.setNativeSdkName(NATIVE_SDK_NAME); + options.setSdkVersion(sdkVersion); - return event; + if (rnOptions.hasKey("debug") && rnOptions.getBoolean("debug")) { + options.setDebug(true); + } + if (rnOptions.hasKey("dsn") && rnOptions.getString("dsn") != null) { + String dsn = rnOptions.getString("dsn"); + logger.log(SentryLevel.INFO, String.format("Starting with DSN: '%s'", dsn)); + options.setDsn(dsn); + } else { + // SentryAndroid needs an empty string fallback for the dsn. + options.setDsn(""); + } + if (rnOptions.hasKey("sampleRate")) { + options.setSampleRate(rnOptions.getDouble("sampleRate")); + } + if (rnOptions.hasKey("sendClientReports")) { + options.setSendClientReports(rnOptions.getBoolean("sendClientReports")); + } + if (rnOptions.hasKey("maxBreadcrumbs")) { + options.setMaxBreadcrumbs(rnOptions.getInt("maxBreadcrumbs")); + } + if (rnOptions.hasKey("maxCacheItems")) { + options.setMaxCacheItems(rnOptions.getInt("maxCacheItems")); + } + if (rnOptions.hasKey("environment") && rnOptions.getString("environment") != null) { + options.setEnvironment(rnOptions.getString("environment")); + } + if (rnOptions.hasKey("release") && rnOptions.getString("release") != null) { + options.setRelease(rnOptions.getString("release")); + } + if (rnOptions.hasKey("dist") && rnOptions.getString("dist") != null) { + options.setDist(rnOptions.getString("dist")); + } + if (rnOptions.hasKey("enableAutoSessionTracking")) { + options.setEnableAutoSessionTracking(rnOptions.getBoolean("enableAutoSessionTracking")); + } + if (rnOptions.hasKey("sessionTrackingIntervalMillis")) { + options.setSessionTrackingIntervalMillis( + rnOptions.getInt("sessionTrackingIntervalMillis")); + } + if (rnOptions.hasKey("shutdownTimeout")) { + options.setShutdownTimeoutMillis(rnOptions.getInt("shutdownTimeout")); + } + if (rnOptions.hasKey("enableNdkScopeSync")) { + options.setEnableScopeSync(rnOptions.getBoolean("enableNdkScopeSync")); + } + if (rnOptions.hasKey("attachStacktrace")) { + options.setAttachStacktrace(rnOptions.getBoolean("attachStacktrace")); + } + if (rnOptions.hasKey("attachThreads")) { + // JS use top level stacktrace and android attaches Threads which hides them so + // by default we hide. + options.setAttachThreads(rnOptions.getBoolean("attachThreads")); + } + if (rnOptions.hasKey("attachScreenshot")) { + options.setAttachScreenshot(rnOptions.getBoolean("attachScreenshot")); + } + if (rnOptions.hasKey("attachViewHierarchy")) { + options.setAttachViewHierarchy(rnOptions.getBoolean("attachViewHierarchy")); + } + if (rnOptions.hasKey("sendDefaultPii")) { + options.setSendDefaultPii(rnOptions.getBoolean("sendDefaultPii")); + } + if (rnOptions.hasKey("maxQueueSize")) { + options.setMaxQueueSize(rnOptions.getInt("maxQueueSize")); + } + if (rnOptions.hasKey("enableNdk")) { + options.setEnableNdk(rnOptions.getBoolean("enableNdk")); + } + if (rnOptions.hasKey("_experiments")) { + options.getExperimental().setSessionReplay(getReplayOptions(rnOptions)); + options + .getReplayController() + .setBreadcrumbConverter(new RNSentryReplayBreadcrumbConverter()); + } + options.setBeforeSend( + (event, hint) -> { + // React native internally throws a JavascriptException + // Since we catch it before that, we don't want to send this one + // because we would send it twice + try { + SentryException ex = event.getExceptions().get(0); + if (null != ex && ex.getType().contains("JavascriptException")) { + return null; + } + } catch (Throwable ignored) { + // We do nothing + } + + setEventOriginTag(event); + addPackages(event, options.getSdkVersion()); + + return event; + }); + + if (rnOptions.hasKey("enableNativeCrashHandling") + && !rnOptions.getBoolean("enableNativeCrashHandling")) { + final List integrations = options.getIntegrations(); + for (final Integration integration : integrations) { + if (integration instanceof UncaughtExceptionHandlerIntegration + || integration instanceof AnrIntegration + || integration instanceof NdkIntegration) { + integrations.remove(integration); + } + } + } + logger.log( + SentryLevel.INFO, + String.format("Native Integrations '%s'", options.getIntegrations())); + + final CurrentActivityHolder currentActivityHolder = CurrentActivityHolder.getInstance(); + final Activity currentActivity = getCurrentActivity(); + if (currentActivity != null) { + currentActivityHolder.setActivity(currentActivity); + } }); - - if (rnOptions.hasKey("enableNativeCrashHandling") - && !rnOptions.getBoolean("enableNativeCrashHandling")) { - final List integrations = options.getIntegrations(); - for (final Integration integration : integrations) { - if (integration instanceof UncaughtExceptionHandlerIntegration - || integration instanceof AnrIntegration - || integration instanceof NdkIntegration) { - integrations.remove(integration); - } - } - } - logger.log( - SentryLevel.INFO, - String.format("Native Integrations '%s'", options.getIntegrations())); - - final CurrentActivityHolder currentActivityHolder = CurrentActivityHolder.getInstance(); - final Activity currentActivity = getCurrentActivity(); - if (currentActivity != null) { - currentActivityHolder.setActivity(currentActivity); - } - }); - + } + catch (Exception ex){ + promise.reject(ex); + } promise.resolve(true); } @@ -745,6 +853,10 @@ public void disableNativeFramesTracking() { } } + public void getNewScreenTimeToDisplay(Promise promise) { + RNSentryTimeToDisplay.GetTimeToDisplay(promise, dateProvider); + } + private String getProfilingTracesDirPath() { if (cacheDirPath == null) { cacheDirPath = diff --git a/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java b/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java index 78f1911da4..6ea8542e8b 100644 --- a/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java +++ b/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java @@ -172,4 +172,9 @@ public String getCurrentReplayId() { public void crashedLastRun(Promise promise) { this.impl.crashedLastRun(promise); } + + @Override + public void getNewScreenTimeToDisplay(Promise promise) { + this.impl.getNewScreenTimeToDisplay(promise); + } } diff --git a/packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java b/packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java index c77d4218df..57fcbf0a73 100644 --- a/packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java +++ b/packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java @@ -172,4 +172,9 @@ public String getCurrentReplayId() { public void crashedLastRun(Promise promise) { this.impl.crashedLastRun(promise); } + + @ReactMethod() + public void getNewScreenTimeToDisplay(Promise promise) { + this.impl.getNewScreenTimeToDisplay(promise); + } } diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index a04733e7b5..3009e46c09 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -1,5 +1,6 @@ #import #import "RNSentry.h" +#import "RNSentryTimeToDisplay.h" #if __has_include() #import @@ -62,6 +63,7 @@ + (void)storeEnvelope:(SentryEnvelope *)envelope; @implementation RNSentry { bool sentHybridSdkDidBecomeActive; bool hasListeners; + RNSentryTimeToDisplay *_timeToDisplay; } - (dispatch_queue_t)methodQueue @@ -139,6 +141,8 @@ - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) [mutableOptions removeObjectForKey:@"tracesSampler"]; [mutableOptions removeObjectForKey:@"enableTracing"]; + _timeToDisplay = [[RNSentryTimeToDisplay alloc] init]; + #if SENTRY_TARGET_REPLAY_SUPPORTED [RNSentryReplay updateOptions:mutableOptions]; #endif @@ -786,4 +790,9 @@ - (NSDictionary*) fetchNativeStackFramesBy: (NSArray*)instructionsAdd } #endif +RCT_EXPORT_METHOD(getNewScreenTimeToDisplay:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + [_timeToDisplay getTimeToDisplay:resolve]; +} + @end diff --git a/packages/core/ios/RNSentryTimeToDisplay.h b/packages/core/ios/RNSentryTimeToDisplay.h new file mode 100644 index 0000000000..fbb468cb23 --- /dev/null +++ b/packages/core/ios/RNSentryTimeToDisplay.h @@ -0,0 +1,7 @@ +#import + +@interface RNSentryTimeToDisplay : NSObject + +- (void)getTimeToDisplay:(RCTResponseSenderBlock)callback; + +@end diff --git a/packages/core/ios/RNSentryTimeToDisplay.m b/packages/core/ios/RNSentryTimeToDisplay.m new file mode 100644 index 0000000000..88e24eedc7 --- /dev/null +++ b/packages/core/ios/RNSentryTimeToDisplay.m @@ -0,0 +1,43 @@ +#import "RNSentryTimeToDisplay.h" +#import +#import + +@implementation RNSentryTimeToDisplay +{ + CADisplayLink *displayLink; + RCTResponseSenderBlock resolveBlock; +} + +// Rename requestAnimationFrame to getTimeToDisplay +- (void)getTimeToDisplay:(RCTResponseSenderBlock)callback +{ + // Store the resolve block to use in the callback. + resolveBlock = callback; + +#if TARGET_OS_IOS + // Create and add a display link to get the callback after the screen is rendered. + displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)]; + [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; +#else + resolveBlock(@[]); // Return nothing if not iOS. +#endif +} + +#if TARGET_OS_IOS +- (void)handleDisplayLink:(CADisplayLink *)link { + // Get the current time + NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970] * 1000.0; // Convert to milliseconds + + // Ensure the callback is valid and pass the current time back + if (resolveBlock) { + resolveBlock(@[@(currentTime)]); // Call the callback with the current time + resolveBlock = nil; // Clear the block after it's called + } + + // Invalidate the display link to stop future callbacks + [displayLink invalidate]; + displayLink = nil; +} +#endif + +@end diff --git a/packages/core/src/js/NativeRNSentry.ts b/packages/core/src/js/NativeRNSentry.ts index a5a7652fba..c82debe900 100644 --- a/packages/core/src/js/NativeRNSentry.ts +++ b/packages/core/src/js/NativeRNSentry.ts @@ -9,6 +9,7 @@ import type { UnsafeObject } from './utils/rnlibrariesinterface'; export interface Spec extends TurboModule { addListener: (eventType: string) => void; removeListeners: (id: number) => void; + getNewScreenTimeToDisplay(): Promise; addBreadcrumb(breadcrumb: UnsafeObject): void; captureEnvelope( bytes: string, diff --git a/packages/core/src/js/tracing/reactnavigation.ts b/packages/core/src/js/tracing/reactnavigation.ts index 871485bd7b..2418e0052c 100644 --- a/packages/core/src/js/tracing/reactnavigation.ts +++ b/packages/core/src/js/tracing/reactnavigation.ts @@ -30,7 +30,8 @@ import { } from './span'; import { manualInitialDisplaySpans, startTimeToInitialDisplaySpan } from './timetodisplay'; import { setSpanDurationAsMeasurementOnSpan } from './utils'; - +import type { SentryEventEmitterFallback } from '../utils/sentryeventemitterfallback'; +import { createSentryFallbackEventEmitter } from '../utils/sentryeventemitterfallback'; export const INTEGRATION_NAME = 'ReactNavigation'; const NAVIGATION_HISTORY_MAX_SIZE = 200; @@ -81,7 +82,7 @@ export const reactNavigationIntegration = ({ registerNavigationContainer: (navigationContainerRef: unknown) => void; } => { let navigationContainer: NavigationContainer | undefined; - let newScreenFrameEventEmitter: SentryEventEmitter | undefined; + let newScreenFrameEventEmitter: SentryEventEmitterFallback | undefined; let tracing: ReactNativeTracingIntegration | undefined; let idleSpanOptions: Parameters[1] = defaultIdleOptions; @@ -95,8 +96,8 @@ export const reactNavigationIntegration = ({ let recentRouteKeys: string[] = []; if (enableTimeToInitialDisplay) { - newScreenFrameEventEmitter = createSentryEventEmitter(); - newScreenFrameEventEmitter.initAsync(NewFrameEventName); + newScreenFrameEventEmitter = createSentryFallbackEventEmitter(); + newScreenFrameEventEmitter.initAsync(); NATIVE.initNativeReactNavigationNewFrameTracking().catch((reason: unknown) => { logger.error(`${INTEGRATION_NAME} Failed to initialize native new frame tracking: ${reason}`); }); @@ -258,9 +259,8 @@ export const reactNavigationIntegration = ({ }); const navigationSpanWithTtid = latestNavigationSpan; - !routeHasBeenSeen && - latestTtidSpan && - newScreenFrameEventEmitter?.once(NewFrameEventName, ({ newFrameTimestampInSeconds }: NewFrameEvent) => { + if (!routeHasBeenSeen && latestTtidSpan) { + newScreenFrameEventEmitter?.onceNewFrame(({ newFrameTimestampInSeconds }: NewFrameEvent) => { const activeSpan = getActiveSpan(); if (activeSpan && manualInitialDisplaySpans.has(activeSpan)) { logger.warn('[ReactNavigationInstrumentation] Detected manual instrumentation for the current active span.'); @@ -271,6 +271,7 @@ export const reactNavigationIntegration = ({ latestTtidSpan.end(newFrameTimestampInSeconds); setSpanDurationAsMeasurementOnSpan('time_to_initial_display', latestTtidSpan, navigationSpanWithTtid); }); + } navigationProcessingSpan?.updateName(`Processing navigation to ${route.name}`); navigationProcessingSpan?.setStatus({ code: SPAN_STATUS_OK }); diff --git a/packages/core/src/js/utils/sentryeventemitterfallback.ts b/packages/core/src/js/utils/sentryeventemitterfallback.ts new file mode 100644 index 0000000000..8eaeb947a8 --- /dev/null +++ b/packages/core/src/js/utils/sentryeventemitterfallback.ts @@ -0,0 +1,98 @@ +import { logger, timestampInSeconds } from '@sentry/utils'; + +import { NATIVE } from '../wrapper'; +import type { NewFrameEvent, SentryEventEmitter } from './sentryeventemitter'; +import { createSentryEventEmitter, NewFrameEventName } from './sentryeventemitter'; + +export const FALLBACK_TIMEOUT_MS = 10_000; + +export type FallBackNewFrameEvent = { newFrameTimestampInSeconds: number; isFallback?: boolean }; +export interface SentryEventEmitterFallback { + /** + * Initializes the fallback event emitter + * This method is synchronous in JS but the event emitter starts asynchronously. + */ + initAsync: () => void; + onceNewFrame: (listener: (event: FallBackNewFrameEvent) => void) => void; +} + +/** + * Creates emitter that allows to listen to UI Frame events when ready. + */ +export function createSentryFallbackEventEmitter( + emitter: SentryEventEmitter = createSentryEventEmitter(), + fallbackTimeoutMs = FALLBACK_TIMEOUT_MS, +): SentryEventEmitterFallback { + let fallbackTimeout: ReturnType | undefined; + let animationFrameTimestampSeconds: number | undefined; + let nativeNewFrameTimestampSeconds: number | undefined; + + function getAnimationFrameTimestampSeconds(): void { + // https://reactnative.dev/docs/timers#timers + // NOTE: The current implementation of requestAnimationFrame is the same + // as setTimeout(0). This isn't exactly how requestAnimationFrame + // is supposed to work on web, so it doesn't get called when UI Frames are rendered.: https://github.com/facebook/react-native/blob/5106933c750fee2ce49fe1945c3e3763eebc92bc/packages/react-native/ReactCommon/react/runtime/TimerManager.cpp#L442-L443 + requestAnimationFrame(() => { + if (fallbackTimeout === undefined) { + return; + } + animationFrameTimestampSeconds = timestampInSeconds(); + }); + } + + function getNativeNewFrameTimestampSeconds(): void { + NATIVE.getNewScreenTimeToDisplay() + .then(resolve => { + if (fallbackTimeout === undefined) { + return; + } + nativeNewFrameTimestampSeconds = resolve ?? undefined; + }) + .catch(reason => { + logger.error('Failed to receive Native fallback timestamp.', reason); + }); + } + + return { + initAsync() { + emitter.initAsync(NewFrameEventName); + }, + + onceNewFrame(listener: (event: FallBackNewFrameEvent) => void) { + animationFrameTimestampSeconds = undefined; + nativeNewFrameTimestampSeconds = undefined; + + const internalListener = (event: NewFrameEvent): void => { + if (fallbackTimeout !== undefined) { + clearTimeout(fallbackTimeout); + fallbackTimeout = undefined; + } + animationFrameTimestampSeconds = undefined; + nativeNewFrameTimestampSeconds = undefined; + listener(event); + }; + fallbackTimeout = setTimeout(() => { + if (nativeNewFrameTimestampSeconds) { + logger.log('Native event emitter did not reply in time'); + return listener({ + newFrameTimestampInSeconds: nativeNewFrameTimestampSeconds, + isFallback: true, + }); + } else if (animationFrameTimestampSeconds) { + logger.log('[Sentry] Native event emitter did not reply in time. Using JavaScript fallback emitter.'); + return listener({ + newFrameTimestampInSeconds: animationFrameTimestampSeconds, + isFallback: true, + }); + } else { + emitter.removeListener(NewFrameEventName, internalListener); + logger.error('Failed to receive any fallback timestamp.'); + } + }, fallbackTimeoutMs); + + getNativeNewFrameTimestampSeconds(); + getAnimationFrameTimestampSeconds(); + emitter.once(NewFrameEventName, internalListener); + }, + }; +} diff --git a/packages/core/src/js/wrapper.ts b/packages/core/src/js/wrapper.ts index d901524793..577eaf99e6 100644 --- a/packages/core/src/js/wrapper.ts +++ b/packages/core/src/js/wrapper.ts @@ -116,6 +116,7 @@ interface SentryNativeWrapper { getCurrentReplayId(): string | null; crashedLastRun(): Promise; + getNewScreenTimeToDisplay(): Promise; } const EOL = utf8ToBytes('\n'); @@ -690,6 +691,14 @@ export const NATIVE: SentryNativeWrapper = { return typeof result === 'boolean' ? result : null; }, + getNewScreenTimeToDisplay(): Promise { + if (!this.enableNative || !this._isModuleLoaded(RNSentry)) { + return Promise.resolve(null); + } + + return RNSentry.getNewScreenTimeToDisplay(); + }, + /** * Gets the event from envelopeItem and applies the level filter to the selected event. * @param data An envelope item containing the event. diff --git a/packages/core/test/mockWrapper.ts b/packages/core/test/mockWrapper.ts index 58387bb8da..82b2b9194c 100644 --- a/packages/core/test/mockWrapper.ts +++ b/packages/core/test/mockWrapper.ts @@ -58,6 +58,7 @@ const NATIVE: MockInterface = { getCurrentReplayId: jest.fn(), crashedLastRun: jest.fn(), + getNewScreenTimeToDisplay: jest.fn().mockResolvedValue(42), }; NATIVE.isNativeAvailable.mockReturnValue(true); diff --git a/packages/core/test/tracing/reactnavigation.ttid.test.tsx b/packages/core/test/tracing/reactnavigation.ttid.test.tsx index 78dacd003c..beb8a1139a 100644 --- a/packages/core/test/tracing/reactnavigation.ttid.test.tsx +++ b/packages/core/test/tracing/reactnavigation.ttid.test.tsx @@ -4,11 +4,11 @@ import * as TestRenderer from '@testing-library/react-native' import * as React from "react"; import * as mockWrapper from '../mockWrapper'; -import * as mockedSentryEventEmitter from '../utils/mockedSentryeventemitter'; +import * as mockedSentryEventEmitter from '../utils/mockedSentryeventemitterfallback'; import * as mockedtimetodisplaynative from './mockedtimetodisplaynative'; jest.mock('../../src/js/wrapper', () => mockWrapper); jest.mock('../../src/js/utils/environment'); -jest.mock('../../src/js/utils/sentryeventemitter', () => mockedSentryEventEmitter); +jest.mock('../../src/js/utils/sentryeventemitterfallback', () => mockedSentryEventEmitter); jest.mock('../../src/js/tracing/timetodisplaynative', () => mockedtimetodisplaynative); import * as Sentry from '../../src/js'; @@ -17,11 +17,11 @@ import { TimeToFullDisplay, TimeToInitialDisplay } from '../../src/js/tracing'; import { _setAppStartEndTimestampMs } from '../../src/js/tracing/integrations/appStart'; import { SPAN_ORIGIN_AUTO_NAVIGATION_REACT_NAVIGATION, SPAN_ORIGIN_AUTO_UI_TIME_TO_DISPLAY, SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY } from '../../src/js/tracing/origin'; import { isHermesEnabled, notWeb } from '../../src/js/utils/environment'; -import { createSentryEventEmitter } from '../../src/js/utils/sentryeventemitter'; +import { createSentryFallbackEventEmitter } from '../../src/js/utils/sentryeventemitterfallback'; import { RN_GLOBAL_OBJ } from '../../src/js/utils/worldwide'; import { MOCK_DSN } from '../mockDsn'; import { secondInFutureTimestampMs } from '../testutils'; -import type { MockedSentryEventEmitter } from '../utils/mockedSentryeventemitter'; +import type { MockedSentryEventEmitterFallback } from '../utils/mockedSentryeventemitterfallback'; import { emitNativeFullDisplayEvent, emitNativeInitialDisplayEvent } from './mockedtimetodisplaynative'; import { createMockNavigationAndAttachTo } from './reactnavigationutils'; @@ -32,7 +32,7 @@ type ScopeWithMaybeSpan = Scope & { }; describe('React Navigation - TTID', () => { - let mockedEventEmitter: MockedSentryEventEmitter; + let mockedEventEmitter: MockedSentryEventEmitterFallback; let transportSendMock: jest.Mock, Parameters>; let mockedNavigation: ReturnType; const mockedAppStartTimeSeconds: number = timestampInSeconds(); @@ -51,8 +51,8 @@ describe('React Navigation - TTID', () => { }); _setAppStartEndTimestampMs(mockedAppStartTimeSeconds * 1000); - mockedEventEmitter = mockedSentryEventEmitter.createMockedSentryEventEmitter(); - (createSentryEventEmitter as jest.Mock).mockReturnValue(mockedEventEmitter); + mockedEventEmitter = mockedSentryEventEmitter.createMockedSentryFallbackEventEmitter(); + (createSentryFallbackEventEmitter as jest.Mock).mockReturnValue(mockedEventEmitter); const sut = createTestedInstrumentation({ enableTimeToInitialDisplay: true }); transportSendMock = initSentry(sut).transportSendMock; diff --git a/packages/core/test/utils/mockedSentryeventemitter.ts b/packages/core/test/utils/mockedSentryeventemitter.ts deleted file mode 100644 index 6849e460be..0000000000 --- a/packages/core/test/utils/mockedSentryeventemitter.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { timestampInSeconds } from '@sentry/utils'; -import * as EventEmitter from 'events'; - -import type { NewFrameEvent, SentryEventEmitter } from '../../src/js/utils/sentryeventemitter'; -import type { MockInterface } from '../testutils'; - -export const NewFrameEventName = 'rn_sentry_new_frame'; -export type NewFrameEventName = typeof NewFrameEventName; - -export interface MockedSentryEventEmitter extends MockInterface { - emitNewFrameEvent: (timestampSeconds?: number) => void; -} - -export function createMockedSentryEventEmitter(): MockedSentryEventEmitter { - const emitter = new EventEmitter(); - - return { - emitNewFrameEvent: jest.fn((timestampSeconds?: number) => { - emitter.emit('rn_sentry_new_frame', { - newFrameTimestampInSeconds: timestampSeconds || timestampInSeconds(), - }); - }), - once: jest.fn((event: NewFrameEventName, listener: (event: NewFrameEvent) => void) => { - emitter.once(event, listener); - }), - removeListener: jest.fn((event: NewFrameEventName, listener: (event: NewFrameEvent) => void) => { - emitter.removeListener(event, listener); - }), - addListener: jest.fn((event: NewFrameEventName, listener: (event: NewFrameEvent) => void) => { - emitter.addListener(event, listener); - }), - initAsync: jest.fn(), - closeAllAsync: jest.fn(() => { - emitter.removeAllListeners(); - }), - }; -} - -export const createSentryEventEmitter = jest.fn(() => createMockedSentryEventEmitter()); diff --git a/packages/core/test/utils/mockedSentryeventemitterfallback.ts b/packages/core/test/utils/mockedSentryeventemitterfallback.ts new file mode 100644 index 0000000000..50d5fbcc27 --- /dev/null +++ b/packages/core/test/utils/mockedSentryeventemitterfallback.ts @@ -0,0 +1,26 @@ +import { timestampInSeconds } from '@sentry/utils'; +import EventEmitter from 'events'; + +import type { NewFrameEvent } from '../../src/js/utils/sentryeventemitter'; +import type { SentryEventEmitterFallback } from '../../src/js/utils/sentryeventemitterfallback'; +import type { MockInterface } from '../testutils'; +export const NewFrameEventName = 'rn_sentry_new_frame'; +export type NewFrameEventName = typeof NewFrameEventName; +export interface MockedSentryEventEmitterFallback extends MockInterface { + emitNewFrameEvent: (timestampSeconds?: number) => void; +} +export function createMockedSentryFallbackEventEmitter(): MockedSentryEventEmitterFallback { + const emitter = new EventEmitter(); + return { + initAsync: jest.fn(), + emitNewFrameEvent: jest.fn((timestampSeconds?: number) => { + emitter.emit(NewFrameEventName, { + newFrameTimestampInSeconds: timestampSeconds || timestampInSeconds(), + }); + }), + onceNewFrame: jest.fn((listener: (event: NewFrameEvent) => void) => { + emitter.once(NewFrameEventName, listener); + }), + }; +} +export const createSentryFallbackEventEmitter = jest.fn(() => createMockedSentryFallbackEventEmitter()); diff --git a/packages/core/test/utils/sentryeventemitterfallback.test.ts b/packages/core/test/utils/sentryeventemitterfallback.test.ts new file mode 100644 index 0000000000..6f8f9ff0d2 --- /dev/null +++ b/packages/core/test/utils/sentryeventemitterfallback.test.ts @@ -0,0 +1,236 @@ +import { NewFrameEventName } from '../../src/js/utils/sentryeventemitter'; +import { createSentryFallbackEventEmitter } from '../../src/js/utils/sentryeventemitterfallback'; + +// Mock dependencies +jest.mock('../../src/js/utils/environment', () => ({ + isTurboModuleEnabled: () => false, +})); + +jest.mock('../../src/js/wrapper', () => jest.requireActual('../mockWrapper')); + +jest.spyOn(logger, 'warn'); +jest.spyOn(logger, 'log'); +jest.spyOn(logger, 'error'); + +import { logger } from '@sentry/utils'; + +import { NATIVE } from '../../src/js/wrapper'; + +describe('SentryEventEmitterFallback', () => { + let emitter: ReturnType; + + beforeEach(() => { + jest.clearAllMocks(); + // @ts-expect-error test + jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => cb()); + emitter = createSentryFallbackEventEmitter(); + NATIVE.getNewScreenTimeToDisplay = jest.fn(() => Promise.resolve(12345)); + }); + + afterEach(() => { + // @ts-expect-error test + window.requestAnimationFrame.mockRestore(); + NATIVE.getNewScreenTimeToDisplay = jest.fn(); + }); + + it('should start listener and use fallback when native call returned undefined/null', async () => { + jest.useFakeTimers(); + const spy = jest.spyOn(require('@sentry/utils'), 'timestampInSeconds'); + const fallbackTime = Date.now() / 1000; + spy.mockReturnValue(fallbackTime); + + (NATIVE.getNewScreenTimeToDisplay as jest.Mock).mockReturnValue(Promise.resolve()); + + const listener = jest.fn(); + emitter.onceNewFrame(listener); + + // Wait for the next event loop to allow startListenerAsync to call NATIVE.getNewScreenTimeToDisplay + await Promise.resolve(); + + await expect(NATIVE.getNewScreenTimeToDisplay).toHaveBeenCalled(); + expect(logger.error).not.toHaveBeenCalledWith('Failed to receive Native fallback timestamp.', expect.any(Error)); + + // Simulate retries and timer + jest.runAllTimers(); + + // Ensure fallback event is emitted + expect(listener).toHaveBeenCalledWith({ + newFrameTimestampInSeconds: fallbackTime, + isFallback: true, + }); + expect(logger.log).toHaveBeenCalledWith( + expect.stringContaining( + '[Sentry] Native event emitter did not reply in time. Using JavaScript fallback emitter.', + ), + ); + }); + + it('should start listener and use fallback when native call fails', async () => { + jest.useFakeTimers(); + + (NATIVE.getNewScreenTimeToDisplay as jest.Mock).mockRejectedValue(new Error('Failed')); + + const spy = jest.spyOn(require('@sentry/utils'), 'timestampInSeconds'); + const fallbackTime = Date.now() / 1000; + spy.mockReturnValue(fallbackTime); + + const listener = jest.fn(); + emitter.onceNewFrame(listener); + + // Wait for the next event loop to allow startListenerAsync to call NATIVE.getNewScreenTimeToDisplay + await Promise.resolve(); + + await expect(NATIVE.getNewScreenTimeToDisplay).toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith('Failed to receive Native fallback timestamp.', expect.any(Error)); + + // Simulate retries and timer + jest.runAllTimers(); + + // Ensure fallback event is emitted + expect(listener).toHaveBeenCalledWith({ + newFrameTimestampInSeconds: fallbackTime, + isFallback: true, + }); + expect(logger.log).toHaveBeenCalledWith( + expect.stringContaining( + '[Sentry] Native event emitter did not reply in time. Using JavaScript fallback emitter.', + ), + ); + }); + + it('should start listener and use fallback when native call fails', async () => { + jest.useFakeTimers(); + const spy = jest.spyOn(require('@sentry/utils'), 'timestampInSeconds'); + const fallbackTime = Date.now() / 1000; + spy.mockReturnValue(fallbackTime); + + (NATIVE.getNewScreenTimeToDisplay as jest.Mock).mockRejectedValue(new Error('Failed')); + + const listener = jest.fn(); + emitter.onceNewFrame(listener); + + // Wait for the next event loop to allow startListenerAsync to call NATIVE.getNewScreenTimeToDisplay + await Promise.resolve(); + + await expect(NATIVE.getNewScreenTimeToDisplay).toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith('Failed to receive Native fallback timestamp.', expect.any(Error)); + + // Simulate retries and timer + jest.runAllTimers(); + + // Ensure fallback event is emitted + expect(listener).toHaveBeenCalledWith({ + newFrameTimestampInSeconds: fallbackTime, + isFallback: true, + }); + expect(logger.log).toHaveBeenCalledWith( + expect.stringContaining( + '[Sentry] Native event emitter did not reply in time. Using JavaScript fallback emitter.', + ), + ); + }); + + it('should start listener and use fallback when native call is not available', async () => { + jest.useFakeTimers(); + const spy = jest.spyOn(require('@sentry/utils'), 'timestampInSeconds'); + const fallbackTime = Date.now() / 1000; + spy.mockReturnValue(fallbackTime); + + NATIVE.getNewScreenTimeToDisplay = () => Promise.resolve(null); + + const listener = jest.fn(); + emitter.onceNewFrame(listener); + + // Wait for the next event loop to allow startListenerAsync to call NATIVE.getNewScreenTimeToDisplay + await Promise.resolve(); + + // Simulate retries and timer + jest.runAllTimers(); + + // Ensure fallback event is emitted + expect(listener).toHaveBeenCalledWith({ + newFrameTimestampInSeconds: fallbackTime, + isFallback: true, + }); + expect(logger.log).toHaveBeenCalledWith( + expect.stringContaining( + '[Sentry] Native event emitter did not reply in time. Using JavaScript fallback emitter.', + ), + ); + }); + + it('should start listener and call native when native module is available', async () => { + const nativeTimestamp = 12345; + + (NATIVE.getNewScreenTimeToDisplay as jest.Mock).mockResolvedValueOnce(nativeTimestamp); + + const listener = jest.fn(); + emitter.onceNewFrame(listener); + + expect(NATIVE.getNewScreenTimeToDisplay).toHaveBeenCalled(); + }); + + it('should not emit if original event emitter was called', async () => { + jest.useFakeTimers(); + + // Capture the callback passed to addListener + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/ban-types + let callback: Function = () => {}; + const mockOnce = jest.fn().mockImplementationOnce((eventName, cb) => { + if (eventName === NewFrameEventName) { + callback = cb; + } + return { + remove: jest.fn(), + }; + }); + + emitter = createSentryFallbackEventEmitter({ + addListener: jest.fn(), + initAsync: jest.fn(), + closeAllAsync: jest.fn(), + removeListener: jest.fn(), + once: mockOnce, + }); + + emitter.initAsync(); + const listener = jest.fn(); + emitter.onceNewFrame(listener); + callback({ + newFrameTimestampInSeconds: 67890, + }); + + // Wait for the next event loop to allow startListenerAsync to call NATIVE.getNewScreenTimeToDisplay + await Promise.resolve(); + + // Simulate retries and timer + jest.runAllTimers(); + + // Ensure fallback event is emitted + expect(listener).toHaveBeenCalledWith({ + newFrameTimestampInSeconds: 67890, + isFallback: undefined, + }); + expect(logger.log).not.toBeCalled(); + }); + + it('should retry up to maxRetries and emit fallback if no response', async () => { + jest.useFakeTimers(); + + const listener = jest.fn(); + emitter.onceNewFrame(listener); + + // Wait for the next event loop to allow startListenerAsync to call NATIVE.getNewScreenTimeToDisplay + await Promise.resolve(); + + expect(logger.log).not.toHaveBeenCalled(); + + // Simulate retries and timer + jest.runAllTimers(); + + expect(listener).toHaveBeenCalledWith(expect.objectContaining({ isFallback: true })); + expect(logger.log).toHaveBeenCalledWith(expect.stringContaining('Native event emitter did not reply in time')); + + jest.useRealTimers(); + }); +}); diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index e39bff854e..1343af7636 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -48,7 +48,10 @@ Sentry.init({ // Replace the example DSN below with your own DSN: dsn: SENTRY_INTERNAL_DSN, debug: true, +// maxBreadcrumbs: 0, +// enableNative: true, environment: 'dev', + beforeSend: (event: ErrorEvent) => { logWithoutTracing('Event beforeSend:', event.event_id); return event; @@ -65,13 +68,16 @@ Sentry.init({ ); }, enableUserInteractionTracing: true, + integrations(integrations) { integrations.push( reactNavigationIntegration, + Sentry.reactNativeTracingIntegration({ // The time to wait in ms until the transaction will be finished, For testing, default is 1000 ms idleTimeoutMs: 5_000, }), + Sentry.httpClientIntegration({ // These options are effective only in JS. // This array can contain tuples of `[begin, end]` (both inclusive), @@ -82,7 +88,7 @@ Sentry.init({ // default: [/.*/] failedRequestTargets: [/.*/], }), - Sentry.mobileReplayIntegration({ + Sentry.mobileReplayIntegration({ maskAllImages: true, maskAllVectors: true, // maskAllText: false, @@ -111,9 +117,9 @@ Sentry.init({ // otherwise they will not work. // release: 'myapp@1.2.3+1', // dist: `1`, - profilesSampleRate: 1.0, +// profilesSampleRate: 1.0, _experiments: { - // replaysSessionSampleRate: 1.0, + replaysSessionSampleRate: 1.0, replaysOnErrorSampleRate: 1.0, }, spotlight: true, From 295a2afbce6b44583fe722b6c62f0fbfe959a80e Mon Sep 17 00:00:00 2001 From: lucas Date: Thu, 17 Oct 2024 16:44:29 -0300 Subject: [PATCH 02/13] fix tests / yarn fix --- packages/core/src/js/tracing/reactnavigation.ts | 7 +++---- .../core/test/utils/mockedSentryeventemitterfallback.ts | 3 ++- .../core/test/utils/sentryeventemitterfallback.test.ts | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/core/src/js/tracing/reactnavigation.ts b/packages/core/src/js/tracing/reactnavigation.ts index 2418e0052c..ce2bb1032c 100644 --- a/packages/core/src/js/tracing/reactnavigation.ts +++ b/packages/core/src/js/tracing/reactnavigation.ts @@ -12,8 +12,9 @@ import { import type { Client, Integration, Span } from '@sentry/types'; import { isPlainObject, logger, timestampInSeconds } from '@sentry/utils'; -import type { NewFrameEvent, SentryEventEmitter } from '../utils/sentryeventemitter'; -import { createSentryEventEmitter, NewFrameEventName } from '../utils/sentryeventemitter'; +import type { NewFrameEvent } from '../utils/sentryeventemitter'; +import type { SentryEventEmitterFallback } from '../utils/sentryeventemitterfallback'; +import { createSentryFallbackEventEmitter } from '../utils/sentryeventemitterfallback'; import { isSentrySpan } from '../utils/span'; import { RN_GLOBAL_OBJ } from '../utils/worldwide'; import { NATIVE } from '../wrapper'; @@ -30,8 +31,6 @@ import { } from './span'; import { manualInitialDisplaySpans, startTimeToInitialDisplaySpan } from './timetodisplay'; import { setSpanDurationAsMeasurementOnSpan } from './utils'; -import type { SentryEventEmitterFallback } from '../utils/sentryeventemitterfallback'; -import { createSentryFallbackEventEmitter } from '../utils/sentryeventemitterfallback'; export const INTEGRATION_NAME = 'ReactNavigation'; const NAVIGATION_HISTORY_MAX_SIZE = 200; diff --git a/packages/core/test/utils/mockedSentryeventemitterfallback.ts b/packages/core/test/utils/mockedSentryeventemitterfallback.ts index 50d5fbcc27..481b109d68 100644 --- a/packages/core/test/utils/mockedSentryeventemitterfallback.ts +++ b/packages/core/test/utils/mockedSentryeventemitterfallback.ts @@ -1,9 +1,10 @@ import { timestampInSeconds } from '@sentry/utils'; -import EventEmitter from 'events'; +import * as EventEmitter from 'events'; import type { NewFrameEvent } from '../../src/js/utils/sentryeventemitter'; import type { SentryEventEmitterFallback } from '../../src/js/utils/sentryeventemitterfallback'; import type { MockInterface } from '../testutils'; + export const NewFrameEventName = 'rn_sentry_new_frame'; export type NewFrameEventName = typeof NewFrameEventName; export interface MockedSentryEventEmitterFallback extends MockInterface { diff --git a/packages/core/test/utils/sentryeventemitterfallback.test.ts b/packages/core/test/utils/sentryeventemitterfallback.test.ts index 6f8f9ff0d2..e96567ae29 100644 --- a/packages/core/test/utils/sentryeventemitterfallback.test.ts +++ b/packages/core/test/utils/sentryeventemitterfallback.test.ts @@ -1,3 +1,5 @@ +import { logger } from '@sentry/utils'; + import { NewFrameEventName } from '../../src/js/utils/sentryeventemitter'; import { createSentryFallbackEventEmitter } from '../../src/js/utils/sentryeventemitterfallback'; @@ -8,14 +10,12 @@ jest.mock('../../src/js/utils/environment', () => ({ jest.mock('../../src/js/wrapper', () => jest.requireActual('../mockWrapper')); +import { NATIVE } from '../../src/js/wrapper'; + jest.spyOn(logger, 'warn'); jest.spyOn(logger, 'log'); jest.spyOn(logger, 'error'); -import { logger } from '@sentry/utils'; - -import { NATIVE } from '../../src/js/wrapper'; - describe('SentryEventEmitterFallback', () => { let emitter: ReturnType; From fe659499555e21ebd1766b0ff2757f85d41cf8d1 Mon Sep 17 00:00:00 2001 From: lucas Date: Thu, 17 Oct 2024 18:10:28 -0300 Subject: [PATCH 03/13] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd943a736a..4a7c1470ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ ### Fixes -- Enhanced accuracy of time-to-display spans. ([#4042](https://github.com/getsentry/sentry-react-native/pull/4042)) +- Enhanced accuracy of time-to-display spans. ([#4189](https://github.com/getsentry/sentry-react-native/pull/4189)) ## 6.0.0 From 652a8dc02a4dc107d4a6f1064fe94d25cfdaa76f Mon Sep 17 00:00:00 2001 From: lucas Date: Thu, 17 Oct 2024 18:14:57 -0300 Subject: [PATCH 04/13] undoo changes to sample --- samples/react-native/src/App.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/samples/react-native/src/App.tsx b/samples/react-native/src/App.tsx index 1343af7636..e39bff854e 100644 --- a/samples/react-native/src/App.tsx +++ b/samples/react-native/src/App.tsx @@ -48,10 +48,7 @@ Sentry.init({ // Replace the example DSN below with your own DSN: dsn: SENTRY_INTERNAL_DSN, debug: true, -// maxBreadcrumbs: 0, -// enableNative: true, environment: 'dev', - beforeSend: (event: ErrorEvent) => { logWithoutTracing('Event beforeSend:', event.event_id); return event; @@ -68,16 +65,13 @@ Sentry.init({ ); }, enableUserInteractionTracing: true, - integrations(integrations) { integrations.push( reactNavigationIntegration, - Sentry.reactNativeTracingIntegration({ // The time to wait in ms until the transaction will be finished, For testing, default is 1000 ms idleTimeoutMs: 5_000, }), - Sentry.httpClientIntegration({ // These options are effective only in JS. // This array can contain tuples of `[begin, end]` (both inclusive), @@ -88,7 +82,7 @@ Sentry.init({ // default: [/.*/] failedRequestTargets: [/.*/], }), - Sentry.mobileReplayIntegration({ + Sentry.mobileReplayIntegration({ maskAllImages: true, maskAllVectors: true, // maskAllText: false, @@ -117,9 +111,9 @@ Sentry.init({ // otherwise they will not work. // release: 'myapp@1.2.3+1', // dist: `1`, -// profilesSampleRate: 1.0, + profilesSampleRate: 1.0, _experiments: { - replaysSessionSampleRate: 1.0, + // replaysSessionSampleRate: 1.0, replaysOnErrorSampleRate: 1.0, }, spotlight: true, From 61b5835e236133067a2b84342d13bee5666a3473 Mon Sep 17 00:00:00 2001 From: lucas Date: Thu, 17 Oct 2024 19:29:50 -0300 Subject: [PATCH 05/13] fix android --- .../java/io/sentry/RNSentryTimeToDisplay.java | 39 ------------------- .../io/sentry/react/RNSentryModuleImpl.java | 12 ------ .../sentry/react/RNSentryTimeToDisplay.java | 39 +++++++++++++++++++ 3 files changed, 39 insertions(+), 51 deletions(-) delete mode 100644 packages/core/android/src/main/java/io/sentry/RNSentryTimeToDisplay.java create mode 100644 packages/core/android/src/main/java/io/sentry/react/RNSentryTimeToDisplay.java diff --git a/packages/core/android/src/main/java/io/sentry/RNSentryTimeToDisplay.java b/packages/core/android/src/main/java/io/sentry/RNSentryTimeToDisplay.java deleted file mode 100644 index 264048c287..0000000000 --- a/packages/core/android/src/main/java/io/sentry/RNSentryTimeToDisplay.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.sentry.react; - -import com.facebook.react.bridge.Promise; - -import android.os.Handler; -import android.os.Looper; -import android.view.Choreographer; - -import org.jetbrains.annotations.NotNull; -import io.sentry.SentryDate; -import io.sentry.SentryDateProvider; -import io.sentry.android.core.SentryAndroidDateProvider; - -public class RNSentryTimeToDisplay { - public static void GetTimeToDisplay(Promise promise, SentryDateProvider dateProvider) { - Looper mainLooper = Looper.getMainLooper(); - - if (mainLooper == null) { - promise.reject("GetTimeToDisplay is not able to measure the time to display: Main looper not available."); - return; - } - - // Ensure the code runs on the main thread - new Handler(mainLooper) - .post(() -> { - try { - Choreographer choreographer = Choreographer.getInstance(); - - // Invoke the callback after the frame is rendered - choreographer.postFrameCallback(frameTimeNanos -> { - final SentryDate endDate = dateProvider.now(); - promise.resolve(endDate.nanoTimestamp() / 1e9); - }); - } catch (Exception exception) { - promise.reject("Failed to receive the instance of Choreographer", exception); - } - }); - } -} diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 71ee344dfd..1a8435bbf2 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -173,7 +173,6 @@ public void initNativeReactNavigationNewFrameTracking(Promise promise) { } public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { -<<<<<<< HEAD SentryAndroid.init( this.getReactApplicationContext(), options -> { @@ -274,17 +273,6 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { } } catch (Throwable ignored) { // NOPMD - We don't want to crash in any case // We do nothing -======= - try { - SentryAndroid.init( - this.getReactApplicationContext(), - options -> { - @Nullable SdkVersion sdkVersion = options.getSdkVersion(); - if (sdkVersion == null) { - sdkVersion = new SdkVersion(ANDROID_SDK_NAME, BuildConfig.VERSION_NAME); - } else { - sdkVersion.setName(ANDROID_SDK_NAME); ->>>>>>> 4ef0e98c (port navigation tracker #4042 to V6 / Todo: Fix tests) } options.setSentryClientName(sdkVersion.getName() + "/" + sdkVersion.getVersion()); diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryTimeToDisplay.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryTimeToDisplay.java new file mode 100644 index 0000000000..3ab96ce23d --- /dev/null +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryTimeToDisplay.java @@ -0,0 +1,39 @@ +package io.sentry.react; + +import android.os.Handler; +import android.os.Looper; +import android.view.Choreographer; +import com.facebook.react.bridge.Promise; +import io.sentry.SentryDate; +import io.sentry.SentryDateProvider; + +public class RNSentryTimeToDisplay { + public static void GetTimeToDisplay(Promise promise, SentryDateProvider dateProvider) { + Looper mainLooper = Looper.getMainLooper(); + + if (mainLooper == null) { + promise.reject( + "GetTimeToDisplay is not able to measure the time to display: Main looper not" + + " available."); + return; + } + + // Ensure the code runs on the main thread + new Handler(mainLooper) + .post( + () -> { + try { + Choreographer choreographer = Choreographer.getInstance(); + + // Invoke the callback after the frame is rendered + choreographer.postFrameCallback( + frameTimeNanos -> { + final SentryDate endDate = dateProvider.now(); + promise.resolve(endDate.nanoTimestamp() / 1e9); + }); + } catch (Exception exception) { + promise.reject("Failed to receive the instance of Choreographer", exception); + } + }); + } +} From f10fb790ec790b71b38c5004ca437c84fbd456bf Mon Sep 17 00:00:00 2001 From: lucas Date: Thu, 17 Oct 2024 19:32:27 -0300 Subject: [PATCH 06/13] NIT --- .../io/sentry/react/RNSentryModuleImpl.java | 150 ++++-------------- 1 file changed, 28 insertions(+), 122 deletions(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 1a8435bbf2..71f22a61d8 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -125,9 +125,9 @@ public class RNSentryModuleImpl { /** Max trace file size in bytes. */ private long maxTraceFileSize = 5 * 1024 * 1024; - private final @NotNull SentryDateProvider dateProvider; + public RNSentryModuleImpl(ReactApplicationContext reactApplicationContext) { packageInfo = getPackageInfo(reactApplicationContext); this.reactApplicationContext = reactApplicationContext; @@ -275,129 +275,34 @@ public void initNativeSdk(final ReadableMap rnOptions, Promise promise) { // We do nothing } - options.setSentryClientName(sdkVersion.getName() + "/" + sdkVersion.getVersion()); - options.setNativeSdkName(NATIVE_SDK_NAME); - options.setSdkVersion(sdkVersion); + setEventOriginTag(event); + addPackages(event, options.getSdkVersion()); - if (rnOptions.hasKey("debug") && rnOptions.getBoolean("debug")) { - options.setDebug(true); - } - if (rnOptions.hasKey("dsn") && rnOptions.getString("dsn") != null) { - String dsn = rnOptions.getString("dsn"); - logger.log(SentryLevel.INFO, String.format("Starting with DSN: '%s'", dsn)); - options.setDsn(dsn); - } else { - // SentryAndroid needs an empty string fallback for the dsn. - options.setDsn(""); - } - if (rnOptions.hasKey("sampleRate")) { - options.setSampleRate(rnOptions.getDouble("sampleRate")); - } - if (rnOptions.hasKey("sendClientReports")) { - options.setSendClientReports(rnOptions.getBoolean("sendClientReports")); - } - if (rnOptions.hasKey("maxBreadcrumbs")) { - options.setMaxBreadcrumbs(rnOptions.getInt("maxBreadcrumbs")); - } - if (rnOptions.hasKey("maxCacheItems")) { - options.setMaxCacheItems(rnOptions.getInt("maxCacheItems")); - } - if (rnOptions.hasKey("environment") && rnOptions.getString("environment") != null) { - options.setEnvironment(rnOptions.getString("environment")); - } - if (rnOptions.hasKey("release") && rnOptions.getString("release") != null) { - options.setRelease(rnOptions.getString("release")); - } - if (rnOptions.hasKey("dist") && rnOptions.getString("dist") != null) { - options.setDist(rnOptions.getString("dist")); - } - if (rnOptions.hasKey("enableAutoSessionTracking")) { - options.setEnableAutoSessionTracking(rnOptions.getBoolean("enableAutoSessionTracking")); - } - if (rnOptions.hasKey("sessionTrackingIntervalMillis")) { - options.setSessionTrackingIntervalMillis( - rnOptions.getInt("sessionTrackingIntervalMillis")); - } - if (rnOptions.hasKey("shutdownTimeout")) { - options.setShutdownTimeoutMillis(rnOptions.getInt("shutdownTimeout")); - } - if (rnOptions.hasKey("enableNdkScopeSync")) { - options.setEnableScopeSync(rnOptions.getBoolean("enableNdkScopeSync")); - } - if (rnOptions.hasKey("attachStacktrace")) { - options.setAttachStacktrace(rnOptions.getBoolean("attachStacktrace")); - } - if (rnOptions.hasKey("attachThreads")) { - // JS use top level stacktrace and android attaches Threads which hides them so - // by default we hide. - options.setAttachThreads(rnOptions.getBoolean("attachThreads")); - } - if (rnOptions.hasKey("attachScreenshot")) { - options.setAttachScreenshot(rnOptions.getBoolean("attachScreenshot")); - } - if (rnOptions.hasKey("attachViewHierarchy")) { - options.setAttachViewHierarchy(rnOptions.getBoolean("attachViewHierarchy")); - } - if (rnOptions.hasKey("sendDefaultPii")) { - options.setSendDefaultPii(rnOptions.getBoolean("sendDefaultPii")); - } - if (rnOptions.hasKey("maxQueueSize")) { - options.setMaxQueueSize(rnOptions.getInt("maxQueueSize")); - } - if (rnOptions.hasKey("enableNdk")) { - options.setEnableNdk(rnOptions.getBoolean("enableNdk")); - } - if (rnOptions.hasKey("_experiments")) { - options.getExperimental().setSessionReplay(getReplayOptions(rnOptions)); - options - .getReplayController() - .setBreadcrumbConverter(new RNSentryReplayBreadcrumbConverter()); - } - options.setBeforeSend( - (event, hint) -> { - // React native internally throws a JavascriptException - // Since we catch it before that, we don't want to send this one - // because we would send it twice - try { - SentryException ex = event.getExceptions().get(0); - if (null != ex && ex.getType().contains("JavascriptException")) { - return null; - } - } catch (Throwable ignored) { - // We do nothing - } - - setEventOriginTag(event); - addPackages(event, options.getSdkVersion()); - - return event; - }); - - if (rnOptions.hasKey("enableNativeCrashHandling") - && !rnOptions.getBoolean("enableNativeCrashHandling")) { - final List integrations = options.getIntegrations(); - for (final Integration integration : integrations) { - if (integration instanceof UncaughtExceptionHandlerIntegration - || integration instanceof AnrIntegration - || integration instanceof NdkIntegration) { - integrations.remove(integration); - } - } - } - logger.log( - SentryLevel.INFO, - String.format("Native Integrations '%s'", options.getIntegrations())); - - final CurrentActivityHolder currentActivityHolder = CurrentActivityHolder.getInstance(); - final Activity currentActivity = getCurrentActivity(); - if (currentActivity != null) { - currentActivityHolder.setActivity(currentActivity); - } + return event; }); - } - catch (Exception ex){ - promise.reject(ex); - } + + if (rnOptions.hasKey("enableNativeCrashHandling") + && !rnOptions.getBoolean("enableNativeCrashHandling")) { + final List integrations = options.getIntegrations(); + for (final Integration integration : integrations) { + if (integration instanceof UncaughtExceptionHandlerIntegration + || integration instanceof AnrIntegration + || integration instanceof NdkIntegration) { + integrations.remove(integration); + } + } + } + logger.log( + SentryLevel.INFO, + String.format("Native Integrations '%s'", options.getIntegrations())); + + final CurrentActivityHolder currentActivityHolder = CurrentActivityHolder.getInstance(); + final Activity currentActivity = getCurrentActivity(); + if (currentActivity != null) { + currentActivityHolder.setActivity(currentActivity); + } + }); + promise.resolve(true); } @@ -845,6 +750,7 @@ public void getNewScreenTimeToDisplay(Promise promise) { RNSentryTimeToDisplay.GetTimeToDisplay(promise, dateProvider); } + private String getProfilingTracesDirPath() { if (cacheDirPath == null) { cacheDirPath = From 1b898a6dc488bf72c8744c5b46b6a7042a49d071 Mon Sep 17 00:00:00 2001 From: lucas Date: Thu, 17 Oct 2024 19:33:13 -0300 Subject: [PATCH 07/13] NIT extra line --- .../src/main/java/io/sentry/react/RNSentryModuleImpl.java | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 71f22a61d8..c50b634d61 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -750,7 +750,6 @@ public void getNewScreenTimeToDisplay(Promise promise) { RNSentryTimeToDisplay.GetTimeToDisplay(promise, dateProvider); } - private String getProfilingTracesDirPath() { if (cacheDirPath == null) { cacheDirPath = From b14f1d8ef0d8dd9ec7c17cea74bd4b7e69ff7449 Mon Sep 17 00:00:00 2001 From: lucas Date: Thu, 17 Oct 2024 23:27:55 -0300 Subject: [PATCH 08/13] fix-android-lint --- .../src/main/java/io/sentry/react/RNSentryModuleImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index c50b634d61..9f5ac7a2e3 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -125,8 +125,8 @@ public class RNSentryModuleImpl { /** Max trace file size in bytes. */ private long maxTraceFileSize = 5 * 1024 * 1024; - private final @NotNull SentryDateProvider dateProvider; + private final @NotNull SentryDateProvider dateProvider; public RNSentryModuleImpl(ReactApplicationContext reactApplicationContext) { packageInfo = getPackageInfo(reactApplicationContext); From 8e1f7c281ffe6db00f04960023d0cc2e697b6cf3 Mon Sep 17 00:00:00 2001 From: lucas Date: Wed, 23 Oct 2024 14:11:46 +0100 Subject: [PATCH 09/13] fix java format --- .../src/main/java/io/sentry/react/RNSentryTimeToDisplay.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryTimeToDisplay.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryTimeToDisplay.java index 3ab96ce23d..1aae29fb51 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryTimeToDisplay.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryTimeToDisplay.java @@ -7,8 +7,8 @@ import io.sentry.SentryDate; import io.sentry.SentryDateProvider; -public class RNSentryTimeToDisplay { - public static void GetTimeToDisplay(Promise promise, SentryDateProvider dateProvider) { +public static class RNSentryTimeToDisplay { + public static void getTimeToDisplay(Promise promise, SentryDateProvider dateProvider) { Looper mainLooper = Looper.getMainLooper(); if (mainLooper == null) { From 5425e83d99ebf7888d459bfdcb3334a4329d0695 Mon Sep 17 00:00:00 2001 From: lucas Date: Wed, 23 Oct 2024 14:36:49 +0100 Subject: [PATCH 10/13] pub private constructor --- .../src/main/java/io/sentry/react/RNSentryModuleImpl.java | 2 +- .../main/java/io/sentry/react/RNSentryTimeToDisplay.java | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index 9f5ac7a2e3..7472187dd4 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -747,7 +747,7 @@ public void disableNativeFramesTracking() { } public void getNewScreenTimeToDisplay(Promise promise) { - RNSentryTimeToDisplay.GetTimeToDisplay(promise, dateProvider); + RNSentryTimeToDisplay.getTimeToDisplay(promise, dateProvider); } private String getProfilingTracesDirPath() { diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryTimeToDisplay.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryTimeToDisplay.java index 1aae29fb51..07cc5c1273 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryTimeToDisplay.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryTimeToDisplay.java @@ -7,7 +7,12 @@ import io.sentry.SentryDate; import io.sentry.SentryDateProvider; -public static class RNSentryTimeToDisplay { +public class RNSentryTimeToDisplay { + + private RNSentryTimeToDisplay() { + throw new UnsupportedOperationException("Utility class"); + } + public static void getTimeToDisplay(Promise promise, SentryDateProvider dateProvider) { Looper mainLooper = Looper.getMainLooper(); From 2cbf9778fdb4dac94487e72af5dc2857bfc1dca6 Mon Sep 17 00:00:00 2001 From: lucas Date: Wed, 23 Oct 2024 14:45:46 +0100 Subject: [PATCH 11/13] make it final --- .../main/java/io/sentry/react/RNSentryTimeToDisplay.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryTimeToDisplay.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryTimeToDisplay.java index 07cc5c1273..503278122f 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryTimeToDisplay.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryTimeToDisplay.java @@ -7,11 +7,7 @@ import io.sentry.SentryDate; import io.sentry.SentryDateProvider; -public class RNSentryTimeToDisplay { - - private RNSentryTimeToDisplay() { - throw new UnsupportedOperationException("Utility class"); - } +public final class RNSentryTimeToDisplay { public static void getTimeToDisplay(Promise promise, SentryDateProvider dateProvider) { Looper mainLooper = Looper.getMainLooper(); From 7437473d4c80e633bc9d251cf30174496eb26482 Mon Sep 17 00:00:00 2001 From: lucas Date: Wed, 23 Oct 2024 14:54:52 +0100 Subject: [PATCH 12/13] test --- .../src/main/java/io/sentry/react/RNSentryTimeToDisplay.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryTimeToDisplay.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryTimeToDisplay.java index 503278122f..b6fab45492 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryTimeToDisplay.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryTimeToDisplay.java @@ -9,6 +9,8 @@ public final class RNSentryTimeToDisplay { + private RNSentryTimeToDisplay() {} + public static void getTimeToDisplay(Promise promise, SentryDateProvider dateProvider) { Looper mainLooper = Looper.getMainLooper(); From daa916de29e9dca0f1474163f4a394d57db61877 Mon Sep 17 00:00:00 2001 From: lucas Date: Mon, 28 Oct 2024 21:42:16 +0000 Subject: [PATCH 13/13] add missing dependencies changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf7d68a14b..b9494e5a10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ ## 6.1.0 +### Dependencies + - Bump JavaScript SDK from v8.33.1 to v8.34.0 ([#3895](https://github.com/getsentry/sentry-react-native/pull/3895)) - [changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md#8340) - [diff](https://github.com/getsentry/sentry-javascript/compare/8.33.1...8.34.0)