Skip to content

Commit 57320c7

Browse files
antonisclaude
andcommitted
feat(tracing): Emit app.vitals.start.screen on standalone app start (RN-691)
Set the `app.vitals.start.screen` attribute on the standalone `app.start` transaction from the current route tracked by the tracing integration. Unlike the non-standalone `ui.load` transaction (named after the screen, which Relay backfills from), the standalone transaction is named `App Start`, so the screen is set explicitly. Omitted when no route has been registered yet at capture time. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1e78698 commit 57320c7

3 files changed

Lines changed: 40 additions & 0 deletions

File tree

packages/core/src/js/tracing/integrations/appStart.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ import {
3131
UI_LOAD as UI_LOAD_OP,
3232
} from '../ops';
3333
import { SPAN_ORIGIN_AUTO_APP_START, SPAN_ORIGIN_MANUAL_APP_START } from '../origin';
34+
import { getCurrentReactNativeTracingIntegration } from '../reactnativetracing';
3435
import {
36+
SEMANTIC_ATTRIBUTE_APP_VITALS_START_SCREEN,
3537
SEMANTIC_ATTRIBUTE_APP_VITALS_START_TYPE,
3638
SEMANTIC_ATTRIBUTE_APP_VITALS_START_VALUE,
3739
SEMANTIC_ATTRIBUTE_SENTRY_OP,
@@ -659,6 +661,15 @@ export const appStartIntegration = ({
659661
event.contexts.trace.data[SEMANTIC_ATTRIBUTE_APP_VITALS_START_VALUE] = appStartDurationMs;
660662
event.contexts.trace.data[SEMANTIC_ATTRIBUTE_APP_VITALS_START_TYPE] = appStart.type;
661663

664+
// Screen shown when app start completes. Unlike the non-standalone `ui.load` transaction
665+
// (whose name is the screen, which Relay backfills from), the standalone transaction is named
666+
// `App Start`, so we set the screen explicitly. Sourced from the current route tracked by the
667+
// tracing integration; omitted when no route has been registered yet at capture time.
668+
const screen = getCurrentReactNativeTracingIntegration()?.state.currentRoute;
669+
if (screen) {
670+
event.contexts.trace.data[SEMANTIC_ATTRIBUTE_APP_VITALS_START_SCREEN] = screen;
671+
}
672+
662673
// Minimal parent referencing the root transaction span, so the breakdown spans attach
663674
// directly under it (the helpers only read op/origin/span_id/trace_id/start_timestamp).
664675
// `data` is shared with the root trace context so frame data lands on the root span.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ export const SEMANTIC_ATTRIBUTE_NAVIGATION_ACTION_TYPE = 'navigation.action_type
2222
// App start vitals (Span V2 / EAP). Emitted on the standalone `app.start` transaction.
2323
export const SEMANTIC_ATTRIBUTE_APP_VITALS_START_VALUE = 'app.vitals.start.value';
2424
export const SEMANTIC_ATTRIBUTE_APP_VITALS_START_TYPE = 'app.vitals.start.type';
25+
export const SEMANTIC_ATTRIBUTE_APP_VITALS_START_SCREEN = 'app.vitals.start.screen';

packages/core/test/tracing/integrations/appStart.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ import {
3636
setRootComponentCreationTimestampMs,
3737
} from '../../../src/js/tracing/integrations/appStart';
3838
import { SPAN_ORIGIN_AUTO_APP_START, SPAN_ORIGIN_MANUAL_APP_START } from '../../../src/js/tracing/origin';
39+
import * as ReactNativeTracing from '../../../src/js/tracing/reactnativetracing';
3940
import {
41+
SEMANTIC_ATTRIBUTE_APP_VITALS_START_SCREEN,
4042
SEMANTIC_ATTRIBUTE_APP_VITALS_START_TYPE,
4143
SEMANTIC_ATTRIBUTE_APP_VITALS_START_VALUE,
4244
} from '../../../src/js/tracing/semanticAttributes';
@@ -147,6 +149,32 @@ describe('App Start Integration', () => {
147149
});
148150
});
149151

152+
it('sets app.vitals.start.screen from the current route', async () => {
153+
const [timeOriginMilliseconds, appStartTimeMilliseconds] = mockAppStart({ cold: true });
154+
const screenSpy = jest
155+
.spyOn(ReactNativeTracing, 'getCurrentReactNativeTracingIntegration')
156+
.mockReturnValue({ state: { currentRoute: 'HomeScreen' } } as ReturnType<
157+
typeof ReactNativeTracing.getCurrentReactNativeTracingIntegration
158+
>);
159+
160+
try {
161+
const actualEvent = await captureStandAloneAppStart();
162+
expect(actualEvent).toEqual(
163+
expectEventWithStandaloneColdAppStart(actualEvent, { timeOriginMilliseconds, appStartTimeMilliseconds }),
164+
);
165+
expect(actualEvent?.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_APP_VITALS_START_SCREEN]).toBe('HomeScreen');
166+
} finally {
167+
screenSpy.mockRestore();
168+
}
169+
});
170+
171+
it('omits app.vitals.start.screen when no route has been registered', async () => {
172+
mockAppStart({ cold: true });
173+
174+
const actualEvent = await captureStandAloneAppStart();
175+
expect(actualEvent?.contexts?.trace?.data).not.toHaveProperty(SEMANTIC_ATTRIBUTE_APP_VITALS_START_SCREEN);
176+
});
177+
150178
it('Does not add any spans or measurements when App Start Span is longer than threshold', async () => {
151179
set__DEV__(false);
152180
mockTooLongAppStart();

0 commit comments

Comments
 (0)