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' },
});
});
});