Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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), [#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.

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Prerendered shell</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const prerender = true;
export const ssr = false;
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
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[] {
return [...html.matchAll(/<meta name="(sentry-[\w-]+|baggage)"/g)].map(match => 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: '/' });
Expand All @@ -14,16 +21,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('<meta name="sentry-trace"');
expect(prerenderedHtml).not.toContain('<meta name="baggage"');
await page.goto('/prerendered-shell');
const pageloadTxn = await pageloadTxnPromise;

// SSR pages still get the meta tags
expect(ssrHtml).toContain('<meta name="sentry-trace"');
expect(ssrHtml).toContain('<meta name="baggage"');
expect(pageloadTxn.contexts?.trace?.data?.[PAGE_SERVER_RENDERING_TYPE]).toBeUndefined();
expect(pageloadTxn.contexts?.trace?.parent_span_id).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>Prerendered shell</h1>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const prerender = true;
export const ssr = false;
Original file line number Diff line number Diff line change
@@ -1,14 +1,96 @@
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';

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(/<meta name="(sentry-[\w-]+|baggage)"/g)].map(match => match[1] ?? '');
}

expect(prerenderedHtml).toContain('Prerendered page');
expect(prerenderedHtml).not.toContain('<meta name="sentry-trace"');
expect(prerenderedHtml).not.toContain('<meta name="baggage"');
const VERCEL_ISR_HEADERS = { 'x-vercel-isr': '1' };

// SSR pages still get the meta tags
expect(ssrHtml).toContain('<meta name="sentry-trace"');
expect(ssrHtml).toContain('<meta name="baggage"');
[
{ 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'] },
{ 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, { headers })).text();

expect(sentryMetaTagNames(html)).toEqual(expected);
});
});

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

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}$/);
});

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' } },
},
]);
});
24 changes: 14 additions & 10 deletions packages/sveltekit/src/client/svelte4BrowserTracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { withCacheOrigin } from './traceMetaTags';

/**
* SvelteKit 2 / Svelte 4 browser tracing (`$app/stores`). Selected at build time, so it's only
Expand All @@ -50,16 +51,19 @@ export function instrumentSvelteKitTracing(
function _instrumentPageload(client: Client, pageStore: Readable<Page>): 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',
},
});
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;
}
Expand Down
24 changes: 14 additions & 10 deletions packages/sveltekit/src/client/svelte5BrowserTracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { withCacheOrigin } from './traceMetaTags';

/**
* SvelteKit 3 / Svelte 5 browser tracing (`$app/state` runes). Selected at build time, so it's only
Expand All @@ -46,16 +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',
},
});
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;
Expand Down
39 changes: 39 additions & 0 deletions packages/sveltekit/src/client/traceMetaTags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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';

// `@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' },
};
}
6 changes: 6 additions & 0 deletions packages/sveltekit/src/common/traceMetaTags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* 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';
Loading