Skip to content

Commit 84f41c3

Browse files
chargomeclaude
andcommitted
fix(nextjs): Apply the prerender trace guard to withSentry on Cloudflare
ISR pages revalidate inside a request of the Worker, where `withSentry` of `@sentry/nextjs/cloudflare` registers its own tracer provider and propagator. The guard only covered the setup of the Node SDK, so a shell prerendered in the Worker got that request's trace. The Next.js integration now marks prerenders on the tracer provider `@sentry/cloudflare` registered and its propagator skips a marked context. Also moves the changelog entry above the contributor line and fixes a test comment that pointed to a file of the follow-up PR. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 218b791 commit 84f41c3

6 files changed

Lines changed: 76 additions & 15 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
66

7+
- **feat(nextjs)**: Browser pageloads of apps with `cacheComponents` enabled now connect to the server request again. The SDK stopped emitting `sentry-trace` meta tags for these apps because a cached shell could replay the trace of the request that rendered it to every later visitor. The server SDK now refuses to hand out trace context while Next.js prerenders a document, so cached shells never carry a trace, and a tag that is in the document was always rendered for the current request. The guard is part of the tracer provider and propagator the SDK registers, so it does not cover apps that set up OpenTelemetry themselves. This also covers ISR pages, which no longer need the client-side meta tag removal based on `generateStaticParams`; the `isrRoutes` field of the injected route manifest is gone.
8+
79
Work in this release was contributed by @Philip-Nwabuwa. Thank you for your contribution!
810

911
## 11.6.0
@@ -87,8 +89,6 @@ Work in this release was contributed by @Philip-Nwabuwa. Thank you for your cont
8789

8890
Work in this release was contributed by @zkasuran. Thank you for your contribution!
8991

90-
- **feat(nextjs)**: Browser pageloads of apps with `cacheComponents` enabled now connect to the server request again. The SDK stopped emitting `sentry-trace` meta tags for these apps because a cached shell could replay the trace of the request that rendered it to every later visitor. The server SDK now refuses to hand out trace context while Next.js prerenders a document, so cached shells never carry a trace, and a tag that is in the document was always rendered for the current request. The guard is part of the tracer provider and propagator the SDK registers, so it does not cover apps that set up OpenTelemetry themselves. This also covers ISR pages, which no longer need the client-side meta tag removal based on `generateStaticParams`; the `isrRoutes` field of the injected route manifest is gone.
91-
9292
## 11.5.0
9393

9494
### Important Changes

‎packages/nextjs/src/cloudflare/index.ts‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,22 @@ import type { CloudflareOptions } from '@sentry/cloudflare';
55
import { withSentry as withSentryCloudflare } from '@sentry/cloudflare';
66
import type { Integration } from '@sentry/core';
77
import { applySdkMetadata, extractTraceparentData, getRootSpan, GLOBAL_OBJ, spanToJSON } from '@sentry/core';
8-
import {
9-
registerPrepareSpanScope,
10-
SentryPropagator,
11-
setOpenTelemetryContextAsyncContextStrategy,
12-
} from '@sentry/opentelemetry';
8+
import { registerPrepareSpanScope, setOpenTelemetryContextAsyncContextStrategy } from '@sentry/opentelemetry';
139
import { ATTR_NEXT_SPAN_TYPE } from '../common/nextSpanAttributes';
1410
import { isTunnelRouteSpan } from '../common/utils/dropMiddlewareTunnelRequests';
1511
import { dropReactControlFlowErrorsEventProcessor } from '../common/utils/dropReactControlFlowErrors';
1612
import { markAsyncContextOwnedByNextjsCloudflare } from '../common/utils/responseEnd';
13+
import {
14+
getGlobalTracerProvider,
15+
markPrerendersOnTracerProvider,
16+
NextSentryPropagator,
17+
} from '../server/nextSentryPropagator';
1718
import { addNextjsServerSpanHooks, NEXTJS_SERVER_IGNORE_SPANS } from '../server/serverSpanHooks';
1819
import { nextjsUseCacheIntegration } from '../server/useCacheInstrumentation';
1920

2021
export * from '@sentry/cloudflare';
2122

22-
class NextjsCloudflarePropagator extends SentryPropagator {
23+
class NextjsCloudflarePropagator extends NextSentryPropagator {
2324
/** @inheritDoc */
2425
public extract(ctx: Context, carrier: unknown, getter: TextMapGetter): Context {
2526
// Next.js extracts the request headers again from the root context when it misses its router server context, e.g.
@@ -43,6 +44,9 @@ const nextjsIntegration = (): Integration => ({
4344
name: 'Nextjs',
4445
setup: client => {
4546
if ((client.getOptions() as CloudflareOptions).enableOpenTelemetrySetup) {
47+
// ISR pages revalidate inside a request of the Worker, so the prerender guard has to cover the tracer
48+
// provider `@sentry/cloudflare` registered as well.
49+
markPrerendersOnTracerProvider(getGlobalTracerProvider());
4650
propagation.setGlobalPropagator(new NextjsCloudflarePropagator());
4751
}
4852
client.getOptions().release ??= (GLOBAL_OBJ as { _sentryRelease?: string })._sentryRelease;

‎packages/nextjs/src/server/index.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,9 @@ export function init(options: NodeOptions): NodeClient | undefined {
184184
}
185185

186186
// Next.js reads the trace meta tags of a document through the global propagator. Only the setup the
187-
// Node SDK owns gets the Next.js-aware one; another OpenTelemetry setup (for example `@vercel/otel`)
188-
// keeps its own tracer provider and propagator.
187+
// Node SDK owns gets the Next.js-aware one here; `withSentry` of `@sentry/nextjs/cloudflare` applies it
188+
// to its own setup, and another OpenTelemetry setup (for example `@vercel/otel`) keeps its own tracer
189+
// provider and propagator.
189190
if (client?.traceProvider) {
190191
registerNextSentryPropagator(client.traceProvider);
191192
}

‎packages/nextjs/src/server/nextSentryPropagator.ts‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,18 @@ export class NextSentryPropagator extends SentryPropagator {
8282
}
8383
}
8484

85+
const markedProviders = new WeakSet<TracerProvider>();
86+
8587
/**
86-
* Makes the tracer provider the Node SDK registered mark prerenders on the OpenTelemetry context, and
87-
* replaces the propagator it registered with one that hands out nothing on a marked context.
88+
* Makes a tracer provider mark prerenders on the OpenTelemetry context, by wrapping the tracers it hands
89+
* out. Safe to call repeatedly with the same provider, which the per-request `init` on Cloudflare does.
8890
*/
89-
export function registerNextSentryPropagator(provider: TracerProvider): void {
91+
export function markPrerendersOnTracerProvider(provider: TracerProvider): void {
92+
if (markedProviders.has(provider)) {
93+
return;
94+
}
95+
markedProviders.add(provider);
96+
9097
const wrappedTracers = new WeakMap<Tracer, Tracer>();
9198
const getTracer = provider.getTracer.bind(provider);
9299
provider.getTracer = (...args) => {
@@ -98,6 +105,22 @@ export function registerNextSentryPropagator(provider: TracerProvider): void {
98105
}
99106
return wrapped;
100107
};
108+
}
109+
110+
/**
111+
* The tracer provider behind the global OpenTelemetry API, which is a proxy around the registered one.
112+
*/
113+
export function getGlobalTracerProvider(): TracerProvider {
114+
const provider = trace.getTracerProvider();
115+
return 'getDelegate' in provider ? (provider as { getDelegate(): TracerProvider }).getDelegate() : provider;
116+
}
117+
118+
/**
119+
* Makes the tracer provider the Node SDK registered mark prerenders on the OpenTelemetry context, and
120+
* replaces the propagator it registered with one that hands out nothing on a marked context.
121+
*/
122+
export function registerNextSentryPropagator(provider: TracerProvider): void {
123+
markPrerendersOnTracerProvider(provider);
101124

102125
// The OpenTelemetry API refuses a second global propagator, so the one from the Node SDK setup has
103126
// to be unregistered first.

‎packages/nextjs/test/cloudflare/withSentry.test.ts‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from '@sentry/core';
1717
import { afterEach, describe, expect, it, vi } from 'vitest';
1818
import { withSentry } from '../../src/cloudflare';
19-
import { ATTR_NEXT_SPAN_TYPE } from '../../src/common/nextSpanAttributes';
19+
import { ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../../src/common/nextSpanAttributes';
2020
import { init as initEdge } from '../../src/edge';
2121
import { init as initServer } from '../../src/server';
2222
import { NEXTJS_SERVER_IGNORE_SPANS } from '../../src/server/serverSpanHooks';
@@ -257,6 +257,39 @@ describe('withSentry', () => {
257257
expect(carrier['sentry-trace']).toBe(`${activeSpan?.spanContext().traceId}-${activeSpan?.spanContext().spanId}-1`);
258258
});
259259

260+
it('injects nothing while Next.js prerenders a page in a request', async () => {
261+
const carrier: Record<string, string> = {};
262+
const requestCarrier: Record<string, string> = {};
263+
const handler = withSentry(() => ({ dsn: DSN, tracesSampleRate: 1 }), {
264+
fetch: () => {
265+
trace.getTracer('next.js').startActiveSpan(
266+
'prerender route (app) /isr',
267+
{
268+
attributes: {
269+
[ATTR_NEXT_SPAN_TYPE]: 'AppRender.getBodyResult',
270+
[ATTR_NEXT_SPAN_NAME]: 'prerender route (app) /isr',
271+
},
272+
},
273+
span => {
274+
propagation.inject(context.active(), carrier);
275+
span.end();
276+
},
277+
);
278+
propagation.inject(context.active(), requestCarrier);
279+
return new Response('ok');
280+
},
281+
});
282+
283+
await handler.fetch?.(new Request('https://example.com/') as never, {}, {
284+
waitUntil: vi.fn(),
285+
passThroughOnException: vi.fn(),
286+
props: {},
287+
} as never);
288+
289+
expect(carrier).toEqual({});
290+
expect(requestCarrier['sentry-trace']).toMatch(/^[0-9a-f]{32}-[0-9a-f]{16}-1$/);
291+
});
292+
260293
it('does not set the global propagator when the callback turns off the OpenTelemetry setup', async () => {
261294
const setGlobalPropagator = vi.spyOn(propagation, 'setGlobalPropagator');
262295
const handler = withSentry(() => ({ dsn: DSN, enableOpenTelemetrySetup: false }), {

‎packages/nextjs/test/config/maybeSetClientTraceMetadataOption.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe('maybeSetClientTraceMetadataOption', () => {
1616
});
1717

1818
it('enables trace meta tags when Cache Components is enabled', () => {
19-
// The client decides per tag whether it may be continued, see `cacheComponentsTraceMetaTags.ts`.
19+
// The server withholds the tags while it prerenders, see `nextSentryPropagator.ts`.
2020
const config: NextConfigObject = { cacheComponents: true };
2121
maybeSetClientTraceMetadataOption(config, '16.0.0');
2222
expect(config.experimental?.clientTraceMetadata).toEqual(['baggage', 'sentry-trace']);

0 commit comments

Comments
 (0)