Repository navigation
Conversation
Under cacheComponents the SDK stopped enabling clientTraceMetadata, because a shell that Next.js prerenders inside one request is cached and served to later visitors, and a sentry-trace tag baked into it would replay that request's trace to all of them. ISR pages had the same problem, patched on the client by removing the tags on generateStaticParams routes. Next.js computes a document's trace tags inside the "prerender route (app)" span when it prerenders and outside of it when it renders for a request, including the resume of a shell. Register a SentryPropagator subclass that hands out no trace context while that span is active, so a prerendered shell never contains a trace and any tag that reaches the browser belongs to the current request. Only swap the propagator when the Node SDK registered its own, so a foreign OpenTelemetry setup stays untouched. Enable clientTraceMetadata again under cacheComponents and remove the client-side ISR heuristic together with the isrRoutes manifest field. The guard covers ISR shells as well, including revalidate routes without generateStaticParams. Refs #24944 Refs JS-3863 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The turbopack dev variant runs with TEST_ENV=dev-turbopack, which the development check did not match, so the tags-absent assertions ran against a dev render where the tags are fresh and kept. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Next.js computes a document's trace meta tags at the top of its prerender function, inside the span it wraps the prerender in. Recognizing that span by its attributes fails when the request is not sampled: the span is then non-recording and has no attributes, so the propagator still wrote a `sampled=0` tag into the cached shell, and every later visitor inherited the negative decision. A span nested in the prerender would have passed the check as well. Mark the prerender on the OpenTelemetry context instead, through the tracer provider the Node SDK registered, and have the propagator skip a marked context. The mark is inherited by everything that runs inside the prerender and exists whether or not the span records. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… caller Next.js starts its spans through `startActiveSpan`, which the tracer wrapper marks on the context. A span started through `startSpan` and set on a context by the caller takes a different path, so the wrapper remembers those spans and the propagator checks the span on the context too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…flare 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>
The ISR shells of the e2e apps come from the build, where the SDK does not run, so the tag assertions passed with or without the guard. A page with a one second window makes the server prerender it again inside a request. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…r file Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An app that sets up OpenTelemetry itself registers its own propagator, which writes the trace meta tags of a prerendered shell. It can now register `NextSentryPropagator` and call `markPrerendersOnTracerProvider` with its tracer provider to get the same guard as the SDK's own setup. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
vinext renders ISR pages for every request, so the trace tags are fresh there and kept, like on the dev server. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… the pageload A document that resumes a prerendered shell gets its sentry-trace tag with the resumed chunk, which lands in <body> after the SDK has usually already run. A pageload created at that point can only link the server trace. Continue whatever tag is in the document, last one wins, and when there is no tag, no Server-Timing trace and the document is still streaming, wait for the tag before creating the pageload span, bounded by DOMContentLoaded and a 10 second cap. The span's start is backdated to the time origin and its children come from the performance timeline, so it looks the same as before, only inside the server's trace. A client-side navigation during the wait starts the pageload with a fresh trace first, so the navigation is filed as its redirect instead of being the only record of the visit. Fixes #24944 Refs JS-3863 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…a navigation A pageload that waits for its trace meta tag can be settled by a navigation, and a `popstate` has already changed the location by then. The browser SDK reads the location when the span starts, so the pageload got the URL of the navigation. Capture the document URL when the instrumentation sets up and put it on the span and the request data instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ntions constants in the test Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Next.js routing instrumentation never flags a navigation as a redirect, so a navigation during the wait ends the pageload like any pageload it interrupts and starts its own trace, as it does today. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s on The wait exists for a document that resumes a prerendered shell, whose tag arrives with the resumed part. Without `cacheComponents` a tag is either in `<head>` from the start or never comes, so waiting only held the pageload back: fetches before the tag were not attached and the span ended earlier. The build now injects `_sentryCacheComponents`, and the client only waits when it is set. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n file Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A visitor of a cached page gets a pageload in its own trace with no server span, which looks like missing server instrumentation. While Next.js prerenders a document, the server now writes a `sentry-shell` meta tag with the trace of the prerender instead of nothing. The client never continues it: it sets `sentry.nextjs.cached_shell` on the pageload and adds a `cache_origin` span link to the request that generated the shell. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
bugbot run |
size-limit report 📦
|
The attribute and the link are part of the start options now, so `tracesSampler` and `ignoreSpans` see that a pageload came from a cached shell. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
bugbot run |
…ork neutral `document.cached` and `sentry-prerender` instead of a Next.js specific attribute and `sentry-shell`, so other frameworks with prerendering can set the same attribute. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 26177fa. Configure here.
| const prerenderMarker = readPrerenderMarker(); | ||
| const prerenderOptions = prerenderMarker ? cachedDocumentSpanOptions(prerenderMarker) : undefined; |
There was a problem hiding this comment.
This is where a mixed case can happen...say a shell was regenerated at runtime and the page has a dynamic hole. The document then has the sentry-prerender marker and a sentry-trace tag.
The pageload continues the server trace, and it also gets document.cached: true and a cache_origin link. Both are kind of true, but the document.cached attribute could differentiate between being a full cache or if it's just the shell (but see first comment regarding the attribute changes).
And we should also test this.
| export interface TraceMetaTagValues { | ||
| sentryTrace: string; | ||
| baggage: string | undefined; | ||
| } | ||
|
|
||
| /** Set on a pageload whose document was served from a cache, so no server render ran for the request. */ | ||
| export const DOCUMENT_CACHED_ATTRIBUTE = 'document.cached'; |
There was a problem hiding this comment.
document.cached reads like browser HTTP cache. Also, with the boolean we can't know if it's a fully or partially (with shells) cached page.
Maybe document.static_html: 'full' | 'partial'?
This would also fit other frameworks and we can differentiate between fully static pages and partial (Astro islands, Next.js holes).
And we should also add this to sentry conventions. Added a PR: getsentry/sentry-conventions#740
There was a problem hiding this comment.
Could we assert the marker here too? (also in the other tests)
await expect(page.locator('meta[name="sentry-prerender"]')).toHaveCount(1);| * to later visitors, so the client never continues it, it only marks the pageload as served from a | ||
| * cached document and links the request that generated it. | ||
| */ | ||
| export const PRERENDER_META_TAG_NAME = 'sentry-prerender'; |
There was a problem hiding this comment.
"prerender" already has a specified meaning in the browser API (document.prerendering - see docs).
What about sentry-cache-origin, to match the link type? The client will turn this it into a cache_origin link.
| // Both link the one request that prerendered the shell, which is neither visitor's own trace. | ||
| const linkOf = (links: unknown): { trace_id: string; attributes?: Record<string, unknown> } => { | ||
| const link = (links as Array<{ trace_id: string; attributes?: Record<string, unknown> }>)[0]; | ||
| expect(link?.attributes?.['sentry.link.type']).toBe('cache_origin'); |
There was a problem hiding this comment.
nit: We can use SENTRY_LINK_TYPE from @sentry/conventions/attributes (also in the streaming app copy)
Builds on #25142.
A visitor of a cached page (a
cacheComponentsshell generated on first visit, an ISR page) gets a pageload in its own trace with no server span. That is correct, but in Sentry it looks like missing server instrumentation.While Next.js prerenders a document, the server now writes a
sentry-prerendermeta tag with thesentry-tracevalue of the prerender instead of nothing. The client never continues it: it setsdocument.cached: trueon the pageload and adds acache_originspan link to the request that prerendered the page, both at span start sotracesSamplerandignoreSpanssee them. Pageloads keep their own trace and sampling decision, so the two hard requirements of #25141 still hold. Pages fromnext buildget no marker, since the SDK does not run during the build.The attribute and the tag are named without Next.js in them on purpose: other frameworks with prerendering (Nuxt, SvelteKit, Astro, React Router) can set the same attribute.
document.cachedis not in@sentry/conventionsyet and needs to be added there before this merges;cache_originis already a documentedsentry.link.typevalue and is what the cache lifecycle view in Sentry reads.Refs JS-3886
🤖 Generated with Claude Code