Skip to content

Commit 26177fa

Browse files
chargomeclaude
andcommitted
ref(nextjs): Name the cached document attribute and marker tag framework 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>
1 parent 5d2d542 commit 26177fa

13 files changed

Lines changed: 72 additions & 72 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
- **feat(nextjs): Mark pageloads that were served from a cached shell**
1212

13-
A pageload whose document is a shell that Next.js prerendered at runtime, for example an ISR page or a `cacheComponents` page generated on first visit, now carries the `sentry.nextjs.cached_shell` attribute and a `cache_origin` span link to the request that generated the shell. That tells apart "nothing ran on the server for this visit" from missing server instrumentation. The link is never continued as a parent, so pageloads keep their own traces and sampling decisions.
13+
A pageload whose document was served from a cache, for example an ISR page or a `cacheComponents` page generated on first visit, now carries the `document.cached` attribute and a `cache_origin` span link to the request that prerendered the page. That tells apart "nothing ran on the server for this visit" from missing server instrumentation. The link is never continued as a parent, so pageloads keep their own traces and sampling decisions.
1414

1515
## 11.7.0
1616

‎dev-packages/e2e-tests/test-applications/nextjs-16-cacheComponents/tests/cacheOrigin-pageloadConnection.spec.ts‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const APP = 'nextjs-16-cacheComponents';
77
// The server only writes `sentry-trace` meta tags for a render that happened for the current request,
88
// never while it prerenders a shell. So a tag in the document can always be continued; the only
99
// question is when it arrives. The resumed part of a prerendered shell arrives after the SDK started,
10-
// which the pageload instrumentation waits for. A shell prerendered at runtime carries a `sentry-shell`
10+
// which the pageload instrumentation waits for. A shell prerendered at runtime carries a `sentry-prerender`
1111
// marker instead, which marks the pageload as served from a cached shell and links the prerender.
1212

1313
test('connects the pageload trace to the runtime server request', async ({ page }) => {
@@ -58,11 +58,11 @@ test('does not share a trace between visitors of a shell generated at request ti
5858
test.skip(process.env.TEST_ENV !== 'production', 'Shells are only cached in production builds');
5959

6060
// `[id]` only lists `test` in `generateStaticParams`, so the first visit prerenders and caches the
61-
// shell for this id. The prerender writes no trace tag, only a `sentry-shell` marker with its own
61+
// shell for this id. The prerender writes no trace tag, only a `sentry-prerender` marker with its own
6262
// trace, so every visitor starts a fresh trace that links the prerender and is marked as a cached shell.
6363
const id = `shell-${Date.now()}`;
6464

65-
const visit = async (): Promise<{ traceId: string | undefined; links: unknown; cachedShell: unknown }> => {
65+
const visit = async (): Promise<{ traceId: string | undefined; links: unknown; documentCached: unknown }> => {
6666
const context = await browser.newContext();
6767
const page: Page = await context.newPage();
6868

@@ -76,15 +76,15 @@ test('does not share a trace between visitors of a shell generated at request ti
7676
await page.goto(`/${id}`);
7777
await expect(page.locator('#result')).toHaveText(`Hello, ${id}!`);
7878
expect(await page.locator('meta[name="sentry-trace"]').count()).toBe(0);
79-
expect(await page.locator('meta[name="sentry-shell"]').count()).toBe(1);
79+
expect(await page.locator('meta[name="sentry-prerender"]').count()).toBe(1);
8080

8181
const pageloadTx = await pageloadTxPromise;
8282
await context.close();
8383

8484
return {
8585
traceId: pageloadTx.contexts?.trace?.trace_id,
8686
links: pageloadTx.contexts?.trace?.links,
87-
cachedShell: pageloadTx.contexts?.trace?.data?.['sentry.nextjs.cached_shell'],
87+
documentCached: pageloadTx.contexts?.trace?.data?.['document.cached'],
8888
};
8989
};
9090

@@ -94,8 +94,8 @@ test('does not share a trace between visitors of a shell generated at request ti
9494
expect(first.traceId).toBeTruthy();
9595
expect(second.traceId).toBeTruthy();
9696
expect(second.traceId).not.toBe(first.traceId);
97-
expect(first.cachedShell).toBe(true);
98-
expect(second.cachedShell).toBe(true);
97+
expect(first.documentCached).toBe(true);
98+
expect(second.documentCached).toBe(true);
9999

100100
// Both link the one request that prerendered the shell, which is neither visitor's own trace.
101101
const linkOf = (links: unknown): { trace_id: string; attributes?: Record<string, unknown> } => {
@@ -122,8 +122,8 @@ test('starts a fresh trace on a fully static page', async ({ page }) => {
122122

123123
// A build-time shell carries no trace context and no marker, the SDK does not run during the build.
124124
expect(await page.locator('meta[name="sentry-trace"]').count()).toBe(0);
125-
expect(await page.locator('meta[name="sentry-shell"]').count()).toBe(0);
125+
expect(await page.locator('meta[name="sentry-prerender"]').count()).toBe(0);
126126
expect(pageloadTx.contexts?.trace?.trace_id).toBeTruthy();
127127
expect(pageloadTx.contexts?.trace?.links).toBeUndefined();
128-
expect(pageloadTx.contexts?.trace?.data?.['sentry.nextjs.cached_shell']).toBeUndefined();
128+
expect(pageloadTx.contexts?.trace?.data?.['document.cached']).toBeUndefined();
129129
});

‎dev-packages/e2e-tests/test-applications/nextjs-16-streaming-cacheComponents/tests/cacheOrigin-pageloadConnection.spec.ts‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const APP = 'nextjs-16-streaming-cacheComponents';
77
// The server only writes `sentry-trace` meta tags for a render that happened for the current request,
88
// never while it prerenders a shell. So a tag in the document can always be continued; the only
99
// question is when it arrives. The resumed part of a prerendered shell arrives after the SDK started,
10-
// which the pageload instrumentation waits for. A shell prerendered at runtime carries a `sentry-shell`
10+
// which the pageload instrumentation waits for. A shell prerendered at runtime carries a `sentry-prerender`
1111
// marker instead, which marks the pageload as served from a cached shell and links the prerender.
1212

1313
test('connects the pageload trace to the runtime server request', async ({ page }) => {
@@ -56,11 +56,11 @@ test('does not share a trace between visitors of a shell generated at request ti
5656
test.skip(process.env.TEST_ENV !== 'production', 'Shells are only cached in production builds');
5757

5858
// `[id]` only lists `test` in `generateStaticParams`, so the first visit prerenders and caches the
59-
// shell for this id. The prerender writes no trace tag, only a `sentry-shell` marker with its own
59+
// shell for this id. The prerender writes no trace tag, only a `sentry-prerender` marker with its own
6060
// trace, so every visitor starts a fresh trace that links the prerender and is marked as a cached shell.
6161
const id = `shell-${Date.now()}`;
6262

63-
const visit = async (): Promise<{ traceId: string | undefined; links: unknown; cachedShell: unknown }> => {
63+
const visit = async (): Promise<{ traceId: string | undefined; links: unknown; documentCached: unknown }> => {
6464
const context = await browser.newContext();
6565
const page: Page = await context.newPage();
6666

@@ -71,15 +71,15 @@ test('does not share a trace between visitors of a shell generated at request ti
7171
await page.goto(`/${id}`);
7272
await expect(page.locator('#result')).toHaveText(`Hello, ${id}!`);
7373
expect(await page.locator('meta[name="sentry-trace"]').count()).toBe(0);
74-
expect(await page.locator('meta[name="sentry-shell"]').count()).toBe(1);
74+
expect(await page.locator('meta[name="sentry-prerender"]').count()).toBe(1);
7575

7676
const pageloadSpan = await pageloadSpanPromise;
7777
await context.close();
7878

7979
return {
8080
traceId: pageloadSpan.trace_id,
8181
links: pageloadSpan.links,
82-
cachedShell: pageloadSpan.attributes['sentry.nextjs.cached_shell']?.value,
82+
documentCached: pageloadSpan.attributes['document.cached']?.value,
8383
};
8484
};
8585

@@ -89,8 +89,8 @@ test('does not share a trace between visitors of a shell generated at request ti
8989
expect(first.traceId).toBeTruthy();
9090
expect(second.traceId).toBeTruthy();
9191
expect(second.traceId).not.toBe(first.traceId);
92-
expect(first.cachedShell).toBe(true);
93-
expect(second.cachedShell).toBe(true);
92+
expect(first.documentCached).toBe(true);
93+
expect(second.documentCached).toBe(true);
9494

9595
// Both link the one request that prerendered the shell, which is neither visitor's own trace.
9696
const linkOf = (links: unknown): { trace_id: string; attributes?: Record<string, { value: unknown }> } => {
@@ -117,8 +117,8 @@ test('starts a fresh trace on a fully static page', async ({ page }) => {
117117

118118
// A build-time shell carries no trace context and no marker, the SDK does not run during the build.
119119
expect(await page.locator('meta[name="sentry-trace"]').count()).toBe(0);
120-
expect(await page.locator('meta[name="sentry-shell"]').count()).toBe(0);
120+
expect(await page.locator('meta[name="sentry-prerender"]').count()).toBe(0);
121121
expect(pageloadSpan.trace_id).toBeTruthy();
122122
expect(pageloadSpan.links).toBeUndefined();
123-
expect(pageloadSpan.attributes['sentry.nextjs.cached_shell']).toBeUndefined();
123+
expect(pageloadSpan.attributes['document.cached']).toBeUndefined();
124124
});

‎packages/nextjs/src/client/routing/appRouterPageloadInstrumentation.ts‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ import { stripTrailingSlash } from './parameterization';
1919
import type { TraceMetaTagValues, TraceMetaTagWait } from './traceMetaTags';
2020
import {
2121
addTraceMetaTagLink,
22-
cachedShellSpanOptions,
22+
cachedDocumentSpanOptions,
2323
canWaitForTraceMetaTag,
24-
readShellMarker,
24+
readPrerenderMarker,
2525
readTraceMetaTags,
2626
waitForTraceMetaTag,
2727
} from './traceMetaTags';
@@ -53,15 +53,15 @@ function startPageloadSpan(
5353
documentUrl: string,
5454
traceMetaTags?: TraceMetaTagValues,
5555
): Span | undefined {
56-
const shellMarker = readShellMarker();
57-
const shellOptions = shellMarker ? cachedShellSpanOptions(shellMarker) : undefined;
56+
const prerenderMarker = readPrerenderMarker();
57+
const prerenderOptions = prerenderMarker ? cachedDocumentSpanOptions(prerenderMarker) : undefined;
5858
const span = startBrowserTracingPageLoadSpan(
5959
client,
60-
shellOptions
60+
prerenderOptions
6161
? {
6262
...spanOptions,
63-
attributes: { ...spanOptions.attributes, ...shellOptions.attributes },
64-
links: shellOptions.links,
63+
attributes: { ...spanOptions.attributes, ...prerenderOptions.attributes },
64+
links: prerenderOptions.links,
6565
}
6666
: spanOptions,
6767
traceMetaTags,

‎packages/nextjs/src/client/routing/traceMetaTags.ts‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { SENTRY_LINK_TYPE } from '@sentry/conventions/attributes';
22
import type { Span, SpanLink, StartSpanOptions } from '@sentry/core';
33
import { extractTraceparentData, GLOBAL_OBJ } from '@sentry/core';
4-
import { SHELL_META_TAG_NAME } from '../../common/traceMetaTags';
4+
import { PRERENDER_META_TAG_NAME } from '../../common/traceMetaTags';
55
import { WINDOW } from '@sentry/react';
66

77
// `@opentelemetry/api` `TraceFlags`, inlined because core does not export its copies.
@@ -13,8 +13,8 @@ export interface TraceMetaTagValues {
1313
baggage: string | undefined;
1414
}
1515

16-
/** Set on a pageload whose document is a shell that Next.js prerendered and served from its cache. */
17-
export const CACHED_SHELL_ATTRIBUTE = 'sentry.nextjs.cached_shell';
16+
/** Set on a pageload whose document was served from a cache, so no server render ran for the request. */
17+
export const DOCUMENT_CACHED_ATTRIBUTE = 'document.cached';
1818

1919
function getDocument(): Document | undefined {
2020
return WINDOW.document;
@@ -159,19 +159,19 @@ export function addTraceMetaTagLink(span: Span, { sentryTrace }: TraceMetaTagVal
159159
}
160160
}
161161

162-
/** Reads the marker the server writes into a shell while it prerenders it, see `nextSentryPropagator.ts`. */
163-
export function readShellMarker(): string | undefined {
162+
/** Reads the marker the server writes into a document while it prerenders it, see `nextSentryPropagator.ts`. */
163+
export function readPrerenderMarker(): string | undefined {
164164
const document = getDocument();
165-
return document && lastMetaContent(document, SHELL_META_TAG_NAME);
165+
return document && lastMetaContent(document, PRERENDER_META_TAG_NAME);
166166
}
167167

168168
/**
169-
* The start options that mark a pageload whose document is a cached shell and link the request that
170-
* prerendered the shell. Applied at span start, so `tracesSampler` and `ignoreSpans` see the attribute.
171-
* The marker is never continued: the shell is served to every later visitor, and the request behind it
172-
* handled none of their visits.
169+
* The start options that mark a pageload whose document was served from a cache and link the request
170+
* that prerendered it. Applied at span start, so `tracesSampler` and `ignoreSpans` see the attribute.
171+
* The marker is never continued: the document is served to every later visitor, and the request behind
172+
* it handled none of their visits.
173173
*/
174-
export function cachedShellSpanOptions(marker: string): Pick<StartSpanOptions, 'attributes' | 'links'> {
174+
export function cachedDocumentSpanOptions(marker: string): Pick<StartSpanOptions, 'attributes' | 'links'> {
175175
const link = traceMetaTagLink(marker, 'cache_origin');
176-
return { attributes: { [CACHED_SHELL_ATTRIBUTE]: true }, ...(link && { links: [link] }) };
176+
return { attributes: { [DOCUMENT_CACHED_ATTRIBUTE]: true }, ...(link && { links: [link] }) };
177177
}
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Name of the meta tag the server writes instead of `sentry-trace` while Next.js prerenders a document.
3-
* Its content is the `sentry-trace` value of the prerender. A prerendered shell is cached and served to
4-
* later visitors, so the client never continues it, it only marks the pageload as served from a cached
5-
* shell and links the request that generated it.
3+
* Its content is the `sentry-trace` value of the prerender. A prerendered document is cached and served
4+
* to later visitors, so the client never continues it, it only marks the pageload as served from a
5+
* cached document and links the request that generated it.
66
*/
7-
export const SHELL_META_TAG_NAME = 'sentry-shell';
7+
export const PRERENDER_META_TAG_NAME = 'sentry-prerender';

‎packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SHELL_META_TAG_NAME } from '../../common/traceMetaTags';
1+
import { PRERENDER_META_TAG_NAME } from '../../common/traceMetaTags';
22
import { debug, isMatchingPattern, parseSemver } from '@sentry/core';
33
import { getSentryRelease } from '@sentry/node';
44
import * as fs from 'fs';
@@ -149,7 +149,7 @@ export function maybeSetClientTraceMetadataOption(
149149
incomingUserNextConfigObject.experimental.clientTraceMetadata = [
150150
'baggage',
151151
'sentry-trace',
152-
SHELL_META_TAG_NAME,
152+
PRERENDER_META_TAG_NAME,
153153
...(incomingUserNextConfigObject.experimental?.clientTraceMetadata || []),
154154
];
155155
}

‎packages/nextjs/src/server/nextSentryPropagator.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import type { Context, TextMapSetter, TracerProvider } from '@opentelemetry/api';
22
import { propagation } from '@opentelemetry/api';
33
import { SentryPropagator } from '@sentry/opentelemetry';
4-
import { SHELL_META_TAG_NAME } from '../common/traceMetaTags';
4+
import { PRERENDER_META_TAG_NAME } from '../common/traceMetaTags';
55
import { isPrerenderContext, markPrerendersOnTracerProvider } from './prerenderContext';
66

77
/**
88
* A `SentryPropagator` that does not hand out the trace meta tags of a document while Next.js
99
* prerenders it. Trace context baked into a prerendered shell would be replayed to every visitor the
1010
* shell is served to, so the browser could continue a trace that never handled its request. The shell
11-
* gets a `sentry-shell` marker tag with the prerender's trace instead, which the client only ever links.
11+
* gets a `sentry-prerender` marker tag with the prerender's trace instead, which the client only ever links.
1212
*
1313
* The SDK registers it when it sets up OpenTelemetry. An app that sets up OpenTelemetry itself
1414
* registers it in place of `SentryPropagator` and calls `markPrerendersOnTracerProvider` with its
@@ -28,7 +28,7 @@ export class NextSentryPropagator extends SentryPropagator {
2828
super.inject(ctx, values, { set: (c, key, value) => ((c as Record<string, string>)[key] = value) });
2929
const sentryTrace = values['sentry-trace'];
3030
if (sentryTrace) {
31-
setter.set(carrier, SHELL_META_TAG_NAME, sentryTrace);
31+
setter.set(carrier, PRERENDER_META_TAG_NAME, sentryTrace);
3232
}
3333
}
3434
}

‎packages/nextjs/test/client/appRouterRoutingInstrumentation.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ describe('appRouterInstrumentPageLoad', () => {
325325
const { core, pageload } = await setup('static');
326326
Object.defineProperty(document, 'readyState', { value: 'complete', configurable: true });
327327
const meta = document.createElement('meta');
328-
meta.name = 'sentry-shell';
328+
meta.name = 'sentry-prerender';
329329
meta.content = '11111111111111111111111111111111-1111111111111111-1';
330330
document.head.appendChild(meta);
331331

@@ -335,7 +335,7 @@ describe('appRouterInstrumentPageLoad', () => {
335335
expect(span).toBeDefined();
336336
const spanJson = core.spanToJSON(span!);
337337
expect(spanJson.attributes?.[SENTRY_OP]).toBe(PAGELOAD);
338-
expect(spanJson.attributes?.['sentry.nextjs.cached_shell']).toBe(true);
338+
expect(spanJson.attributes?.['document.cached']).toBe(true);
339339
expect(spanJson.trace_id).not.toBe('11111111111111111111111111111111');
340340
expect(spanJson.links).toEqual([
341341
expect.objectContaining({

‎packages/nextjs/test/client/traceMetaTags.test.ts‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import type { Span } from '@sentry/core';
44
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
55
import {
66
addTraceMetaTagLink,
7-
cachedShellSpanOptions,
8-
readShellMarker,
7+
cachedDocumentSpanOptions,
8+
readPrerenderMarker,
99
canWaitForTraceMetaTag,
1010
readTraceMetaTags,
1111
waitForTraceMetaTag,
@@ -220,23 +220,23 @@ describe('addTraceMetaTagLink', () => {
220220
});
221221
});
222222

223-
describe('readShellMarker', () => {
224-
it('returns nothing without a sentry-shell tag', () => {
225-
expect(readShellMarker()).toBeUndefined();
223+
describe('readPrerenderMarker', () => {
224+
it('returns nothing without a sentry-prerender tag', () => {
225+
expect(readPrerenderMarker()).toBeUndefined();
226226
});
227227

228-
it('reads the last sentry-shell tag', () => {
229-
appendMeta(document.head, 'sentry-shell', HEAD_TRACE);
230-
appendMeta(document.body, 'sentry-shell', BODY_TRACE);
228+
it('reads the last sentry-prerender tag', () => {
229+
appendMeta(document.head, 'sentry-prerender', HEAD_TRACE);
230+
appendMeta(document.body, 'sentry-prerender', BODY_TRACE);
231231

232-
expect(readShellMarker()).toBe(BODY_TRACE);
232+
expect(readPrerenderMarker()).toBe(BODY_TRACE);
233233
});
234234
});
235235

236-
describe('cachedShellSpanOptions', () => {
236+
describe('cachedDocumentSpanOptions', () => {
237237
it('marks the span and links the prerender as cache origin', () => {
238-
expect(cachedShellSpanOptions(HEAD_TRACE)).toEqual({
239-
attributes: { 'sentry.nextjs.cached_shell': true },
238+
expect(cachedDocumentSpanOptions(HEAD_TRACE)).toEqual({
239+
attributes: { 'document.cached': true },
240240
links: [
241241
{
242242
context: { traceId: '11111111111111111111111111111111', spanId: '1111111111111111', traceFlags: 1 },
@@ -247,6 +247,6 @@ describe('cachedShellSpanOptions', () => {
247247
});
248248

249249
it('marks the span even when the marker is not a sentry-trace value', () => {
250-
expect(cachedShellSpanOptions('prerendered')).toEqual({ attributes: { 'sentry.nextjs.cached_shell': true } });
250+
expect(cachedDocumentSpanOptions('prerendered')).toEqual({ attributes: { 'document.cached': true } });
251251
});
252252
});

0 commit comments

Comments
 (0)