Skip to content

feat(nextjs): Mark pageloads that were served from a cached shell - #25318

Draft
chargome wants to merge 26 commits into
developfrom
chargome/feat/nextjs-cached-shell-marker
Draft

chargome wants to merge 26 commits into
developfrom
chargome/feat/nextjs-cached-shell-marker

Conversation

@chargome

@chargome chargome commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Builds on #25142.

A visitor of a cached page (a cacheComponents shell 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-prerender meta tag with the sentry-trace value of the prerender instead of nothing. The client never continues it: it sets document.cached: true on the pageload and adds a cache_origin span link to the request that prerendered the page, both at span start so tracesSampler and ignoreSpans see them. Pageloads keep their own trace and sampling decision, so the two hard requirements of #25141 still hold. Pages from next build get 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.cached is not in @sentry/conventions yet and needs to be added there before this merges; cache_origin is already a documented sentry.link.type value and is what the cache lifecycle view in Sentry reads.

Refs JS-3886

🤖 Generated with Claude Code

chargome and others added 24 commits October 13, 2026 10:49
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>
@chargome chargome self-assigned this Oct 13, 2026
@linear-code

linear-code Bot commented Oct 13, 2026

Copy link
Copy Markdown

JS-3886

@chargome

Copy link
Copy Markdown
Member Author

bugbot run

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 29.83 kB - -
@sentry/browser - with treeshaking flags 27.96 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.87 kB - -
@sentry/browser (incl. Tracing) 52.02 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 52.02 kB - -
@sentry/browser (incl. Tracing, Profiling) 54.96 kB - -
@sentry/browser (incl. Tracing, Replay) 91.75 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 80.62 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 96.47 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 109.43 kB - -
@sentry/browser (incl. Feedback) 47.35 kB - -
@sentry/browser (incl. sendFeedback) 34.89 kB - -
@sentry/browser (incl. FeedbackAsync) 40 kB - -
@sentry/browser (incl. Metrics) 30.85 kB - -
@sentry/browser (incl. Logs) 31.13 kB - -
@sentry/browser (incl. Metrics & Logs) 31.78 kB - -
@sentry/react 31.64 kB - -
@sentry/react (incl. Tracing) 54.34 kB - -
@sentry/vue 37.98 kB - -
@sentry/vue (incl. Tracing) 54.95 kB - -
@sentry/svelte 29.86 kB - -
@sentry/remix (Remix 3 client bundle) 57.02 kB - -
CDN Bundle 31.56 kB - -
CDN Bundle (incl. Tracing) 52.52 kB - -
CDN Bundle (incl. Logs, Metrics) 33.77 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 54.46 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 74.7 kB - -
CDN Bundle (incl. Tracing, Replay) 90.19 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 92.11 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 96.34 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 98.28 kB - -
CDN Bundle - uncompressed 92.98 kB - -
CDN Bundle (incl. Tracing) - uncompressed 155.83 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 99.52 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 161.79 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 229.56 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 276 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 281.94 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 289.7 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 295.63 kB - -
@sentry/nextjs (client) 57.26 kB +0.9% +506 B 🔺
@sentry/sveltekit (client) 52.41 kB - -
@sentry/core/server 40.99 kB - -
@sentry/core/browser 13.81 kB - -
@sentry/node 151.96 kB - -
@sentry/node/import (ESM hook with diagnostics-channel injection) 83.78 kB - -
@sentry/node - without tracing 94.54 kB +0.01% +3 B 🔺
@sentry/node - without channel injection 130.14 kB +0.01% +2 B 🔺
@sentry/aws-serverless 102.69 kB +0.01% +7 B 🔺
@sentry/cloudflare (withSentry) - minified 210.38 kB - -
@sentry/cloudflare (withSentry) 521.61 kB - -
@sentry/nextjs/cloudflare (withSentry) - minified 229.49 kB +0.63% +1.43 kB 🔺

View base workflow run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/nextjs/src/client/routing/appRouterPageloadInstrumentation.ts Outdated
Comment thread packages/nextjs/test/client/appRouterRoutingInstrumentation.test.ts Outdated
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>
@chargome

Copy link
Copy Markdown
Member Author

bugbot run

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@chargome
chargome added this pull request to stack #25143 October 13, 2026 11:55
…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>
@chargome

Copy link
Copy Markdown
Member Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

Base automatically changed from chargome/feat/nextjs-deferred-pageload-start to develop October 20, 2026 15:37

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant