Skip to content

feat(nextjs): Mark pageloads of cached pages - #25318

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

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

Conversation

@chargome

@chargome chargome commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

When Next.js serves a page from its cache (an ISR page, or a cacheComponents shell that was prerendered at runtime), the pageload starts its own trace and has 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-cache-origin meta tag with the sentry-trace value of the prerender (instead of nothing). The client never continues it: it sets page.server_rendering.type on the pageload (static when the whole page came from the cache, partially_static when parts of it rendered for this request) and links the prerender with a cache_origin link, both at span start, so tracesSampler and ignoreSpans see them. Like a use cache hit, the pageload links only a sampled prerender.

So the fixes of #25141 still hold: a cached page does not attach its visitors to a request that was not theirs, and a sampled=0 value in the tag cannot drop their pageloads. Pages from next build get no tag, because 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. page.server_rendering.type is being added to @sentry/conventions in getsentry/sentry-conventions#740; 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 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 📦

Compared: d6a96a2 (PR test merge using develop at 53daa00)

Against: 632cbfe (develop) · base run #64190

⚠️ Baseline mismatch: 1 develop commit(s) in the PR build are missing from the baseline. Their size changes also affect this comparison. Compare commits. Re-run once develop's baseline build for 53daa00 finishes.

Path Size % Change Change
@sentry/browser 30.45 kB - -
@sentry/browser - with treeshaking flags 28.56 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 28.46 kB - -
@sentry/browser (incl. Tracing) 52.8 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 52.8 kB - -
@sentry/browser (incl. Tracing, Profiling) 55.75 kB - -
@sentry/browser (incl. Tracing, Replay) 93.08 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 81.65 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 97.83 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 110.83 kB - -
@sentry/browser (incl. Feedback) 48.02 kB - -
@sentry/browser (incl. sendFeedback) 35.53 kB - -
@sentry/browser (incl. FeedbackAsync) 40.63 kB - -
@sentry/browser (incl. Metrics) 31.46 kB - -
@sentry/browser (incl. Logs) 31.73 kB - -
@sentry/browser (incl. Metrics & Logs) 32.44 kB - -
@sentry/react 32.25 kB - -
@sentry/react (incl. Tracing) 55.1 kB - -
@sentry/vue 38.74 kB - -
@sentry/vue (incl. Tracing) 55.67 kB - -
@sentry/svelte 30.47 kB - -
@sentry/remix (Remix 3 client bundle) 57.83 kB - -
CDN Bundle 32.18 kB - -
CDN Bundle (incl. Tracing) 53.24 kB - -
CDN Bundle (incl. Logs, Metrics) 34.43 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 55.18 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 75.87 kB - -
CDN Bundle (incl. Tracing, Replay) 91.45 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 93.4 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 97.57 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 99.58 kB - -
CDN Bundle - uncompressed 94.62 kB - -
CDN Bundle (incl. Tracing) - uncompressed 157.71 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 101.17 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 163.68 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 232.73 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 279.4 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 285.35 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 293.1 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 299.05 kB - -
@sentry/nextjs (client) 58.09 kB +0.26% +145 B 🔺
@sentry/sveltekit (client) 53.27 kB - -
@sentry/core/server 44.34 kB - -
@sentry/core/browser 13.95 kB - -
@sentry/node 155.12 kB +0.02% +22 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 81.67 kB - -
@sentry/node - without tracing 95.92 kB +0.02% +19 B 🔺
@sentry/node - without channel injection 135.8 kB +0.02% +18 B 🔺
@sentry/aws-serverless 104.11 kB +0.03% +23 B 🔺
@sentry/cloudflare (withSentry) - minified 197.95 kB - -
@sentry/cloudflare (withSentry) 491.92 kB - -
@sentry/nextjs/cloudflare (withSentry) - minified 218.52 kB +0.06% +112 B 🔺

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

Base automatically changed from chargome/feat/nextjs-deferred-pageload-start to develop October 20, 2026 15:37
Comment on lines +56 to +57
const prerenderMarker = readPrerenderMarker();
const prerenderOptions = prerenderMarker ? cachedDocumentSpanOptions(prerenderMarker) : undefined;

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.

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.

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 a unit test and an E2E test (/runtime-shell). After a runtime prerender, the document has both tags.

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';

@s1gr1d s1gr1d Oct 21, 2026 •

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.

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

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.

After discussing this with the team we decided on page.server_rendering.type: static | partially_static | dynamic. dynamic or others maybe added later on.

We could also add *.age or *.revalidate for showing how old the ISR page is and the revalidate interval.

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.

Could we assert the marker here too? (also in the other tests)

    await expect(page.locator('meta[name="sentry-prerender"]')).toHaveCount(1);

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 for the runtime page

* 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';

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.

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

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.

Renamed to sentry-cache-origin

// 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');

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.

nit: We can use SENTRY_LINK_TYPE from @sentry/conventions/attributes (also in the streaming app copy)

chargome and others added 4 commits October 22, 2026 10:52
…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>
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>
…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>
@s1gr1d s1gr1d changed the title feat(nextjs): Mark pageloads that were served from a cached shell feat(nextjs): Mark pageloads of cached pages Oct 22, 2026
@s1gr1d
s1gr1d force-pushed the chargome/feat/nextjs-cached-shell-marker branch from 26177fa to 86f1ac5 Compare October 27, 2026 07:52
@s1gr1d

s1gr1d commented Oct 27, 2026

Copy link
Copy Markdown
Member

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 86f1ac5. Configure here.

…js-cached-shell-marker

# Conflicts:
#	CHANGELOG.md
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Optional E2E tests

Some optional checks were not successful. They do not block the merge. 1 not successful, 56 successful. See the run.

Check Status
❌ CI: Optional E2E Tests / E2E node-anthropic-send-to-sentry (latest) Test (optional) Failing after 3m Details
Successful checks
Check Status
✅ CI: Optional E2E Tests / E2E cloudflare-mastra Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E cloudflare-pi-durable Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E deno (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E cloudflare-agent Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E cloudflare-pi-durable (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nuxt-5-sourcemaps Test (optional) Successful in 44s Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (vinext, bun) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (cloudflare, latest) Test (optional) Successful in 4m Details
✅ CI: Optional E2E Tests / E2E nextjs-15 (latest) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E cloudflare-flue-send-to-sentry Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nestjs-12 (latest) Test (optional) Successful in 45s Details
✅ CI: Optional E2E Tests / E2E cloudflare-workers-send-to-sentry Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E gen-ai-libraries Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E cloudflare-think Test (optional) Successful in 56s Details
✅ CI: Optional E2E Tests / E2E node-anthropic-send-to-sentry Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nextjs-14 (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-eve (latest) Test (optional) Successful in 48s Details
✅ CI: Optional E2E Tests / E2E node-express-mcp-v1 Test (optional) Successful in 15s Details
✅ CI: Optional E2E Tests / E2E node-eve (orchestrion) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nextjs-16-tunnel (cloudflare) Test (optional) Successful in 42s Details
✅ CI: Optional E2E Tests / E2E node-fastify-6 Test (optional) Successful in 23s Details
✅ CI: Optional E2E Tests / E2E node-eve Test (optional) Successful in 47s Details
✅ CI: Optional E2E Tests / E2E nextjs-15 (turbo) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E node-flue (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (vinext) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-express-mcp-v2 Test (optional) Successful in 20s Details
✅ CI: Optional E2E Tests / E2E astro-7 Test (optional) Successful in 54s Details
✅ CI: Optional E2E Tests / E2E cloudflare-think (ai v6) Test (optional) Successful in 59s Details
✅ CI: Optional E2E Tests / E2E node-openai-send-to-sentry Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E tanstackstart-react-sourcemaps Test (optional) Successful in 25s Details
✅ CI: Optional E2E Tests / E2E node-fastify-6 (tracer provider) Test (optional) Successful in 36s Details
✅ CI: Optional E2E Tests / E2E debug-id-sourcemaps Test (optional) Successful in 27s Details
✅ CI: Optional E2E Tests / E2E node-mastra Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nextjs-orpc Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-openai-decisions (latest) Test (optional) Successful in 17s Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (deno) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nuxt-5-sourcemaps (static / nuxt generate) Test (optional) Successful in 22s Details
✅ CI: Optional E2E Tests / E2E nuxt-5-sourcemaps (client source maps kept) Test (optional) Successful in 23s Details
✅ CI: Optional E2E Tests / E2E vue-3 (no Options API) Test (optional) Successful in 58s Details
✅ CI: Optional E2E Tests / E2E node-flue Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E solidstart-2 Test (optional) Successful in 43s Details
✅ CI: Optional E2E Tests / E2E node-pi-durable (latest) Test (optional) Successful in 25s Details
✅ CI: Optional E2E Tests / E2E node-mastra (latest) Test (optional) Successful in 50s Details
✅ CI: Optional E2E Tests / E2E node-openai-send-to-sentry (latest) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E node-pi-durable Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E tanstackstart-react-cloudflare Test (optional) Successful in 41s Details
✅ CI: Optional E2E Tests / E2E node-express-send-to-sentry Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E supabase-nextjs Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E node-openai-decisions Test (optional) Successful in 24s Details
✅ CI: Optional E2E Tests / E2E astro-7-static Test (optional) Successful in 52s Details
✅ CI: Optional E2E Tests / E2E node-express-send-to-sentry-static Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E tanstackstart-react (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nuxt-5 Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E gen-ai-libraries (cloudflare) Test (optional) Successful in 36s Details
✅ CI: Optional E2E Tests / E2E react-send-to-sentry Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E react-router-7-framework-instrumentation Test (optional) Successful in 2m Details

…js-cached-shell-marker

# Conflicts:
#	CHANGELOG.md

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.

2 participants