Skip to content

Commit b50a192

Browse files
committed
Fix race condition and improve navigation state management
1 parent b1cc849 commit b50a192

File tree

4 files changed

+49
-37
lines changed

4 files changed

+49
-37
lines changed

packages/remix/src/client/performance.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import * as React from 'react';
1414
import { DEBUG_BUILD } from '../utils/debug-build';
1515
import { hasManifest, maybeParameterizeRemixRoute } from './remixRouteParameterization';
1616
import {
17+
clearNavigationTraceCache,
1718
getMetaTagTraceContext,
1819
getNavigationTraceContext,
1920
getNavigationTraceContextAsync,
@@ -93,6 +94,14 @@ function getTransactionNameAndSource(
9394
// Flag to prevent async callback from creating a span after navigation has occurred
9495
let pageloadSpanStarted = false;
9596

97+
/**
98+
* Resets navigation state and trace cache when starting a new navigation.
99+
*/
100+
function resetNavigationState(): void {
101+
clearNavigationTraceCache();
102+
pageloadSpanStarted = true;
103+
}
104+
96105
export function startPageloadSpan(client: Client): void {
97106
const initPathName = getInitPathName();
98107

@@ -230,8 +239,8 @@ export function withSentry<P extends Record<string, unknown>, R extends React.Co
230239
}
231240

232241
if (_instrumentNavigation && matches?.length) {
233-
// Mark pageload as started to prevent async callback from firing after navigation
234-
pageloadSpanStarted = true;
242+
// Reset navigation state when starting a new navigation
243+
resetNavigationState();
235244

236245
if (activeRootSpan) {
237246
activeRootSpan.end();

packages/remix/src/client/serverTimingTracePropagation.ts

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ function parseServerTimingTrace(serverTiming: readonly PerformanceServerTiming[]
7272

7373
const traceparentData = extractTraceparentData(sentryTrace);
7474
if (!traceparentData?.traceId || !traceparentData?.parentSpanId) {
75-
DEBUG_BUILD && debug.warn('[Server-Timing] Invalid sentry-trace format:', sentryTrace);
75+
DEBUG_BUILD && debug.warn('Invalid sentry-trace format:', sentryTrace);
7676
return null;
7777
}
7878

@@ -117,17 +117,15 @@ function tryGetNavigationTraceContext(): NavigationTraceResult {
117117
}
118118

119119
/**
120-
* Get trace context from the initial navigation (page load).
121-
* Reads the Server-Timing header from the navigation performance entry.
122-
* Results are cached after first successful retrieval.
120+
* Get trace context from Server-Timing header synchronously. Results are cached.
123121
*/
124122
export function getNavigationTraceContext(): ServerTimingTraceContext | null {
125123
if (navigationTraceCache !== undefined) {
126124
return navigationTraceCache;
127125
}
128126

129127
if (!isServerTimingSupported()) {
130-
DEBUG_BUILD && debug.log('[Server-Timing] Server-Timing API not supported');
128+
DEBUG_BUILD && debug.log('Server-Timing API not supported');
131129
navigationTraceCache = null;
132130
return null;
133131
}
@@ -147,10 +145,8 @@ export function getNavigationTraceContext(): ServerTimingTraceContext | null {
147145
}
148146

149147
/**
150-
* Get trace context from navigation with retry mechanism.
151-
* Useful during SDK init when browser may not have finished processing headers.
152-
*
153-
* @returns Cleanup function to cancel pending retries (e.g., on navigation)
148+
* Get trace context from Server-Timing header with retry mechanism for early SDK initialization.
149+
* Returns a cleanup function to cancel pending retries.
154150
*/
155151
export function getNavigationTraceContextAsync(
156152
callback: (trace: ServerTimingTraceContext | null) => void,
@@ -167,7 +163,7 @@ export function getNavigationTraceContextAsync(
167163
}
168164

169165
if (!isServerTimingSupported()) {
170-
DEBUG_BUILD && debug.log('[Server-Timing] Server-Timing API not supported');
166+
DEBUG_BUILD && debug.log('Server-Timing API not supported');
171167
navigationTraceCache = null;
172168
callback(null);
173169
return () => {
@@ -187,21 +183,27 @@ export function getNavigationTraceContextAsync(
187183

188184
switch (result.status) {
189185
case 'unavailable':
190-
navigationTraceCache = null;
191-
callback(null);
186+
if (!state.cancelled) {
187+
navigationTraceCache = null;
188+
callback(null);
189+
}
192190
return;
193191
case 'pending':
194192
if (attempts < maxAttempts) {
195193
setTimeout(tryGet, delayMs);
196194
return;
197195
}
198-
DEBUG_BUILD && debug.warn('[Server-Timing] Max retry attempts reached, trace context unavailable');
199-
navigationTraceCache = null;
200-
callback(null);
196+
DEBUG_BUILD && debug.warn('Max retry attempts reached, trace context unavailable');
197+
if (!state.cancelled) {
198+
navigationTraceCache = null;
199+
callback(null);
200+
}
201201
return;
202202
case 'available':
203-
navigationTraceCache = result.data;
204-
callback(result.data);
203+
if (!state.cancelled) {
204+
navigationTraceCache = result.data;
205+
callback(result.data);
206+
}
205207
}
206208
};
207209

@@ -213,8 +215,7 @@ export function getNavigationTraceContextAsync(
213215
}
214216

215217
/**
216-
* Get trace context from meta tags.
217-
* Looks for `<meta name="sentry-trace">` and `<meta name="baggage">` tags.
218+
* Get trace context from meta tags as a fallback for browsers without Server-Timing support.
218219
*/
219220
export function getMetaTagTraceContext(): ServerTimingTraceContext | null {
220221
if (typeof WINDOW === 'undefined' || !WINDOW.document) {
@@ -233,7 +234,7 @@ export function getMetaTagTraceContext(): ServerTimingTraceContext | null {
233234

234235
const traceparentData = extractTraceparentData(sentryTrace);
235236
if (!traceparentData?.traceId || !traceparentData?.parentSpanId) {
236-
DEBUG_BUILD && debug.warn('[Server-Timing] Invalid sentry-trace format in meta tag:', sentryTrace);
237+
DEBUG_BUILD && debug.warn('Invalid sentry-trace format in meta tag:', sentryTrace);
237238
return null;
238239
}
239240

@@ -246,7 +247,10 @@ export function getMetaTagTraceContext(): ServerTimingTraceContext | null {
246247
}
247248
}
248249

249-
/** @internal */
250+
/**
251+
* Resets the navigation trace cache for fresh retrieval.
252+
* @internal
253+
*/
250254
export function clearNavigationTraceCache(): void {
251255
navigationTraceCache = undefined;
252256
}

packages/remix/src/server/instrumentServer.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,9 @@ export function wrapHandleErrorWithSentry(
103103
}
104104

105105
/**
106-
* Get trace context for injection into loader response data (for meta tags).
107-
* Returns empty object when Server-Timing headers are available, as they take priority.
108-
* Only provides trace data for meta tags as a fallback mechanism.
106+
* Get trace context for meta tag injection. Returns empty object when Server-Timing
107+
* headers will be used (active span in Node.js/Cloudflare), as Server-Timing takes
108+
* priority over meta tags for trace propagation.
109109
*/
110110
function getTraceAndBaggage(): {
111111
sentryTrace?: string;
@@ -117,8 +117,9 @@ function getTraceAndBaggage(): {
117117
if (isNodeEnv() || isCloudflareEnv()) {
118118
const activeSpan = getActiveSpan();
119119
if (activeSpan) {
120-
// Server-Timing header will be available, skip meta tag injection
121-
DEBUG_BUILD && debug.log('[getTraceAndBaggage] Skipping meta tag injection - Server-Timing header will be used');
120+
// Active span exists - Server-Timing header will be injected by makeWrappedDocumentRequestFunction.
121+
// Return empty to avoid duplicate trace context in meta tags.
122+
DEBUG_BUILD && debug.log('Skipping meta tag injection - Server-Timing header will be used');
122123
return {};
123124
}
124125

@@ -130,15 +131,15 @@ function getTraceAndBaggage(): {
130131

131132
if (propagationContext.traceId && spanId) {
132133
const fallbackTrace = generateSentryTraceHeader(propagationContext.traceId, spanId, propagationContext.sampled);
133-
DEBUG_BUILD && debug.log('[getTraceAndBaggage] Using meta tags fallback - no active span for Server-Timing');
134+
DEBUG_BUILD && debug.log('Using meta tags fallback - no active span for Server-Timing');
134135

135136
return {
136137
sentryTrace: fallbackTrace,
137138
sentryBaggage: traceData.baggage,
138139
};
139140
}
140141

141-
DEBUG_BUILD && debug.log('[getTraceAndBaggage] No valid trace context available');
142+
DEBUG_BUILD && debug.log('No valid trace context available');
142143
return {};
143144
}
144145

packages/remix/src/server/serverTimingTracePropagation.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ export function isCloudflareEnv(): boolean {
3333
}
3434

3535
/**
36-
* Generate Server-Timing header value containing Sentry trace context.
37-
* Enables trace propagation from server to client via the Performance API.
36+
* Generate a Server-Timing header value containing Sentry trace context.
37+
* Called automatically by instrumented `handleDocumentRequest`.
3838
*/
3939
export function generateSentryServerTimingHeader(options: ServerTimingTraceOptions = {}): string | null {
4040
// Only generate on server environments
@@ -62,8 +62,6 @@ export function generateSentryServerTimingHeader(options: ServerTimingTraceOptio
6262
// Get DSC from span and ensure trace_id consistency
6363
const dsc = getDynamicSamplingContextFromSpan(span);
6464

65-
// Build baggage string, ensuring trace_id matches the span's trace_id
66-
// The DSC may have a different trace_id if it was frozen from an earlier context
6765
const baggageEntries: string[] = [];
6866
for (const [key, value] of Object.entries(dsc)) {
6967
if (value) {
@@ -98,7 +96,7 @@ export function generateSentryServerTimingHeader(options: ServerTimingTraceOptio
9896
}
9997

10098
/**
101-
* Merge Sentry Server-Timing with an existing Server-Timing header value.
99+
* Merge Sentry trace context with an existing Server-Timing header value.
102100
*/
103101
export function mergeSentryServerTimingHeader(
104102
existingHeader: string | null | undefined,
@@ -146,8 +144,8 @@ export function injectServerTimingHeaderValue(response: Response, serverTimingVa
146144
}
147145

148146
/**
149-
* Add Sentry trace context to Response headers via Server-Timing.
150-
* Returns a new Response with the header added.
147+
* Add Sentry trace context to a Response via the Server-Timing header.
148+
* Returns a new Response with the header added (original is not modified).
151149
*/
152150
export function addSentryServerTimingHeader(response: Response, options?: ServerTimingTraceOptions): Response {
153151
const sentryTiming = generateSentryServerTimingHeader(options);

0 commit comments

Comments
 (0)