From 3927a11ea3b6c4f365161318f93be235678adb94 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 11 Nov 2026 15:12:52 +0100 Subject: [PATCH 1/3] feat(sveltekit): Mark pageloads of prerendered pages --- .../src/routes/prerendered-shell/+page.svelte | 1 + .../src/routes/prerendered-shell/+page.ts | 2 + .../sveltekit-2-static/tests/sdk.test.ts | 47 ++++++++++++--- .../src/routes/prerendered-shell/+page.svelte | 1 + .../src/routes/prerendered-shell/+page.ts | 2 + .../sveltekit-3/tests/prerender.test.ts | 59 +++++++++++++++--- .../src/client/svelte4BrowserTracing.ts | 2 + .../src/client/svelte5BrowserTracing.ts | 2 + .../sveltekit/src/client/traceMetaTags.ts | 10 ++++ .../sveltekit/src/common/traceMetaTags.ts | 5 ++ .../sveltekit/src/server-common/handle.ts | 18 +++++- .../client/browserTracingIntegration.test.ts | 32 +++++++++- .../test/client/svelte5BrowserTracing.test.ts | 29 ++++++++- .../test/server-common/handle.test.ts | 60 +++++++++++++++++-- 14 files changed, 242 insertions(+), 28 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-static/src/routes/prerendered-shell/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-2-static/src/routes/prerendered-shell/+page.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-3/src/routes/prerendered-shell/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-3/src/routes/prerendered-shell/+page.ts create mode 100644 packages/sveltekit/src/client/traceMetaTags.ts create mode 100644 packages/sveltekit/src/common/traceMetaTags.ts diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-static/src/routes/prerendered-shell/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-static/src/routes/prerendered-shell/+page.svelte new file mode 100644 index 000000000000..d3c9d3fe79fc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-static/src/routes/prerendered-shell/+page.svelte @@ -0,0 +1 @@ +

Prerendered shell

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-static/src/routes/prerendered-shell/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-static/src/routes/prerendered-shell/+page.ts new file mode 100644 index 000000000000..ceccaaf67a21 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-static/src/routes/prerendered-shell/+page.ts @@ -0,0 +1,2 @@ +export const prerender = true; +export const ssr = false; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-static/tests/sdk.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-static/tests/sdk.test.ts index 23bce25b7ec4..a8d4a59d790c 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-static/tests/sdk.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-static/tests/sdk.test.ts @@ -1,6 +1,11 @@ import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; import { waitForInitialPageload } from './utils'; +function sentryMetaTagNames(html: string): string[] { + return [...html.matchAll(/ match[1] ?? ''); +} + test.describe('SDK-internal behavior', () => { test("Doesn't inject fetch proxy script for SvelteKit>=2.16.0", async ({ page }) => { await waitForInitialPageload(page, { route: '/' }); @@ -14,16 +19,40 @@ test.describe('SDK-internal behavior', () => { expect(proxyHandle).toBe('undefined'); }); - test("Doesn't add trace meta tags to prerendered pages", async ({ request }) => { - const prerenderedHtml = await (await request.get('/prerendered')).text(); - const ssrHtml = await (await request.get('/')).text(); + [ + { path: '/prerendered', kind: 'a prerendered page', expected: ['sentry-cache-origin'] }, + { path: '/prerendered-shell', kind: 'a prerendered client-side shell', expected: [] }, + { path: '/', kind: 'a server-rendered page', expected: ['sentry-trace', 'baggage'] }, + ].forEach(({ path, kind, expected }) => { + test(`Writes the expected Sentry meta tags into ${kind} (${path})`, async ({ request }) => { + const html = await (await request.get(path)).text(); + + expect(sentryMetaTagNames(html)).toEqual(expected); + }); + }); + + test('Marks the pageload of a prerendered page as statically rendered, without a link', async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('sveltekit-2-static', txnEvent => { + return txnEvent?.transaction === '/prerendered' && txnEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto('/prerendered'); + const pageloadTxn = await pageloadTxnPromise; + + expect(pageloadTxn.contexts?.trace?.data?.['page.server_rendering.type']).toBe('static'); + expect(pageloadTxn.contexts?.trace?.links).toBeUndefined(); + expect(pageloadTxn.contexts?.trace?.parent_span_id).toBeUndefined(); + }); + + test("Doesn't mark the pageload of a prerendered client-side shell", async ({ page }) => { + const pageloadTxnPromise = waitForTransaction('sveltekit-2-static', txnEvent => { + return txnEvent?.transaction === '/prerendered-shell' && txnEvent.contexts?.trace?.op === 'pageload'; + }); - expect(prerenderedHtml).toContain('Prerendered page'); - expect(prerenderedHtml).not.toContain('Prerendered shell diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3/src/routes/prerendered-shell/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit-3/src/routes/prerendered-shell/+page.ts new file mode 100644 index 000000000000..ceccaaf67a21 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-3/src/routes/prerendered-shell/+page.ts @@ -0,0 +1,2 @@ +export const prerender = true; +export const ssr = false; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3/tests/prerender.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-3/tests/prerender.test.ts index 9e9a5bbf4be8..d030334c2188 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-3/tests/prerender.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-3/tests/prerender.test.ts @@ -1,14 +1,55 @@ import { expect, test } from '@playwright/test'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; -test("Doesn't add trace meta tags to prerendered pages", async ({ request }) => { - const prerenderedHtml = await (await request.get('/prerendered')).text(); - const ssrHtml = await (await request.get('/')).text(); +function sentryMetaTagNames(html: string): string[] { + return [...html.matchAll(/ match[1] ?? ''); +} - expect(prerenderedHtml).toContain('Prerendered page'); - expect(prerenderedHtml).not.toContain(' { + test(`writes the expected Sentry meta tags into ${kind} (${path})`, async ({ request }) => { + const html = await (await request.get(path)).text(); - // SSR pages still get the meta tags - expect(ssrHtml).toContain(' { + const pageloadSpanPromise = waitForStreamedSpan('sveltekit-3', span => { + return span.name === '/prerendered' && getSpanOp(span) === 'pageload' && span.is_segment; + }); + + await page.goto('/prerendered'); + const pageloadSpan = await pageloadSpanPromise; + + expect(pageloadSpan.attributes?.['page.server_rendering.type']).toEqual({ type: 'string', value: 'static' }); + expect(pageloadSpan.links).toBeUndefined(); + expect(pageloadSpan.parent_span_id).toBeUndefined(); +}); + +test("doesn't mark the pageload of a prerendered client-side shell", async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('sveltekit-3', span => { + return span.name === '/prerendered-shell' && getSpanOp(span) === 'pageload' && span.is_segment; + }); + + await page.goto('/prerendered-shell'); + const pageloadSpan = await pageloadSpanPromise; + + expect(pageloadSpan.attributes?.['page.server_rendering.type']).toBeUndefined(); + expect(pageloadSpan.parent_span_id).toBeUndefined(); +}); + +test("doesn't mark the pageload of a server-rendered page, which continues the server trace", async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('sveltekit-3', span => { + return span.name === '/' && getSpanOp(span) === 'pageload' && span.is_segment; + }); + + await page.goto('/'); + const pageloadSpan = await pageloadSpanPromise; + + expect(pageloadSpan.attributes?.['page.server_rendering.type']).toBeUndefined(); + expect(pageloadSpan.parent_span_id).toMatch(/^[a-f0-9]{16}$/); }); diff --git a/packages/sveltekit/src/client/svelte4BrowserTracing.ts b/packages/sveltekit/src/client/svelte4BrowserTracing.ts index 9dbb21336021..6ecafdc5cb1f 100644 --- a/packages/sveltekit/src/client/svelte4BrowserTracing.ts +++ b/packages/sveltekit/src/client/svelte4BrowserTracing.ts @@ -26,6 +26,7 @@ import type { Navigation, Page } from '@sveltejs/kit'; // eslint-disable-next-line typescript/no-deprecated import { navigating, page } from '$app/stores'; import type { Readable } from 'svelte/store'; +import { getServerRenderingAttributes } from './traceMetaTags'; /** * SvelteKit 2 / Svelte 4 browser tracing (`$app/stores`). Selected at build time, so it's only @@ -58,6 +59,7 @@ function _instrumentPageload(client: Client, pageStore: Readable): void { [SENTRY_OP]: PAGELOAD, [SENTRY_ORIGIN]: 'auto.pageload.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', + ...getServerRenderingAttributes(), }, }); if (!pageloadSpan) { diff --git a/packages/sveltekit/src/client/svelte5BrowserTracing.ts b/packages/sveltekit/src/client/svelte5BrowserTracing.ts index b8388adc91b6..8930775c56b4 100644 --- a/packages/sveltekit/src/client/svelte5BrowserTracing.ts +++ b/packages/sveltekit/src/client/svelte5BrowserTracing.ts @@ -24,6 +24,7 @@ import { import { NAVIGATION, PAGELOAD, ROUTER } from '@sentry/conventions/op'; import type { Navigation } from '@sveltejs/kit'; import { getCurrentNavigation, onNavigationChange, onPageRouteChange } from './navigationState.svelte'; +import { getServerRenderingAttributes } from './traceMetaTags'; /** * SvelteKit 3 / Svelte 5 browser tracing (`$app/state` runes). Selected at build time, so it's only @@ -54,6 +55,7 @@ function _instrumentPageLoad(client: Client): void { [SENTRY_OP]: PAGELOAD, [SENTRY_ORIGIN]: 'auto.pageload.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', + ...getServerRenderingAttributes(), }, }); diff --git a/packages/sveltekit/src/client/traceMetaTags.ts b/packages/sveltekit/src/client/traceMetaTags.ts new file mode 100644 index 000000000000..b8e09933acce --- /dev/null +++ b/packages/sveltekit/src/client/traceMetaTags.ts @@ -0,0 +1,10 @@ +import { PAGE_SERVER_RENDERING_TYPE } from '@sentry/conventions/attributes'; +import type { SpanAttributes } from '@sentry/core'; +import { WINDOW } from '@sentry/svelte'; +import { CACHE_ORIGIN_META_TAG_NAME } from '../common/traceMetaTags'; + +/** Set at span start, so `tracesSampler` sees it. */ +export function getServerRenderingAttributes(): SpanAttributes { + const isPrerendered = !!WINDOW.document?.querySelector(`meta[name="${CACHE_ORIGIN_META_TAG_NAME}"]`); + return isPrerendered ? { [PAGE_SERVER_RENDERING_TYPE]: 'static' } : {}; +} diff --git a/packages/sveltekit/src/common/traceMetaTags.ts b/packages/sveltekit/src/common/traceMetaTags.ts new file mode 100644 index 000000000000..d6618a156999 --- /dev/null +++ b/packages/sveltekit/src/common/traceMetaTags.ts @@ -0,0 +1,5 @@ +/** + * Replaces the trace meta tags of pages prerendered during the build. Empty, unlike in Next.js: spans + * from the build are not reliably sent, so there is no origin to link. + */ +export const CACHE_ORIGIN_META_TAG_NAME = 'sentry-cache-origin'; diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts index 38291b226679..bb996f16f7ca 100644 --- a/packages/sveltekit/src/server-common/handle.ts +++ b/packages/sveltekit/src/server-common/handle.ts @@ -25,6 +25,7 @@ import { _INTERNAL_setTunnelDefaults as setTunnelDefaults, } from '@sentry/core/server'; import { getTunnelBasePath } from '../common/tunnel'; +import { CACHE_ORIGIN_META_TAG_NAME } from '../common/traceMetaTags'; import type { Handle, ResolveOptions } from './handleTypes'; import { DEBUG_BUILD } from '../common/debug-build'; import { getTracePropagationData, sendErrorToSentry } from './utils'; @@ -83,7 +84,7 @@ export function addSentryCodeToPage(options: { injectFetchProxyScript: boolean; }): NonNullable { return ({ html }) => { - const metaTags = isSvelteKitBuilding() ? '' : getTraceMetaTags(); + const metaTags = getPageMetaTags(html); const headWithMetaTags = metaTags ? `\n${metaTags}` : ''; const headWithFetchScript = options.injectFetchProxyScript ? `\n` : ''; @@ -94,9 +95,20 @@ export function addSentryCodeToPage(options: { }; } +/** Trace meta tags written during the build would put one trace into the HTML of every visitor. */ +function getPageMetaTags(html: string): string { + if (!isSvelteKitBuilding()) { + return getTraceMetaTags(); + } + return isServerRenderedPage(html) ? `` : ''; +} + +/** Kit writes `node_ids` only for server-rendered pages, not for shells (`ssr = false`, SPA fallback). */ +function isServerRenderedPage(html: string): boolean { + return html.includes('node_ids:'); +} + /** - * Prerendered pages would bake one trace id and sampling decision into the HTML for every visitor. - * * `building` from `$app/environment` isn't available here (the SDK is loaded from node_modules at * runtime), so we rely on `_SENTRY_SVELTEKIT_BUILDING`, which our Vite plugin sets during `vite build` * and SvelteKit copies into the worker it prerenders in. diff --git a/packages/sveltekit/test/client/browserTracingIntegration.test.ts b/packages/sveltekit/test/client/browserTracingIntegration.test.ts index c27d9db8ddb6..89210d628788 100644 --- a/packages/sveltekit/test/client/browserTracingIntegration.test.ts +++ b/packages/sveltekit/test/client/browserTracingIntegration.test.ts @@ -9,7 +9,14 @@ import { writable } from 'svelte/store'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { navigating, page } from '$app/stores'; import { browserTracingIntegration } from '../../src/client'; -import { SENTRY_SEGMENT_NAME_SOURCE, SENTRY_OP, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { CACHE_ORIGIN_META_TAG_NAME } from '../../src/common/traceMetaTags'; +import { + PAGE_SERVER_RENDERING_TYPE, + SENTRY_SEGMENT_NAME_SOURCE, + SENTRY_OP, + URL_TEMPLATE, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; // we have to overwrite the global mock from `vitest.setup.ts` here to reset the // `navigating` store for each test. @@ -72,6 +79,7 @@ describe('browserTracingIntegration', () => { beforeEach(() => { createdRootSpan = undefined; navigatingStore = writable(); + document.head.innerHTML = ''; vi.clearAllMocks(); }); @@ -171,6 +179,28 @@ describe('browserTracingIntegration', () => { expect(scopeSetTransactionNameSpy).toHaveBeenLastCalledWith('testRoute/:id'); }); + it('marks the pageload of a prerendered page as statically rendered, without a link', async () => { + const marker = document.createElement('meta'); + marker.name = CACHE_ORIGIN_META_TAG_NAME; + marker.content = ''; + document.head.appendChild(marker); + + const integration = browserTracingIntegration(); + // @ts-expect-error - the fakeClient doesn't satisfy Client but that's fine + integration.afterAllSetup(fakeClient); + await vi.dynamicImportSettled(); + + expect(startBrowserTracingPageLoadSpanSpy).toHaveBeenCalledWith(fakeClient, { + name: '/', + attributes: { + [SENTRY_OP]: 'pageload', + [SENTRY_ORIGIN]: 'auto.pageload.sveltekit', + [SENTRY_SEGMENT_NAME_SOURCE]: 'url', + [PAGE_SERVER_RENDERING_TYPE]: 'static', + }, + }); + }); + it("doesn't start a navigation span when `instrumentNavigation` is false", async () => { const integration = browserTracingIntegration({ instrumentNavigation: false, diff --git a/packages/sveltekit/test/client/svelte5BrowserTracing.test.ts b/packages/sveltekit/test/client/svelte5BrowserTracing.test.ts index eca41882dd76..a2975a0bafa5 100644 --- a/packages/sveltekit/test/client/svelte5BrowserTracing.test.ts +++ b/packages/sveltekit/test/client/svelte5BrowserTracing.test.ts @@ -5,9 +5,16 @@ /* eslint-disable @typescript-eslint/unbound-method */ import type { Span } from '@sentry/core'; import * as SentrySvelte from '@sentry/svelte'; -import { SENTRY_SEGMENT_NAME_SOURCE, URL_TEMPLATE, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { + PAGE_SERVER_RENDERING_TYPE, + SENTRY_OP, + SENTRY_SEGMENT_NAME_SOURCE, + URL_TEMPLATE, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { instrumentSvelteKitTracing } from '../../src/client/svelte5BrowserTracing'; +import { CACHE_ORIGIN_META_TAG_NAME } from '../../src/common/traceMetaTags'; // The Svelte 5 variant reads navigation state from rune-backed helpers (`navigationState.svelte.ts`), // which need the Svelte compiler. We mock them here so the variant's orchestration logic (fetch @@ -70,6 +77,7 @@ describe('svelte5 browser tracing', () => { navigationChangeCb = undefined; pageRouteChangeCb = undefined; createdRootSpan = undefined; + document.head.innerHTML = ''; // The variant wraps `window.fetch`; give it a resolvable fetch to wrap and reset each test. window.fetch = vi.fn().mockResolvedValue(undefined); }); @@ -97,6 +105,25 @@ describe('svelte5 browser tracing', () => { expect(setTransactionNameSpy).toHaveBeenCalledWith('/users/[id]'); }); + it('marks the pageload of a prerendered page as statically rendered, without a link', () => { + const marker = document.createElement('meta'); + marker.name = CACHE_ORIGIN_META_TAG_NAME; + marker.content = ''; + document.head.appendChild(marker); + + instrumentSvelteKitTracing(client, {}); + + expect(startPageLoadSpanSpy).toHaveBeenCalledWith(client, { + name: '/', + attributes: { + [SENTRY_OP]: 'pageload', + [SENTRY_ORIGIN]: 'auto.pageload.sveltekit', + [SENTRY_SEGMENT_NAME_SOURCE]: 'url', + [PAGE_SERVER_RENDERING_TYPE]: 'static', + }, + }); + }); + it("doesn't upgrade the pageload span while the route id is still null", () => { instrumentSvelteKitTracing(client, {}); pageRouteChangeCb?.(null); diff --git a/packages/sveltekit/test/server-common/handle.test.ts b/packages/sveltekit/test/server-common/handle.test.ts index 9e55877cfdad..e7b703f08c44 100644 --- a/packages/sveltekit/test/server-common/handle.test.ts +++ b/packages/sveltekit/test/server-common/handle.test.ts @@ -553,6 +553,28 @@ describe('addSentryCodeToPage', () => { `; + const withBootScript = (startArgs: string): string => + html.replace( + '%sveltekit.body%', + `

Pricing

+ `, + ); + const serverRenderedHtml = withBootScript(`app, element, { + node_ids: [0, 4], + data: [null,{type:"data",data:{plans:3},uses:{}}], + form: null, + error: null + }`); + const clientShellHtml = withBootScript('app, element'); + it("Adds add meta tags and fetch proxy script if there's no active transaction", () => { const transformPageChunk = addSentryCodeToPage({ injectFetchProxyScript: true }); const transformed = transformPageChunk({ html, done: true }); @@ -575,6 +597,15 @@ describe('addSentryCodeToPage', () => { }); }); + it('does not add the cache origin marker to a page rendered for a request', () => { + const transformPageChunk = addSentryCodeToPage({ injectFetchProxyScript: false }); + + const transformed = transformPageChunk({ html: serverRenderedHtml, done: true }); + + expect(transformed).toContain(' { const transformPageChunk = addSentryCodeToPage({ injectFetchProxyScript: false }); const transformed = transformPageChunk({ html, done: true }) as string; @@ -585,18 +616,37 @@ describe('addSentryCodeToPage', () => { }); describe('while SvelteKit builds the app', () => { + beforeEach(() => { + vi.stubEnv('_SENTRY_SVELTEKIT_BUILDING', 'true'); + }); + afterEach(() => { vi.unstubAllEnvs(); }); - it("doesn't add meta tags but still adds the fetch proxy script if _SENTRY_SVELTEKIT_BUILDING is set", () => { - vi.stubEnv('_SENTRY_SVELTEKIT_BUILDING', 'true'); + it('replaces the trace meta tags of a server-rendered page with an empty cache origin marker', () => { + const transformPageChunk = addSentryCodeToPage({ injectFetchProxyScript: false }); + + const transformed = transformPageChunk({ html: serverRenderedHtml, done: true }); + + expect(transformed).toBe( + serverRenderedHtml.replace('', '\n'), + ); + }); + + it('adds no meta tags to a client-side shell, such as an `ssr = false` page or the fallback page', () => { + const transformPageChunk = addSentryCodeToPage({ injectFetchProxyScript: false }); + const transformed = transformPageChunk({ html: clientShellHtml, done: true }); + + expect(transformed).toBe(clientShellHtml); + }); + + it('still adds the fetch proxy script', () => { const transformPageChunk = addSentryCodeToPage({ injectFetchProxyScript: true }); - const transformed = transformPageChunk({ html, done: true }) as string; - expect(transformed).not.toContain('${FETCH_PROXY_SCRIPT}`); }); }); From 395abdb868bed69b14aa07c877814c50adfa4e81 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:44:32 +0100 Subject: [PATCH 2/3] feat(sveltekit): Mark and link pageloads of Vercel ISR pages --- CHANGELOG.md | 6 ++ .../sveltekit-3/tests/prerender.test.ts | 43 +++++++++++++- .../src/client/svelte4BrowserTracing.ts | 26 +++++---- .../src/client/svelte5BrowserTracing.ts | 26 +++++---- .../sveltekit/src/client/traceMetaTags.ts | 41 ++++++++++++-- .../sveltekit/src/common/traceMetaTags.ts | 5 +- .../sveltekit/src/server-common/handle.ts | 26 +++++++-- .../test/client/traceMetaTags.test.ts | 55 ++++++++++++++++++ .../test/server-common/handle.test.ts | 56 +++++++++++++++++++ 9 files changed, 245 insertions(+), 39 deletions(-) create mode 100644 packages/sveltekit/test/client/traceMetaTags.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 94cb6dcefa9b..882ef74e97c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,12 @@ Work in this release was contributed by @lazerg, @rjmcgee53192, @adityankale1908 Builds in development mode or without an auth token, org and project do not change. +- **feat(sveltekit): Mark pageloads of prerendered and Vercel ISR pages ([#26301](https://github.com/getsentry/sentry-javascript/pull/26301), [#26306](https://github.com/getsentry/sentry-javascript/pull/26306))** + + When SvelteKit serves a page that it rendered earlier, the pageload now has the `page.server_rendering.type: static` attribute. This covers pages prerendered during `vite build` and pages that Vercel caches with ISR (`config.isr`). For an ISR page, the pageload also links to the request that rendered the page, if that request was sampled. + + Before, every visitor of a cached ISR page continued the trace of that one request. Now each pageload starts its own trace. Pages with `ssr = false` get no attribute, because the server rendered none of their content. + ## 11.11.0 ### Important Changes diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3/tests/prerender.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-3/tests/prerender.test.ts index d030334c2188..d5c3fb0ba4eb 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-3/tests/prerender.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-3/tests/prerender.test.ts @@ -5,13 +5,16 @@ function sentryMetaTagNames(html: string): string[] { return [...html.matchAll(/ match[1] ?? ''); } +const VERCEL_ISR_HEADERS = { 'x-vercel-isr': '1' }; + [ { path: '/prerendered', kind: 'a prerendered page', expected: ['sentry-cache-origin'] }, { path: '/prerendered-shell', kind: 'a prerendered client-side shell', expected: [] }, { path: '/', kind: 'a server-rendered page', expected: ['sentry-trace', 'baggage'] }, -].forEach(({ path, kind, expected }) => { + { path: '/', headers: VERCEL_ISR_HEADERS, kind: 'a Vercel ISR render', expected: ['sentry-cache-origin'] }, +].forEach(({ path, headers, kind, expected }) => { test(`writes the expected Sentry meta tags into ${kind} (${path})`, async ({ request }) => { - const html = await (await request.get(path)).text(); + const html = await (await request.get(path, { headers })).text(); expect(sentryMetaTagNames(html)).toEqual(expected); }); @@ -53,3 +56,39 @@ test("doesn't mark the pageload of a server-rendered page, which continues the s expect(pageloadSpan.attributes?.['page.server_rendering.type']).toBeUndefined(); expect(pageloadSpan.parent_span_id).toMatch(/^[a-f0-9]{16}$/); }); + +test('marks the pageload of a Vercel ISR page as statically rendered and links the render', async ({ page }) => { + // The event proxy replays spans from this time on, so the server span of the render can be awaited + // after its trace id is known from the HTML. Same clock as the proxy (`process.hrtime`). + const [seconds, nanoseconds] = process.hrtime(); + const requestStart = seconds * 1e9 + nanoseconds; + const pageloadSpanPromise = waitForStreamedSpan('sveltekit-3', span => { + return span.name === '/' && getSpanOp(span) === 'pageload' && span.is_segment; + }); + + // Only on the document: on the SDK's cross-origin envelope requests, the header would need a preflight. + await page.route( + url => url.pathname === '/', + route => route.continue({ headers: { ...route.request().headers(), ...VERCEL_ISR_HEADERS } }), + ); + await page.goto('/'); + const cacheOrigin = await page.locator('meta[name="sentry-cache-origin"]').getAttribute('content'); + const [originTraceId, originSpanId] = cacheOrigin?.split('-') ?? []; + const [originSpan, pageloadSpan] = await Promise.all([ + waitForStreamedSpan('sveltekit-3', span => span.span_id === originSpanId, requestStart), + pageloadSpanPromise, + ]); + + expect(originSpan.trace_id).toBe(originTraceId); + expect(pageloadSpan.attributes?.['page.server_rendering.type']).toEqual({ type: 'string', value: 'static' }); + expect(pageloadSpan.parent_span_id).toBeUndefined(); + expect(pageloadSpan.trace_id).not.toBe(originTraceId); + expect(pageloadSpan.links).toEqual([ + { + trace_id: originTraceId, + span_id: originSpanId, + sampled: true, + attributes: { 'sentry.link.type': { type: 'string', value: 'cache_origin' } }, + }, + ]); +}); diff --git a/packages/sveltekit/src/client/svelte4BrowserTracing.ts b/packages/sveltekit/src/client/svelte4BrowserTracing.ts index 6ecafdc5cb1f..06a1c24b03b2 100644 --- a/packages/sveltekit/src/client/svelte4BrowserTracing.ts +++ b/packages/sveltekit/src/client/svelte4BrowserTracing.ts @@ -26,7 +26,7 @@ import type { Navigation, Page } from '@sveltejs/kit'; // eslint-disable-next-line typescript/no-deprecated import { navigating, page } from '$app/stores'; import type { Readable } from 'svelte/store'; -import { getServerRenderingAttributes } from './traceMetaTags'; +import { withCacheOrigin } from './traceMetaTags'; /** * SvelteKit 2 / Svelte 4 browser tracing (`$app/stores`). Selected at build time, so it's only @@ -51,17 +51,19 @@ export function instrumentSvelteKitTracing( function _instrumentPageload(client: Client, pageStore: Readable): void { const initialPath = WINDOW.location?.pathname; - const pageloadSpan = startBrowserTracingPageLoadSpan(client, { - // With span streaming, span names have to be low cardinality. The route id is only available - // asynchronously, which updates the span name then. - name: hasSpanStreamingEnabled(client) ? PAGELOAD_SPAN_NAME_FALLBACK : initialPath, - attributes: { - [SENTRY_OP]: PAGELOAD, - [SENTRY_ORIGIN]: 'auto.pageload.sveltekit', - [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - ...getServerRenderingAttributes(), - }, - }); + const pageloadSpan = startBrowserTracingPageLoadSpan( + client, + withCacheOrigin({ + // With span streaming, span names have to be low cardinality. The route id is only available + // asynchronously, which updates the span name then. + name: hasSpanStreamingEnabled(client) ? PAGELOAD_SPAN_NAME_FALLBACK : initialPath, + attributes: { + [SENTRY_OP]: PAGELOAD, + [SENTRY_ORIGIN]: 'auto.pageload.sveltekit', + [SENTRY_SEGMENT_NAME_SOURCE]: 'url', + }, + }), + ); if (!pageloadSpan) { return; } diff --git a/packages/sveltekit/src/client/svelte5BrowserTracing.ts b/packages/sveltekit/src/client/svelte5BrowserTracing.ts index 8930775c56b4..135928af8c7e 100644 --- a/packages/sveltekit/src/client/svelte5BrowserTracing.ts +++ b/packages/sveltekit/src/client/svelte5BrowserTracing.ts @@ -24,7 +24,7 @@ import { import { NAVIGATION, PAGELOAD, ROUTER } from '@sentry/conventions/op'; import type { Navigation } from '@sveltejs/kit'; import { getCurrentNavigation, onNavigationChange, onPageRouteChange } from './navigationState.svelte'; -import { getServerRenderingAttributes } from './traceMetaTags'; +import { withCacheOrigin } from './traceMetaTags'; /** * SvelteKit 3 / Svelte 5 browser tracing (`$app/state` runes). Selected at build time, so it's only @@ -47,17 +47,19 @@ export function instrumentSvelteKitTracing( function _instrumentPageLoad(client: Client): void { const initialPath = WINDOW.location?.pathname; - const pageLoadSpan = startBrowserTracingPageLoadSpan(client, { - // With span streaming, span names have to be low cardinality. The route id is only available - // asynchronously, which updates the span name then. - name: hasSpanStreamingEnabled(client) ? PAGELOAD_SPAN_NAME_FALLBACK : initialPath, - attributes: { - [SENTRY_OP]: PAGELOAD, - [SENTRY_ORIGIN]: 'auto.pageload.sveltekit', - [SENTRY_SEGMENT_NAME_SOURCE]: 'url', - ...getServerRenderingAttributes(), - }, - }); + const pageLoadSpan = startBrowserTracingPageLoadSpan( + client, + withCacheOrigin({ + // With span streaming, span names have to be low cardinality. The route id is only available + // asynchronously, which updates the span name then. + name: hasSpanStreamingEnabled(client) ? PAGELOAD_SPAN_NAME_FALLBACK : initialPath, + attributes: { + [SENTRY_OP]: PAGELOAD, + [SENTRY_ORIGIN]: 'auto.pageload.sveltekit', + [SENTRY_SEGMENT_NAME_SOURCE]: 'url', + }, + }), + ); if (!pageLoadSpan) { return; diff --git a/packages/sveltekit/src/client/traceMetaTags.ts b/packages/sveltekit/src/client/traceMetaTags.ts index b8e09933acce..a658310606ed 100644 --- a/packages/sveltekit/src/client/traceMetaTags.ts +++ b/packages/sveltekit/src/client/traceMetaTags.ts @@ -1,10 +1,39 @@ -import { PAGE_SERVER_RENDERING_TYPE } from '@sentry/conventions/attributes'; -import type { SpanAttributes } from '@sentry/core'; +import { PAGE_SERVER_RENDERING_TYPE, SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import type { SpanLink, StartSpanOptions } from '@sentry/core'; +import { extractTraceparentData } from '@sentry/core'; import { WINDOW } from '@sentry/svelte'; import { CACHE_ORIGIN_META_TAG_NAME } from '../common/traceMetaTags'; -/** Set at span start, so `tracesSampler` sees it. */ -export function getServerRenderingAttributes(): SpanAttributes { - const isPrerendered = !!WINDOW.document?.querySelector(`meta[name="${CACHE_ORIGIN_META_TAG_NAME}"]`); - return isPrerendered ? { [PAGE_SERVER_RENDERING_TYPE]: 'static' } : {}; +// `@opentelemetry/api` `TraceFlags.SAMPLED`, inlined because core does not export its copy. +const TRACE_FLAG_SAMPLED = 0x1; + +/** + * Marks the pageload of a reused page as `static`, and links the render that the page reuses. Set at + * span start, so `tracesSampler` sees the attribute. Like a `use cache` hit, links only a sampled render. + */ +export function withCacheOrigin(spanOptions: StartSpanOptions): StartSpanOptions { + const cacheOrigin = WINDOW.document + ?.querySelector(`meta[name="${CACHE_ORIGIN_META_TAG_NAME}"]`) + ?.getAttribute('content'); + if (cacheOrigin == null) { + return spanOptions; + } + + const link = getSampledCacheOriginLink(cacheOrigin); + return { + ...spanOptions, + attributes: { ...spanOptions.attributes, [PAGE_SERVER_RENDERING_TYPE]: 'static' }, + ...(link && { links: [link] }), + }; +} + +function getSampledCacheOriginLink(sentryTrace: string): SpanLink | undefined { + const traceparent = extractTraceparentData(sentryTrace); + if (!traceparent?.traceId || !traceparent.parentSpanId || !traceparent.parentSampled) { + return undefined; + } + return { + context: { traceId: traceparent.traceId, spanId: traceparent.parentSpanId, traceFlags: TRACE_FLAG_SAMPLED }, + attributes: { [SENTRY_LINK_TYPE]: 'cache_origin' }, + }; } diff --git a/packages/sveltekit/src/common/traceMetaTags.ts b/packages/sveltekit/src/common/traceMetaTags.ts index d6618a156999..8dcbf853cfb4 100644 --- a/packages/sveltekit/src/common/traceMetaTags.ts +++ b/packages/sveltekit/src/common/traceMetaTags.ts @@ -1,5 +1,6 @@ /** - * Replaces the trace meta tags of pages prerendered during the build. Empty, unlike in Next.js: spans - * from the build are not reliably sent, so there is no origin to link. + * Replaces the trace meta tags of pages that are reused. Holds the `sentry-trace` of the render that a + * Vercel ISR page reuses, or is empty for a page prerendered during the build: spans from the build are + * not reliably sent, so there is no origin to link. */ export const CACHE_ORIGIN_META_TAG_NAME = 'sentry-cache-origin'; diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts index bb996f16f7ca..449cfc83702b 100644 --- a/packages/sveltekit/src/server-common/handle.ts +++ b/packages/sveltekit/src/server-common/handle.ts @@ -6,6 +6,7 @@ import { getCurrentScope, getDefaultIsolationScope, getIsolationScope, + getTraceData, getTraceMetaTags, hasSpanStreamingEnabled, httpHeadersToSpanAttributes, @@ -82,9 +83,10 @@ export const FETCH_PROXY_SCRIPT = ` */ export function addSentryCodeToPage(options: { injectFetchProxyScript: boolean; + isVercelIsrRender?: boolean; }): NonNullable { return ({ html }) => { - const metaTags = getPageMetaTags(html); + const metaTags = getPageMetaTags(html, !!options.isVercelIsrRender); const headWithMetaTags = metaTags ? `\n${metaTags}` : ''; const headWithFetchScript = options.injectFetchProxyScript ? `\n` : ''; @@ -95,12 +97,24 @@ export function addSentryCodeToPage(options: { }; } -/** Trace meta tags written during the build would put one trace into the HTML of every visitor. */ -function getPageMetaTags(html: string): string { - if (!isSvelteKitBuilding()) { +/** Trace meta tags in a page that other visitors get too would put one trace into all of their pageloads. */ +function getPageMetaTags(html: string, isVercelIsrRender: boolean): string { + const cacheOrigin = getCacheOrigin(isVercelIsrRender); + if (cacheOrigin === undefined) { return getTraceMetaTags(); } - return isServerRenderedPage(html) ? `` : ''; + return isServerRenderedPage(html) ? `` : ''; +} + +/** + * The `sentry-trace` of the render that other visitors reuse, empty for the build (its spans are not + * reliably sent), or `undefined` if the page is rendered for this request only. + */ +function getCacheOrigin(isVercelIsrRender: boolean): string | undefined { + if (isVercelIsrRender) { + return getTraceData()['sentry-trace'] ?? ''; + } + return isSvelteKitBuilding() ? '' : undefined; } /** Kit writes `node_ids` only for server-rendered pages, not for shells (`ssr = false`, SPA fallback). */ @@ -240,6 +254,8 @@ async function instrumentHandle( const res = await resolve(event, { transformPageChunk: addSentryCodeToPage({ injectFetchProxyScript: options.injectFetchProxyScript ?? true, + // Vercel sets this on every invocation of an ISR function, also for background revalidations + isVercelIsrRender: event.request.headers.has('x-vercel-isr'), }), }); diff --git a/packages/sveltekit/test/client/traceMetaTags.test.ts b/packages/sveltekit/test/client/traceMetaTags.test.ts new file mode 100644 index 000000000000..7d2c882a47c6 --- /dev/null +++ b/packages/sveltekit/test/client/traceMetaTags.test.ts @@ -0,0 +1,55 @@ +/** + * @vitest-environment jsdom + */ + +import { PAGE_SERVER_RENDERING_TYPE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { withCacheOrigin } from '../../src/client/traceMetaTags'; + +const spanOptions = { name: '/pricing', attributes: { [SENTRY_OP]: 'pageload' } }; + +function addCacheOriginTag(content: string): void { + const tag = document.createElement('meta'); + tag.name = 'sentry-cache-origin'; + tag.content = content; + document.head.appendChild(tag); +} + +describe('withCacheOrigin', () => { + beforeEach(() => { + document.head.innerHTML = ''; + }); + + it('returns the span options unchanged for a page without a cache origin tag', () => { + expect(withCacheOrigin(spanOptions)).toBe(spanOptions); + }); + + it('marks the pageload as static and links a sampled render', () => { + addCacheOriginTag('0b37aaf4f0d74c0e80875099e7d7ca68-bc3a52360214dcf3-1'); + + expect(withCacheOrigin(spanOptions)).toEqual({ + name: '/pricing', + attributes: { [SENTRY_OP]: 'pageload', [PAGE_SERVER_RENDERING_TYPE]: 'static' }, + links: [ + { + context: { traceId: '0b37aaf4f0d74c0e80875099e7d7ca68', spanId: 'bc3a52360214dcf3', traceFlags: 1 }, + attributes: { 'sentry.link.type': 'cache_origin' }, + }, + ], + }); + }); + + it.each([ + { kind: 'an empty tag (prerendered during the build)', content: '' }, + { kind: 'an unsampled render', content: '0b37aaf4f0d74c0e80875099e7d7ca68-bc3a52360214dcf3-0' }, + { kind: 'a render without a sampling decision', content: '0b37aaf4f0d74c0e80875099e7d7ca68-bc3a52360214dcf3' }, + { kind: 'a malformed value', content: 'not-a-trace' }, + ])('marks the pageload as static without a link for $kind', ({ content }) => { + addCacheOriginTag(content); + + expect(withCacheOrigin(spanOptions)).toEqual({ + name: '/pricing', + attributes: { [SENTRY_OP]: 'pageload', [PAGE_SERVER_RENDERING_TYPE]: 'static' }, + }); + }); +}); diff --git a/packages/sveltekit/test/server-common/handle.test.ts b/packages/sveltekit/test/server-common/handle.test.ts index e7b703f08c44..1a880c5817d4 100644 --- a/packages/sveltekit/test/server-common/handle.test.ts +++ b/packages/sveltekit/test/server-common/handle.test.ts @@ -540,6 +540,36 @@ describe('sentryHandle', () => { }); }); +describe('sentryHandle on Vercel ISR', () => { + it('writes the trace of the request span as the cache origin of a request that Vercel marks as an ISR render', async () => { + const event = mockEvent({ + request: new Request('https://shop.example.com/pricing', { headers: { 'x-vercel-isr': '1' } }), + url: new URL('https://shop.example.com/pricing'), + route: { id: '/pricing' }, + }); + let html: string | undefined; + let requestSpan: Span | undefined; + + await sentryHandle({ injectFetchProxyScript: false })({ + event, + resolve: async (_event, opts) => { + requestSpan = SentryCore.getActiveSpan(); + html = + (await opts?.transformPageChunk?.({ + html: '', + done: true, + })) ?? undefined; + return mockResponse; + }, + }); + + const { traceId, spanId } = requestSpan!.spanContext(); + expect(html).toBe( + `\n`, + ); + }); +}); + describe('addSentryCodeToPage', () => { const html = ` @@ -650,6 +680,32 @@ describe('addSentryCodeToPage', () => { expect(transformed).toContain(``); }); }); + + describe('while Vercel renders a page for its ISR cache', () => { + it('replaces the trace meta tags of a server-rendered page with the cache origin of this render', () => { + const transformPageChunk = addSentryCodeToPage({ injectFetchProxyScript: false, isVercelIsrRender: true }); + + const transformed = SentryCore.startSpan({ name: 'GET /pricing' }, span => { + const { traceId, spanId } = span.spanContext(); + return { html: transformPageChunk({ html: serverRenderedHtml, done: true }), traceId, spanId }; + }); + + expect(transformed.html).toBe( + serverRenderedHtml.replace( + '', + `\n`, + ), + ); + }); + + it('adds no meta tags to a client-side shell', () => { + const transformPageChunk = addSentryCodeToPage({ injectFetchProxyScript: false, isVercelIsrRender: true }); + + const transformed = transformPageChunk({ html: clientShellHtml, done: true }); + + expect(transformed).toBe(clientShellHtml); + }); + }); }); describe('isFetchProxyRequired', () => { From 1cab2c9efa0cf6615a7bcb982524d238b9fe6f2b Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 11 Nov 2026 17:14:21 +0100 Subject: [PATCH 3/3] fixes --- CHANGELOG.md | 2 +- .../sveltekit-2-static/package.json | 1 + .../sveltekit-2-static/tests/sdk.test.ts | 10 ++++++---- .../sveltekit-3/package.json | 1 + .../sveltekit-3/tests/prerender.test.ts | 20 ++++++++++--------- .../client/browserTracingIntegration.test.ts | 3 ++- .../test/client/svelte5BrowserTracing.test.ts | 3 ++- .../test/client/traceMetaTags.test.ts | 11 +++++----- 8 files changed, 30 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 882ef74e97c3..248cc79d2085 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ Work in this release was contributed by @lazerg, @rjmcgee53192, @adityankale1908 Builds in development mode or without an auth token, org and project do not change. -- **feat(sveltekit): Mark pageloads of prerendered and Vercel ISR pages ([#26301](https://github.com/getsentry/sentry-javascript/pull/26301), [#26306](https://github.com/getsentry/sentry-javascript/pull/26306))** +- **feat(sveltekit): Mark pageloads of prerendered and Vercel ISR pages ([#26301](https://github.com/getsentry/sentry-javascript/pull/26301), [#26307](https://github.com/getsentry/sentry-javascript/pull/26307))** When SvelteKit serves a page that it rendered earlier, the pageload now has the `page.server_rendering.type: static` attribute. This covers pages prerendered during `vite build` and pages that Vercel caches with ISR (`config.isr`). For an ISR page, the pageload also links to the request that rendered the page, if that request was sampled. diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-static/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-static/package.json index c4686c849dd6..4f66f0712160 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-static/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-static/package.json @@ -27,6 +27,7 @@ "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", + "@sentry/conventions": "0.32.0", "@sveltejs/adapter-node": "^2.0.0", "@sveltejs/kit": "2.70.2", "@sveltejs/vite-plugin-svelte": "^3.0.0", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-static/tests/sdk.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-static/tests/sdk.test.ts index a8d4a59d790c..431b64457609 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-static/tests/sdk.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-static/tests/sdk.test.ts @@ -1,5 +1,7 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; +import { PAGE_SERVER_RENDERING_TYPE } from '@sentry/conventions/attributes'; +import { PAGELOAD } from '@sentry/conventions/op'; import { waitForInitialPageload } from './utils'; function sentryMetaTagNames(html: string): string[] { @@ -33,26 +35,26 @@ test.describe('SDK-internal behavior', () => { test('Marks the pageload of a prerendered page as statically rendered, without a link', async ({ page }) => { const pageloadTxnPromise = waitForTransaction('sveltekit-2-static', txnEvent => { - return txnEvent?.transaction === '/prerendered' && txnEvent.contexts?.trace?.op === 'pageload'; + return txnEvent?.transaction === '/prerendered' && txnEvent.contexts?.trace?.op === PAGELOAD; }); await page.goto('/prerendered'); const pageloadTxn = await pageloadTxnPromise; - expect(pageloadTxn.contexts?.trace?.data?.['page.server_rendering.type']).toBe('static'); + expect(pageloadTxn.contexts?.trace?.data?.[PAGE_SERVER_RENDERING_TYPE]).toBe('static'); expect(pageloadTxn.contexts?.trace?.links).toBeUndefined(); expect(pageloadTxn.contexts?.trace?.parent_span_id).toBeUndefined(); }); test("Doesn't mark the pageload of a prerendered client-side shell", async ({ page }) => { const pageloadTxnPromise = waitForTransaction('sveltekit-2-static', txnEvent => { - return txnEvent?.transaction === '/prerendered-shell' && txnEvent.contexts?.trace?.op === 'pageload'; + return txnEvent?.transaction === '/prerendered-shell' && txnEvent.contexts?.trace?.op === PAGELOAD; }); await page.goto('/prerendered-shell'); const pageloadTxn = await pageloadTxnPromise; - expect(pageloadTxn.contexts?.trace?.data?.['page.server_rendering.type']).toBeUndefined(); + expect(pageloadTxn.contexts?.trace?.data?.[PAGE_SERVER_RENDERING_TYPE]).toBeUndefined(); expect(pageloadTxn.contexts?.trace?.parent_span_id).toBeUndefined(); }); }); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-3/package.json index 6d6dd79531a7..2bdb4261a836 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-3/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-3/package.json @@ -24,6 +24,7 @@ "devDependencies": { "@playwright/test": "1.64.1", "@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz", + "@sentry/conventions": "0.32.0", "@sveltejs/adapter-node": "^6.0.0", "@sveltejs/kit": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^7.3.1", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-3/tests/prerender.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-3/tests/prerender.test.ts index d5c3fb0ba4eb..87179ffb7dda 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-3/tests/prerender.test.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-3/tests/prerender.test.ts @@ -1,5 +1,7 @@ import { expect, test } from '@playwright/test'; import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; +import { PAGE_SERVER_RENDERING_TYPE, SENTRY_LINK_TYPE } from '@sentry/conventions/attributes'; +import { PAGELOAD } from '@sentry/conventions/op'; function sentryMetaTagNames(html: string): string[] { return [...html.matchAll(/ match[1] ?? ''); @@ -22,38 +24,38 @@ const VERCEL_ISR_HEADERS = { 'x-vercel-isr': '1' }; test('marks the pageload of a prerendered page as statically rendered, without a link', async ({ page }) => { const pageloadSpanPromise = waitForStreamedSpan('sveltekit-3', span => { - return span.name === '/prerendered' && getSpanOp(span) === 'pageload' && span.is_segment; + return span.name === '/prerendered' && getSpanOp(span) === PAGELOAD && span.is_segment; }); await page.goto('/prerendered'); const pageloadSpan = await pageloadSpanPromise; - expect(pageloadSpan.attributes?.['page.server_rendering.type']).toEqual({ type: 'string', value: 'static' }); + expect(pageloadSpan.attributes?.[PAGE_SERVER_RENDERING_TYPE]).toEqual({ type: 'string', value: 'static' }); expect(pageloadSpan.links).toBeUndefined(); expect(pageloadSpan.parent_span_id).toBeUndefined(); }); test("doesn't mark the pageload of a prerendered client-side shell", async ({ page }) => { const pageloadSpanPromise = waitForStreamedSpan('sveltekit-3', span => { - return span.name === '/prerendered-shell' && getSpanOp(span) === 'pageload' && span.is_segment; + return span.name === '/prerendered-shell' && getSpanOp(span) === PAGELOAD && span.is_segment; }); await page.goto('/prerendered-shell'); const pageloadSpan = await pageloadSpanPromise; - expect(pageloadSpan.attributes?.['page.server_rendering.type']).toBeUndefined(); + expect(pageloadSpan.attributes?.[PAGE_SERVER_RENDERING_TYPE]).toBeUndefined(); expect(pageloadSpan.parent_span_id).toBeUndefined(); }); test("doesn't mark the pageload of a server-rendered page, which continues the server trace", async ({ page }) => { const pageloadSpanPromise = waitForStreamedSpan('sveltekit-3', span => { - return span.name === '/' && getSpanOp(span) === 'pageload' && span.is_segment; + return span.name === '/' && getSpanOp(span) === PAGELOAD && span.is_segment; }); await page.goto('/'); const pageloadSpan = await pageloadSpanPromise; - expect(pageloadSpan.attributes?.['page.server_rendering.type']).toBeUndefined(); + expect(pageloadSpan.attributes?.[PAGE_SERVER_RENDERING_TYPE]).toBeUndefined(); expect(pageloadSpan.parent_span_id).toMatch(/^[a-f0-9]{16}$/); }); @@ -63,7 +65,7 @@ test('marks the pageload of a Vercel ISR page as statically rendered and links t const [seconds, nanoseconds] = process.hrtime(); const requestStart = seconds * 1e9 + nanoseconds; const pageloadSpanPromise = waitForStreamedSpan('sveltekit-3', span => { - return span.name === '/' && getSpanOp(span) === 'pageload' && span.is_segment; + return span.name === '/' && getSpanOp(span) === PAGELOAD && span.is_segment; }); // Only on the document: on the SDK's cross-origin envelope requests, the header would need a preflight. @@ -80,7 +82,7 @@ test('marks the pageload of a Vercel ISR page as statically rendered and links t ]); expect(originSpan.trace_id).toBe(originTraceId); - expect(pageloadSpan.attributes?.['page.server_rendering.type']).toEqual({ type: 'string', value: 'static' }); + expect(pageloadSpan.attributes?.[PAGE_SERVER_RENDERING_TYPE]).toEqual({ type: 'string', value: 'static' }); expect(pageloadSpan.parent_span_id).toBeUndefined(); expect(pageloadSpan.trace_id).not.toBe(originTraceId); expect(pageloadSpan.links).toEqual([ @@ -88,7 +90,7 @@ test('marks the pageload of a Vercel ISR page as statically rendered and links t trace_id: originTraceId, span_id: originSpanId, sampled: true, - attributes: { 'sentry.link.type': { type: 'string', value: 'cache_origin' } }, + attributes: { [SENTRY_LINK_TYPE]: { type: 'string', value: 'cache_origin' } }, }, ]); }); diff --git a/packages/sveltekit/test/client/browserTracingIntegration.test.ts b/packages/sveltekit/test/client/browserTracingIntegration.test.ts index 89210d628788..a2b17a736c70 100644 --- a/packages/sveltekit/test/client/browserTracingIntegration.test.ts +++ b/packages/sveltekit/test/client/browserTracingIntegration.test.ts @@ -17,6 +17,7 @@ import { URL_TEMPLATE, SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; +import { PAGELOAD } from '@sentry/conventions/op'; // we have to overwrite the global mock from `vitest.setup.ts` here to reset the // `navigating` store for each test. @@ -193,7 +194,7 @@ describe('browserTracingIntegration', () => { expect(startBrowserTracingPageLoadSpanSpy).toHaveBeenCalledWith(fakeClient, { name: '/', attributes: { - [SENTRY_OP]: 'pageload', + [SENTRY_OP]: PAGELOAD, [SENTRY_ORIGIN]: 'auto.pageload.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [PAGE_SERVER_RENDERING_TYPE]: 'static', diff --git a/packages/sveltekit/test/client/svelte5BrowserTracing.test.ts b/packages/sveltekit/test/client/svelte5BrowserTracing.test.ts index a2975a0bafa5..176f7bf5c50a 100644 --- a/packages/sveltekit/test/client/svelte5BrowserTracing.test.ts +++ b/packages/sveltekit/test/client/svelte5BrowserTracing.test.ts @@ -12,6 +12,7 @@ import { URL_TEMPLATE, SENTRY_ORIGIN, } from '@sentry/conventions/attributes'; +import { PAGELOAD } from '@sentry/conventions/op'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { instrumentSvelteKitTracing } from '../../src/client/svelte5BrowserTracing'; import { CACHE_ORIGIN_META_TAG_NAME } from '../../src/common/traceMetaTags'; @@ -116,7 +117,7 @@ describe('svelte5 browser tracing', () => { expect(startPageLoadSpanSpy).toHaveBeenCalledWith(client, { name: '/', attributes: { - [SENTRY_OP]: 'pageload', + [SENTRY_OP]: PAGELOAD, [SENTRY_ORIGIN]: 'auto.pageload.sveltekit', [SENTRY_SEGMENT_NAME_SOURCE]: 'url', [PAGE_SERVER_RENDERING_TYPE]: 'static', diff --git a/packages/sveltekit/test/client/traceMetaTags.test.ts b/packages/sveltekit/test/client/traceMetaTags.test.ts index 7d2c882a47c6..b491fff60f62 100644 --- a/packages/sveltekit/test/client/traceMetaTags.test.ts +++ b/packages/sveltekit/test/client/traceMetaTags.test.ts @@ -2,11 +2,12 @@ * @vitest-environment jsdom */ -import { PAGE_SERVER_RENDERING_TYPE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { PAGE_SERVER_RENDERING_TYPE, SENTRY_LINK_TYPE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { PAGELOAD } from '@sentry/conventions/op'; import { beforeEach, describe, expect, it } from 'vitest'; import { withCacheOrigin } from '../../src/client/traceMetaTags'; -const spanOptions = { name: '/pricing', attributes: { [SENTRY_OP]: 'pageload' } }; +const spanOptions = { name: '/pricing', attributes: { [SENTRY_OP]: PAGELOAD } }; function addCacheOriginTag(content: string): void { const tag = document.createElement('meta'); @@ -29,11 +30,11 @@ describe('withCacheOrigin', () => { expect(withCacheOrigin(spanOptions)).toEqual({ name: '/pricing', - attributes: { [SENTRY_OP]: 'pageload', [PAGE_SERVER_RENDERING_TYPE]: 'static' }, + attributes: { [SENTRY_OP]: PAGELOAD, [PAGE_SERVER_RENDERING_TYPE]: 'static' }, links: [ { context: { traceId: '0b37aaf4f0d74c0e80875099e7d7ca68', spanId: 'bc3a52360214dcf3', traceFlags: 1 }, - attributes: { 'sentry.link.type': 'cache_origin' }, + attributes: { [SENTRY_LINK_TYPE]: 'cache_origin' }, }, ], }); @@ -49,7 +50,7 @@ describe('withCacheOrigin', () => { expect(withCacheOrigin(spanOptions)).toEqual({ name: '/pricing', - attributes: { [SENTRY_OP]: 'pageload', [PAGE_SERVER_RENDERING_TYPE]: 'static' }, + attributes: { [SENTRY_OP]: PAGELOAD, [PAGE_SERVER_RENDERING_TYPE]: 'static' }, }); }); });