+
I18n Test Page
+
Current locale: {locale || 'default'}
+
This page tests i18n route parameterization
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/i18n-routing.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/i18n-routing.test.ts
new file mode 100644
index 000000000000..fda0645fa1a3
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/i18n-routing.test.ts
@@ -0,0 +1,56 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('should create consistent parameterized transaction for i18n routes - locale: en', async ({ page }) => {
+ const transactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
+ return transactionEvent.transaction === '/:locale/i18n-test' && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto(`/en/i18n-test`);
+
+ const transaction = await transactionPromise;
+
+ expect(transaction).toMatchObject({
+ contexts: {
+ trace: {
+ data: {
+ 'sentry.op': 'pageload',
+ 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation',
+ 'sentry.source': 'route',
+ },
+ op: 'pageload',
+ origin: 'auto.pageload.nextjs.app_router_instrumentation',
+ },
+ },
+ transaction: '/:locale/i18n-test',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ });
+});
+
+test('should create consistent parameterized transaction for i18n routes - locale: ar', async ({ page }) => {
+ const transactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
+ return transactionEvent.transaction === '/:locale/i18n-test' && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto(`/ar/i18n-test`);
+
+ const transaction = await transactionPromise;
+
+ expect(transaction).toMatchObject({
+ contexts: {
+ trace: {
+ data: {
+ 'sentry.op': 'pageload',
+ 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation',
+ 'sentry.source': 'route',
+ },
+ op: 'pageload',
+ origin: 'auto.pageload.nextjs.app_router_instrumentation',
+ },
+ },
+ transaction: '/:locale/i18n-test',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-16/.npmrc
new file mode 100644
index 000000000000..a3160f4de175
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/.npmrc
@@ -0,0 +1,4 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
+public-hoist-pattern[]=*import-in-the-middle*
+public-hoist-pattern[]=*require-in-the-middle*
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/endpoint-behind-middleware/route.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/endpoint-behind-middleware/route.ts
new file mode 100644
index 000000000000..2733cc918f44
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/app/api/endpoint-behind-middleware/route.ts
@@ -0,0 +1,3 @@
+export function GET() {
+ return Response.json({ name: 'John Doe' });
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json
index 1fd09523ddb2..2da23b152807 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/package.json
@@ -22,6 +22,7 @@
},
"dependencies": {
"@sentry/nextjs": "latest || *",
+ "@sentry/core": "latest || *",
"ai": "^3.0.0",
"import-in-the-middle": "^1",
"next": "16.0.0-beta.0",
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/proxy.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/proxy.ts
new file mode 100644
index 000000000000..60722f329fa0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/proxy.ts
@@ -0,0 +1,24 @@
+import { getDefaultIsolationScope } from '@sentry/core';
+import * as Sentry from '@sentry/nextjs';
+import { NextResponse } from 'next/server';
+import type { NextRequest } from 'next/server';
+
+export async function proxy(request: NextRequest) {
+ Sentry.setTag('my-isolated-tag', true);
+ Sentry.setTag('my-global-scope-isolated-tag', getDefaultIsolationScope().getScopeData().tags['my-isolated-tag']); // We set this tag to be able to assert that the previously set tag has not leaked into the global isolation scope
+
+ if (request.headers.has('x-should-throw')) {
+ throw new Error('Middleware Error');
+ }
+
+ if (request.headers.has('x-should-make-request')) {
+ await fetch('http://localhost:3030/');
+ }
+
+ return NextResponse.next();
+}
+
+// See "Matching Paths" below to learn more
+export const config = {
+ matcher: ['/api/endpoint-behind-middleware', '/api/endpoint-behind-faulty-middleware'],
+};
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.edge.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.edge.config.ts
index 85bd765c9c44..2199afc46eaf 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.edge.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.edge.config.ts
@@ -6,4 +6,5 @@ Sentry.init({
tunnel: `http://localhost:3031/`, // proxy server
tracesSampleRate: 1.0,
sendDefaultPii: true,
+ // debug: true,
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts
index 8da0a18497a0..08d5d580b314 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/sentry.server.config.ts
@@ -6,5 +6,6 @@ Sentry.init({
tunnel: `http://localhost:3031/`, // proxy server
tracesSampleRate: 1.0,
sendDefaultPii: true,
+ // debug: true,
integrations: [Sentry.vercelAIIntegration()],
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts
new file mode 100644
index 000000000000..4ed289eb6215
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts
@@ -0,0 +1,108 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Should create a transaction for middleware', async ({ request }) => {
+ const middlewareTransactionPromise = waitForTransaction('nextjs-16', async transactionEvent => {
+ return transactionEvent?.transaction === 'middleware GET';
+ });
+
+ const response = await request.get('/api/endpoint-behind-middleware');
+ expect(await response.json()).toStrictEqual({ name: 'John Doe' });
+
+ const middlewareTransaction = await middlewareTransactionPromise;
+
+ expect(middlewareTransaction.contexts?.trace?.status).toBe('ok');
+ expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
+ expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
+ expect(middlewareTransaction.transaction_info?.source).toBe('url');
+
+ // Assert that isolation scope works properly
+ expect(middlewareTransaction.tags?.['my-isolated-tag']).toBe(true);
+ expect(middlewareTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
+});
+
+test('Faulty middlewares', async ({ request }) => {
+ const middlewareTransactionPromise = waitForTransaction('nextjs-16', async transactionEvent => {
+ return transactionEvent?.transaction === 'middleware GET';
+ });
+
+ const errorEventPromise = waitForError('nextjs-16', errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'Middleware Error';
+ });
+
+ request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-throw': '1' } }).catch(() => {
+ // Noop
+ });
+
+ await test.step('should record transactions', async () => {
+ const middlewareTransaction = await middlewareTransactionPromise;
+ expect(middlewareTransaction.contexts?.trace?.status).toBe('unknown_error');
+ expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
+ expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
+ expect(middlewareTransaction.transaction_info?.source).toBe('url');
+ });
+
+ await test.step('should record exceptions', async () => {
+ const errorEvent = await errorEventPromise;
+
+ // Assert that isolation scope works properly
+ expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
+ expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
+ expect([
+ 'middleware GET', // non-otel webpack versions
+ '/middleware', // middleware file
+ '/proxy', // proxy file
+ ]).toContain(errorEvent.transaction);
+ });
+});
+
+test('Should trace outgoing fetch requests inside middleware and create breadcrumbs for it', async ({ request }) => {
+ const middlewareTransactionPromise = waitForTransaction('nextjs-16', async transactionEvent => {
+ return (
+ transactionEvent?.transaction === 'middleware GET' &&
+ !!transactionEvent.spans?.find(span => span.op === 'http.client')
+ );
+ });
+
+ request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-make-request': '1' } }).catch(() => {
+ // Noop
+ });
+
+ const middlewareTransaction = await middlewareTransactionPromise;
+
+ expect(middlewareTransaction.spans).toEqual(
+ expect.arrayContaining([
+ {
+ data: {
+ 'http.method': 'GET',
+ 'http.response.status_code': 200,
+ type: 'fetch',
+ url: 'http://localhost:3030/',
+ 'http.url': 'http://localhost:3030/',
+ 'server.address': 'localhost:3030',
+ 'sentry.op': 'http.client',
+ 'sentry.origin': 'auto.http.wintercg_fetch',
+ },
+ description: 'GET http://localhost:3030/',
+ op: 'http.client',
+ origin: 'auto.http.wintercg_fetch',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ status: 'ok',
+ timestamp: expect.any(Number),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ },
+ ]),
+ );
+ expect(middlewareTransaction.breadcrumbs).toEqual(
+ expect.arrayContaining([
+ {
+ category: 'fetch',
+ data: { method: 'GET', status_code: 200, url: 'http://localhost:3030/' },
+ timestamp: expect.any(Number),
+ type: 'http',
+ },
+ ]),
+ );
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/middleware.test.ts
index b9c0e7b4b602..45a89f683be4 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/middleware.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-pages-dir/tests/middleware.test.ts
@@ -3,7 +3,7 @@ import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
test('Should create a transaction for middleware', async ({ request }) => {
const middlewareTransactionPromise = waitForTransaction('nextjs-pages-dir', async transactionEvent => {
- return transactionEvent?.transaction === 'middleware GET /api/endpoint-behind-middleware';
+ return transactionEvent?.transaction === 'middleware GET';
});
const response = await request.get('/api/endpoint-behind-middleware');
@@ -23,7 +23,7 @@ test('Should create a transaction for middleware', async ({ request }) => {
test('Faulty middlewares', async ({ request }) => {
const middlewareTransactionPromise = waitForTransaction('nextjs-pages-dir', async transactionEvent => {
- return transactionEvent?.transaction === 'middleware GET /api/endpoint-behind-faulty-middleware';
+ return transactionEvent?.transaction === 'middleware GET';
});
const errorEventPromise = waitForError('nextjs-pages-dir', errorEvent => {
@@ -48,14 +48,14 @@ test('Faulty middlewares', async ({ request }) => {
// Assert that isolation scope works properly
expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
- expect(errorEvent.transaction).toBe('middleware GET /api/endpoint-behind-faulty-middleware');
+ expect(errorEvent.transaction).toBe('middleware GET');
});
});
test('Should trace outgoing fetch requests inside middleware and create breadcrumbs for it', async ({ request }) => {
const middlewareTransactionPromise = waitForTransaction('nextjs-pages-dir', async transactionEvent => {
return (
- transactionEvent?.transaction === 'middleware GET /api/endpoint-behind-middleware' &&
+ transactionEvent?.transaction === 'middleware GET' &&
!!transactionEvent.spans?.find(span => span.op === 'http.client')
);
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/nuxt.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/nuxt.config.ts
index 0fcccd560af9..8f920a41e76e 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3/nuxt.config.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3/nuxt.config.ts
@@ -11,4 +11,28 @@ export default defineNuxtConfig({
},
},
},
+ nitro: {
+ experimental: {
+ database: true,
+ },
+ database: {
+ default: {
+ connector: 'sqlite',
+ options: { name: 'db' },
+ },
+ users: {
+ connector: 'sqlite',
+ options: { name: 'users_db' },
+ },
+ analytics: {
+ connector: 'sqlite',
+ options: { name: 'analytics_db' },
+ },
+ },
+ storage: {
+ 'test-storage': {
+ driver: 'memory',
+ },
+ },
+ },
});
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json
index b38943d6e3eb..bbf0ced23c12 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json
@@ -33,6 +33,7 @@
]
},
"volta": {
- "extends": "../../package.json"
+ "extends": "../../package.json",
+ "node": "22.20.0"
}
}
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/server/api/cache-test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/server/api/cache-test.ts
new file mode 100644
index 000000000000..b19530e18c96
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3/server/api/cache-test.ts
@@ -0,0 +1,84 @@
+import { cachedFunction, defineCachedEventHandler, defineEventHandler, getQuery } from '#imports';
+
+// Test cachedFunction
+const getCachedUser = cachedFunction(
+ async (userId: string) => {
+ return {
+ id: userId,
+ name: `User ${userId}`,
+ email: `user${userId}@example.com`,
+ timestamp: Date.now(),
+ };
+ },
+ {
+ maxAge: 60,
+ name: 'getCachedUser',
+ getKey: (userId: string) => `user:${userId}`,
+ },
+);
+
+// Test cachedFunction with different options
+const getCachedData = cachedFunction(
+ async (key: string) => {
+ return {
+ key,
+ value: `cached-value-${key}`,
+ timestamp: Date.now(),
+ };
+ },
+ {
+ maxAge: 120,
+ name: 'getCachedData',
+ getKey: (key: string) => `data:${key}`,
+ },
+);
+
+// Test defineCachedEventHandler
+const cachedHandler = defineCachedEventHandler(
+ async event => {
+ return {
+ message: 'This response is cached',
+ timestamp: Date.now(),
+ path: event.path,
+ };
+ },
+ {
+ maxAge: 60,
+ name: 'cachedHandler',
+ },
+);
+
+export default defineEventHandler(async event => {
+ const results: Record