Skip to content

Commit 993f0a4

Browse files
Merge d0c0cf7 into 0abe24e
2 parents 0abe24e + d0c0cf7 commit 993f0a4

File tree

2 files changed

+89
-3
lines changed

2 files changed

+89
-3
lines changed

src/js/tracing/reactnavigation.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { logger, timestampInSeconds } from '@sentry/utils';
55

66
import type { NewFrameEvent } from '../utils/sentryeventemitter';
77
import { type SentryEventEmitter, createSentryEventEmitter, NewFrameEventName } from '../utils/sentryeventemitter';
8+
import { type SentryEventEmitterFallback, createSentryFallbackEventEmitter } from '../utils/sentryeventemitterfallback';
89
import { RN_GLOBAL_OBJ } from '../utils/worldwide';
910
import { NATIVE } from '../wrapper';
1011
import type { OnConfirmRoute, TransactionCreator } from './routingInstrumentation';
@@ -69,7 +70,7 @@ export class ReactNavigationInstrumentation extends InternalRoutingInstrumentati
6970

7071
private _navigationContainer: NavigationContainer | null = null;
7172
private _newScreenFrameEventEmitter: SentryEventEmitter | null = null;
72-
73+
private _newFallbackEventEmitter: SentryEventEmitterFallback | null = null;
7374
private readonly _maxRecentRouteLen: number = 200;
7475

7576
private _latestRoute?: NavigationRoute;
@@ -92,7 +93,9 @@ export class ReactNavigationInstrumentation extends InternalRoutingInstrumentati
9293

9394
if (this._options.enableTimeToInitialDisplay) {
9495
this._newScreenFrameEventEmitter = createSentryEventEmitter();
96+
this._newFallbackEventEmitter = createSentryFallbackEventEmitter();
9597
this._newScreenFrameEventEmitter.initAsync(NewFrameEventName);
98+
this._newFallbackEventEmitter.initAsync();
9699
NATIVE.initNativeReactNavigationNewFrameTracking().catch((reason: unknown) => {
97100
logger.error(`[ReactNavigationInstrumentation] Failed to initialize native new frame tracking: ${reason}`);
98101
});
@@ -238,8 +241,8 @@ export class ReactNavigationInstrumentation extends InternalRoutingInstrumentati
238241
isAutoInstrumented: true,
239242
});
240243

241-
!routeHasBeenSeen &&
242-
latestTtidSpan &&
244+
if (!routeHasBeenSeen && latestTtidSpan) {
245+
this._newFallbackEventEmitter?.startListenerAsync();
243246
this._newScreenFrameEventEmitter?.once(
244247
NewFrameEventName,
245248
({ newFrameTimestampInSeconds }: NewFrameEvent) => {
@@ -256,6 +259,7 @@ export class ReactNavigationInstrumentation extends InternalRoutingInstrumentati
256259
setSpanDurationAsMeasurementOnTransaction(latestTransaction, 'time_to_initial_display', latestTtidSpan);
257260
},
258261
);
262+
}
259263

260264
this._navigationProcessingSpan?.updateName(`Processing navigation to ${route.name}`);
261265
this._navigationProcessingSpan?.setStatus('ok');
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { logger } from '@sentry/utils';
2+
import type { EmitterSubscription } from 'react-native';
3+
import { DeviceEventEmitter } from 'react-native';
4+
5+
import { NewFrameEventName } from './sentryeventemitter';
6+
7+
export type FallBackNewFrameEvent = { newFrameTimestampInSeconds: number, isFallback?: boolean };
8+
export interface SentryEventEmitterFallback {
9+
/**
10+
* Initializes the fallback event emitter
11+
* This method is synchronous in JS but the event emitter starts asynchronously
12+
* https://github.com/facebook/react-native/blob/d09c02f9e2d468e4d0bde51890e312ae7003a3e6/packages/react-native/React/Modules/RCTEventEmitter.m#L95
13+
*/
14+
initAsync: () => void;
15+
closeAllAsync: () => void;
16+
startListenerAsync: () => void;
17+
}
18+
19+
function timeNowNanosecond(): number {
20+
return Date.now() / 1000; // Convert to nanoseconds
21+
}
22+
23+
/**
24+
* Creates emitter that allows to listen to UI Frame events when ready.
25+
*/
26+
export function createSentryFallbackEventEmitter(): SentryEventEmitterFallback {
27+
let NativeEmitterCalled: boolean = false;
28+
let subscription: EmitterSubscription | undefined = undefined;
29+
let isListening = false;
30+
return {
31+
initAsync() {
32+
33+
subscription = DeviceEventEmitter.addListener(NewFrameEventName, () => {
34+
// Avoid noise from pages that we do not want to track.
35+
if (isListening) {
36+
NativeEmitterCalled = true;
37+
}
38+
});
39+
},
40+
41+
startListenerAsync() {
42+
isListening = true;
43+
44+
// Schedule the callback to be executed when all UI Frames have flushed.
45+
requestAnimationFrame(() => {
46+
if (NativeEmitterCalled) {
47+
NativeEmitterCalled = false;
48+
isListening = false;
49+
return;
50+
}
51+
const timestampInSeconds = timeNowNanosecond();
52+
const maxRetries = 3;
53+
let retries = 0;
54+
55+
const retryCheck = (): void => {
56+
if (NativeEmitterCalled) {
57+
NativeEmitterCalled = false;
58+
isListening = false;
59+
return; // Native Repplied the bridge with a given timestamp.
60+
}
61+
62+
retries++;
63+
if (retries < maxRetries) {
64+
setTimeout(retryCheck, 1_000);
65+
} else {
66+
logger.log('Native timestamp did not reply in time, using fallback.');
67+
isListening = false;
68+
DeviceEventEmitter.emit(NewFrameEventName, { newFrameTimestampInSeconds: timestampInSeconds, isFallback: true });
69+
}
70+
};
71+
72+
// Start the retry process
73+
retryCheck();
74+
75+
});
76+
},
77+
78+
closeAllAsync() {
79+
subscription?.remove();
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)