Skip to content

Commit 315a163

Browse files
antonisclaude
andcommitted
fix(tracing): Fix orphaned TTID/TTFD spans in the trace view (#6432)
TTID/TTFD spans created in the TimeToDisplay integration passed `parent_span_id` but no `trace_id`, so `createSpanJSON` minted a random `uuid4()` trace_id. Sentry groups spans by `trace_id` first, so the spans rendered as orphaned top-level traces despite a correct `parent_span_id`. Thread the transaction's `trace_id` (from `event.contexts.trace.trace_id`) into all three `createSpanJSON` call sites, matching the pattern already used by the app start integration and `createChildSpanJSON`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 26843eb commit 315a163

4 files changed

Lines changed: 45 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212

1313
- Add `Sentry.reportFullyDisplayed()` imperative API for signaling Time to Full Display ([#6419](https://github.com/getsentry/sentry-react-native/pull/6419))
1414

15+
### Fixes
16+
17+
- Fix orphaned TTID/TTFD spans in the trace view ([#6437](https://github.com/getsentry/sentry-react-native/pull/6437))
18+
1519
## 8.18.0
1620

1721
### Features

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ export const timeToDisplayIntegration = (): Integration => {
4040
return event;
4141
}
4242

43+
const traceId = event.contexts?.trace?.trace_id;
44+
if (!traceId) {
45+
debug.warn(`[${INTEGRATION_NAME}] No trace id found in transaction.`);
46+
return event;
47+
}
48+
4349
const transactionStartTimestampSeconds = event.start_timestamp;
4450
if (!transactionStartTimestampSeconds) {
4551
// This should never happen
@@ -53,10 +59,17 @@ export const timeToDisplayIntegration = (): Integration => {
5359
const ttidSpan = await addTimeToInitialDisplay({
5460
event,
5561
rootSpanId,
62+
traceId,
5663
transactionStartTimestampSeconds,
5764
enableTimeToInitialDisplayForPreloadedRoutes,
5865
});
59-
const ttfdSpan = await addTimeToFullDisplay({ event, rootSpanId, transactionStartTimestampSeconds, ttidSpan });
66+
const ttfdSpan = await addTimeToFullDisplay({
67+
event,
68+
rootSpanId,
69+
traceId,
70+
transactionStartTimestampSeconds,
71+
ttidSpan,
72+
});
6073

6174
const ttidDurationMs =
6275
ttidSpan?.start_timestamp && ttidSpan?.timestamp
@@ -109,11 +122,13 @@ export const timeToDisplayIntegration = (): Integration => {
109122
async function addTimeToInitialDisplay({
110123
event,
111124
rootSpanId,
125+
traceId,
112126
transactionStartTimestampSeconds,
113127
enableTimeToInitialDisplayForPreloadedRoutes,
114128
}: {
115129
event: Event;
116130
rootSpanId: string;
131+
traceId: string;
117132
transactionStartTimestampSeconds: number;
118133
enableTimeToInitialDisplayForPreloadedRoutes: boolean;
119134
}): Promise<SpanJSON | undefined> {
@@ -133,6 +148,7 @@ async function addTimeToInitialDisplay({
133148
return addAutomaticTimeToInitialDisplay({
134149
event,
135150
rootSpanId,
151+
traceId,
136152
transactionStartTimestampSeconds,
137153
enableTimeToInitialDisplayForPreloadedRoutes,
138154
});
@@ -156,6 +172,7 @@ async function addTimeToInitialDisplay({
156172
timestamp: ttidEndTimestampSeconds,
157173
origin: SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY,
158174
parent_span_id: rootSpanId,
175+
trace_id: traceId,
159176
data: {
160177
[SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT,
161178
},
@@ -168,11 +185,13 @@ async function addTimeToInitialDisplay({
168185
async function addAutomaticTimeToInitialDisplay({
169186
event,
170187
rootSpanId,
188+
traceId,
171189
transactionStartTimestampSeconds,
172190
enableTimeToInitialDisplayForPreloadedRoutes,
173191
}: {
174192
event: Event;
175193
rootSpanId: string;
194+
traceId: string;
176195
transactionStartTimestampSeconds: number;
177196
enableTimeToInitialDisplayForPreloadedRoutes: boolean;
178197
}): Promise<SpanJSON | undefined> {
@@ -205,6 +224,7 @@ async function addAutomaticTimeToInitialDisplay({
205224
timestamp: ttidTimestampSeconds,
206225
origin: SPAN_ORIGIN_AUTO_UI_TIME_TO_DISPLAY,
207226
parent_span_id: rootSpanId,
227+
trace_id: traceId,
208228
data: {
209229
[SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT,
210230
},
@@ -217,11 +237,13 @@ async function addAutomaticTimeToInitialDisplay({
217237
async function addTimeToFullDisplay({
218238
event,
219239
rootSpanId,
240+
traceId,
220241
transactionStartTimestampSeconds,
221242
ttidSpan,
222243
}: {
223244
event: Event;
224245
rootSpanId: string;
246+
traceId: string;
225247
transactionStartTimestampSeconds: number;
226248
ttidSpan: SpanJSON | undefined;
227249
}): Promise<SpanJSON | undefined> {
@@ -271,6 +293,7 @@ async function addTimeToFullDisplay({
271293
timestamp: ttfdAdjustedEndTimestampSeconds,
272294
origin: SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY,
273295
parent_span_id: rootSpanId,
296+
trace_id: traceId,
274297
data: {
275298
[SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT,
276299
},

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,20 @@ describe('React Navigation - TTID', () => {
9898
);
9999
});
100100

101+
test('automatic ttid span inherits trace_id and parent_span_id from the transaction', () => {
102+
jest.runOnlyPendingTimers(); // Flush app start transaction
103+
104+
mockedNavigation.navigateToNewScreen();
105+
mockAutomaticTimeToDisplay();
106+
jest.runOnlyPendingTimers(); // Flush ttid transaction
107+
108+
const transaction = getLastTransaction(transportSendMock);
109+
const ttidSpan = transaction.spans?.find(s => s.op === 'ui.load.initial_display');
110+
expect(ttidSpan).toBeDefined();
111+
expect(ttidSpan?.trace_id).toBe(transaction.contexts?.trace?.trace_id);
112+
expect(ttidSpan?.parent_span_id).toBe(transaction.contexts?.trace?.span_id);
113+
});
114+
101115
test('should end ttid with measurements even when active span was removed from the scope', () => {
102116
jest.runOnlyPendingTimers(); // Flush app start transaction
103117

packages/core/test/tracing/timetodisplay.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ function expectFinishedInitialDisplaySpan(event: Event) {
438438
description: 'Time To Initial Display',
439439
op: 'ui.load.initial_display',
440440
parent_span_id: event.contexts.trace.span_id,
441+
trace_id: event.contexts.trace.trace_id,
441442
start_timestamp: event.start_timestamp,
442443
status: 'ok',
443444
timestamp: expect.any(Number),
@@ -456,6 +457,7 @@ function expectFinishedFullDisplaySpan(event: Event) {
456457
description: 'Time To Full Display',
457458
op: 'ui.load.full_display',
458459
parent_span_id: event.contexts.trace.span_id,
460+
trace_id: event.contexts.trace.trace_id,
459461
start_timestamp: event.start_timestamp,
460462
status: 'ok',
461463
timestamp: expect.any(Number),
@@ -474,6 +476,7 @@ function expectDeadlineExceededFullDisplaySpan(event: Event) {
474476
description: 'Time To Full Display',
475477
op: 'ui.load.full_display',
476478
parent_span_id: event.contexts.trace.span_id,
479+
trace_id: event.contexts.trace.trace_id,
477480
start_timestamp: event.start_timestamp,
478481
status: 'deadline_exceeded',
479482
timestamp: expect.any(Number),

0 commit comments

Comments
 (0)