diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/event-proxy-server.ts index 66a9e744846e..4c2df32399f0 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/event-proxy-server.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/event-proxy-server.ts @@ -6,7 +6,7 @@ import * as os from 'os'; import * as path from 'path'; import * as util from 'util'; import * as zlib from 'zlib'; -import type { Envelope, EnvelopeItem, Event } from '@sentry/types'; +import type { Envelope, EnvelopeItem, SerializedEvent } from '@sentry/types'; import { parseEnvelope } from '@sentry/utils'; const readFile = util.promisify(fs.readFile); @@ -210,13 +210,13 @@ export function waitForEnvelopeItem( export function waitForError( proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { + callback: (transactionEvent: SerializedEvent) => Promise | boolean, +): Promise { return new Promise((resolve, reject) => { waitForEnvelopeItem(proxyServerName, async envelopeItem => { const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); + if (envelopeItemHeader.type === 'event' && (await callback(envelopeItemBody as SerializedEvent))) { + resolve(envelopeItemBody as SerializedEvent); return true; } return false; @@ -226,13 +226,13 @@ export function waitForError( export function waitForTransaction( proxyServerName: string, - callback: (transactionEvent: Event) => Promise | boolean, -): Promise { + callback: (transactionEvent: SerializedEvent) => Promise | boolean, +): Promise { return new Promise((resolve, reject) => { waitForEnvelopeItem(proxyServerName, async envelopeItem => { const [envelopeItemHeader, envelopeItemBody] = envelopeItem; - if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as Event))) { - resolve(envelopeItemBody as Event); + if (envelopeItemHeader.type === 'transaction' && (await callback(envelopeItemBody as SerializedEvent))) { + resolve(envelopeItemBody as SerializedEvent); return true; } return false; @@ -247,7 +247,7 @@ async function registerCallbackServerPort(serverName: string, port: string): Pro await writeFile(tmpFilePath, port, { encoding: 'utf8' }); } -async function retrieveCallbackServerPort(serverName: string): Promise { +function retrieveCallbackServerPort(serverName: string): Promise { const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return await readFile(tmpFilePath, 'utf8'); + return readFile(tmpFilePath, 'utf8'); } diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json index b55d9ff74df6..a323c3c415be 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json @@ -10,20 +10,19 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "test:prod": "TEST_ENV=production playwright test", - "test:dev": "TEST_ENV=development playwright test", - "test:build": "pnpm install && pnpm build", - "test:assert": "pnpm -v" + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:assert": "pnpm test:prod" }, "dependencies": { "@sentry/sveltekit": "latest || *" }, "devDependencies": { - "@playwright/test": "^1.27.1", + "@playwright/test": "^1.36.2", "@sentry/types": "latest || *", "@sentry/utils": "latest || *", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-node": "^2.0.0", - "@sveltejs/kit": "^2.0.0", + "@sveltejs/kit": "^2.5.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", "svelte": "^4.2.8", "svelte-check": "^3.6.0", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/playwright.config.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/playwright.config.ts index bfa29df7d549..5e028dc2e29a 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/playwright.config.ts @@ -1,13 +1,19 @@ import type { PlaywrightTestConfig } from '@playwright/test'; import { devices } from '@playwright/test'; +// Fix urls not resolving to localhost on Node v17+ +// See: https://github.com/axios/axios/issues/3821#issuecomment-1413727575 +import { setDefaultResultOrder } from 'dns'; +setDefaultResultOrder('ipv4first'); + const testEnv = process.env.TEST_ENV; if (!testEnv) { throw new Error('No test env defined'); } -const port = 3030; +const svelteKitPort = 3030; +const eventProxyPort = 3031; /** * See https://playwright.dev/docs/test-configuration. @@ -24,7 +30,8 @@ const config: PlaywrightTestConfig = { timeout: 10000, }, /* Run tests in files in parallel */ - fullyParallel: true, + fullyParallel: false, + workers: 1, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* `next dev` is incredibly buggy with the app dir */ @@ -36,7 +43,7 @@ const config: PlaywrightTestConfig = { /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ actionTimeout: 0, /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: `http://localhost:${port}`, + baseURL: `http://localhost:${svelteKitPort}`, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', @@ -55,15 +62,17 @@ const config: PlaywrightTestConfig = { /* Run your local dev server before starting the tests */ webServer: [ { - command: 'pnpm ts-node --esm start-event-proxy.ts', - port: 3031, + command: 'pnpm ts-node-script start-event-proxy.ts', + port: eventProxyPort, + reuseExistingServer: false, }, { command: testEnv === 'development' - ? `pnpm wait-port ${port} && pnpm dev --port ${port}` - : `pnpm wait-port ${port} && pnpm preview --port ${port}`, - port, + ? `pnpm wait-port ${eventProxyPort} && pnpm dev --port ${svelteKitPort}` + : `pnpm wait-port ${eventProxyPort} && PORT=${svelteKitPort} node build`, + port: svelteKitPort, + reuseExistingServer: false, }, ], }; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/app.html b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/app.html index 117bd026151a..435cf39f2268 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/app.html +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/app.html @@ -6,7 +6,7 @@ %sveltekit.head% - +
%sveltekit.body%
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/hooks.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/hooks.server.ts index ae99e0e0e7b4..2a2abbb870dd 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/hooks.server.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/hooks.server.ts @@ -9,10 +9,7 @@ Sentry.init({ tracesSampleRate: 1.0, }); -const myErrorHandler = ({ error, event }: any) => { - console.error('An error occurred on the server side:', error, event); -}; - -export const handleError = Sentry.handleErrorWithSentry(myErrorHandler); +// not logging anything to console to avoid noise in the test output +export const handleError = Sentry.handleErrorWithSentry(() => {}); export const handle = Sentry.sentryHandle(); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/+layout.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/+layout.svelte new file mode 100644 index 000000000000..08c4b6468a93 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/+layout.svelte @@ -0,0 +1,13 @@ + + +

Sveltekit E2E Test app

+
+ +
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/+page.svelte index 5982b0ae37dd..aeb12d58603f 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/+page.svelte +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/+page.svelte @@ -1,2 +1,26 @@

Welcome to SvelteKit

Visit kit.svelte.dev to read the documentation

+ + diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/api/users/+server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/api/users/+server.ts new file mode 100644 index 000000000000..d0e4371c594b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/api/users/+server.ts @@ -0,0 +1,3 @@ +export const GET = () => { + return new Response(JSON.stringify({ users: ['alice', 'bob', 'carol'] })); +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.svelte index fde274c60705..b27edb70053d 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.svelte +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/building/+page.svelte @@ -1,3 +1,18 @@ + +

Check Build

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/client-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/client-error/+page.svelte new file mode 100644 index 000000000000..ba6b464e9324 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/client-error/+page.svelte @@ -0,0 +1,9 @@ + + +

Client error

+ + diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-load-error/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-load-error/+page.server.ts new file mode 100644 index 000000000000..17dd53fb5bbb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-load-error/+page.server.ts @@ -0,0 +1,6 @@ +export const load = async () => { + throw new Error('Server Load Error'); + return { + msg: 'Hello World', + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-load-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-load-error/+page.svelte new file mode 100644 index 000000000000..3a0942971d06 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-load-error/+page.svelte @@ -0,0 +1,9 @@ + + +

Server load error

+ +

+ Message: {data.msg} +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+page.svelte new file mode 100644 index 000000000000..3d682e7e3462 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+page.svelte @@ -0,0 +1,9 @@ + + +

Server Route error

+ +

+ Message: {data.msg} +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+page.ts new file mode 100644 index 000000000000..298240827714 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+page.ts @@ -0,0 +1,7 @@ +export const load = async ({ fetch }) => { + const res = await fetch('/server-route-error'); + const data = await res.json(); + return { + msg: data, + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+server.ts new file mode 100644 index 000000000000..f1a4b94b7706 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/server-route-error/+server.ts @@ -0,0 +1,6 @@ +export const GET = async () => { + throw new Error('Server Route Error'); + return { + msg: 'Hello World', + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-error/+page.svelte new file mode 100644 index 000000000000..dc2d311a0ece --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-error/+page.svelte @@ -0,0 +1,17 @@ + + +

Universal load error

+ +

+ To trigger from client: Load on another route, then navigate to this route. +

+ +

+ To trigger from server: Load on this route +

+ +

+ Message: {data.msg} +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-error/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-error/+page.ts new file mode 100644 index 000000000000..3d72bf4a890f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-error/+page.ts @@ -0,0 +1,8 @@ +import { browser } from '$app/environment'; + +export const load = async () => { + throw new Error(`Universal Load Error (${browser ? 'browser' : 'server'})`); + return { + msg: 'Hello World', + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-fetch/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-fetch/+page.svelte new file mode 100644 index 000000000000..563c51e8c850 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-fetch/+page.svelte @@ -0,0 +1,14 @@ + + +

Fetching in universal load

+ +

Here's a list of a few users:

+ +
    + {#each data.users as user} +
  • {user}
  • + {/each} +
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-fetch/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-fetch/+page.ts new file mode 100644 index 000000000000..63c1ee68e1cb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/universal-load-fetch/+page.ts @@ -0,0 +1,5 @@ +export const load = async ({ fetch }) => { + const usersRes = await fetch('/api/users'); + const data = await usersRes.json(); + return { users: data.users }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/+page.server.ts new file mode 100644 index 000000000000..a34c5450f682 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/+page.server.ts @@ -0,0 +1,5 @@ +export const load = async () => { + return { + msg: 'Hi everyone!', + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/+page.svelte new file mode 100644 index 000000000000..aa804a4518fa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/+page.svelte @@ -0,0 +1,10 @@ + +

+ All Users: +

+ +

+ message: {data.msg} +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/[id]/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/[id]/+page.server.ts new file mode 100644 index 000000000000..9388f3927018 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/[id]/+page.server.ts @@ -0,0 +1,5 @@ +export const load = async ({ params }) => { + return { + msg: `This is a special message for user ${params.id}`, + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/[id]/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/[id]/+page.svelte new file mode 100644 index 000000000000..d348a8c57dad --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/src/routes/users/[id]/+page.svelte @@ -0,0 +1,14 @@ + + +

Route with dynamic params

+ +

+ User id: {$page.params.id} +

+ +

+ Secret message for user: {data.msg} +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.client.test.ts new file mode 100644 index 000000000000..7942b96b41b0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.client.test.ts @@ -0,0 +1,56 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '../event-proxy-server'; +import { waitForInitialPageload } from './utils'; + +test.describe('client-side errors', () => { + test('captures error thrown on click', async ({ page }) => { + await page.goto('/client-error'); + + await expect(page.getByText('Client error')).toBeVisible(); + + const errorEventPromise = waitForError('sveltekit-2', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Click Error'; + }); + + const clickPromise = page.getByText('Throw error').click(); + + const [errorEvent, _] = await Promise.all([errorEventPromise, clickPromise]); + + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + function: expect.stringContaining('HTMLButtonElement'), + lineno: 1, + in_app: true, + }), + ); + + expect(errorEvent.tags).toMatchObject({ runtime: 'browser' }); + }); + + test('captures universal load error', async ({ page }) => { + await waitForInitialPageload(page); + await page.reload(); + + const errorEventPromise = waitForError('sveltekit-2', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Universal Load Error (browser)'; + }); + + // navigating triggers the error on the client + await page.getByText('Universal Load error').click(); + + const errorEvent = await errorEventPromise; + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + const lastFrame = errorEventFrames?.[errorEventFrames?.length - 1]; + expect(lastFrame).toEqual( + expect.objectContaining({ + lineno: 1, + in_app: true, + }), + ); + + expect(errorEvent.tags).toMatchObject({ runtime: 'browser' }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.server.test.ts new file mode 100644 index 000000000000..b7966325560a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/errors.server.test.ts @@ -0,0 +1,68 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '../event-proxy-server'; + +test.describe('server-side errors', () => { + test('captures universal load error', async ({ page }) => { + const errorEventPromise = waitForError('sveltekit-2', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Universal Load Error (server)'; + }); + + await page.goto('/universal-load-error'); + + const errorEvent = await errorEventPromise; + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + function: 'load$1', + in_app: true, + }), + ); + + expect(errorEvent.tags).toMatchObject({ runtime: 'node' }); + }); + + test('captures server load error', async ({ page }) => { + const errorEventPromise = waitForError('sveltekit-2', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Server Load Error'; + }); + + await page.goto('/server-load-error'); + + const errorEvent = await errorEventPromise; + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + function: 'load$1', + in_app: true, + }), + ); + + expect(errorEvent.tags).toMatchObject({ runtime: 'node' }); + }); + + test('captures server route (GET) error', async ({ page }) => { + const errorEventPromise = waitForError('sveltekit-2', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Server Route Error'; + }); + + await page.goto('/server-route-error'); + + const errorEvent = await errorEventPromise; + const errorEventFrames = errorEvent.exception?.values?.[0]?.stacktrace?.frames; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + filename: expect.stringContaining('app:///_server.ts'), + function: 'GET', + in_app: true, + }), + ); + + expect(errorEvent.tags).toMatchObject({ + runtime: 'node', + transaction: 'GET /server-route-error', + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.test.ts new file mode 100644 index 000000000000..aed2040392e7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/performance.test.ts @@ -0,0 +1,187 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '../event-proxy-server'; +import { waitForInitialPageload } from './utils'; + +test.describe('performance events', () => { + test('capture a distributed pageload trace', async ({ page }) => { + await page.goto('/users/123xyz'); + + const clientTxnEventPromise = waitForTransaction('sveltekit-2', txnEvent => { + return txnEvent?.transaction === '/users/[id]'; + }); + + const serverTxnEventPromise = waitForTransaction('sveltekit-2', txnEvent => { + return txnEvent?.transaction === 'GET /users/[id]'; + }); + + const [clientTxnEvent, serverTxnEvent, _] = await Promise.all([ + clientTxnEventPromise, + serverTxnEventPromise, + expect(page.getByText('User id: 123xyz')).toBeVisible(), + ]); + + expect(clientTxnEvent).toMatchObject({ + transaction: '/users/[id]', + tags: { runtime: 'browser' }, + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.sveltekit', + }, + }, + }); + + expect(serverTxnEvent).toMatchObject({ + transaction: 'GET /users/[id]', + tags: { runtime: 'node' }, + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.sveltekit', + }, + }, + }); + + expect(clientTxnEvent.spans?.length).toBeGreaterThan(5); + + // connected trace + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverTxnEvent.contexts?.trace?.trace_id); + + // weird but server txn is parent of client txn + expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverTxnEvent.contexts?.trace?.span_id); + }); + + test('capture a distributed navigation trace', async ({ page }) => { + await waitForInitialPageload(page); + + const clientNavigationTxnEventPromise = waitForTransaction('sveltekit-2', txnEvent => { + return txnEvent?.transaction === '/users' && txnEvent.contexts?.trace?.op === 'navigation'; + }); + + const serverTxnEventPromise = waitForTransaction('sveltekit-2', txnEvent => { + return txnEvent?.transaction === 'GET /users'; + }); + + // navigation to page + const clickPromise = page.getByText('Route with Server Load').click(); + + const [clientTxnEvent, serverTxnEvent, _1, _2] = await Promise.all([ + clientNavigationTxnEventPromise, + serverTxnEventPromise, + clickPromise, + expect(page.getByText('Hi everyone')).toBeVisible(), + ]); + + expect(clientTxnEvent).toMatchObject({ + transaction: '/users', + tags: { runtime: 'browser' }, + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.sveltekit', + }, + }, + }); + + expect(serverTxnEvent).toMatchObject({ + transaction: 'GET /users', + tags: { runtime: 'node' }, + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.sveltekit', + }, + }, + }); + + // trace is connected + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverTxnEvent.contexts?.trace?.trace_id); + }); + + test('record client-side universal load fetch span and trace', async ({ page }) => { + await waitForInitialPageload(page); + + const clientNavigationTxnEventPromise = waitForTransaction('sveltekit-2', txnEvent => { + return txnEvent?.transaction === '/universal-load-fetch' && txnEvent.contexts?.trace?.op === 'navigation'; + }); + + // this transaction should be created because of the fetch call + // it should also be part of the trace + const serverTxnEventPromise = waitForTransaction('sveltekit-2', txnEvent => { + return txnEvent?.transaction === 'GET /api/users'; + }); + + // navigation to page + const clickPromise = page.getByText('Route with fetch in universal load').click(); + + const [clientTxnEvent, serverTxnEvent, _1, _2] = await Promise.all([ + clientNavigationTxnEventPromise, + serverTxnEventPromise, + clickPromise, + expect(page.getByText('alice')).toBeVisible(), + ]); + + expect(clientTxnEvent).toMatchObject({ + transaction: '/universal-load-fetch', + tags: { runtime: 'browser' }, + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.sveltekit', + }, + }, + }); + + expect(serverTxnEvent).toMatchObject({ + transaction: 'GET /api/users', + tags: { runtime: 'node' }, + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'http.server', + origin: 'auto.http.sveltekit', + }, + }, + }); + + // trace is connected + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverTxnEvent.contexts?.trace?.trace_id); + + const clientFetchSpan = clientTxnEvent.spans?.find(s => s.op === 'http.client'); + + expect(clientFetchSpan).toMatchObject({ + description: expect.stringMatching(/^GET.*\/api\/users/), + op: 'http.client', + origin: 'auto.http.browser', + data: { + url: expect.stringContaining('/api/users'), + type: 'fetch', + 'http.method': 'GET', + 'http.response.status_code': 200, + 'network.protocol.version': '1.1', + 'network.protocol.name': 'http', + 'http.request.redirect_start': expect.any(Number), + 'http.request.fetch_start': expect.any(Number), + 'http.request.domain_lookup_start': expect.any(Number), + 'http.request.domain_lookup_end': expect.any(Number), + 'http.request.connect_start': expect.any(Number), + 'http.request.secure_connection_start': expect.any(Number), + 'http.request.connection_end': expect.any(Number), + 'http.request.request_start': expect.any(Number), + 'http.request.response_start': expect.any(Number), + 'http.request.response_end': expect.any(Number), + }, + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/transaction.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/transaction.test.ts deleted file mode 100644 index 7d621af34dcf..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/transaction.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { expect, test } from '@playwright/test'; -import axios, { AxiosError } from 'axios'; -// @ts-expect-error ok ok -import { waitForTransaction } from '../event-proxy-server.ts'; - -const authToken = process.env.E2E_TEST_AUTH_TOKEN; -const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; -const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; -const EVENT_POLLING_TIMEOUT = 90_000; - -test('Sends a pageload transaction', async ({ page }) => { - const pageloadTransactionEventPromise = waitForTransaction('sveltekit', (transactionEvent: any) => { - return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/'; - }); - - await page.goto('/'); - - const transactionEvent = await pageloadTransactionEventPromise; - const transactionEventId = transactionEvent.event_id; - - await expect - .poll( - async () => { - try { - const response = await axios.get( - `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, - { headers: { Authorization: `Bearer ${authToken}` } }, - ); - - return response.status; - } catch (e) { - if (e instanceof AxiosError && e.response) { - if (e.response.status !== 404) { - throw e; - } else { - return e.response.status; - } - } else { - throw e; - } - } - }, - { - timeout: EVENT_POLLING_TIMEOUT, - }, - ) - .toBe(200); -}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/test/utils.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/utils.ts new file mode 100644 index 000000000000..b48b949abdd5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/test/utils.ts @@ -0,0 +1,49 @@ +import { Page } from '@playwright/test'; +import { waitForTransaction } from '../event-proxy-server'; + +/** + * Helper function that waits for the initial pageload to complete. + * + * This function + * - loads the given route ("/" by default) + * - waits for SvelteKit's hydration + * - waits for the pageload transaction to be sent (doesn't assert on it though) + * + * Useful for tests that test outcomes of _navigations_ after an initial pageload. + * Waiting on the pageload transaction excludes edge cases where navigations occur + * so quickly that the pageload idle transaction is still active. This might lead + * to cases where the routing span would be attached to the pageload transaction + * and hence eliminates a lot of flakiness. + * + */ +export async function waitForInitialPageload( + page: Page, + opts?: { route?: string; parameterizedRoute?: string; debug: boolean }, +) { + const route = opts?.route ?? '/'; + const txnName = opts?.parameterizedRoute ?? route; + const debug = opts?.debug ?? false; + + const clientPageloadTxnEventPromise = waitForTransaction('sveltekit-2', txnEvent => { + debug && + console.log({ + txn: txnEvent?.transaction, + op: txnEvent.contexts?.trace?.op, + trace: txnEvent.contexts?.trace?.trace_id, + span: txnEvent.contexts?.trace?.span_id, + parent: txnEvent.contexts?.trace?.parent_span_id, + }); + + return txnEvent?.transaction === txnName && txnEvent.contexts?.trace?.op === 'pageload'; + }); + + await Promise.all([ + page.goto(route), + // the test app adds the "hydrated" class to the body when hydrating + page.waitForSelector('body.hydrated'), + // also waiting for the initial pageload txn so that later navigations don't interfere + clientPageloadTxnEventPromise, + ]); + + debug && console.log('hydrated'); +}