Skip to content
Merged
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
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,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(/<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 +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('<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
@@ -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,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(/<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"');
[
{ 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();

// SSR pages still get the meta tags
expect(ssrHtml).toContain('<meta name="sentry-trace"');
expect(ssrHtml).toContain('<meta name="baggage"');
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}$/);
});
2 changes: 2 additions & 0 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 { getServerRenderingAttributes } from './traceMetaTags';

/**
* SvelteKit 2 / Svelte 4 browser tracing (`$app/stores`). Selected at build time, so it's only
Expand Down Expand Up @@ -58,6 +59,7 @@ function _instrumentPageload(client: Client, pageStore: Readable<Page>): void {
[SENTRY_OP]: PAGELOAD,
[SENTRY_ORIGIN]: 'auto.pageload.sveltekit',
[SENTRY_SEGMENT_NAME_SOURCE]: 'url',
...getServerRenderingAttributes(),
},
});
if (!pageloadSpan) {
Expand Down
2 changes: 2 additions & 0 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 { getServerRenderingAttributes } from './traceMetaTags';

/**
* SvelteKit 3 / Svelte 5 browser tracing (`$app/state` runes). Selected at build time, so it's only
Expand Down Expand Up @@ -54,6 +55,7 @@ function _instrumentPageLoad(client: Client): void {
[SENTRY_OP]: PAGELOAD,
[SENTRY_ORIGIN]: 'auto.pageload.sveltekit',
[SENTRY_SEGMENT_NAME_SOURCE]: 'url',
...getServerRenderingAttributes(),
},
});

Expand Down
10 changes: 10 additions & 0 deletions packages/sveltekit/src/client/traceMetaTags.ts
Original file line number Diff line number Diff line change
@@ -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' } : {};
}
5 changes: 5 additions & 0 deletions packages/sveltekit/src/common/traceMetaTags.ts
Original file line number Diff line number Diff line change
@@ -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';
18 changes: 15 additions & 3 deletions packages/sveltekit/src/server-common/handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -83,7 +84,7 @@ export function addSentryCodeToPage(options: {
injectFetchProxyScript: boolean;
}): NonNullable<ResolveOptions['transformPageChunk']> {
return ({ html }) => {
const metaTags = isSvelteKitBuilding() ? '' : getTraceMetaTags();
const metaTags = getPageMetaTags(html);
const headWithMetaTags = metaTags ? `<head>\n${metaTags}` : '<head>';

const headWithFetchScript = options.injectFetchProxyScript ? `\n<script>${FETCH_PROXY_SCRIPT}</script>` : '';
Expand All @@ -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) ? `<meta name="${CACHE_ORIGIN_META_TAG_NAME}" content=""/>` : '';
}

/** 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -72,6 +79,7 @@ describe('browserTracingIntegration', () => {
beforeEach(() => {
createdRootSpan = undefined;
navigatingStore = writable();
document.head.innerHTML = '';
vi.clearAllMocks();
});

Expand Down Expand Up @@ -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,
Expand Down
29 changes: 28 additions & 1 deletion packages/sveltekit/test/client/svelte5BrowserTracing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading