diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/event-proxy-server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/event-proxy-server.ts index 66a9e744846e..1bc419bd0b4c 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/event-proxy-server.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/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, Event, SerializedEvent } from '@sentry/types'; import { parseEnvelope } from '@sentry/utils'; const readFile = util.promisify(fs.readFile); @@ -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; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/package.json b/dev-packages/e2e-tests/test-applications/sveltekit/package.json index ad6bf6456843..ea21787939c3 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit/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.41.1", "@sentry/types": "latest || *", "@sentry/utils": "latest || *", "@sveltejs/adapter-auto": "^2.0.0", "@sveltejs/adapter-node": "^1.2.4", - "@sveltejs/kit": "^1.5.0", + "@sveltejs/kit": "^1.30.3", "svelte": "^3.54.0", "svelte-check": "^3.0.1", "ts-node": "10.9.1", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/playwright.config.ts b/dev-packages/e2e-tests/test-applications/sveltekit/playwright.config.ts index bfa29df7d549..779757c8807f 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/playwright.config.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/playwright.config.ts @@ -8,6 +8,7 @@ if (!testEnv) { } const port = 3030; +const eventProxyPort = 3031; /** * See https://playwright.dev/docs/test-configuration. @@ -23,8 +24,9 @@ const config: PlaywrightTestConfig = { */ timeout: 10000, }, + workers: 1, /* Run tests in files in parallel */ - fullyParallel: true, + fullyParallel: false, /* 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 */ @@ -61,8 +63,8 @@ const config: PlaywrightTestConfig = { { command: testEnv === 'development' - ? `pnpm wait-port ${port} && pnpm dev --port ${port}` - : `pnpm wait-port ${port} && pnpm preview --port ${port}`, + ? `pnpm wait-port ${eventProxyPort} && pnpm dev --port ${port}` + : `pnpm wait-port ${eventProxyPort} && pnpm preview --port ${port}`, port, }, ], diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/app.html b/dev-packages/e2e-tests/test-applications/sveltekit/src/app.html index 117bd026151a..435cf39f2268 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/app.html +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/app.html @@ -6,7 +6,7 @@ %sveltekit.head% - +
%sveltekit.body%
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/hooks.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/hooks.server.ts index 2d9cb9b02328..375b8d2c170a 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/hooks.server.ts +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/hooks.server.ts @@ -9,9 +9,8 @@ Sentry.init({ tracesSampleRate: 1.0, }); -const myErrorHandler = ({ error, event }: any) => { - console.error('An error occurred on the server side:', error, event); -}; +// not logging anything to console to avoid noise in the test output +const myErrorHandler = ({ error, event }: any) => {}; export const handleError = Sentry.handleErrorWithSentry(myErrorHandler); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+layout.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+layout.svelte new file mode 100644 index 000000000000..8b7db6f720bf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+layout.svelte @@ -0,0 +1,10 @@ + + + diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+page.svelte index 5982b0ae37dd..aeb12d58603f 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/+page.svelte +++ b/dev-packages/e2e-tests/test-applications/sveltekit/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/src/routes/api/users/+server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/api/users/+server.ts new file mode 100644 index 000000000000..d0e4371c594b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/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/src/routes/client-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/client-error/+page.svelte new file mode 100644 index 000000000000..ba6b464e9324 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/client-error/+page.svelte @@ -0,0 +1,9 @@ + + +

Client error

+ + diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-error/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/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/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/src/routes/server-load-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-load-error/+page.svelte new file mode 100644 index 000000000000..3a0942971d06 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/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/src/routes/server-route-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+page.svelte new file mode 100644 index 000000000000..3d682e7e3462 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/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/src/routes/server-route-error/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+page.ts new file mode 100644 index 000000000000..298240827714 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/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/src/routes/server-route-error/+server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/server-route-error/+server.ts new file mode 100644 index 000000000000..f1a4b94b7706 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/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/src/routes/universal-load-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-error/+page.svelte new file mode 100644 index 000000000000..dc2d311a0ece --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/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/src/routes/universal-load-error/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-error/+page.ts new file mode 100644 index 000000000000..3d72bf4a890f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/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/src/routes/universal-load-fetch/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-fetch/+page.svelte new file mode 100644 index 000000000000..563c51e8c850 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/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/src/routes/universal-load-fetch/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/universal-load-fetch/+page.ts new file mode 100644 index 000000000000..63c1ee68e1cb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/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/src/routes/users/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/+page.server.ts new file mode 100644 index 000000000000..a34c5450f682 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/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/src/routes/users/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/+page.svelte new file mode 100644 index 000000000000..aa804a4518fa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/+page.svelte @@ -0,0 +1,10 @@ + +

+ All Users: +

+ +

+ message: {data.msg} +

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/[id]/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/[id]/+page.server.ts new file mode 100644 index 000000000000..9388f3927018 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/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/src/routes/users/[id]/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit/src/routes/users/[id]/+page.svelte new file mode 100644 index 000000000000..d348a8c57dad --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/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/test/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.client.test.ts new file mode 100644 index 000000000000..7cd1a545165b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.client.test.ts @@ -0,0 +1,55 @@ +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', 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', 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; + + expect(errorEventFrames?.[errorEventFrames?.length - 1]).toEqual( + expect.objectContaining({ + lineno: 1, + in_app: true, + }), + ); + + expect(errorEvent.tags).toMatchObject({ runtime: 'browser' }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts new file mode 100644 index 000000000000..5493659b19db --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/test/errors.server.test.ts @@ -0,0 +1,71 @@ +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', 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', + lineno: 3, + in_app: true, + }), + ); + + expect(errorEvent.tags).toMatchObject({ runtime: 'node' }); + }); + + test('captures server load error', async ({ page }) => { + const errorEventPromise = waitForError('sveltekit', 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', + lineno: 3, + in_app: true, + }), + ); + + expect(errorEvent.tags).toMatchObject({ runtime: 'node' }); + }); + + test('captures server route (GET) error', async ({ page }) => { + const errorEventPromise = waitForError('sveltekit', 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: 'app:///_server.ts.js', + function: 'GET', + lineno: 2, + in_app: true, + }), + ); + + expect(errorEvent.tags).toMatchObject({ + runtime: 'node', + transaction: 'GET /server-route-error', + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.test.ts new file mode 100644 index 000000000000..e0d3d16df1ab --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/test/performance.test.ts @@ -0,0 +1,124 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '../event-proxy-server.js'; +import { waitForInitialPageload } from '../utils.js'; + +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; + + expect(transactionEvent).toMatchObject({ + transaction: '/', + transaction_info: { + source: 'route', + }, + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.sveltekit', + }, + }, + tags: { + 'routing.instrumentation': '@sentry/sveltekit', + }, + }); +}); + +test('captures a distributed pageload trace', async ({ page }) => { + await page.goto('/users/123xyz'); + + const clientTxnEventPromise = waitForTransaction('sveltekit', txnEvent => { + return txnEvent?.transaction === '/users/[id]'; + }); + + const serverTxnEventPromise = waitForTransaction('sveltekit', txnEvent => { + return txnEvent?.transaction === 'GET /users/[id]'; + }); + + const [clientTxnEvent, serverTxnEvent] = await Promise.all([clientTxnEventPromise, serverTxnEventPromise]); + + 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', + }, + }, + }); + // 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('captures a distributed navigation trace', async ({ page }) => { + await waitForInitialPageload(page); + + const clientNavigationTxnEventPromise = waitForTransaction('sveltekit', txnEvent => { + return txnEvent?.transaction === '/users/[id]'; + }); + + const serverTxnEventPromise = waitForTransaction('sveltekit', txnEvent => { + return txnEvent?.transaction === 'GET /users/[id]'; + }); + + // navigation to page + const clickPromise = page.getByText('Route with Params').click(); + + const [clientTxnEvent, serverTxnEvent, _1] = await Promise.all([ + clientNavigationTxnEventPromise, + serverTxnEventPromise, + clickPromise, + ]); + + expect(clientTxnEvent).toMatchObject({ + transaction: '/users/[id]', + tags: { runtime: 'browser' }, + transaction_info: { source: 'route' }, + type: 'transaction', + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.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', + }, + }, + }); + + // trace is connected + expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverTxnEvent.contexts?.trace?.trace_id); +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit/test/transaction.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit/test/transaction.test.ts deleted file mode 100644 index 7d621af34dcf..000000000000 --- a/dev-packages/e2e-tests/test-applications/sveltekit/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/utils.ts b/dev-packages/e2e-tests/test-applications/sveltekit/utils.ts new file mode 100644 index 000000000000..2886873bb8fb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit/utils.ts @@ -0,0 +1,53 @@ +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', 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, + ]); + + // let's add a buffer because it seems like the hydrated flag isn't enough :( + // guess: The layout finishes hydration/mounting before the components within finish + // await page.waitForTimeout(10_000); + + debug && console.log('hydrated'); +} diff --git a/package.json b/package.json index da7149544f46..9a226b3d8408 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "clean:build": "lerna run clean", "clean:caches": "yarn rimraf eslintcache .nxcache && yarn jest --clearCache", "clean:deps": "lerna clean --yes && rm -rf node_modules && yarn", - "clean:all": "run-s clean:build clean:caches clean:deps", + "clean:tarballs": "rimraf **/*.tgz", + "clean:all": "run-s clean:build clean:tarballs clean:caches clean:deps", "codecov": "codecov", "fix": "run-s fix:biome fix:prettier fix:lerna", "fix:lerna": "lerna run fix",