diff --git a/android/src/main/java/io/sentry/react/RNSentryTimeToDisplayModule.java b/android/src/main/java/io/sentry/react/RNSentryTimeToDisplayModule.java index 364fc8d0b..66d62befb 100644 --- a/android/src/main/java/io/sentry/react/RNSentryTimeToDisplayModule.java +++ b/android/src/main/java/io/sentry/react/RNSentryTimeToDisplayModule.java @@ -18,7 +18,7 @@ public class RNSentryTimeToDisplayModule extends NativeRNSentryTimeToDisplaySpec { - public static final String NAME = "RNSentryTimeToDisplayModule"; + public static final String NAME = "RNSentryTimeToDisplay"; public RNSentryTimeToDisplayModule(ReactApplicationContext reactContext) { super(reactContext); @@ -40,4 +40,9 @@ public void doFrame(long frameTimeNanos) { } }); } + + @ReactMethod(isBlockingSynchronousMethod = true) + public boolean isAvailable() { + return true; + } } diff --git a/ios/RNSentryTimeToDisplay.m b/ios/RNSentryTimeToDisplay.m index 4dc561b62..bb4ffbaaa 100644 --- a/ios/RNSentryTimeToDisplay.m +++ b/ios/RNSentryTimeToDisplay.m @@ -16,15 +16,18 @@ @implementation RNSentryTimeToDisplay // Store the resolve block to use in the callback resolveBlock = resolve; +#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 +#endif } +#if TARGET_OS_IOS - (void)handleDisplayLink:(CADisplayLink *)link { NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970]; - if (resolveBlock) { resolveBlock(@(currentTime)); resolveBlock = nil; @@ -34,5 +37,15 @@ - (void)handleDisplayLink:(CADisplayLink *)link [displayLink invalidate]; displayLink = nil; } +#endif + +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(isAvailable) +{ +#if TARGET_OS_IOS + return @(YES); +#else + return @(NO); // MacOS +#endif +} @end diff --git a/src/js/NativeRNSentry.ts b/src/js/NativeRNSentry.ts index abfd546f6..ddc1a5272 100644 --- a/src/js/NativeRNSentry.ts +++ b/src/js/NativeRNSentry.ts @@ -156,6 +156,7 @@ export type NativeScreenshot = { export interface RNSentryTimeToDisplayModuleSpec { requestAnimationFrame(): Promise; + isAvailable(): boolean; } // The export must be here to pass codegen even if not used diff --git a/src/js/NativeRNSentryTimeToDisplay.ts b/src/js/NativeRNSentryTimeToDisplay.ts index 02e3e2d9a..912bc10c0 100644 --- a/src/js/NativeRNSentryTimeToDisplay.ts +++ b/src/js/NativeRNSentryTimeToDisplay.ts @@ -5,6 +5,7 @@ import { TurboModuleRegistry } from 'react-native'; // Only extra allowed definitions are types (probably codegen bug) export interface Spec extends TurboModule { requestAnimationFrame(): Promise; + isAvailable(): boolean; } // The export must be here to pass codegen even if not used diff --git a/src/js/utils/sentryeventemitterfallback.ts b/src/js/utils/sentryeventemitterfallback.ts index e05420cde..c361ccf73 100644 --- a/src/js/utils/sentryeventemitterfallback.ts +++ b/src/js/utils/sentryeventemitterfallback.ts @@ -3,7 +3,6 @@ import type { EmitterSubscription } from 'react-native'; import { DeviceEventEmitter, NativeModules } from 'react-native'; import type { Spec } from '../NativeRNSentryTimeToDisplay'; -import { NATIVE } from '../wrapper'; import { isTurboModuleEnabled } from './environment'; import { ReactNativeLibraries } from './rnlibraries'; import { NewFrameEventName } from './sentryeventemitter'; @@ -93,7 +92,7 @@ export function createSentryFallbackEventEmitter(): SentryEventEmitterFallback { startListenerAsync() { isListening = true; - if (NATIVE.isNativeAvailable() && RNSentryTimeToDisplay !== undefined) { + if (RNSentryTimeToDisplay && RNSentryTimeToDisplay.isAvailable()) { RNSentryTimeToDisplay.requestAnimationFrame() .then((time: number) => { waitForNativeResponseOrFallback(time, 'Native'); diff --git a/test/utils/sentryeventemitterfallback.test.ts b/test/utils/sentryeventemitterfallback.test.ts index af9253f1f..c53f551b9 100644 --- a/test/utils/sentryeventemitterfallback.test.ts +++ b/test/utils/sentryeventemitterfallback.test.ts @@ -10,6 +10,7 @@ import { createSentryFallbackEventEmitter } from '../../src/js/utils/sentryevent jest.mock('react-native', () => { const RNSentryTimeToDisplay: Spec = { requestAnimationFrame: jest.fn(() => Promise.resolve(12345)), + isAvailable: () => true, }; return { DeviceEventEmitter: { @@ -53,6 +54,7 @@ describe('SentryEventEmitterFallback', () => { afterEach(() => { // @ts-expect-error test window.requestAnimationFrame.mockRestore(); + RNSentryTimeToDisplay.isAvailable = () => true; }); it('should initialize and add a listener', () => { @@ -116,7 +118,33 @@ describe('SentryEventEmitterFallback', () => { ); }); - it('should start listener and call native when native is available', async () => { + it('should start listener and use fallback when native call is not available', async () => { + jest.useFakeTimers(); + const fallbackTime = Date.now() / 1000; + + RNSentryTimeToDisplay.isAvailable = () => false; + const animation = RNSentryTimeToDisplay.requestAnimationFrame as jest.Mock; + + emitter.startListenerAsync(); + await animation; // wait for the Native execution to be completed. + + // Simulate retries and timer + jest.runAllTimers(); + + // Ensure fallback event is emitted + expect(DeviceEventEmitter.emit).toHaveBeenCalledWith(NewFrameEventName, { + 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; (RNSentryTimeToDisplay.requestAnimationFrame as jest.Mock).mockResolvedValueOnce(nativeTimestamp);