+ {{ $route.params.param }} - {{ $route.params.param }}
+
+
+
+
+
+
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts
new file mode 100644
index 000000000000..90f26a8ea6b6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/nuxt.config.ts
@@ -0,0 +1,16 @@
+// https://nuxt.com/docs/api/configuration/nuxt-config
+export default defineNuxtConfig({
+ future: { compatibilityVersion: 4 },
+ compatibilityDate: '2024-04-03',
+ imports: { autoImport: false },
+
+ modules: ['@sentry/nuxt/module'],
+
+ runtimeConfig: {
+ public: {
+ sentry: {
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ },
+ },
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/package.json b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
new file mode 100644
index 000000000000..d39dca9154a0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "nuxt-4",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "nuxt build",
+ "dev": "nuxt dev",
+ "generate": "nuxt generate",
+ "preview": "NODE_OPTIONS='--import ./public/instrument.server.mjs' nuxt preview",
+ "clean": "npx nuxi cleanup",
+ "test": "playwright test",
+ "test:build": "pnpm install && npx playwright install && pnpm build",
+ "test:assert": "pnpm test"
+ },
+ "dependencies": {
+ "@sentry/nuxt": "latest || *",
+ "nuxt": "^3.13.2",
+ "vue": "latest",
+ "vue-router": "latest"
+ },
+ "devDependencies": {
+ "@nuxt/test-utils": "^3.14.1",
+ "@playwright/test": "^1.44.1",
+ "@sentry-internal/test-utils": "link:../../../test-utils"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/playwright.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/playwright.config.ts
new file mode 100644
index 000000000000..d1094993131d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/playwright.config.ts
@@ -0,0 +1,19 @@
+import { fileURLToPath } from 'node:url';
+import type { ConfigOptions } from '@nuxt/test-utils/playwright';
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const nuxtConfigOptions: ConfigOptions = {
+ nuxt: {
+ rootDir: fileURLToPath(new URL('.', import.meta.url)),
+ },
+};
+
+/* Make sure to import from '@nuxt/test-utils/playwright' in the tests
+ * Like this: import { expect, test } from '@nuxt/test-utils/playwright' */
+
+const config = getPlaywrightConfig({
+ startCommand: `pnpm preview`,
+ use: { ...nuxtConfigOptions },
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/public/favicon.ico b/dev-packages/e2e-tests/test-applications/nuxt-4/public/favicon.ico
new file mode 100644
index 000000000000..18993ad91cfd
Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/nuxt-4/public/favicon.ico differ
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/public/instrument.server.mjs b/dev-packages/e2e-tests/test-applications/nuxt-4/public/instrument.server.mjs
new file mode 100644
index 000000000000..729b2296c683
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/public/instrument.server.mjs
@@ -0,0 +1,8 @@
+import * as Sentry from '@sentry/nuxt';
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ tracesSampleRate: 1.0, // Capture 100% of the transactions
+ tunnel: 'http://localhost:3031/', // proxy server
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.client.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.client.config.ts
new file mode 100644
index 000000000000..7547bafa6618
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/sentry.client.config.ts
@@ -0,0 +1,10 @@
+import * as Sentry from '@sentry/nuxt';
+import { useRuntimeConfig } from '#imports';
+
+Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: useRuntimeConfig().public.sentry.dsn,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1.0,
+ trackComponents: true,
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/param-error/[param].ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/param-error/[param].ts
new file mode 100644
index 000000000000..389d8ac4d633
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/param-error/[param].ts
@@ -0,0 +1,5 @@
+import { defineEventHandler } from '#imports';
+
+export default defineEventHandler(_e => {
+ throw new Error('Nuxt 3 Param Server error');
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/server-error.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/server-error.ts
new file mode 100644
index 000000000000..ec961a010510
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/server-error.ts
@@ -0,0 +1,5 @@
+import { defineEventHandler } from '#imports';
+
+export default defineEventHandler(event => {
+ throw new Error('Nuxt 3 Server error');
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/test-param/[param].ts b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/test-param/[param].ts
new file mode 100644
index 000000000000..1867874cd494
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/api/test-param/[param].ts
@@ -0,0 +1,7 @@
+import { defineEventHandler, getRouterParam } from '#imports';
+
+export default defineEventHandler(event => {
+ const param = getRouterParam(event, 'param');
+
+ return `Param: ${param}!`;
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/server/tsconfig.json b/dev-packages/e2e-tests/test-applications/nuxt-4/server/tsconfig.json
new file mode 100644
index 000000000000..b9ed69c19eaf
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/server/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../.nuxt/tsconfig.server.json"
+}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nuxt-4/start-event-proxy.mjs
new file mode 100644
index 000000000000..e76648280867
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'nuxt-4',
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.client.test.ts
new file mode 100644
index 000000000000..14760356d093
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.client.test.ts
@@ -0,0 +1,105 @@
+import { expect, test } from '@nuxt/test-utils/playwright';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test.describe('client-side errors', async () => {
+ test('captures error thrown on click', async ({ page }) => {
+ const errorPromise = waitForError('nuxt-4', async errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from nuxt-4 E2E test app';
+ });
+
+ await page.goto(`/client-error`);
+ await page.locator('#errorBtn').click();
+
+ const error = await errorPromise;
+
+ expect(error.transaction).toEqual('/client-error');
+ expect(error).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Error thrown from nuxt-4 E2E test app',
+ mechanism: {
+ handled: false,
+ },
+ },
+ ],
+ },
+ });
+ });
+
+ test('shows parametrized route on button error', async ({ page }) => {
+ const errorPromise = waitForError('nuxt-4', async errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Param Route Button';
+ });
+
+ await page.goto(`/test-param/1234`);
+ await page.locator('#errorBtn').click();
+
+ const error = await errorPromise;
+
+ expect(error.sdk.name).toEqual('sentry.javascript.nuxt');
+ expect(error.transaction).toEqual('/test-param/:param()');
+ expect(error.request.url).toMatch(/\/test-param\/1234/);
+ expect(error).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Error thrown from Param Route Button',
+ mechanism: {
+ handled: false,
+ },
+ },
+ ],
+ },
+ });
+ });
+
+ test('page is still interactive after client error', async ({ page }) => {
+ const error1Promise = waitForError('nuxt-4', async errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from nuxt-4 E2E test app';
+ });
+
+ await page.goto(`/client-error`);
+ await page.locator('#errorBtn').click();
+
+ const error1 = await error1Promise;
+
+ const error2Promise = waitForError('nuxt-4', async errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'Another Error thrown from nuxt-4 E2E test app';
+ });
+
+ await page.locator('#errorBtn2').click();
+
+ const error2 = await error2Promise;
+
+ expect(error1).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Error thrown from nuxt-4 E2E test app',
+ mechanism: {
+ handled: false,
+ },
+ },
+ ],
+ },
+ });
+
+ expect(error2).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Another Error thrown from nuxt-4 E2E test app',
+ mechanism: {
+ handled: false,
+ },
+ },
+ ],
+ },
+ });
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts
new file mode 100644
index 000000000000..ba2bb0e3a88e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/errors.server.test.ts
@@ -0,0 +1,40 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+test.describe('server-side errors', async () => {
+ test('captures api fetch error (fetched on click)', async ({ page }) => {
+ const errorPromise = waitForError('nuxt-4', async errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 3 Server error';
+ });
+
+ await page.goto(`/fetch-server-error`);
+ await page.getByText('Fetch Server Data').click();
+
+ const error = await errorPromise;
+
+ expect(error.transaction).toEqual('GET /api/server-error');
+
+ const exception = error.exception.values[0];
+ expect(exception.type).toEqual('Error');
+ expect(exception.value).toEqual('Nuxt 3 Server error');
+ expect(exception.mechanism.handled).toBe(false);
+ });
+
+ test('captures api fetch error (fetched on click) with parametrized route', async ({ page }) => {
+ const errorPromise = waitForError('nuxt-4', async errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'Nuxt 3 Param Server error';
+ });
+
+ await page.goto(`/test-param/1234`);
+ await page.getByText('Fetch Server Data').click();
+
+ const error = await errorPromise;
+
+ expect(error.transaction).toEqual('GET /api/param-error/1234');
+
+ const exception = error.exception.values[0];
+ expect(exception.type).toEqual('Error');
+ expect(exception.value).toEqual('Nuxt 3 Param Server error');
+ expect(exception.mechanism.handled).toBe(false);
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.client.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.client.test.ts
new file mode 100644
index 000000000000..871d8b19513c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-4/tests/tracing.client.test.ts
@@ -0,0 +1,57 @@
+import { expect, test } from '@nuxt/test-utils/playwright';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+import type { Span } from '@sentry/nuxt';
+
+test('sends a pageload root span with a parameterized URL', async ({ page }) => {
+ const transactionPromise = waitForTransaction('nuxt-4', async transactionEvent => {
+ return transactionEvent.transaction === '/test-param/:param()';
+ });
+
+ await page.goto(`/test-param/1234`);
+
+ const rootSpan = await transactionPromise;
+
+ expect(rootSpan).toMatchObject({
+ contexts: {
+ trace: {
+ data: {
+ 'sentry.source': 'route',
+ 'sentry.origin': 'auto.pageload.vue',
+ 'sentry.op': 'pageload',
+ 'params.param': '1234',
+ },
+ op: 'pageload',
+ origin: 'auto.pageload.vue',
+ },
+ },
+ transaction: '/test-param/:param()',
+ transaction_info: {
+ source: 'route',
+ },
+ });
+});
+
+test('sends component tracking spans when `trackComponents` is enabled', async ({ page }) => {
+ const transactionPromise = waitForTransaction('nuxt-4', async transactionEvent => {
+ return transactionEvent.transaction === '/client-error';
+ });
+
+ await page.goto(`/client-error`);
+
+ const rootSpan = await transactionPromise;
+ const errorButtonSpan = rootSpan.spans.find((span: Span) => span.description === 'Vue