diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/.gitignore b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/.gitignore
new file mode 100644
index 000000000000..87c54ab857fc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/.gitignore
@@ -0,0 +1,15 @@
+.DS_Store
+node_modules
+/build
+/.svelte-kit
+/package
+.env
+.env.*
+!.env.example
+vite.config.js.timestamp-*
+vite.config.ts.timestamp-*
+
+#temporarily excluding my remote function routes. To be removed with the next PR:
+./src/routes/remote-functions/data.remote.ts
+./src/routes/remote-functions/+page.svelte
+./tests/tracing.remote-functions.ts
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/.npmrc b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/README.md b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/README.md
new file mode 100644
index 000000000000..684cabccfe02
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/README.md
@@ -0,0 +1,41 @@
+# create-svelte
+
+Everything you need to build a Svelte project, powered by
+[`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
+
+## Creating a project
+
+If you're seeing this, you've probably already done this step. Congrats!
+
+```bash
+# create a new project in the current directory
+npm create svelte@latest
+
+# create a new project in my-app
+npm create svelte@latest my-app
+```
+
+## Developing
+
+Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a
+development server:
+
+```bash
+npm run dev
+
+# or start the server and open the app in a new browser tab
+npm run dev -- --open
+```
+
+## Building
+
+To create a production version of your app:
+
+```bash
+npm run build
+```
+
+You can preview the production build with `npm run preview`.
+
+> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target
+> environment.
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json
new file mode 100644
index 000000000000..03e5441733da
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "sveltekit-2-kit-tracing",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "preview": "vite preview",
+ "proxy": "node start-event-proxy.mjs",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml",
+ "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:build": "pnpm install && pnpm build",
+ "test:assert": "pnpm test:prod"
+ },
+ "dependencies": {
+ "@sentry/sveltekit": "latest || *",
+ "@spotlightjs/spotlight": "2.0.0-alpha.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "~1.53.2",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "@sveltejs/adapter-node": "^5.3.1",
+ "@sveltejs/kit": "^2.31.0",
+ "@sveltejs/vite-plugin-svelte": "^6.1.3",
+ "svelte": "^5.38.3",
+ "svelte-check": "^4.3.1",
+ "tslib": "^2.4.1",
+ "typescript": "^5.0.0",
+ "vite": "^7.1.3"
+ },
+ "type": "module",
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/playwright.config.mjs
new file mode 100644
index 000000000000..6ae8142df247
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/playwright.config.mjs
@@ -0,0 +1,8 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const config = getPlaywrightConfig({
+ startCommand: 'ORIGIN=http://localhost:3030 node ./build/index.js',
+ port: 3030,
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/app.d.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/app.d.ts
new file mode 100644
index 000000000000..ede601ab93e2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/app.d.ts
@@ -0,0 +1,13 @@
+// See https://kit.svelte.dev/docs/types#app
+// for information about these interfaces
+declare global {
+ namespace App {
+ // interface Error {}
+ // interface Locals {}
+ // interface PageData {}
+ // interface PageState {}
+ // interface Platform {}
+ }
+}
+
+export {};
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/app.html b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/app.html
new file mode 100644
index 000000000000..77a5ff52c923
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/app.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ %sveltekit.head%
+
+
+ %sveltekit.body%
+
+
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/hooks.client.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/hooks.client.ts
new file mode 100644
index 000000000000..91592e7ab932
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/hooks.client.ts
@@ -0,0 +1,23 @@
+import { env } from '$env/dynamic/public';
+import * as Sentry from '@sentry/sveltekit';
+import * as Spotlight from '@spotlightjs/spotlight';
+
+Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: env.PUBLIC_E2E_TEST_DSN,
+ debug: !!env.PUBLIC_DEBUG,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1.0,
+});
+
+const myErrorHandler = ({ error, event }: any) => {
+ console.error('An error occurred on the client side:', error, event);
+};
+
+export const handleError = Sentry.handleErrorWithSentry(myErrorHandler);
+
+if (import.meta.env.DEV) {
+ Spotlight.init({
+ injectImmediately: true,
+ });
+}
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/hooks.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/hooks.server.ts
new file mode 100644
index 000000000000..de32b0f9901b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/hooks.server.ts
@@ -0,0 +1,12 @@
+import * as Sentry from '@sentry/sveltekit';
+import { setupSidecar } from '@spotlightjs/spotlight/sidecar';
+import { sequence } from '@sveltejs/kit/hooks';
+
+// not logging anything to console to avoid noise in the test output
+export const handleError = Sentry.handleErrorWithSentry(() => {});
+
+export const handle = sequence(Sentry.sentryHandle());
+
+if (import.meta.env.DEV) {
+ setupSidecar();
+}
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/instrumentation.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/instrumentation.server.ts
new file mode 100644
index 000000000000..136b51a44dee
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/instrumentation.server.ts
@@ -0,0 +1,11 @@
+import * as Sentry from '@sentry/sveltekit';
+import { E2E_TEST_DSN } from '$env/static/private';
+
+Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: E2E_TEST_DSN,
+ debug: !!process.env.DEBUG,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1.0,
+ spotlight: import.meta.env.DEV,
+});
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/+layout.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/+layout.svelte
new file mode 100644
index 000000000000..d1fadd2ea5a3
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/+layout.svelte
@@ -0,0 +1,15 @@
+
+
+Sveltekit E2E Test app
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/+page.svelte
new file mode 100644
index 000000000000..95b18ea87844
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/+page.svelte
@@ -0,0 +1,47 @@
+Welcome to SvelteKit 2 with Svelte 5!
+Visit kit.svelte.dev to read the documentation
+
+
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/api/users/+server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/api/users/+server.ts
new file mode 100644
index 000000000000..d0e4371c594b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/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-kit-tracing/src/routes/building/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/building/+page.server.ts
new file mode 100644
index 000000000000..b07376ba97c9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/building/+page.server.ts
@@ -0,0 +1,5 @@
+import type { PageServerLoad } from './$types';
+
+export const load = (async _event => {
+ return { name: 'building (server)' };
+}) satisfies PageServerLoad;
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/building/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/building/+page.svelte
new file mode 100644
index 000000000000..b27edb70053d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/building/+page.svelte
@@ -0,0 +1,21 @@
+
+
+Check Build
+
+
+ This route only exists to check that Typescript definitions
+ and auto instrumentation are working when the project is built.
+
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/building/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/building/+page.ts
new file mode 100644
index 000000000000..049acdc1fafa
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/building/+page.ts
@@ -0,0 +1,5 @@
+import type { PageLoad } from './$types';
+
+export const load = (async _event => {
+ return { name: 'building' };
+}) satisfies PageLoad;
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/client-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/client-error/+page.svelte
new file mode 100644
index 000000000000..ba6b464e9324
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/client-error/+page.svelte
@@ -0,0 +1,9 @@
+
+
+Client error
+
+
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/+page.svelte
new file mode 100644
index 000000000000..eff3fa3f2e8d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/+page.svelte
@@ -0,0 +1,15 @@
+
+Demonstrating Component Tracking
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component1.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component1.svelte
new file mode 100644
index 000000000000..a675711e4b68
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component1.svelte
@@ -0,0 +1,10 @@
+
+Howdy, I'm component 1
+
+
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component2.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component2.svelte
new file mode 100644
index 000000000000..2b2f38308077
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component2.svelte
@@ -0,0 +1,9 @@
+
+Howdy, I'm component 2
+
+
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component3.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component3.svelte
new file mode 100644
index 000000000000..9b4e028f78e7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/components/Component3.svelte
@@ -0,0 +1,6 @@
+
+
+Howdy, I'm component 3
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/form-action/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/form-action/+page.server.ts
new file mode 100644
index 000000000000..5efea8345851
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/form-action/+page.server.ts
@@ -0,0 +1,7 @@
+export const actions = {
+ default: async ({ request }) => {
+ const formData = await request.formData();
+ const name = formData.get('name');
+ return { name };
+ },
+};
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/form-action/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/form-action/+page.svelte
new file mode 100644
index 000000000000..25b71a1c9fc7
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/form-action/+page.svelte
@@ -0,0 +1,15 @@
+
+
+
+
+{#if form?.name}
+ Hello {form.name}
+{/if}
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/nav1/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/nav1/+page.svelte
new file mode 100644
index 000000000000..31abffc512a2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/nav1/+page.svelte
@@ -0,0 +1 @@
+Navigation 1
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/nav2/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/nav2/+page.svelte
new file mode 100644
index 000000000000..20b44bb32da9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/nav2/+page.svelte
@@ -0,0 +1 @@
+Navigation 2
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/redirect1/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/redirect1/+page.ts
new file mode 100644
index 000000000000..3f462bf810fd
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/redirect1/+page.ts
@@ -0,0 +1,5 @@
+import { redirect } from '@sveltejs/kit';
+
+export const load = async () => {
+ redirect(301, '/redirect2');
+};
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/redirect2/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/redirect2/+page.ts
new file mode 100644
index 000000000000..99a810761d18
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/redirect2/+page.ts
@@ -0,0 +1,5 @@
+import { redirect } from '@sveltejs/kit';
+
+export const load = async () => {
+ redirect(301, '/users/789');
+};
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-error/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/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-kit-tracing/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-kit-tracing/src/routes/server-load-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/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-kit-tracing/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-kit-tracing/src/routes/server-load-fetch/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-fetch/+page.server.ts
new file mode 100644
index 000000000000..709e52bcf351
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-fetch/+page.server.ts
@@ -0,0 +1,5 @@
+export const load = async ({ fetch }) => {
+ const res = await fetch('/api/users');
+ const data = await res.json();
+ return { data };
+};
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-fetch/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-fetch/+page.svelte
new file mode 100644
index 000000000000..f7f814d31b4d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-load-fetch/+page.svelte
@@ -0,0 +1,8 @@
+
+
+
+ Server Load Fetch
+ {JSON.stringify(data, null, 2)}
+
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/server-route-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/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-kit-tracing/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-kit-tracing/src/routes/server-route-error/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/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-kit-tracing/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-kit-tracing/src/routes/server-route-error/+server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/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-kit-tracing/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-kit-tracing/src/routes/universal-load-error/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/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-kit-tracing/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-kit-tracing/src/routes/universal-load-error/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/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-kit-tracing/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-kit-tracing/src/routes/universal-load-fetch/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/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-kit-tracing/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-kit-tracing/src/routes/universal-load-fetch/+page.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/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-kit-tracing/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-kit-tracing/src/routes/users/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/users/+page.server.ts
new file mode 100644
index 000000000000..a34c5450f682
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/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-kit-tracing/src/routes/users/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/users/+page.svelte
new file mode 100644
index 000000000000..aa804a4518fa
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/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-kit-tracing/src/routes/users/[id]/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/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-kit-tracing/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-kit-tracing/src/routes/users/[id]/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/src/routes/users/[id]/+page.svelte
new file mode 100644
index 000000000000..d348a8c57dad
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/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-kit-tracing/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/start-event-proxy.mjs
new file mode 100644
index 000000000000..eea0add65374
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'sveltekit-2-kit-tracing',
+});
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/static/favicon.png b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/static/favicon.png
new file mode 100644
index 000000000000..825b9e65af7c
Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/static/favicon.png differ
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/svelte.config.js b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/svelte.config.js
new file mode 100644
index 000000000000..a31271021354
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/svelte.config.js
@@ -0,0 +1,32 @@
+import adapter from '@sveltejs/adapter-node';
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ // Consult https://kit.svelte.dev/docs/integrations#preprocessors
+ // for more information about preprocessors
+ preprocess: vitePreprocess(),
+
+ kit: {
+ // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
+ // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
+ // See https://kit.svelte.dev/docs/adapters for more information about adapters.
+ adapter: adapter(),
+ experimental: {
+ instrumentation: {
+ server: true,
+ },
+ tracing: {
+ server: true,
+ },
+ remoteFunctions: true,
+ },
+ },
+ compilerOptions: {
+ experimental: {
+ async: true, // for remote functions
+ },
+ },
+};
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/errors.client.test.ts
new file mode 100644
index 000000000000..47c1e49b2b87
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/errors.client.test.ts
@@ -0,0 +1,56 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+import { waitForInitialPageload } from './utils';
+
+test.describe('client-side errors', () => {
+ test('captures error thrown on click', async ({ page }) => {
+ await waitForInitialPageload(page, { route: '/client-error' });
+
+ const errorEventPromise = waitForError('sveltekit-2-kit-tracing', errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'Click Error';
+ });
+
+ await page.getByText('Throw error').click();
+
+ await expect(errorEventPromise).resolves.toBeDefined();
+
+ const errorEvent = await errorEventPromise;
+
+ 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.transaction).toEqual('/client-error');
+ });
+
+ test('captures universal load error', async ({ page }) => {
+ await waitForInitialPageload(page);
+ await page.reload();
+
+ const errorEventPromise = waitForError('sveltekit-2-kit-tracing', 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.transaction).toEqual('/universal-load-error');
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/errors.server.test.ts
new file mode 100644
index 000000000000..e60531b43d6e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/errors.server.test.ts
@@ -0,0 +1,91 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test.describe('server-side errors', () => {
+ test('captures universal load error', async ({ page }) => {
+ const errorEventPromise = waitForError('sveltekit-2-kit-tracing', 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',
+ in_app: true,
+ }),
+ );
+
+ expect(errorEvent.request).toEqual({
+ cookies: {},
+ headers: expect.objectContaining({
+ accept: expect.any(String),
+ 'user-agent': expect.any(String),
+ }),
+ method: 'GET',
+ // SvelteKit's node adapter defaults to https in the protocol even if served on http
+ url: 'http://localhost:3030/universal-load-error',
+ });
+ });
+
+ test('captures server load error', async ({ page }) => {
+ const errorEventPromise = waitForError('sveltekit-2-kit-tracing', 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',
+ in_app: true,
+ }),
+ );
+
+ expect(errorEvent.request).toEqual({
+ cookies: {},
+ headers: expect.objectContaining({
+ accept: expect.any(String),
+ 'user-agent': expect.any(String),
+ }),
+ method: 'GET',
+ url: 'http://localhost:3030/server-load-error',
+ });
+ });
+
+ test('captures server route (GET) error', async ({ page }) => {
+ const errorEventPromise = waitForError('sveltekit-2-kit-tracing', 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.stringMatching(/app:\/\/\/_server.ts-.+.js/),
+ function: 'GET',
+ in_app: true,
+ }),
+ );
+
+ expect(errorEvent.transaction).toEqual('GET /server-route-error');
+
+ expect(errorEvent.request).toEqual({
+ cookies: {},
+ headers: expect.objectContaining({
+ accept: expect.any(String),
+ }),
+ method: 'GET',
+ url: 'http://localhost:3030/server-route-error',
+ });
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.client.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.client.test.ts
new file mode 100644
index 000000000000..e9f4a9c1425f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.client.test.ts
@@ -0,0 +1,115 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+import { waitForInitialPageload } from './utils';
+
+test.describe('client-specific performance events', () => {
+ test('multiple navigations have distinct traces', async ({ page }) => {
+ const navigationTxn1EventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => {
+ return txnEvent?.transaction === '/nav1' && txnEvent.contexts?.trace?.op === 'navigation';
+ });
+
+ const navigationTxn2EventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => {
+ return txnEvent?.transaction === '/' && txnEvent.contexts?.trace?.op === 'navigation';
+ });
+
+ const navigationTxn3EventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => {
+ return txnEvent?.transaction === '/nav2' && txnEvent.contexts?.trace?.op === 'navigation';
+ });
+
+ await waitForInitialPageload(page);
+
+ await page.getByText('Nav 1').click();
+ const navigationTxn1Event = await navigationTxn1EventPromise;
+
+ await page.goBack();
+ const navigationTxn2Event = await navigationTxn2EventPromise;
+
+ await page.getByText('Nav 2').click();
+ const navigationTxn3Event = await navigationTxn3EventPromise;
+
+ expect(navigationTxn1Event).toMatchObject({
+ transaction: '/nav1',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.sveltekit',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ },
+ },
+ });
+
+ expect(navigationTxn2Event).toMatchObject({
+ transaction: '/',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.sveltekit',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ },
+ },
+ });
+
+ expect(navigationTxn3Event).toMatchObject({
+ transaction: '/nav2',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.sveltekit',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ },
+ },
+ });
+
+ // traces should NOT be connected
+ expect(navigationTxn1Event.contexts?.trace?.trace_id).not.toBe(navigationTxn2Event.contexts?.trace?.trace_id);
+ expect(navigationTxn2Event.contexts?.trace?.trace_id).not.toBe(navigationTxn3Event.contexts?.trace?.trace_id);
+ expect(navigationTxn1Event.contexts?.trace?.trace_id).not.toBe(navigationTxn3Event.contexts?.trace?.trace_id);
+ });
+
+ test('records manually added component tracking spans', async ({ page }) => {
+ const componentTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => {
+ return txnEvent?.transaction === '/components';
+ });
+
+ await waitForInitialPageload(page);
+
+ await page.getByText('Component Tracking').click();
+
+ const componentTxnEvent = await componentTxnEventPromise;
+
+ expect(componentTxnEvent.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ data: { 'sentry.op': 'ui.svelte.init', 'sentry.origin': 'auto.ui.svelte' },
+ description: '',
+ op: 'ui.svelte.init',
+ origin: 'auto.ui.svelte',
+ }),
+ expect.objectContaining({
+ data: { 'sentry.op': 'ui.svelte.init', 'sentry.origin': 'auto.ui.svelte' },
+ description: '',
+ op: 'ui.svelte.init',
+ origin: 'auto.ui.svelte',
+ }),
+ expect.objectContaining({
+ data: { 'sentry.op': 'ui.svelte.init', 'sentry.origin': 'auto.ui.svelte' },
+ description: '',
+ op: 'ui.svelte.init',
+ origin: 'auto.ui.svelte',
+ }),
+ expect.objectContaining({
+ data: { 'sentry.op': 'ui.svelte.init', 'sentry.origin': 'auto.ui.svelte' },
+ description: '',
+ op: 'ui.svelte.init',
+ origin: 'auto.ui.svelte',
+ }),
+ ]),
+ );
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.server.test.ts
new file mode 100644
index 000000000000..c6de70d0e6a1
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.server.test.ts
@@ -0,0 +1,190 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/sveltekit';
+
+test('server pageload request span has nested request span for sub request', async ({ page }) => {
+ const serverTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => {
+ return txnEvent?.transaction === 'GET /server-load-fetch';
+ });
+
+ await page.goto('/server-load-fetch');
+
+ const serverTxnEvent = await serverTxnEventPromise;
+ const spans = serverTxnEvent.spans;
+
+ expect(serverTxnEvent).toMatchObject({
+ transaction: 'GET /server-load-fetch',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ contexts: {
+ trace: {
+ op: 'http.server',
+ origin: 'auto.http.sveltekit',
+ data: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
+ 'http.method': 'GET',
+ 'http.route': '/server-load-fetch',
+ 'sveltekit.tracing.original_name': 'sveltekit.handle.root',
+ },
+ },
+ },
+ });
+
+ expect(spans).toHaveLength(6);
+
+ expect(spans).toEqual(
+ expect.arrayContaining([
+ // initial resolve span:
+ expect.objectContaining({
+ data: expect.objectContaining({
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.sveltekit.resolve',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
+ 'http.route': '/server-load-fetch',
+ }),
+ op: 'function.sveltekit.resolve',
+ description: 'sveltekit.resolve',
+ origin: 'auto.http.sveltekit',
+ status: 'ok',
+ }),
+
+ // sequenced handler span:
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'sentry.origin': 'auto.function.sveltekit.handle',
+ 'sentry.op': 'function.sveltekit.handle',
+ }),
+ description: 'sveltekit.handle.sequenced.sentryRequestHandler',
+ op: 'function.sveltekit.handle',
+ origin: 'auto.function.sveltekit.handle',
+ status: 'ok',
+ }),
+
+ // load span where the server load function initiates the sub request:
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'http.route': '/server-load-fetch',
+ 'sentry.op': 'function.sveltekit.load',
+ 'sentry.origin': 'auto.function.sveltekit.load',
+ 'sveltekit.load.environment': 'server',
+ 'sveltekit.load.node_id': 'src/routes/server-load-fetch/+page.server.ts',
+ 'sveltekit.load.node_type': '+page.server',
+ }),
+ description: 'sveltekit.load',
+ op: 'function.sveltekit.load',
+ origin: 'auto.function.sveltekit.load',
+ status: 'ok',
+ }),
+
+ // sub request http.server span:
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'http.method': 'GET',
+ 'http.route': '/api/users',
+ 'http.url': 'http://localhost:3030/api/users',
+ 'sentry.op': 'http.server',
+ 'sentry.origin': 'auto.http.sveltekit',
+ 'sentry.source': 'route',
+ 'sveltekit.is_data_request': false,
+ 'sveltekit.is_sub_request': true,
+ 'sveltekit.tracing.original_name': 'sveltekit.handle.root',
+ url: 'http://localhost:3030/api/users',
+ }),
+ description: 'GET /api/users',
+ op: 'http.server',
+ origin: 'auto.http.sveltekit',
+ status: 'ok',
+ }),
+
+ // sub requestsequenced handler span:
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'sentry.origin': 'auto.function.sveltekit.handle',
+ 'sentry.op': 'function.sveltekit.handle',
+ }),
+ description: 'sveltekit.handle.sequenced.sentryRequestHandler',
+ op: 'function.sveltekit.handle',
+ origin: 'auto.function.sveltekit.handle',
+ status: 'ok',
+ }),
+
+ // sub request resolve span:
+ expect.objectContaining({
+ data: expect.objectContaining({
+ 'http.route': '/api/users',
+ 'sentry.op': 'function.sveltekit.resolve',
+ 'sentry.origin': 'auto.http.sveltekit',
+ }),
+ description: 'sveltekit.resolve',
+ op: 'function.sveltekit.resolve',
+ origin: 'auto.http.sveltekit',
+ status: 'ok',
+ }),
+ ]),
+ );
+
+ expect(serverTxnEvent.request).toEqual({
+ cookies: {},
+ headers: expect.objectContaining({
+ accept: expect.any(String),
+ 'user-agent': expect.any(String),
+ }),
+ method: 'GET',
+ url: 'http://localhost:3030/server-load-fetch',
+ });
+});
+
+test('server trace includes form action span', async ({ page }) => {
+ const serverTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => {
+ return txnEvent?.transaction === 'POST /form-action';
+ });
+
+ await page.goto('/form-action');
+
+ await page.locator('#inputName').fill('H4cktor');
+ await page.locator('#buttonSubmit').click();
+
+ const serverTxnEvent = await serverTxnEventPromise;
+
+ expect(serverTxnEvent).toMatchObject({
+ transaction: 'POST /form-action',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ contexts: {
+ trace: {
+ op: 'http.server',
+ origin: 'auto.http.sveltekit',
+ },
+ },
+ });
+
+ expect(serverTxnEvent.spans).toHaveLength(3);
+
+ expect(serverTxnEvent.spans).toEqual(
+ expect.arrayContaining([
+ // sequenced handler span
+ expect.objectContaining({
+ description: 'sveltekit.handle.sequenced.sentryRequestHandler',
+ op: 'function.sveltekit.handle',
+ origin: 'auto.function.sveltekit.handle',
+ }),
+
+ // resolve span
+ expect.objectContaining({
+ description: 'sveltekit.resolve',
+ op: 'function.sveltekit.resolve',
+ origin: 'auto.http.sveltekit',
+ }),
+
+ // form action span
+ expect.objectContaining({
+ description: 'sveltekit.form_action',
+ op: 'function.sveltekit.form_action',
+ origin: 'auto.function.sveltekit.action',
+ data: expect.objectContaining({
+ 'sveltekit.form_action.name': 'default',
+ }),
+ }),
+ ]),
+ );
+});
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.test.ts
new file mode 100644
index 000000000000..004182b32fb3
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/tracing.test.ts
@@ -0,0 +1,387 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+import { waitForInitialPageload } from './utils';
+
+test('capture a distributed pageload trace', async ({ page }) => {
+ const clientTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => {
+ return txnEvent?.transaction === '/users/[id]';
+ });
+
+ const serverTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => {
+ return txnEvent?.transaction === 'GET /users/[id]';
+ });
+
+ const [_, clientTxnEvent, serverTxnEvent] = await Promise.all([
+ page.goto('/users/123xyz'),
+ clientTxnEventPromise,
+ serverTxnEventPromise,
+ expect(page.getByText('User id: 123xyz')).toBeVisible(),
+ ]);
+
+ expect(clientTxnEvent).toMatchObject({
+ transaction: '/users/[id]',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ contexts: {
+ trace: {
+ op: 'pageload',
+ origin: 'auto.pageload.sveltekit',
+ },
+ },
+ });
+
+ expect(serverTxnEvent).toMatchObject({
+ transaction: 'GET /users/[id]',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ contexts: {
+ trace: {
+ op: 'http.server',
+ origin: 'auto.http.sveltekit',
+ },
+ },
+ });
+
+ expect(clientTxnEvent.spans?.length).toBeGreaterThan(5);
+
+ const serverKitResolveSpan = serverTxnEvent.spans?.find(s => s.description === 'sveltekit.resolve');
+ expect(serverKitResolveSpan).toMatchObject({
+ description: 'sveltekit.resolve',
+ op: 'function.sveltekit.resolve',
+ origin: 'auto.http.sveltekit',
+ status: 'ok',
+ });
+
+ // connected trace
+ expect(clientTxnEvent.contexts?.trace?.trace_id).toBe(serverTxnEvent.contexts?.trace?.trace_id);
+
+ // Sveltekit resolve span is the parent span of the client span
+ expect(clientTxnEvent.contexts?.trace?.parent_span_id).toBe(serverKitResolveSpan?.span_id);
+});
+
+test('capture a distributed navigation trace', async ({ page }) => {
+ const clientNavigationTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => {
+ return txnEvent?.transaction === '/users' && txnEvent.contexts?.trace?.op === 'navigation';
+ });
+
+ const serverTxnEventPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => {
+ return txnEvent?.transaction === 'GET /users';
+ });
+
+ await waitForInitialPageload(page);
+
+ // 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',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.sveltekit',
+ },
+ },
+ });
+
+ expect(serverTxnEvent).toMatchObject({
+ transaction: 'GET /users',
+ 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-kit-tracing', 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-kit-tracing', 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',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.sveltekit',
+ },
+ },
+ });
+
+ expect(serverTxnEvent).toMatchObject({
+ transaction: 'GET /api/users',
+ 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),
+ },
+ });
+});
+
+test('captures a navigation transaction directly after pageload', async ({ page }) => {
+ const clientPageloadTxnPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => {
+ return txnEvent?.contexts?.trace?.op === 'pageload';
+ });
+
+ const clientNavigationTxnPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => {
+ return txnEvent?.contexts?.trace?.op === 'navigation';
+ });
+
+ await waitForInitialPageload(page, { route: '/' });
+
+ const navigationClickPromise = page.locator('#routeWithParamsLink').click();
+
+ const [pageloadTxnEvent, navigationTxnEvent, _] = await Promise.all([
+ clientPageloadTxnPromise,
+ clientNavigationTxnPromise,
+ navigationClickPromise,
+ ]);
+
+ expect(pageloadTxnEvent).toMatchObject({
+ transaction: '/',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ contexts: {
+ trace: {
+ op: 'pageload',
+ origin: 'auto.pageload.sveltekit',
+ },
+ },
+ });
+
+ expect(navigationTxnEvent).toMatchObject({
+ transaction: '/users/[id]',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.sveltekit',
+ data: {
+ 'sentry.sveltekit.navigation.from': '/',
+ 'sentry.sveltekit.navigation.to': '/users/[id]',
+ 'sentry.sveltekit.navigation.type': 'link',
+ },
+ },
+ },
+ });
+
+ const routingSpans = navigationTxnEvent.spans?.filter(s => s.op === 'ui.sveltekit.routing');
+ expect(routingSpans).toHaveLength(1);
+
+ const routingSpan = routingSpans && routingSpans[0];
+ expect(routingSpan).toMatchObject({
+ op: 'ui.sveltekit.routing',
+ description: 'SvelteKit Route Change',
+ data: {
+ 'sentry.op': 'ui.sveltekit.routing',
+ 'sentry.origin': 'auto.ui.sveltekit',
+ 'sentry.sveltekit.navigation.from': '/',
+ 'sentry.sveltekit.navigation.to': '/users/[id]',
+ 'sentry.sveltekit.navigation.type': 'link',
+ },
+ });
+});
+
+test('captures one navigation transaction per redirect', async ({ page }) => {
+ const clientNavigationRedirect1TxnPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => {
+ return txnEvent?.contexts?.trace?.op === 'navigation' && txnEvent?.transaction === '/redirect1';
+ });
+
+ const clientNavigationRedirect2TxnPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => {
+ return txnEvent?.contexts?.trace?.op === 'navigation' && txnEvent?.transaction === '/redirect2';
+ });
+
+ const clientNavigationRedirect3TxnPromise = waitForTransaction('sveltekit-2-kit-tracing', txnEvent => {
+ return txnEvent?.contexts?.trace?.op === 'navigation' && txnEvent?.transaction === '/users/[id]';
+ });
+
+ await waitForInitialPageload(page, { route: '/' });
+
+ const navigationClickPromise = page.locator('#redirectLink').click();
+
+ const [redirect1TxnEvent, redirect2TxnEvent, redirect3TxnEvent, _] = await Promise.all([
+ clientNavigationRedirect1TxnPromise,
+ clientNavigationRedirect2TxnPromise,
+ clientNavigationRedirect3TxnPromise,
+ navigationClickPromise,
+ ]);
+
+ expect(redirect1TxnEvent).toMatchObject({
+ transaction: '/redirect1',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.sveltekit',
+ data: {
+ 'sentry.origin': 'auto.navigation.sveltekit',
+ 'sentry.op': 'navigation',
+ 'sentry.source': 'route',
+ 'sentry.sveltekit.navigation.type': 'link',
+ 'sentry.sveltekit.navigation.from': '/',
+ 'sentry.sveltekit.navigation.to': '/redirect1',
+ 'sentry.sample_rate': 1,
+ },
+ },
+ },
+ });
+
+ const redirect1Spans = redirect1TxnEvent.spans?.filter(s => s.op === 'ui.sveltekit.routing');
+ expect(redirect1Spans).toHaveLength(1);
+
+ const redirect1Span = redirect1Spans && redirect1Spans[0];
+ expect(redirect1Span).toMatchObject({
+ op: 'ui.sveltekit.routing',
+ description: 'SvelteKit Route Change',
+ data: {
+ 'sentry.op': 'ui.sveltekit.routing',
+ 'sentry.origin': 'auto.ui.sveltekit',
+ 'sentry.sveltekit.navigation.from': '/',
+ 'sentry.sveltekit.navigation.to': '/redirect1',
+ 'sentry.sveltekit.navigation.type': 'link',
+ },
+ });
+
+ expect(redirect2TxnEvent).toMatchObject({
+ transaction: '/redirect2',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.sveltekit',
+ data: {
+ 'sentry.origin': 'auto.navigation.sveltekit',
+ 'sentry.op': 'navigation',
+ 'sentry.source': 'route',
+ 'sentry.sveltekit.navigation.type': 'goto',
+ 'sentry.sveltekit.navigation.from': '/',
+ 'sentry.sveltekit.navigation.to': '/redirect2',
+ 'sentry.sample_rate': 1,
+ },
+ },
+ },
+ });
+
+ const redirect2Spans = redirect2TxnEvent.spans?.filter(s => s.op === 'ui.sveltekit.routing');
+ expect(redirect2Spans).toHaveLength(1);
+
+ const redirect2Span = redirect2Spans && redirect2Spans[0];
+ expect(redirect2Span).toMatchObject({
+ op: 'ui.sveltekit.routing',
+ description: 'SvelteKit Route Change',
+ data: {
+ 'sentry.op': 'ui.sveltekit.routing',
+ 'sentry.origin': 'auto.ui.sveltekit',
+ 'sentry.sveltekit.navigation.from': '/',
+ 'sentry.sveltekit.navigation.to': '/redirect2',
+ 'sentry.sveltekit.navigation.type': 'goto',
+ },
+ });
+
+ expect(redirect3TxnEvent).toMatchObject({
+ transaction: '/users/[id]',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.sveltekit',
+ data: {
+ 'sentry.origin': 'auto.navigation.sveltekit',
+ 'sentry.op': 'navigation',
+ 'sentry.source': 'route',
+ 'sentry.sveltekit.navigation.type': 'goto',
+ 'sentry.sveltekit.navigation.from': '/',
+ 'sentry.sveltekit.navigation.to': '/users/[id]',
+ 'sentry.sample_rate': 1,
+ },
+ },
+ },
+ });
+
+ const redirect3Spans = redirect3TxnEvent.spans?.filter(s => s.op === 'ui.sveltekit.routing');
+ expect(redirect3Spans).toHaveLength(1);
+
+ const redirect3Span = redirect3Spans && redirect3Spans[0];
+ expect(redirect3Span).toMatchObject({
+ op: 'ui.sveltekit.routing',
+ description: 'SvelteKit Route Change',
+ data: {
+ 'sentry.op': 'ui.sveltekit.routing',
+ 'sentry.origin': 'auto.ui.sveltekit',
+ 'sentry.sveltekit.navigation.from': '/',
+ 'sentry.sveltekit.navigation.to': '/users/[id]',
+ 'sentry.sveltekit.navigation.type': 'goto',
+ },
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/utils.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/utils.ts
new file mode 100644
index 000000000000..a628f558a4bf
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tests/utils.ts
@@ -0,0 +1,49 @@
+import { Page } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+/**
+ * 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-kit-tracing', 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');
+}
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tsconfig.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tsconfig.json
new file mode 100644
index 000000000000..ba6aa4e6610a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true,
+ "moduleResolution": "bundler"
+ },
+ // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
+ // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
+ //
+ // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
+ // from the referenced tsconfig.json - TypeScript does not merge them in
+}
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/vite.config.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/vite.config.ts
new file mode 100644
index 000000000000..1a410bee7e11
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-kit-tracing/vite.config.ts
@@ -0,0 +1,12 @@
+import { sentrySvelteKit } from '@sentry/sveltekit';
+import { sveltekit } from '@sveltejs/kit/vite';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [
+ sentrySvelteKit({
+ autoUploadSourceMaps: false,
+ }),
+ sveltekit(),
+ ],
+});
diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts
index 696c3d765c5b..3f5797efd211 100644
--- a/packages/sveltekit/src/server-common/handle.ts
+++ b/packages/sveltekit/src/server-common/handle.ts
@@ -7,10 +7,13 @@ import {
getDefaultIsolationScope,
getIsolationScope,
getTraceMetaTags,
+ SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
setHttpStatus,
+ spanToJSON,
startSpan,
+ updateSpanName,
winterCGRequestToRequestData,
withIsolationScope,
} from '@sentry/core';
@@ -88,11 +91,33 @@ export function isFetchProxyRequired(version: string): boolean {
return true;
}
+interface BackwardsForwardsCompatibleEvent {
+ /**
+ * For now taken from: https://github.com/sveltejs/kit/pull/13899
+ * Access to spans for tracing. If tracing is not enabled or the function is being run in the browser, these spans will do nothing.
+ * @since 2.31.0
+ */
+ tracing?: {
+ /** Whether tracing is enabled. */
+ enabled: boolean;
+ current: Span;
+ root: Span;
+ };
+}
+
async function instrumentHandle(
- { event, resolve }: Parameters[0],
+ {
+ event,
+ resolve,
+ }: {
+ event: Parameters[0]['event'] & BackwardsForwardsCompatibleEvent;
+ resolve: Parameters[0]['resolve'];
+ },
options: SentryHandleOptions,
): Promise {
- if (!event.route?.id && !options.handleUnknownRoutes) {
+ const routeId = event.route?.id;
+
+ if (!routeId && !options.handleUnknownRoutes) {
return resolve(event);
}
@@ -108,7 +133,7 @@ async function instrumentHandle(
}
}
- const routeName = `${event.request.method} ${event.route?.id || event.url.pathname}`;
+ const routeName = `${event.request.method} ${routeId || event.url.pathname}`;
if (getIsolationScope() !== getDefaultIsolationScope()) {
getIsolationScope().setTransactionName(routeName);
@@ -116,34 +141,72 @@ async function instrumentHandle(
DEBUG_BUILD && debug.warn('Isolation scope is default isolation scope - skipping setting transactionName');
}
+ // We only start a span if SvelteKit's native tracing is not enabled. Two reasons:
+ // - Used Kit version doesn't yet support tracing
+ // - Users didn't enable tracing
+ const kitTracingEnabled = event.tracing?.enabled;
+
try {
- const resolveResult = await startSpan(
- {
- op: 'http.server',
- attributes: {
+ const resolveWithSentry: (sentrySpan?: Span) => Promise = async (sentrySpan?: Span) => {
+ getCurrentScope().setSDKProcessingMetadata({
+ // We specifically avoid cloning the request here to avoid double read errors.
+ // We only read request headers so we're not consuming the body anyway.
+ // Note to future readers: This sounds counter-intuitive but please read
+ // https://github.com/getsentry/sentry-javascript/issues/14583
+ normalizedRequest: winterCGRequestToRequestData(event.request),
+ });
+ const kitRootSpan = event.tracing?.enabled ? event.tracing?.root : undefined;
+
+ if (kitRootSpan) {
+ // Update the root span emitted from SvelteKit to resemble a `http.server` span
+ // We're doing this here instead of an event processor to ensure we update the
+ // span name as early as possible (for dynamic sampling, et al.)
+ // Other spans are enhanced in the `processKitSpans` integration.
+ const spanJson = spanToJSON(kitRootSpan);
+ const kitRootSpanAttributes = spanJson.data;
+ const originalName = spanJson.description;
+
+ const routeName = kitRootSpanAttributes['http.route'];
+ if (routeName && typeof routeName === 'string') {
+ updateSpanName(kitRootSpan, `${event.request.method ?? 'GET'} ${routeName}`);
+ }
+
+ kitRootSpan.setAttributes({
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: event.route?.id ? 'route' : 'url',
- 'http.method': event.request.method,
- },
- name: routeName,
- },
- async (span?: Span) => {
- getCurrentScope().setSDKProcessingMetadata({
- // We specifically avoid cloning the request here to avoid double read errors.
- // We only read request headers so we're not consuming the body anyway.
- // Note to future readers: This sounds counter-intuitive but please read
- // https://github.com/getsentry/sentry-javascript/issues/14583
- normalizedRequest: winterCGRequestToRequestData(event.request),
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeName ? 'route' : 'url',
+ 'sveltekit.tracing.original_name': originalName,
});
- const res = await resolve(event, {
- transformPageChunk: addSentryCodeToPage({ injectFetchProxyScript: options.injectFetchProxyScript ?? true }),
- });
- if (span) {
- setHttpStatus(span, res.status);
- }
- return res;
- },
- );
+ }
+
+ const res = await resolve(event, {
+ transformPageChunk: addSentryCodeToPage({
+ injectFetchProxyScript: options.injectFetchProxyScript ?? true,
+ }),
+ });
+
+ if (sentrySpan) {
+ setHttpStatus(sentrySpan, res.status);
+ }
+
+ return res;
+ };
+
+ const resolveResult = kitTracingEnabled
+ ? await resolveWithSentry()
+ : await startSpan(
+ {
+ op: 'http.server',
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeId ? 'route' : 'url',
+ 'http.method': event.request.method,
+ },
+ name: routeName,
+ },
+ resolveWithSentry,
+ );
+
return resolveResult;
} catch (e: unknown) {
sendErrorToSentry(e, 'handle');
@@ -176,9 +239,12 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle {
};
const sentryRequestHandler: Handle = input => {
+ const backwardsForwardsCompatibleEvent = input.event as typeof input.event & BackwardsForwardsCompatibleEvent;
+
// Escape hatch to suppress request isolation and trace continuation (see initCloudflareSentryHandle)
const skipIsolation =
- '_sentrySkipRequestIsolation' in input.event.locals && input.event.locals._sentrySkipRequestIsolation;
+ '_sentrySkipRequestIsolation' in backwardsForwardsCompatibleEvent.locals &&
+ backwardsForwardsCompatibleEvent.locals._sentrySkipRequestIsolation;
// In case of a same-origin `fetch` call within a server`load` function,
// SvelteKit will actually just re-enter the `handle` function and set `isSubRequest`
@@ -187,7 +253,9 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle {
// currently active span instead of a new root span to correctly reflect this
// behavior.
if (skipIsolation || input.event.isSubRequest) {
- return instrumentHandle(input, options);
+ return instrumentHandle(input, {
+ ...options,
+ });
}
return withIsolationScope(isolationScope => {
@@ -200,6 +268,13 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle {
// https://github.com/getsentry/sentry-javascript/issues/14583
normalizedRequest: winterCGRequestToRequestData(input.event.request),
});
+
+ if (backwardsForwardsCompatibleEvent.tracing?.enabled) {
+ // if sveltekit tracing is enabled (since 2.31.0), trace continuation is handled by
+ // kit before our hook is executed. No noeed to call `continueTrace` from our end
+ return instrumentHandle(input, options);
+ }
+
return continueTrace(getTracePropagationData(input.event), () => instrumentHandle(input, options));
});
};
diff --git a/packages/sveltekit/src/server-common/rewriteFramesIntegration.ts b/packages/sveltekit/src/server-common/integrations/rewriteFramesIntegration.ts
similarity index 95%
rename from packages/sveltekit/src/server-common/rewriteFramesIntegration.ts
rename to packages/sveltekit/src/server-common/integrations/rewriteFramesIntegration.ts
index 412dd6f98815..9f6f0add6944 100644
--- a/packages/sveltekit/src/server-common/rewriteFramesIntegration.ts
+++ b/packages/sveltekit/src/server-common/integrations/rewriteFramesIntegration.ts
@@ -7,8 +7,8 @@ import {
join,
rewriteFramesIntegration as originalRewriteFramesIntegration,
} from '@sentry/core';
-import { WRAPPED_MODULE_SUFFIX } from '../common/utils';
-import type { GlobalWithSentryValues } from '../vite/injectGlobalValues';
+import { WRAPPED_MODULE_SUFFIX } from '../../common/utils';
+import type { GlobalWithSentryValues } from '../../vite/injectGlobalValues';
type StackFrameIteratee = (frame: StackFrame) => StackFrame;
interface RewriteFramesOptions {
diff --git a/packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts b/packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts
new file mode 100644
index 000000000000..5ab24a731279
--- /dev/null
+++ b/packages/sveltekit/src/server-common/integrations/svelteKitSpans.ts
@@ -0,0 +1,78 @@
+import type { Integration, SpanOrigin } from '@sentry/core';
+import { type SpanJSON, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
+
+/**
+ * A small integration that preprocesses spans so that SvelteKit-generated spans
+ * (via Kit's tracing feature since 2.31.0) get the correct Sentry attributes
+ * and data.
+ */
+export function svelteKitSpansIntegration(): Integration {
+ return {
+ name: 'SvelteKitSpansEnhancement',
+ // Using preprocessEvent to ensure the processing happens before user-configured
+ // event processors are executed
+ preprocessEvent(event) {
+ // only iterate over the spans if the root span was emitted by SvelteKit
+ // TODO: Right now, we can't optimize this to only check traces with a kit-emitted root span
+ // this is because in Cloudflare, the kit-emitted root span is missing but our cloudflare
+ // SDK emits the http.server span.
+ if (event.type === 'transaction') {
+ event.spans?.forEach(_enhanceKitSpan);
+ }
+ },
+ };
+}
+
+/**
+ * Adds sentry-specific attributes and data to a span emitted by SvelteKit's native tracing (since 2.31.0)
+ * @exported for testing
+ */
+export function _enhanceKitSpan(span: SpanJSON): void {
+ let op: string | undefined = undefined;
+ let origin: SpanOrigin | undefined = undefined;
+
+ const spanName = span.description;
+
+ const previousOp = span.op || span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP];
+ const previousOrigin = span.origin || span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN];
+
+ switch (spanName) {
+ case 'sveltekit.resolve':
+ op = 'function.sveltekit.resolve';
+ origin = 'auto.http.sveltekit';
+ break;
+ case 'sveltekit.load':
+ op = 'function.sveltekit.load';
+ origin = 'auto.function.sveltekit.load';
+ break;
+ case 'sveltekit.form_action':
+ op = 'function.sveltekit.form_action';
+ origin = 'auto.function.sveltekit.action';
+ break;
+ case 'sveltekit.remote.call':
+ op = 'function.sveltekit.remote';
+ origin = 'auto.rpc.sveltekit.remote';
+ break;
+ case 'sveltekit.handle.root':
+ // We don't want to overwrite the root handle span at this point since
+ // we already enhance the root span in our `sentryHandle` hook.
+ break;
+ default: {
+ if (spanName?.startsWith('sveltekit.handle.sequenced.')) {
+ op = 'function.sveltekit.handle';
+ origin = 'auto.function.sveltekit.handle';
+ }
+ break;
+ }
+ }
+
+ if (!previousOp && op) {
+ span.op = op;
+ span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP] = op;
+ }
+
+ if ((!previousOrigin || previousOrigin === 'manual') && origin) {
+ span.origin = origin;
+ span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] = origin;
+ }
+}
diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts
index 56400dcc5423..d287331df14d 100644
--- a/packages/sveltekit/src/server/index.ts
+++ b/packages/sveltekit/src/server/index.ts
@@ -53,7 +53,6 @@ export {
getTraceMetaTags,
graphqlIntegration,
hapiIntegration,
- httpIntegration,
// eslint-disable-next-line deprecation/deprecation
inboundFiltersIntegration,
eventFiltersIntegration,
@@ -141,6 +140,7 @@ export { wrapLoadWithSentry, wrapServerLoadWithSentry } from '../server-common/l
export { sentryHandle } from '../server-common/handle';
export { initCloudflareSentryHandle } from './handle';
export { wrapServerRouteWithSentry } from '../server-common/serverRoute';
+export { httpIntegration } from './integrations/http';
/**
* Tracks the Svelte component's initialization and mounting operation as well as
diff --git a/packages/sveltekit/src/server/integrations/http.ts b/packages/sveltekit/src/server/integrations/http.ts
new file mode 100644
index 000000000000..4d6844017d1d
--- /dev/null
+++ b/packages/sveltekit/src/server/integrations/http.ts
@@ -0,0 +1,39 @@
+import type { IntegrationFn } from '@sentry/core';
+import { httpIntegration as originalHttpIntegration } from '@sentry/node';
+
+type HttpOptions = Parameters[0];
+
+/**
+ * The http integration instruments Node's internal http and https modules.
+ * It creates breadcrumbs and spans for outgoing HTTP requests which will be attached to the currently active span.
+ *
+ * For SvelteKit, does not create spans for incoming requests but instead we use SvelteKit's own spans.
+ * If you need to create incoming spans, set the `disableIncomingRequestSpans` option to `false`.
+ * (You likely don't need this!)
+ *
+ */
+export const httpIntegration = ((options: HttpOptions = {}) => {
+ /*
+ * This is a slightly modified version of the original httpIntegration: We avoid creating
+ * incoming request spans because:
+ *
+ * - If Kit-tracing is available and enabled, we take the `sveltekit.handle.root` span
+ * as the root span and make it the `http.server` span. This gives us a single root
+ * span across all deployment plaftorms (while httpIntegration doesn't apply on e.g.
+ * AWS Lambda or edge)
+ * - If Kit-tracing is N/A or disabled and users follow the current/old docs, httpIntegration
+ * does nothing anyway, so this isn't a concern.
+ * - Which leaves the undocumented case that users --import an instrument.mjs file
+ * in which they initialize the SDK. IMHO it's fine to ignore this for now since it was
+ * well ... undocumented. Given in the future there won't be be an easy way for us
+ * to detect where the SDK is initialized, we should simply redirect users to use
+ * instrumentation.server.ts instead. If users want to, they can simply import and
+ * register `httpIntegration` and explicitly enable incoming request spans.
+ */
+
+ return originalHttpIntegration({
+ // We disable incoming request spans here, because otherwise we'd end up with duplicate spans.
+ disableIncomingRequestSpans: true,
+ ...options,
+ });
+}) satisfies IntegrationFn;
diff --git a/packages/sveltekit/src/server/sdk.ts b/packages/sveltekit/src/server/sdk.ts
index 19a0a8f9f5ad..fb7a5dbbb471 100644
--- a/packages/sveltekit/src/server/sdk.ts
+++ b/packages/sveltekit/src/server/sdk.ts
@@ -1,15 +1,24 @@
import { applySdkMetadata } from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { getDefaultIntegrations as getDefaultNodeIntegrations, init as initNodeSdk } from '@sentry/node';
-import { rewriteFramesIntegration } from '../server-common/rewriteFramesIntegration';
+import { rewriteFramesIntegration } from '../server-common/integrations/rewriteFramesIntegration';
+import { svelteKitSpansIntegration } from '../server-common/integrations/svelteKitSpans';
+import { httpIntegration } from './integrations/http';
/**
* Initialize the Server-side Sentry SDK
* @param options
*/
export function init(options: NodeOptions): NodeClient | undefined {
+ const defaultIntegrations = [
+ ...getDefaultNodeIntegrations(options).filter(integration => integration.name !== 'Http'),
+ rewriteFramesIntegration(),
+ httpIntegration(),
+ svelteKitSpansIntegration(),
+ ];
+
const opts = {
- defaultIntegrations: [...getDefaultNodeIntegrations(options), rewriteFramesIntegration()],
+ defaultIntegrations,
...options,
};
diff --git a/packages/sveltekit/src/vite/autoInstrument.ts b/packages/sveltekit/src/vite/autoInstrument.ts
index 63f4888257de..58862e452ddc 100644
--- a/packages/sveltekit/src/vite/autoInstrument.ts
+++ b/packages/sveltekit/src/vite/autoInstrument.ts
@@ -27,6 +27,7 @@ export type AutoInstrumentSelection = {
type AutoInstrumentPluginOptions = AutoInstrumentSelection & {
debug: boolean;
+ onlyInstrumentClient: boolean;
};
/**
@@ -41,12 +42,26 @@ type AutoInstrumentPluginOptions = AutoInstrumentSelection & {
export function makeAutoInstrumentationPlugin(options: AutoInstrumentPluginOptions): Plugin {
const { load: wrapLoadEnabled, serverLoad: wrapServerLoadEnabled, debug } = options;
+ let isServerBuild: boolean | undefined = undefined;
+
return {
name: 'sentry-auto-instrumentation',
// This plugin needs to run as early as possible, before the SvelteKit plugin virtualizes all paths and ids
enforce: 'pre',
+ configResolved: config => {
+ // The SvelteKit plugins trigger additional builds within the main (SSR) build.
+ // We just need a mechanism to upload source maps only once.
+ // `config.build.ssr` is `true` for that first build and `false` in the other ones.
+ // Hence we can use it as a switch to upload source maps only once in main build.
+ isServerBuild = !!config.build.ssr;
+ },
+
async load(id) {
+ if (options.onlyInstrumentClient && isServerBuild) {
+ return null;
+ }
+
const applyUniversalLoadWrapper =
wrapLoadEnabled &&
/^\+(page|layout)\.(js|ts|mjs|mts)$/.test(path.basename(id)) &&
@@ -58,6 +73,12 @@ export function makeAutoInstrumentationPlugin(options: AutoInstrumentPluginOptio
return getWrapperCode('wrapLoadWithSentry', `${id}${WRAPPED_MODULE_SUFFIX}`);
}
+ if (options.onlyInstrumentClient) {
+ // Now that we've checked universal files, we can early return and avoid further
+ // regexp checks below for server-only files, in case `onlyInstrumentClient` is `true`.
+ return null;
+ }
+
const applyServerLoadWrapper =
wrapServerLoadEnabled &&
/^\+(page|layout)\.server\.(js|ts|mjs|mts)$/.test(path.basename(id)) &&
diff --git a/packages/sveltekit/src/vite/injectGlobalValues.ts b/packages/sveltekit/src/vite/injectGlobalValues.ts
index 96ad05123ce6..20f446b6b46f 100644
--- a/packages/sveltekit/src/vite/injectGlobalValues.ts
+++ b/packages/sveltekit/src/vite/injectGlobalValues.ts
@@ -1,4 +1,8 @@
-import type { InternalGlobal } from '@sentry/core';
+import { type InternalGlobal, escapeStringForRegex } from '@sentry/core';
+import MagicString from 'magic-string';
+import type { Plugin } from 'vite';
+import { type BackwardsForwardsCompatibleSvelteConfig, getAdapterOutputDir, getHooksFileName } from './svelteConfig';
+import type { SentrySvelteKitPluginOptions } from './types';
export type GlobalSentryValues = {
__sentry_sveltekit_output_dir?: string;
@@ -27,3 +31,71 @@ export function getGlobalValueInjectionCode(globalSentryValues: GlobalSentryValu
return `${injectedValuesCode}\n`;
}
+
+/**
+ * Injects SvelteKit app configuration values the svelte.config.js into the
+ * server's global object so that the SDK can pick up the information at runtime
+ */
+export async function makeGlobalValuesInjectionPlugin(
+ svelteConfig: BackwardsForwardsCompatibleSvelteConfig,
+ options: Pick,
+): Promise {
+ const { adapter = 'other', debug = false } = options;
+
+ const serverHooksFile = getHooksFileName(svelteConfig, 'server');
+ const adapterOutputDir = await getAdapterOutputDir(svelteConfig, adapter);
+
+ const globalSentryValues: GlobalSentryValues = {
+ __sentry_sveltekit_output_dir: adapterOutputDir,
+ };
+
+ if (debug) {
+ // eslint-disable-next-line no-console
+ console.log('[Sentry SvelteKit] Global values:', globalSentryValues);
+ }
+
+ // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- not end user input + escaped anyway
+ const hooksFileRegexp = new RegExp(`/${escapeStringForRegex(serverHooksFile)}(.(js|ts|mjs|mts))?`);
+
+ return {
+ name: 'sentry-sveltekit-global-values-injection-plugin',
+ resolveId: (id, _importer, _ref) => {
+ if (id === VIRTUAL_GLOBAL_VALUES_FILE) {
+ return {
+ id: VIRTUAL_GLOBAL_VALUES_FILE,
+ external: false,
+ moduleSideEffects: true,
+ };
+ }
+ return null;
+ },
+
+ load: id => {
+ if (id === VIRTUAL_GLOBAL_VALUES_FILE) {
+ return {
+ code: getGlobalValueInjectionCode(globalSentryValues),
+ };
+ }
+ return null;
+ },
+
+ transform: async (code, id) => {
+ const isServerEntryFile = /instrumentation\.server\./.test(id) || hooksFileRegexp.test(id);
+
+ if (isServerEntryFile) {
+ if (debug) {
+ // eslint-disable-next-line no-console
+ console.log('[Global Values Plugin] Injecting global values into', id);
+ }
+ const ms = new MagicString(code);
+ ms.append(`\n; import "${VIRTUAL_GLOBAL_VALUES_FILE}";\n`);
+ return {
+ code: ms.toString(),
+ map: ms.generateMap({ hires: true }),
+ };
+ }
+
+ return null;
+ },
+ };
+}
diff --git a/packages/sveltekit/src/vite/sentryVitePlugins.ts b/packages/sveltekit/src/vite/sentryVitePlugins.ts
index 4444ba9a6ab7..61b388a94cf2 100644
--- a/packages/sveltekit/src/vite/sentryVitePlugins.ts
+++ b/packages/sveltekit/src/vite/sentryVitePlugins.ts
@@ -2,7 +2,9 @@ import type { Plugin } from 'vite';
import type { AutoInstrumentSelection } from './autoInstrument';
import { makeAutoInstrumentationPlugin } from './autoInstrument';
import { detectAdapter } from './detectAdapter';
+import { makeGlobalValuesInjectionPlugin } from './injectGlobalValues';
import { makeCustomSentryVitePlugins } from './sourceMaps';
+import { loadSvelteConfig } from './svelteConfig';
import type { CustomSentryVitePluginOptions, SentrySvelteKitPluginOptions } from './types';
const DEFAULT_PLUGIN_OPTIONS: SentrySvelteKitPluginOptions = {
@@ -25,9 +27,14 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {}
adapter: options.adapter || (await detectAdapter(options.debug)),
};
+ const svelteConfig = await loadSvelteConfig();
+
const sentryPlugins: Plugin[] = [];
if (mergedOptions.autoInstrument) {
+ // TODO: Once tracing is promoted stable, we need to adjust this check!
+ const kitTracingEnabled = !!svelteConfig.kit?.experimental?.tracing?.server;
+
const pluginOptions: AutoInstrumentSelection = {
load: true,
serverLoad: true,
@@ -38,15 +45,26 @@ export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {}
makeAutoInstrumentationPlugin({
...pluginOptions,
debug: options.debug || false,
+ // if kit-internal tracing is enabled, we only want to wrap and instrument client-side code.
+ onlyInstrumentClient: kitTracingEnabled,
}),
);
}
const sentryVitePluginsOptions = generateVitePluginOptions(mergedOptions);
- if (sentryVitePluginsOptions) {
- const sentryVitePlugins = await makeCustomSentryVitePlugins(sentryVitePluginsOptions);
+ if (mergedOptions.autoUploadSourceMaps) {
+ // When source maps are enabled, we need to inject the output directory to get a correct
+ // stack trace, by using this SDK's `rewriteFrames` integration.
+ // This integration picks up the value.
+ // TODO: I don't think this is technically correct. Either we always or never inject the output directory.
+ // Stack traces shouldn't be different, depending on source maps config. With debugIds, we might not even
+ // need to rewrite frames anymore.
+ sentryPlugins.push(await makeGlobalValuesInjectionPlugin(svelteConfig, mergedOptions));
+ }
+ if (sentryVitePluginsOptions) {
+ const sentryVitePlugins = await makeCustomSentryVitePlugins(sentryVitePluginsOptions, svelteConfig);
sentryPlugins.push(...sentryVitePlugins);
}
diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts
index eb3b449144f8..57bf21055bf1 100644
--- a/packages/sveltekit/src/vite/sourceMaps.ts
+++ b/packages/sveltekit/src/vite/sourceMaps.ts
@@ -1,17 +1,14 @@
-/* eslint-disable max-lines */
import { escapeStringForRegex, uuid4 } from '@sentry/core';
import { getSentryRelease } from '@sentry/node';
import type { SentryVitePluginOptions } from '@sentry/vite-plugin';
import { sentryVitePlugin } from '@sentry/vite-plugin';
import * as child_process from 'child_process';
import * as fs from 'fs';
-import MagicString from 'magic-string';
import * as path from 'path';
import type { Plugin, UserConfig } from 'vite';
import { WRAPPED_MODULE_SUFFIX } from '../common/utils';
-import type { GlobalSentryValues } from './injectGlobalValues';
-import { getGlobalValueInjectionCode, VIRTUAL_GLOBAL_VALUES_FILE } from './injectGlobalValues';
-import { getAdapterOutputDir, getHooksFileName, loadSvelteConfig } from './svelteConfig';
+import type { BackwardsForwardsCompatibleSvelteConfig } from './svelteConfig';
+import { getAdapterOutputDir } from './svelteConfig';
import type { CustomSentryVitePluginOptions } from './types';
// sorcery has no types, so these are some basic type definitions:
@@ -45,9 +42,10 @@ type FilesToDeleteAfterUpload = string | string[] | undefined;
*
* @returns the custom Sentry Vite plugin
*/
-export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePluginOptions): Promise {
- const svelteConfig = await loadSvelteConfig();
-
+export async function makeCustomSentryVitePlugins(
+ options: CustomSentryVitePluginOptions,
+ svelteConfig: BackwardsForwardsCompatibleSvelteConfig,
+): Promise {
const usedAdapter = options?.adapter || 'other';
const adapterOutputDir = await getAdapterOutputDir(svelteConfig, usedAdapter);
@@ -149,12 +147,6 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug
let isSSRBuild = true;
- const serverHooksFile = getHooksFileName(svelteConfig, 'server');
-
- const globalSentryValues: GlobalSentryValues = {
- __sentry_sveltekit_output_dir: adapterOutputDir,
- };
-
const sourceMapSettingsPlugin: Plugin = {
name: 'sentry-sveltekit-update-source-map-setting-plugin',
apply: 'build', // only apply this plugin at build time
@@ -202,26 +194,6 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug
name: 'sentry-sveltekit-debug-id-upload-plugin',
apply: 'build', // only apply this plugin at build time
enforce: 'post', // this needs to be set to post, otherwise we don't pick up the output from the SvelteKit adapter
- resolveId: (id, _importer, _ref) => {
- if (id === VIRTUAL_GLOBAL_VALUES_FILE) {
- return {
- id: VIRTUAL_GLOBAL_VALUES_FILE,
- external: false,
- moduleSideEffects: true,
- };
- }
- return null;
- },
-
- load: id => {
- if (id === VIRTUAL_GLOBAL_VALUES_FILE) {
- return {
- code: getGlobalValueInjectionCode(globalSentryValues),
- };
- }
- return null;
- },
-
configResolved: config => {
// The SvelteKit plugins trigger additional builds within the main (SSR) build.
// We just need a mechanism to upload source maps only once.
@@ -232,22 +204,6 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug
}
},
- transform: async (code, id) => {
- // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- not end user input + escaped anyway
- const isServerHooksFile = new RegExp(`/${escapeStringForRegex(serverHooksFile)}(.(js|ts|mjs|mts))?`).test(id);
-
- if (isServerHooksFile) {
- const ms = new MagicString(code);
- ms.append(`\n; import "${VIRTUAL_GLOBAL_VALUES_FILE}";\n`);
- return {
- code: ms.toString(),
- map: ms.generateMap({ hires: true }),
- };
- }
-
- return null;
- },
-
// We need to start uploading source maps later than in the original plugin
// because SvelteKit is invoking the adapter at closeBundle.
// This means that we need to wait until the adapter is done before we start uploading.
diff --git a/packages/sveltekit/src/vite/svelteConfig.ts b/packages/sveltekit/src/vite/svelteConfig.ts
index 34874bfd2f97..f1e908af9c93 100644
--- a/packages/sveltekit/src/vite/svelteConfig.ts
+++ b/packages/sveltekit/src/vite/svelteConfig.ts
@@ -4,12 +4,32 @@ import * as path from 'path';
import * as url from 'url';
import type { SupportedSvelteKitAdapters } from './detectAdapter';
+export type SvelteKitTracingConfig = {
+ tracing?: {
+ server: boolean;
+ };
+ // TODO: Once instrumentation is promoted stable, this will be removed!
+ instrumentation?: {
+ server: boolean;
+ };
+};
+
+/**
+ * Experimental tracing and instrumentation config is available @since 2.31.0
+ * // TODO: Once instrumentation and tracing is promoted stable, adjust this type!s
+ */
+type BackwardsForwardsCompatibleKitConfig = Config['kit'] & { experimental?: SvelteKitTracingConfig };
+
+export interface BackwardsForwardsCompatibleSvelteConfig extends Config {
+ kit?: BackwardsForwardsCompatibleKitConfig;
+}
+
/**
* Imports the svelte.config.js file and returns the config object.
* The sveltekit plugins import the config in the same way.
* See: https://github.com/sveltejs/kit/blob/master/packages/kit/src/core/config/index.js#L63
*/
-export async function loadSvelteConfig(): Promise {
+export async function loadSvelteConfig(): Promise {
// This can only be .js (see https://github.com/sveltejs/kit/pull/4031#issuecomment-1049475388)
const SVELTE_CONFIG_FILE = 'svelte.config.js';
@@ -23,7 +43,7 @@ export async function loadSvelteConfig(): Promise {
const svelteConfigModule = await import(`${url.pathToFileURL(configFile).href}`);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
- return (svelteConfigModule?.default as Config) || {};
+ return (svelteConfigModule?.default as BackwardsForwardsCompatibleSvelteConfig) || {};
} catch (e) {
// eslint-disable-next-line no-console
console.warn("[Source Maps Plugin] Couldn't load svelte.config.js:");
diff --git a/packages/sveltekit/src/vite/types.ts b/packages/sveltekit/src/vite/types.ts
index 0f6717a2c7e9..4267ce378bb1 100644
--- a/packages/sveltekit/src/vite/types.ts
+++ b/packages/sveltekit/src/vite/types.ts
@@ -219,6 +219,7 @@ export type SentrySvelteKitPluginOptions = {
* @default true`.
*/
autoUploadSourceMaps?: boolean;
+
/**
* Options related to source maps upload to Sentry
*/
diff --git a/packages/sveltekit/src/worker/cloudflare.ts b/packages/sveltekit/src/worker/cloudflare.ts
index b27ceba87780..612b174f6c69 100644
--- a/packages/sveltekit/src/worker/cloudflare.ts
+++ b/packages/sveltekit/src/worker/cloudflare.ts
@@ -6,7 +6,8 @@ import {
} from '@sentry/cloudflare';
import { addNonEnumerableProperty } from '@sentry/core';
import type { Handle } from '@sveltejs/kit';
-import { rewriteFramesIntegration } from '../server-common/rewriteFramesIntegration';
+import { rewriteFramesIntegration } from '../server-common/integrations/rewriteFramesIntegration';
+import { svelteKitSpansIntegration } from '../server-common/integrations/svelteKitSpans';
/**
* Initializes Sentry SvelteKit Cloudflare SDK
@@ -16,7 +17,11 @@ import { rewriteFramesIntegration } from '../server-common/rewriteFramesIntegrat
*/
export function initCloudflareSentryHandle(options: CloudflareOptions): Handle {
const opts: CloudflareOptions = {
- defaultIntegrations: [...getDefaultCloudflareIntegrations(options), rewriteFramesIntegration()],
+ defaultIntegrations: [
+ ...getDefaultCloudflareIntegrations(options),
+ rewriteFramesIntegration(),
+ svelteKitSpansIntegration(),
+ ],
...options,
};
diff --git a/packages/sveltekit/test/server-common/handle.test.ts b/packages/sveltekit/test/server-common/handle.test.ts
index 79c0f88e0b5d..db1e1fe4811f 100644
--- a/packages/sveltekit/test/server-common/handle.test.ts
+++ b/packages/sveltekit/test/server-common/handle.test.ts
@@ -111,7 +111,7 @@ describe('sentryHandle', () => {
[Type.Async, true, undefined],
[Type.Async, false, mockResponse],
])('%s resolve with error %s', (type, isError, mockResponse) => {
- it('should return a response', async () => {
+ it('returns a response', async () => {
let response: any = undefined;
try {
response = await sentryHandle()({ event: mockEvent(), resolve: resolve(type, isError) });
@@ -123,7 +123,7 @@ describe('sentryHandle', () => {
expect(response).toEqual(mockResponse);
});
- it("creates a transaction if there's no active span", async () => {
+ it("starts a span if there's no active span", async () => {
let _span: Span | undefined = undefined;
client.on('spanEnd', span => {
if (span === getRootSpan(span)) {
@@ -150,7 +150,27 @@ describe('sentryHandle', () => {
expect(spans).toHaveLength(1);
});
- it('creates a child span for nested server calls (i.e. if there is an active span)', async () => {
+ it("doesn't start a span if sveltekit tracing is enabled", async () => {
+ let _span: Span | undefined = undefined;
+ client.on('spanEnd', span => {
+ if (span === getRootSpan(span)) {
+ _span = span;
+ }
+ });
+
+ try {
+ await sentryHandle()({
+ event: mockEvent({ tracing: { enabled: true } }),
+ resolve: resolve(type, isError),
+ });
+ } catch {
+ //
+ }
+
+ expect(_span).toBeUndefined();
+ });
+
+ it('starts a child span for nested server calls (i.e. if there is an active span)', async () => {
let _span: Span | undefined = undefined;
let txnCount = 0;
client.on('spanEnd', span => {
@@ -197,7 +217,7 @@ describe('sentryHandle', () => {
);
});
- it("creates a transaction from sentry-trace header but doesn't populate a new DSC", async () => {
+ it("starts a span from sentry-trace header but doesn't populate a new DSC", async () => {
const event = mockEvent({
request: {
headers: {
diff --git a/packages/sveltekit/test/server-common/rewriteFramesIntegration.test.ts b/packages/sveltekit/test/server-common/integrations/rewriteFramesIntegration.test.ts
similarity index 93%
rename from packages/sveltekit/test/server-common/rewriteFramesIntegration.test.ts
rename to packages/sveltekit/test/server-common/integrations/rewriteFramesIntegration.test.ts
index 20f9c52a8e27..836152a81eb0 100644
--- a/packages/sveltekit/test/server-common/rewriteFramesIntegration.test.ts
+++ b/packages/sveltekit/test/server-common/integrations/rewriteFramesIntegration.test.ts
@@ -2,8 +2,8 @@ import { rewriteFramesIntegration } from '@sentry/browser';
import type { Event, StackFrame } from '@sentry/core';
import { basename } from '@sentry/core';
import { describe, expect, it } from 'vitest';
-import { rewriteFramesIteratee } from '../../src/server-common/rewriteFramesIntegration';
-import type { GlobalWithSentryValues } from '../../src/vite/injectGlobalValues';
+import { rewriteFramesIteratee } from '../../../src/server-common/integrations/rewriteFramesIntegration';
+import type { GlobalWithSentryValues } from '../../../src/vite/injectGlobalValues';
describe('rewriteFramesIteratee', () => {
it('removes the module property from the frame', () => {
diff --git a/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts b/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts
new file mode 100644
index 000000000000..0d95cb3d6fb6
--- /dev/null
+++ b/packages/sveltekit/test/server-common/integrations/svelteKitSpans.test.ts
@@ -0,0 +1,172 @@
+import type { SpanJSON, TransactionEvent } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
+import { describe, expect, it } from 'vitest';
+import { _enhanceKitSpan, svelteKitSpansIntegration } from '../../../src/server-common/integrations/svelteKitSpans';
+
+describe('svelteKitSpansIntegration', () => {
+ it('has a name and a preprocessEventHook', () => {
+ const integration = svelteKitSpansIntegration();
+
+ expect(integration.name).toBe('SvelteKitSpansEnhancement');
+ expect(typeof integration.preprocessEvent).toBe('function');
+ });
+
+ it('enhances spans from SvelteKit', () => {
+ const event: TransactionEvent = {
+ type: 'transaction',
+ contexts: {
+ trace: {
+ span_id: '123',
+ trace_id: 'abc',
+ data: {
+ 'sveltekit.tracing.original_name': 'sveltekit.handle.root',
+ },
+ },
+ },
+ spans: [
+ {
+ description: 'sveltekit.resolve',
+ data: {
+ someAttribute: 'someValue',
+ },
+ span_id: '123',
+ trace_id: 'abc',
+ start_timestamp: 0,
+ },
+ ],
+ };
+
+ // @ts-expect-error -- passing in an empty option for client but it is unused in the integration
+ svelteKitSpansIntegration().preprocessEvent?.(event, {}, {});
+
+ expect(event.spans).toHaveLength(1);
+ expect(event.spans?.[0]?.op).toBe('function.sveltekit.resolve');
+ expect(event.spans?.[0]?.origin).toBe('auto.http.sveltekit');
+ expect(event.spans?.[0]?.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('function.sveltekit.resolve');
+ expect(event.spans?.[0]?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit');
+ });
+
+ describe('_enhanceKitSpan', () => {
+ it.each([
+ ['sveltekit.resolve', 'function.sveltekit.resolve', 'auto.http.sveltekit'],
+ ['sveltekit.load', 'function.sveltekit.load', 'auto.function.sveltekit.load'],
+ ['sveltekit.form_action', 'function.sveltekit.form_action', 'auto.function.sveltekit.action'],
+ ['sveltekit.remote.call', 'function.sveltekit.remote', 'auto.rpc.sveltekit.remote'],
+ ['sveltekit.handle.sequenced.0', 'function.sveltekit.handle', 'auto.function.sveltekit.handle'],
+ ['sveltekit.handle.sequenced.myHandler', 'function.sveltekit.handle', 'auto.function.sveltekit.handle'],
+ ])('enhances %s span with the correct op and origin', (spanName, op, origin) => {
+ const span = {
+ description: spanName,
+ data: {
+ someAttribute: 'someValue',
+ },
+ span_id: '123',
+ trace_id: 'abc',
+ start_timestamp: 0,
+ } as SpanJSON;
+
+ _enhanceKitSpan(span);
+
+ expect(span.op).toBe(op);
+ expect(span.origin).toBe(origin);
+ expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe(op);
+ expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe(origin);
+ });
+
+ it("doesn't change spans from other origins", () => {
+ const span = {
+ description: 'someOtherSpan',
+ data: {},
+ } as SpanJSON;
+
+ _enhanceKitSpan(span);
+
+ expect(span.op).toBeUndefined();
+ expect(span.origin).toBeUndefined();
+ expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBeUndefined();
+ expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBeUndefined();
+ });
+
+ it("doesn't overwrite the sveltekit.handle.root span", () => {
+ const rootHandleSpan = {
+ description: 'sveltekit.handle.root',
+ op: 'http.server',
+ origin: 'auto.http.sveltekit',
+ data: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
+ },
+ span_id: '123',
+ trace_id: 'abc',
+ start_timestamp: 0,
+ } as SpanJSON;
+
+ _enhanceKitSpan(rootHandleSpan);
+
+ expect(rootHandleSpan.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('http.server');
+ expect(rootHandleSpan.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.http.sveltekit');
+ expect(rootHandleSpan.op).toBe('http.server');
+ expect(rootHandleSpan.origin).toBe('auto.http.sveltekit');
+ });
+
+ it("doesn't enhance unrelated spans", () => {
+ const span = {
+ description: 'someOtherSpan',
+ data: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.pg',
+ },
+ op: 'db',
+ origin: 'auto.db.pg',
+ span_id: '123',
+ trace_id: 'abc',
+ start_timestamp: 0,
+ } as SpanJSON;
+
+ _enhanceKitSpan(span);
+
+ expect(span.op).toBe('db');
+ expect(span.origin).toBe('auto.db.pg');
+ expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('db');
+ expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]).toBe('auto.db.pg');
+ });
+
+ it("doesn't overwrite already set ops or origins on sveltekit spans", () => {
+ // for example, if users manually set this (for whatever reason)
+ const span = {
+ description: 'sveltekit.resolve',
+ origin: 'auto.custom.origin',
+ data: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'custom.op',
+ },
+ span_id: '123',
+ trace_id: 'abc',
+ start_timestamp: 0,
+ } as SpanJSON;
+
+ _enhanceKitSpan(span);
+
+ expect(span.origin).toBe('auto.custom.origin');
+ expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('custom.op');
+ });
+
+ it('overwrites previously set "manual" origins on sveltekit spans', () => {
+ // for example, if users manually set this (for whatever reason)
+ const span = {
+ description: 'sveltekit.resolve',
+ origin: 'manual',
+ data: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'custom.op',
+ },
+ span_id: '123',
+ trace_id: 'abc',
+ start_timestamp: 0,
+ } as SpanJSON;
+
+ _enhanceKitSpan(span);
+
+ expect(span.origin).toBe('auto.http.sveltekit');
+ expect(span.data[SEMANTIC_ATTRIBUTE_SENTRY_OP]).toBe('custom.op');
+ });
+ });
+});
diff --git a/packages/sveltekit/test/server/integrations/http.test.ts b/packages/sveltekit/test/server/integrations/http.test.ts
new file mode 100644
index 000000000000..08a6fffcd06f
--- /dev/null
+++ b/packages/sveltekit/test/server/integrations/http.test.ts
@@ -0,0 +1,27 @@
+import * as SentryNode from '@sentry/node';
+import { describe, expect, it, vi } from 'vitest';
+import { httpIntegration as svelteKitHttpIntegration } from '../../../src/server/integrations/http';
+
+describe('httpIntegration', () => {
+ it('calls the original httpIntegration with incoming request span recording disabled', () => {
+ const sentryNodeHttpIntegration = vi.spyOn(SentryNode, 'httpIntegration');
+ svelteKitHttpIntegration({ breadcrumbs: false });
+
+ expect(sentryNodeHttpIntegration).toHaveBeenCalledTimes(1);
+ expect(sentryNodeHttpIntegration).toHaveBeenCalledWith({
+ breadcrumbs: false, // leaves other options untouched
+ disableIncomingRequestSpans: true,
+ });
+ });
+
+ it('allows users to override incoming request span recording', () => {
+ const sentryNodeHttpIntegration = vi.spyOn(SentryNode, 'httpIntegration');
+ svelteKitHttpIntegration({ breadcrumbs: false, disableIncomingRequestSpans: false });
+
+ expect(sentryNodeHttpIntegration).toHaveBeenCalledTimes(1);
+ expect(sentryNodeHttpIntegration).toHaveBeenCalledWith({
+ breadcrumbs: false,
+ disableIncomingRequestSpans: false,
+ });
+ });
+});
diff --git a/packages/sveltekit/test/vite/autoInstrument.test.ts b/packages/sveltekit/test/vite/autoInstrument.test.ts
index 364680e31bf3..b58f0280cdea 100644
--- a/packages/sveltekit/test/vite/autoInstrument.test.ts
+++ b/packages/sveltekit/test/vite/autoInstrument.test.ts
@@ -41,7 +41,12 @@ describe('makeAutoInstrumentationPlugin()', () => {
});
it('returns the auto instrumentation plugin', async () => {
- const plugin = makeAutoInstrumentationPlugin({ debug: true, load: true, serverLoad: true });
+ const plugin = makeAutoInstrumentationPlugin({
+ debug: true,
+ load: true,
+ serverLoad: true,
+ onlyInstrumentClient: false,
+ });
expect(plugin.name).toEqual('sentry-auto-instrumentation');
expect(plugin.enforce).toEqual('pre');
expect(plugin.load).toEqual(expect.any(Function));
@@ -58,7 +63,12 @@ describe('makeAutoInstrumentationPlugin()', () => {
'path/to/+layout.mjs',
])('transform %s files', (path: string) => {
it('wraps universal load if `load` option is `true`', async () => {
- const plugin = makeAutoInstrumentationPlugin({ debug: false, load: true, serverLoad: true });
+ const plugin = makeAutoInstrumentationPlugin({
+ debug: false,
+ load: true,
+ serverLoad: true,
+ onlyInstrumentClient: false,
+ });
// @ts-expect-error this exists
const loadResult = await plugin.load(path);
expect(loadResult).toEqual(
@@ -74,6 +84,7 @@ describe('makeAutoInstrumentationPlugin()', () => {
debug: false,
load: false,
serverLoad: false,
+ onlyInstrumentClient: false,
});
// @ts-expect-error this exists
const loadResult = await plugin.load(path);
@@ -92,7 +103,12 @@ describe('makeAutoInstrumentationPlugin()', () => {
'path/to/+layout.server.mjs',
])('transform %s files', (path: string) => {
it('wraps universal load if `load` option is `true`', async () => {
- const plugin = makeAutoInstrumentationPlugin({ debug: false, load: false, serverLoad: true });
+ const plugin = makeAutoInstrumentationPlugin({
+ debug: false,
+ load: false,
+ serverLoad: true,
+ onlyInstrumentClient: false,
+ });
// @ts-expect-error this exists
const loadResult = await plugin.load(path);
expect(loadResult).toEqual(
@@ -108,12 +124,101 @@ describe('makeAutoInstrumentationPlugin()', () => {
debug: false,
load: false,
serverLoad: false,
+ onlyInstrumentClient: false,
});
// @ts-expect-error this exists
const loadResult = await plugin.load(path);
expect(loadResult).toEqual(null);
});
});
+
+ describe('when `onlyInstrumentClient` is `true`', () => {
+ it.each([
+ // server-only files
+ 'path/to/+page.server.ts',
+ 'path/to/+layout.server.js',
+ // universal files
+ 'path/to/+page.mts',
+ 'path/to/+layout.mjs',
+ ])("doesn't wrap code in SSR build in %s", async (path: string) => {
+ const plugin = makeAutoInstrumentationPlugin({
+ debug: false,
+ load: true,
+ serverLoad: true,
+ onlyInstrumentClient: true,
+ });
+
+ // @ts-expect-error this exists and is callable
+ plugin.configResolved({
+ build: {
+ ssr: true,
+ },
+ });
+
+ // @ts-expect-error this exists
+ const loadResult = await plugin.load(path);
+
+ expect(loadResult).toEqual(null);
+ });
+
+ it.each(['path/to/+page.ts', 'path/to/+layout.js'])(
+ 'wraps client-side code in universal files in %s',
+ async (path: string) => {
+ const plugin = makeAutoInstrumentationPlugin({
+ debug: false,
+ load: true,
+ serverLoad: true,
+ onlyInstrumentClient: true,
+ });
+
+ // @ts-expect-error this exists and is callable
+ plugin.configResolved({
+ build: {
+ ssr: false,
+ },
+ });
+
+ // @ts-expect-error this exists and is callable
+ const loadResult = await plugin.load(path);
+
+ expect(loadResult).toBe(
+ 'import { wrapLoadWithSentry } from "@sentry/sveltekit";' +
+ `import * as userModule from "${path}?sentry-auto-wrap";` +
+ 'export const load = userModule.load ? wrapLoadWithSentry(userModule.load) : undefined;' +
+ `export * from "${path}?sentry-auto-wrap";`,
+ );
+ },
+ );
+
+ /**
+ * This is a bit of a constructed case because in a client build, server-only files
+ * shouldn't even be passed into the load hook. But just to be extra careful, let's
+ * make sure we don't wrap server-only files in a client build.
+ */
+ it.each(['path/to/+page.server.ts', 'path/to/+layout.server.js'])(
+ "doesn't wrap client-side code in server-only files in %s",
+ async (path: string) => {
+ const plugin = makeAutoInstrumentationPlugin({
+ debug: false,
+ load: true,
+ serverLoad: true,
+ onlyInstrumentClient: true,
+ });
+
+ // @ts-expect-error this exists and is callable
+ plugin.configResolved({
+ build: {
+ ssr: false,
+ },
+ });
+
+ // @ts-expect-error this exists and is callable
+ const loadResult = await plugin.load(path);
+
+ expect(loadResult).toBe(null);
+ },
+ );
+ });
});
describe('canWrapLoad', () => {
diff --git a/packages/sveltekit/test/vite/injectGlobalValues.test.ts b/packages/sveltekit/test/vite/injectGlobalValues.test.ts
index 3e07bf7e26a7..50f41c84880f 100644
--- a/packages/sveltekit/test/vite/injectGlobalValues.test.ts
+++ b/packages/sveltekit/test/vite/injectGlobalValues.test.ts
@@ -4,17 +4,18 @@ import { getGlobalValueInjectionCode } from '../../src/vite/injectGlobalValues';
describe('getGlobalValueInjectionCode', () => {
it('returns code that injects values into the global object', () => {
const injectionCode = getGlobalValueInjectionCode({
- // @ts-expect-error - just want to test this with multiple values
- something: 'else',
__sentry_sveltekit_output_dir: '.svelte-kit/output',
});
- expect(injectionCode).toEqual(`globalThis["something"] = "else";
-globalThis["__sentry_sveltekit_output_dir"] = ".svelte-kit/output";
-`);
+
+ expect(injectionCode).toMatchInlineSnapshot(`
+ "globalThis["__sentry_sveltekit_output_dir"] = ".svelte-kit/output";
+ "
+ `);
// Check that the code above is in fact valid and works as expected
// The return value of eval here is the value of the last expression in the code
- expect(eval(`${injectionCode}`)).toEqual('.svelte-kit/output');
+ eval(injectionCode);
+ expect(globalThis.__sentry_sveltekit_output_dir).toEqual('.svelte-kit/output');
delete globalThis.__sentry_sveltekit_output_dir;
});
diff --git a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts
index d21bcf0d8d04..fa8df96f03a6 100644
--- a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts
+++ b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts
@@ -41,8 +41,8 @@ describe('sentrySvelteKit()', () => {
const plugins = await getSentrySvelteKitPlugins();
expect(plugins).toBeInstanceOf(Array);
- // 1 auto instrument plugin + 5 source maps plugins
- expect(plugins).toHaveLength(9);
+ // 1 auto instrument plugin + 1 global values injection plugin + 5 source maps plugins
+ expect(plugins).toHaveLength(10);
});
it('returns the custom sentry source maps upload plugin, unmodified sourcemaps plugins and the auto-instrument plugin by default', async () => {
@@ -51,6 +51,8 @@ describe('sentrySvelteKit()', () => {
expect(pluginNames).toEqual([
// auto instrument plugin:
'sentry-auto-instrumentation',
+ // global values injection plugin:
+ 'sentry-sveltekit-global-values-injection-plugin',
// default source maps plugins:
'sentry-telemetry-plugin',
'sentry-vite-release-injection-plugin',
@@ -68,7 +70,7 @@ describe('sentrySvelteKit()', () => {
it("doesn't return the sentry source maps plugins if autoUploadSourcemaps is `false`", async () => {
const plugins = await getSentrySvelteKitPlugins({ autoUploadSourceMaps: false });
- expect(plugins).toHaveLength(1);
+ expect(plugins).toHaveLength(1); // auto instrument
});
it("doesn't return the sentry source maps plugins if `NODE_ENV` is development", async () => {
@@ -78,7 +80,7 @@ describe('sentrySvelteKit()', () => {
const plugins = await getSentrySvelteKitPlugins({ autoUploadSourceMaps: true, autoInstrument: true });
const instrumentPlugin = plugins[0];
- expect(plugins).toHaveLength(1);
+ expect(plugins).toHaveLength(2); // auto instrument + global values injection
expect(instrumentPlugin?.name).toEqual('sentry-auto-instrumentation');
process.env.NODE_ENV = previousEnv;
@@ -87,8 +89,8 @@ describe('sentrySvelteKit()', () => {
it("doesn't return the auto instrument plugin if autoInstrument is `false`", async () => {
const plugins = await getSentrySvelteKitPlugins({ autoInstrument: false });
const pluginNames = plugins.map(plugin => plugin.name);
- expect(plugins).toHaveLength(8);
- expect(pluginNames).not.toContain('sentry-upload-source-maps');
+ expect(plugins).toHaveLength(9); // global values injection + 5 source maps plugins + 3 default plugins
+ expect(pluginNames).not.toContain('sentry-auto-instrumentation');
});
it('passes user-specified vite plugin options to the custom sentry source maps plugin', async () => {
@@ -106,15 +108,18 @@ describe('sentrySvelteKit()', () => {
adapter: 'vercel',
});
- expect(makePluginSpy).toHaveBeenCalledWith({
- debug: true,
- sourcemaps: {
- assets: ['foo/*.js'],
- ignore: ['bar/*.js'],
- filesToDeleteAfterUpload: ['baz/*.js'],
+ expect(makePluginSpy).toHaveBeenCalledWith(
+ {
+ debug: true,
+ sourcemaps: {
+ assets: ['foo/*.js'],
+ ignore: ['bar/*.js'],
+ filesToDeleteAfterUpload: ['baz/*.js'],
+ },
+ adapter: 'vercel',
},
- adapter: 'vercel',
- });
+ {},
+ );
});
it('passes user-specified vite plugin options to the custom sentry source maps plugin', async () => {
@@ -152,26 +157,29 @@ describe('sentrySvelteKit()', () => {
adapter: 'vercel',
});
- expect(makePluginSpy).toHaveBeenCalledWith({
- debug: true,
- org: 'other-org',
- sourcemaps: {
- assets: ['foo/*.js'],
- ignore: ['bar/*.js'],
- filesToDeleteAfterUpload: ['baz/*.js'],
- },
- release: {
- inject: false,
- name: '3.0.0',
- setCommits: {
- auto: true,
+ expect(makePluginSpy).toHaveBeenCalledWith(
+ {
+ debug: true,
+ org: 'other-org',
+ sourcemaps: {
+ assets: ['foo/*.js'],
+ ignore: ['bar/*.js'],
+ filesToDeleteAfterUpload: ['baz/*.js'],
},
+ release: {
+ inject: false,
+ name: '3.0.0',
+ setCommits: {
+ auto: true,
+ },
+ },
+ headers: {
+ 'X-My-Header': 'foo',
+ },
+ adapter: 'vercel',
},
- headers: {
- 'X-My-Header': 'foo',
- },
- adapter: 'vercel',
- });
+ {},
+ );
});
it('passes user-specified options to the auto instrument plugin', async () => {
@@ -192,27 +200,36 @@ describe('sentrySvelteKit()', () => {
debug: true,
load: true,
serverLoad: false,
+ onlyInstrumentClient: false,
});
});
});
describe('generateVitePluginOptions', () => {
- it('should return null if no relevant options are provided', () => {
+ it('returns null if no relevant options are provided', () => {
const options: SentrySvelteKitPluginOptions = {};
const result = generateVitePluginOptions(options);
expect(result).toBeNull();
});
- it('should use default `debug` value if only default options are provided', () => {
+ it('uses default `debug` value if only default options are provided', () => {
+ const originalEnv = process.env.NODE_ENV;
+ process.env.NODE_ENV = 'production'; // Ensure we're not in development mode
+
const options: SentrySvelteKitPluginOptions = { autoUploadSourceMaps: true, autoInstrument: true, debug: false };
const expected: CustomSentryVitePluginOptions = {
debug: false,
};
const result = generateVitePluginOptions(options);
expect(result).toEqual(expected);
+
+ process.env.NODE_ENV = originalEnv;
});
- it('should apply user-defined sourceMapsUploadOptions', () => {
+ it('applies user-defined sourceMapsUploadOptions', () => {
+ const originalEnv = process.env.NODE_ENV;
+ process.env.NODE_ENV = 'production'; // Ensure we're not in development mode
+
const options: SentrySvelteKitPluginOptions = {
autoUploadSourceMaps: true,
sourceMapsUploadOptions: {
@@ -234,9 +251,14 @@ describe('generateVitePluginOptions', () => {
};
const result = generateVitePluginOptions(options);
expect(result).toEqual(expected);
+
+ process.env.NODE_ENV = originalEnv;
});
- it('should override options with unstable_sentryVitePluginOptions', () => {
+ it('overrides options with unstable_sentryVitePluginOptions', () => {
+ const originalEnv = process.env.NODE_ENV;
+ process.env.NODE_ENV = 'production'; // Ensure we're not in development mode
+
const options: SentrySvelteKitPluginOptions = {
autoUploadSourceMaps: true,
sourceMapsUploadOptions: {
@@ -264,9 +286,14 @@ describe('generateVitePluginOptions', () => {
};
const result = generateVitePluginOptions(options);
expect(result).toEqual(expected);
+
+ process.env.NODE_ENV = originalEnv;
});
- it('should merge release options correctly', () => {
+ it('merges release options correctly', () => {
+ const originalEnv = process.env.NODE_ENV;
+ process.env.NODE_ENV = 'production'; // Ensure we're not in development mode
+
const options: SentrySvelteKitPluginOptions = {
autoUploadSourceMaps: true,
sourceMapsUploadOptions: {
@@ -293,9 +320,14 @@ describe('generateVitePluginOptions', () => {
};
const result = generateVitePluginOptions(options);
expect(result).toEqual(expected);
+
+ process.env.NODE_ENV = originalEnv;
});
- it('should handle adapter and debug options correctly', () => {
+ it('handles adapter and debug options correctly', () => {
+ const originalEnv = process.env.NODE_ENV;
+ process.env.NODE_ENV = 'production'; // Ensure we're not in development mode
+
const options: SentrySvelteKitPluginOptions = {
autoUploadSourceMaps: true,
adapter: 'vercel',
@@ -315,9 +347,14 @@ describe('generateVitePluginOptions', () => {
};
const result = generateVitePluginOptions(options);
expect(result).toEqual(expected);
+
+ process.env.NODE_ENV = originalEnv;
});
- it('should apply bundleSizeOptimizations AND sourceMapsUploadOptions when both are set', () => {
+ it('applies bundleSizeOptimizations AND sourceMapsUploadOptions when both are set', () => {
+ const originalEnv = process.env.NODE_ENV;
+ process.env.NODE_ENV = 'production'; // Ensure we're not in development mode
+
const options: SentrySvelteKitPluginOptions = {
bundleSizeOptimizations: {
excludeTracing: true,
@@ -349,5 +386,7 @@ describe('generateVitePluginOptions', () => {
};
const result = generateVitePluginOptions(options);
expect(result).toEqual(expected);
+
+ process.env.NODE_ENV = originalEnv;
});
});
diff --git a/packages/sveltekit/test/vite/sourceMaps.test.ts b/packages/sveltekit/test/vite/sourceMaps.test.ts
index 97d223dc309b..45d0b74ad6ff 100644
--- a/packages/sveltekit/test/vite/sourceMaps.test.ts
+++ b/packages/sveltekit/test/vite/sourceMaps.test.ts
@@ -52,12 +52,15 @@ beforeEach(() => {
});
async function getSentryViteSubPlugin(name: string): Promise {
- const plugins = await makeCustomSentryVitePlugins({
- authToken: 'token',
- org: 'org',
- project: 'project',
- adapter: 'other',
- });
+ const plugins = await makeCustomSentryVitePlugins(
+ {
+ authToken: 'token',
+ org: 'org',
+ project: 'project',
+ adapter: 'other',
+ },
+ { kit: {} },
+ );
return plugins.find(plugin => plugin.name === name);
}
@@ -79,8 +82,8 @@ describe('makeCustomSentryVitePlugins()', () => {
expect(plugin?.apply).toEqual('build');
expect(plugin?.enforce).toEqual('post');
- expect(plugin?.resolveId).toBeInstanceOf(Function);
- expect(plugin?.transform).toBeInstanceOf(Function);
+ expect(plugin?.resolveId).toBeUndefined();
+ expect(plugin?.transform).toBeUndefined();
expect(plugin?.configResolved).toBeInstanceOf(Function);
@@ -178,18 +181,10 @@ describe('makeCustomSentryVitePlugins()', () => {
});
});
- describe('Custom debug id source maps plugin plugin', () => {
- it('injects the output dir into the server hooks file', async () => {
- const plugin = await getSentryViteSubPlugin('sentry-sveltekit-debug-id-upload-plugin');
- // @ts-expect-error this function exists!
- const transformOutput = await plugin.transform('foo', '/src/hooks.server.ts');
- const transformedCode = transformOutput.code;
- const transformedSourcemap = transformOutput.map;
- const expectedTransformedCode = 'foo\n; import "\0sentry-inject-global-values-file";\n';
- expect(transformedCode).toEqual(expectedTransformedCode);
- expect(transformedSourcemap).toBeDefined();
- });
+ // Note: The global values injection plugin tests are now in a separate test file
+ // since the plugin was moved to injectGlobalValues.ts
+ describe('Custom debug id source maps plugin plugin', () => {
it('uploads source maps during the SSR build', async () => {
const plugin = await getSentryViteSubPlugin('sentry-sveltekit-debug-id-upload-plugin');
// @ts-expect-error this function exists!
@@ -423,12 +418,15 @@ describe('deleteFilesAfterUpload', () => {
};
});
- const plugins = await makeCustomSentryVitePlugins({
- authToken: 'token',
- org: 'org',
- project: 'project',
- adapter: 'other',
- });
+ const plugins = await makeCustomSentryVitePlugins(
+ {
+ authToken: 'token',
+ org: 'org',
+ project: 'project',
+ adapter: 'other',
+ },
+ { kit: {} },
+ );
// @ts-expect-error this function exists!
const mergedOptions = sentryVitePlugin.mock.calls[0][0];
@@ -498,15 +496,18 @@ describe('deleteFilesAfterUpload', () => {
};
});
- const plugins = await makeCustomSentryVitePlugins({
- authToken: 'token',
- org: 'org',
- project: 'project',
- adapter: 'other',
- sourcemaps: {
- filesToDeleteAfterUpload,
+ const plugins = await makeCustomSentryVitePlugins(
+ {
+ authToken: 'token',
+ org: 'org',
+ project: 'project',
+ adapter: 'other',
+ sourcemaps: {
+ filesToDeleteAfterUpload,
+ },
},
- });
+ { kit: {} },
+ );
// @ts-expect-error this function exists!
const mergedOptions = sentryVitePlugin.mock.calls[0][0];