Skip to content

feat(nextjs): Wait for the resumed part of a document before starting the pageload - #25142

Open
chargome wants to merge 9 commits into
developfrom
chargome/feat/nextjs-deferred-pageload-start
Open

chargome wants to merge 9 commits into
developfrom
chargome/feat/nextjs-deferred-pageload-start

Conversation

@chargome

@chargome chargome commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

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.

figure-ppr

Fixes #24944

🤖 Generated with Claude Code

@chargome
chargome added this pull request to stack #25143 October 7, 2026 14:59
@chargome chargome self-assigned this Oct 7, 2026
@chargome

chargome commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

bugbot run

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

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) 57.28 kB +0.95% +534 B 🔺
@sentry/sveltekit (client) 52.45 kB - -
@sentry/core/server 41.17 kB - -
@sentry/core/browser 13.81 kB - -
@sentry/node 155.15 kB - -
@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 -0.01% -1 B 🔽
@sentry/aws-serverless 105.02 kB - -
@sentry/cloudflare (withSentry) - minified 210.89 kB - -
@sentry/cloudflare (withSentry) 523.17 kB - -
@sentry/nextjs/cloudflare (withSentry) - minified 230.51 kB - -

View base workflow run

@chargome
chargome force-pushed the chargome/feat/nextjs-deferred-pageload-start branch from 217546f to fa2f4e1 Compare October 7, 2026 15:19
@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/client/routing/appRouterRoutingInstrumentation.ts Outdated
@chargome
chargome force-pushed the chargome/feat/nextjs-deferred-pageload-start branch from fa2f4e1 to c617c75 Compare October 8, 2026 07:55
@chargome

chargome commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

bugbot run

@chargome
chargome force-pushed the chargome/feat/nextjs-deferred-pageload-start branch from c617c75 to ecb46df Compare October 8, 2026 07:59
@chargome

chargome commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

bugbot run

@chargome
chargome force-pushed the chargome/feat/nextjs-deferred-pageload-start branch from ecb46df to 8ead4b1 Compare October 8, 2026 08:02
@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/client/routing/appRouterRoutingInstrumentation.ts Outdated
Comment thread packages/nextjs/test/client/appRouterRoutingInstrumentation.test.ts
@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.

@chargome
chargome force-pushed the chargome/feat/nextjs-deferred-pageload-start branch from d3fd241 to 1ae9f0e Compare October 9, 2026 14:25
@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.

Stale Bugbot comment from a previous run.

Comment thread packages/nextjs/src/client/browserTracingIntegration.ts
@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.

Stale Bugbot comment from a previous run.

@chargome chargome closed this Oct 12, 2026
@chargome chargome reopened this Oct 12, 2026
@chargome
chargome force-pushed the chargome/feat/nextjs-deferred-pageload-start branch from 7c3abfc to 2358cdb Compare October 12, 2026 09:23
@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 2358cdb. Configure here.

@chargome
chargome force-pushed the chargome/feat/nextjs-deferred-pageload-start branch from 2358cdb to 85c7756 Compare October 12, 2026 09:55
@chargome
chargome force-pushed the chargome/feat/nextjs-deferred-pageload-start branch 5 times, most recently from 669a79a to e0b0cae Compare October 12, 2026 14:27
@chargome
chargome force-pushed the chargome/feat/nextjs-deferred-pageload-start branch from 9f0f16b to c248cc5 Compare October 13, 2026 08:49
* How long the pageload start may wait for the resumed part of a Cache Components document. The wait
* normally ends with `DOMContentLoaded`; the cap only matters for a response the server keeps open.
*/
const PAGELOAD_TRACE_META_TAG_TIMEOUT_MS = 10_000;

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.

this one is arbitrarily chosen and should likely never be met in real world conditions

@chargome
chargome marked this pull request as ready for review October 13, 2026 09:55
@chargome
chargome requested a review from a team as a code owner October 13, 2026 09:55
@chargome
chargome requested review from nicohrubec and s1gr1d and removed request for a team October 13, 2026 09:55
Comment on lines +49 to +51
if (instrumentPageLoad) {
client.on('startNavigationSpan', settlePendingPageloadWait);
}

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.

M: this hook is registered in afterAllSetup which runs later than setup (this is where the other browserTracingInstrumentation calls this hook).

Can we add an E2E test that clicks a link during a slow dynamic hole? I think that would catch it.
Because if we now have a navigation that starts during the waiting time (slow dynamic hole), we would get a navigation span before the pageload span. maybeEndActiveSpan() would probably cancel that. Definitely worth testing at least.

const scope = getCurrentScope();
const { normalizedRequest } = scope.getScopeData().sdkProcessingMetadata;
if (normalizedRequest) {
scope.setSDKProcessingMetadata({ normalizedRequest: { ...normalizedRequest, url: documentUrl } });

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.

What do we get in the documentUrl? Does this need a check on dataCollection.urlQueryParams?

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'll check again and guard it if needed

Comment on lines +141 to +147
span.addLink({
context: {
traceId: traceparent.traceId,
spanId: traceparent.parentSpanId,
traceFlags: traceparent.parentSampled ? TRACE_FLAG_SAMPLED : TRACE_FLAG_NONE,
},
});

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.

We should probably add a new link type for that. Maybe server_request?

@chargome chargome Oct 13, 2026 •

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.

Adding the link is a very rare case do you think this is actually needed? What would we gain?

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.

The information in the Sentry frontend what this link actually is. Right now, it's just linking but without contextual information about the meaning of this link.

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.

Added it there: getsentry/sentry-docs#20112

@github-actions

Copy link
Copy Markdown
Contributor

👋 @nicohrubec — Please review this PR when you get a chance!

Base automatically changed from chargome/feat/nextjs-prerender-trace-guard to develop October 20, 2026 14:06
chargome and others added 6 commits October 20, 2026 16:06
… 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>
s1gr1d added a commit that referenced this pull request Oct 20, 2026
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.

<img width="2240" height="1332" alt="figure-shell"
src="https://github.com/user-attachments/assets/809726dd-cddb-4fab-bfee-83bcdc35dbab"
/>

Refs #24944, see #24298

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: s1gr1d <32902192+s1gr1d@users.noreply.github.com>
@s1gr1d
s1gr1d force-pushed the chargome/feat/nextjs-deferred-pageload-start branch from c248cc5 to 7a4cf16 Compare October 20, 2026 14:07
`browserTracingIntegration` registers its `startNavigationSpan` handler in
`setup`, before the Next.js handler in `afterAllSetup`. A navigation during
the wait therefore created its span first, and the late pageload ended it.
Settling on `beforeStartNavigationSpan` does not depend on that order.
…uest data

The pageload wrote the raw document URL into `normalizedRequest`, so the
`dataCollection.urlQueryParams` denylist no longer applied to `request.url` of
any App Router pageload. The URL is now filtered once, when it is captured.
Without a type, the link from a pageload to the server request whose trace
context arrived too late shows up in Sentry with no meaning. The type is
documented in getsentry/sentry-docs#20112.

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.

Client decision rule for trace meta tags under cacheComponents

2 participants