Skip to content

Commit 7dca19f

Browse files
feat(ttid): Add support for measuring Time to Initial Display for already seen routes (#4661)
1 parent 8f0282e commit 7dca19f

File tree

4 files changed

+81
-7
lines changed

4 files changed

+81
-7
lines changed

CHANGELOG.md

+9
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@
1313
- Add thread information to spans ([#4579](https://github.com/getsentry/sentry-react-native/pull/4579))
1414
- Exposed `getDataFromUri` as a public API to retrieve data from a URI ([#4638](https://github.com/getsentry/sentry-react-native/pull/4638))
1515
- Improve Warm App Start reporting on Android ([#4641](https://github.com/getsentry/sentry-react-native/pull/4641))
16+
- Add support for measuring Time to Initial Display for already seen routes ([#4661](https://github.com/getsentry/sentry-react-native/pull/4661))
17+
- Introduce `enableTimeToInitialDisplayForPreloadedRoutes` option to the React Navigation integration.
18+
19+
```js
20+
Sentry.reactNavigationIntegration({
21+
enableTimeToInitialDisplayForPreloadedRoutes: true,
22+
});
23+
```
24+
1625
- Add experimental flags `enableExperimentalViewRenderer` and `enableFastViewRendering` to enable up to 5x times more performance in Session Replay on iOS ([#4660](https://github.com/getsentry/sentry-react-native/pull/4660))
1726

1827
```js

packages/core/src/js/tracing/reactnavigation.ts

+17-5
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ interface ReactNavigationIntegrationOptions {
6262
* @default true
6363
*/
6464
ignoreEmptyBackNavigationTransactions: boolean;
65+
66+
/**
67+
* Enabled measuring Time to Initial Display for routes that are already loaded in memory.
68+
* (a.k.a., Routes that the navigation integration has already seen.)
69+
*
70+
* @default false
71+
*/
72+
enableTimeToInitialDisplayForPreloadedRoutes: boolean;
6573
}
6674

6775
/**
@@ -76,6 +84,7 @@ export const reactNavigationIntegration = ({
7684
routeChangeTimeoutMs = 1_000,
7785
enableTimeToInitialDisplay = false,
7886
ignoreEmptyBackNavigationTransactions = true,
87+
enableTimeToInitialDisplayForPreloadedRoutes = false,
7988
}: Partial<ReactNavigationIntegrationOptions> = {}): Integration & {
8089
/**
8190
* Pass the ref to the navigation container to register it to the instrumentation
@@ -268,16 +277,19 @@ export const reactNavigationIntegration = ({
268277
}
269278

270279
const routeHasBeenSeen = recentRouteKeys.includes(route.key);
271-
const latestTtidSpan =
272-
!routeHasBeenSeen &&
273-
enableTimeToInitialDisplay &&
274-
startTimeToInitialDisplaySpan({
280+
const startTtidForNewRoute = enableTimeToInitialDisplay && !routeHasBeenSeen;
281+
const startTtidForAllRoutes = enableTimeToInitialDisplay && enableTimeToInitialDisplayForPreloadedRoutes;
282+
283+
let latestTtidSpan: Span | undefined = undefined;
284+
if (startTtidForNewRoute || startTtidForAllRoutes) {
285+
latestTtidSpan = startTimeToInitialDisplaySpan({
275286
name: `${route.name} initial display`,
276287
isAutoInstrumented: true,
277288
});
289+
}
278290

279291
const navigationSpanWithTtid = latestNavigationSpan;
280-
if (!routeHasBeenSeen && latestTtidSpan) {
292+
if (latestTtidSpan) {
281293
newScreenFrameEventEmitter?.onceNewFrame(({ newFrameTimestampInSeconds }: NewFrameEvent) => {
282294
const activeSpan = getActiveSpan();
283295
if (activeSpan && manualInitialDisplaySpans.has(activeSpan)) {

packages/core/test/tracing/reactnavigation.ttid.test.tsx

+54-2
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,55 @@ describe('React Navigation - TTID', () => {
589589
});
590590
});
591591

592+
describe('ttid for preloaded/seen routes', () => {
593+
beforeEach(() => {
594+
jest.useFakeTimers();
595+
(notWeb as jest.Mock).mockReturnValue(true);
596+
(isHermesEnabled as jest.Mock).mockReturnValue(true);
597+
});
598+
599+
afterEach(() => {
600+
jest.useRealTimers();
601+
});
602+
603+
it('should add ttid span and measurement for already seen route', () => {
604+
const sut = createTestedInstrumentation({
605+
enableTimeToInitialDisplay: true,
606+
ignoreEmptyBackNavigationTransactions: false,
607+
enableTimeToInitialDisplayForPreloadedRoutes: true,
608+
});
609+
transportSendMock = initSentry(sut).transportSendMock;
610+
611+
mockedNavigation = createMockNavigationAndAttachTo(sut);
612+
613+
jest.runOnlyPendingTimers(); // Flush app start transaction
614+
mockedNavigation.navigateToNewScreen();
615+
jest.runOnlyPendingTimers(); // Flush navigation transaction
616+
mockedNavigation.navigateToInitialScreen();
617+
mockedEventEmitter.emitNewFrameEvent();
618+
jest.runOnlyPendingTimers(); // Flush navigation transaction
619+
620+
const transaction = getLastTransaction(transportSendMock);
621+
expect(transaction).toEqual(
622+
expect.objectContaining<TransactionEvent>({
623+
type: 'transaction',
624+
spans: expect.arrayContaining([
625+
expect.objectContaining<Partial<SpanJSON>>({
626+
op: 'ui.load.initial_display',
627+
description: 'Initial Screen initial display',
628+
}),
629+
]),
630+
measurements: expect.objectContaining<Required<TransactionEvent>['measurements']>({
631+
time_to_initial_display: {
632+
value: expect.any(Number),
633+
unit: 'millisecond',
634+
},
635+
}),
636+
}),
637+
);
638+
});
639+
});
640+
592641
function getSpanDurationMs(transaction: TransactionEvent, op: string): number | undefined {
593642
const ttidSpan = transaction.spans?.find(span => span.op === op);
594643
if (!ttidSpan) {
@@ -603,10 +652,13 @@ describe('React Navigation - TTID', () => {
603652
return (spanJSON.timestamp - spanJSON.start_timestamp) * 1000;
604653
}
605654

606-
function createTestedInstrumentation(options?: { enableTimeToInitialDisplay?: boolean }) {
655+
function createTestedInstrumentation(options?: {
656+
enableTimeToInitialDisplay?: boolean
657+
enableTimeToInitialDisplayForPreloadedRoutes?: boolean
658+
ignoreEmptyBackNavigationTransactions?: boolean
659+
}) {
607660
const sut = Sentry.reactNavigationIntegration({
608661
...options,
609-
ignoreEmptyBackNavigationTransactions: true, // default true
610662
});
611663
return sut;
612664
}

samples/react-native/src/App.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const reactNavigationIntegration = Sentry.reactNavigationIntegration({
5353
routeChangeTimeoutMs: 500, // How long it will wait for the route change to complete. Default is 1000ms
5454
enableTimeToInitialDisplay: isMobileOs,
5555
ignoreEmptyBackNavigationTransactions: true,
56+
enableTimeToInitialDisplayForPreloadedRoutes: true,
5657
});
5758

5859
Sentry.init({

0 commit comments

Comments
 (0)