Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ttid): Add support for measuring Time to Initial Display for already seen routes #4661

Merged
merged 3 commits into from
Mar 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@
- Add thread information to spans ([#4579](https://github.com/getsentry/sentry-react-native/pull/4579))
- Exposed `getDataFromUri` as a public API to retrieve data from a URI ([#4638](https://github.com/getsentry/sentry-react-native/pull/4638))
- Improve Warm App Start reporting on Android ([#4641](https://github.com/getsentry/sentry-react-native/pull/4641))
- Add support for measuring Time to Initial Display for already seen routes ([#4661](https://github.com/getsentry/sentry-react-native/pull/4661))
- Introduce `enableTimeToInitialDisplayForPreloadedRoutes` option to the React Navigation integration.

```js
Sentry.reactNavigationIntegration({
enableTimeToInitialDisplayForPreloadedRoutes: true,
});
```

- 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))

```js
Expand Down
22 changes: 17 additions & 5 deletions packages/core/src/js/tracing/reactnavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ interface ReactNavigationIntegrationOptions {
* @default true
*/
ignoreEmptyBackNavigationTransactions: boolean;

/**
* Enabled measuring Time to Initial Display for routes that are already loaded in memory.
* (a.k.a., Routes that the navigation integration has already seen.)
*
* @default false
*/
enableTimeToInitialDisplayForPreloadedRoutes: boolean;
}

/**
Expand All @@ -76,6 +84,7 @@ export const reactNavigationIntegration = ({
routeChangeTimeoutMs = 1_000,
enableTimeToInitialDisplay = false,
ignoreEmptyBackNavigationTransactions = true,
enableTimeToInitialDisplayForPreloadedRoutes = false,
}: Partial<ReactNavigationIntegrationOptions> = {}): Integration & {
/**
* Pass the ref to the navigation container to register it to the instrumentation
Expand Down Expand Up @@ -268,16 +277,19 @@ export const reactNavigationIntegration = ({
}

const routeHasBeenSeen = recentRouteKeys.includes(route.key);
const latestTtidSpan =
!routeHasBeenSeen &&
enableTimeToInitialDisplay &&
startTimeToInitialDisplaySpan({
const startTtidForNewRoute = enableTimeToInitialDisplay && !routeHasBeenSeen;
const startTtidForAllRoutes = enableTimeToInitialDisplay && enableTimeToInitialDisplayForPreloadedRoutes;

let latestTtidSpan: Span | undefined = undefined;
if (startTtidForNewRoute || startTtidForAllRoutes) {
latestTtidSpan = startTimeToInitialDisplaySpan({
name: `${route.name} initial display`,
isAutoInstrumented: true,
});
}

const navigationSpanWithTtid = latestNavigationSpan;
if (!routeHasBeenSeen && latestTtidSpan) {
if (latestTtidSpan) {
newScreenFrameEventEmitter?.onceNewFrame(({ newFrameTimestampInSeconds }: NewFrameEvent) => {
const activeSpan = getActiveSpan();
if (activeSpan && manualInitialDisplaySpans.has(activeSpan)) {
Expand Down
56 changes: 54 additions & 2 deletions packages/core/test/tracing/reactnavigation.ttid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,55 @@ describe('React Navigation - TTID', () => {
});
});

describe('ttid for preloaded/seen routes', () => {
beforeEach(() => {
jest.useFakeTimers();
(notWeb as jest.Mock).mockReturnValue(true);
(isHermesEnabled as jest.Mock).mockReturnValue(true);
});

afterEach(() => {
jest.useRealTimers();
});

it('should add ttid span and measurement for already seen route', () => {
const sut = createTestedInstrumentation({
enableTimeToInitialDisplay: true,
ignoreEmptyBackNavigationTransactions: false,
enableTimeToInitialDisplayForPreloadedRoutes: true,
});
transportSendMock = initSentry(sut).transportSendMock;

mockedNavigation = createMockNavigationAndAttachTo(sut);

jest.runOnlyPendingTimers(); // Flush app start transaction
mockedNavigation.navigateToNewScreen();
jest.runOnlyPendingTimers(); // Flush navigation transaction
mockedNavigation.navigateToInitialScreen();
mockedEventEmitter.emitNewFrameEvent();
jest.runOnlyPendingTimers(); // Flush navigation transaction

const transaction = getLastTransaction(transportSendMock);
expect(transaction).toEqual(
expect.objectContaining<TransactionEvent>({
type: 'transaction',
spans: expect.arrayContaining([
expect.objectContaining<Partial<SpanJSON>>({
op: 'ui.load.initial_display',
description: 'Initial Screen initial display',
}),
]),
measurements: expect.objectContaining<Required<TransactionEvent>['measurements']>({
time_to_initial_display: {
value: expect.any(Number),
unit: 'millisecond',
},
}),
}),
);
});
});

function getSpanDurationMs(transaction: TransactionEvent, op: string): number | undefined {
const ttidSpan = transaction.spans?.find(span => span.op === op);
if (!ttidSpan) {
Expand All @@ -603,10 +652,13 @@ describe('React Navigation - TTID', () => {
return (spanJSON.timestamp - spanJSON.start_timestamp) * 1000;
}

function createTestedInstrumentation(options?: { enableTimeToInitialDisplay?: boolean }) {
function createTestedInstrumentation(options?: {
enableTimeToInitialDisplay?: boolean
enableTimeToInitialDisplayForPreloadedRoutes?: boolean
ignoreEmptyBackNavigationTransactions?: boolean
}) {
const sut = Sentry.reactNavigationIntegration({
...options,
ignoreEmptyBackNavigationTransactions: true, // default true
});
return sut;
}
Expand Down
1 change: 1 addition & 0 deletions samples/react-native/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const reactNavigationIntegration = Sentry.reactNavigationIntegration({
routeChangeTimeoutMs: 500, // How long it will wait for the route change to complete. Default is 1000ms
enableTimeToInitialDisplay: isMobileOs,
ignoreEmptyBackNavigationTransactions: true,
enableTimeToInitialDisplayForPreloadedRoutes: true,
});

Sentry.init({
Expand Down
Loading