Skip to content

feat(nextjs): Never emit trace meta tags from a prerender - #25141

Merged
s1gr1d merged 19 commits into
developfrom
chargome/feat/nextjs-prerender-trace-guard
Oct 20, 2026
Merged

s1gr1d merged 19 commits into
developfrom
chargome/feat/nextjs-prerender-trace-guard

Conversation

@chargome

@chargome chargome commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Under cacheComponents the SDK did not enable experimental.clientTraceMetadata (#21141). A shell that Next.js prerenders during one request is cached and served to later visitors, and a sentry-trace tag inside it would attach all of them to that one request. ISR pages had the same problem, worked around on the client by removing the tags on routes with generateStaticParams (#18192).

Next.js computes the trace tags of a document inside the prerender route (app) span when it prerenders, and outside of it when it renders for a request, including when it resumes a shell. The server SDK now marks that span on the OpenTelemetry context, and its propagator hands out nothing on a marked context. A prerendered shell never contains a trace, so any tag that reaches the browser belongs to the current request. The mark sits on the context rather than the span because an unsampled request makes the span non-recording, with no attributes to recognize it by, and a sampled=0 tag in the shell would drop every later pageload. Checked in Next.js 15.0 through 16.4.

With that, clientTraceMetadata is enabled again under cacheComponents, and the client-side ISR workaround and the isrRoutes manifest field are removed. The guard covers ISR shells too, including revalidate routes the file scan never found.

The guard is applied to the tracer provider and propagator the Node SDK registers, and withSentry of @sentry/nextjs/cloudflare applies it to its own setup, since ISR pages revalidate inside a request of the Worker. A custom OpenTelemetry setup does not get it automatically. If it registers SentryPropagator, it can register the exported NextSentryPropagator instead and pass its tracer provider to markPrerendersOnTracerProvider. Setups with only the OpenTelemetry W3C propagators, which includes @vercel/otel by default, write no sentry-trace tag, so nothing changes for them. The docs follow in sentry-docs.

An ISR page with revalidate = 1 makes the e2e suite exercise the guard at runtime; the other ISR shells come from the build, where the SDK does not run.

On its own this PR keeps the current client behaviour: tags in <head> are continued and are now always fresh. The tag of a resumed shell arrives with the resumed part, which the client only reads in #25142.

figure-shell

Refs #24944, see #24298

@chargome

chargome commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

bugbot run

1 similar comment
@chargome

chargome commented Oct 7, 2026

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.

Comment thread packages/nextjs/src/server/nextSentryPropagator.ts
Comment thread packages/nextjs/src/server/index.ts
@chargome

chargome commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

bugbot run

@chargome
chargome force-pushed the chargome/feat/nextjs-prerender-trace-guard branch from 33c6ae1 to feab86e Compare October 8, 2026 07:58
@chargome

chargome commented Oct 8, 2026

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.

Comment thread packages/nextjs/src/server/nextSentryPropagator.ts Outdated
@chargome

chargome commented Oct 8, 2026

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.

@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Path Size % Change Change
@sentry/browser 29.88 kB - -
@sentry/browser - with treeshaking flags 28.02 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.92 kB - -
@sentry/browser (incl. Tracing) 52.07 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 52.07 kB - -
@sentry/browser (incl. Tracing, Profiling) 55.02 kB - -
@sentry/browser (incl. Tracing, Replay) 91.82 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 80.68 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 96.55 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 109.49 kB - -
@sentry/browser (incl. Feedback) 47.4 kB - -
@sentry/browser (incl. sendFeedback) 34.94 kB - -
@sentry/browser (incl. FeedbackAsync) 40.05 kB - -
@sentry/browser (incl. Metrics) 30.9 kB - -
@sentry/browser (incl. Logs) 31.18 kB - -
@sentry/browser (incl. Metrics & Logs) 31.81 kB - -
@sentry/react 31.7 kB - -
@sentry/react (incl. Tracing) 54.39 kB - -
@sentry/vue 38.04 kB - -
@sentry/vue (incl. Tracing) 55.01 kB - -
@sentry/svelte 29.9 kB - -
@sentry/remix (Remix 3 client bundle) 57.08 kB - -
CDN Bundle 31.64 kB - -
CDN Bundle (incl. Tracing) 52.58 kB - -
CDN Bundle (incl. Logs, Metrics) 33.84 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 54.52 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 74.77 kB - -
CDN Bundle (incl. Tracing, Replay) 90.25 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 92.18 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 96.4 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 98.34 kB - -
CDN Bundle - uncompressed 93.18 kB - -
CDN Bundle (incl. Tracing) - uncompressed 156.03 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 99.72 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 161.99 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 229.76 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 276.2 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 282.14 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 289.9 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 295.84 kB - -
@sentry/nextjs (client) 56.74 kB -0.25% -139 B 🔽
@sentry/sveltekit (client) 52.45 kB - -
@sentry/core/server 41.17 kB - -
@sentry/core/browser 13.81 kB - -
@sentry/node 155.15 kB -0.01% -1 B 🔽
@sentry/node/import (ESM hook with diagnostics-channel injection) 84.06 kB - -
@sentry/node - without tracing 96.81 kB - -
@sentry/node - without channel injection 133.17 kB - -
@sentry/aws-serverless 105.02 kB -0.01% -1 B 🔽
@sentry/cloudflare (withSentry) - minified 210.89 kB - -
@sentry/cloudflare (withSentry) 523.17 kB - -
@sentry/nextjs/cloudflare (withSentry) - minified 230.51 kB +0.57% +1.3 kB 🔺

View base workflow run

@chargome
chargome force-pushed the chargome/feat/nextjs-prerender-trace-guard branch from cf27bae to 576a529 Compare October 9, 2026 14:24
@chargome

chargome commented Oct 9, 2026

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 576a529. Configure here.

@chargome
chargome marked this pull request as ready for review October 12, 2026 09:40
@chargome
chargome requested a review from a team as a code owner October 12, 2026 09:40
@chargome
chargome requested review from nicohrubec and s1gr1d and removed request for a team and nicohrubec October 12, 2026 09:40
@chargome
chargome force-pushed the chargome/feat/nextjs-prerender-trace-guard branch from 40f56e9 to c63eae2 Compare October 12, 2026 12:01
Comment thread packages/nextjs/src/server/nextSentryPropagator.ts
Comment thread packages/nextjs/src/server/nextSentryPropagator.ts Outdated
Comment thread packages/nextjs/src/server/nextSentryPropagator.ts
Comment thread packages/nextjs/src/server/nextSentryPropagator.ts Outdated
Comment thread CHANGELOG.md Outdated
@chargome
chargome requested a review from s1gr1d October 12, 2026 15:01
chargome and others added 4 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>
chargome and others added 12 commits October 13, 2026 10:49
…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>
Comment on lines +245 to +246
export { NextSentryPropagator } from './nextSentryPropagator';
export { markPrerendersOnTracerProvider } from './prerenderContext';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe you could mention those two new public exports in the changelog.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I left them out on purpose as it is a very niche case, but I will move that to docs for sure

@s1gr1d
s1gr1d merged commit 2ad0528 into develop Oct 20, 2026
92 checks passed
@s1gr1d
s1gr1d deleted the chargome/feat/nextjs-prerender-trace-guard branch October 20, 2026 14:06
s1gr1d added a commit that referenced this pull request Oct 20, 2026
… the pageload (#25142)

Builds on #25141.

When a document resumes a prerendered shell, its `sentry-trace` tag sits
at the start of the resumed part, which arrives after the SDK has
started. In a local demo six of seven visits found no tag at init, so
the pageload could only link the server trace instead of joining it.

The app router pageload now continues whatever tag is in the document,
last one wins. In apps with `cacheComponents` enabled, if there is no
tag, no `Server-Timing` trace and the document is still streaming, it
waits for the tag before creating the pageload span. Other apps keep the
immediate start: without `cacheComponents` a tag is either in `<head>`
from the start or never comes, so waiting only held the pageload back.
The wait ends at `DOMContentLoaded` or after 10 seconds. The span still
starts at the time origin and gets its children from the performance
timeline, so it looks the same as before, only inside the server's
trace. No tag by the end of parsing means a fresh trace, which is the
fully static case.

A client-side navigation during the wait starts the pageload with a
fresh trace first, so the navigation ends it like any pageload it
interrupts, as today. Started later, the pageload would end the
navigation span instead. The pageload keeps the URL the document was
loaded with, even if the navigation already changed it.

Waiting was chosen over moving a started span to another trace. That
needs core changes and does not work with span streaming once children
are flushed. The cost: fetches, errors and clicks between SDK start and
the resumed part are not attached to the pageload. The `Server-Timing`
follow-ups (JS-3864, JS-3866) remove that window where the header can be
set.

<img width="2240" height="1904" alt="figure-ppr"
src="https://github.com/user-attachments/assets/c6049dff-1ceb-4a09-9467-b2436648cf6c"
/>

Fixes #24944

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: s1gr1d <32902192+s1gr1d@users.noreply.github.com>
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.

2 participants