diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/app/[...slug]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/app/[...slug]/page.tsx
new file mode 100644
index 000000000000..477881513d3f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/app/[...slug]/page.tsx
@@ -0,0 +1,3 @@
+export default function CatchAllPage() {
+ return
Catch-all page
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/app/layout.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/app/layout.tsx
new file mode 100644
index 000000000000..c8f9cee0b787
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/app/layout.tsx
@@ -0,0 +1,7 @@
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/app/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/app/page.tsx
new file mode 100644
index 000000000000..b55e84109d2c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/app/page.tsx
@@ -0,0 +1,20 @@
+import Link from 'next/link';
+
+export default function Page() {
+ return (
+
+
Next 16 trailing slash test app
+
+ -
+ Static Page
+
+ -
+ Parameterized
+
+ -
+ Parameterized Static
+
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/app/parameterized/[param]/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/app/parameterized/[param]/page.tsx
new file mode 100644
index 000000000000..e3a3dafe45f2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/app/parameterized/[param]/page.tsx
@@ -0,0 +1,3 @@
+export default function ParameterizedPage() {
+ return Dynamic parameterized page
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/app/parameterized/static/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/app/parameterized/static/page.tsx
new file mode 100644
index 000000000000..4cb7f5887fc4
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/app/parameterized/static/page.tsx
@@ -0,0 +1,3 @@
+export default function ParameterizedStaticPage() {
+ return Parameterized static page
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/app/static-page/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/app/static-page/page.tsx
new file mode 100644
index 000000000000..16ef0482d53b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/app/static-page/page.tsx
@@ -0,0 +1,3 @@
+export default function StaticPage() {
+ return Static page
;
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/instrumentation-client.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/instrumentation-client.ts
new file mode 100644
index 000000000000..ee12630ac228
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/instrumentation-client.ts
@@ -0,0 +1,13 @@
+import * as Sentry from '@sentry/nextjs';
+
+Sentry.init({
+ // This app is the static counterpart of `nextjs-16-trailing-slash`, which streams spans.
+ // Do not port it: it exists to keep the static trace lifecycle covered end to end.
+ traceLifecycle: 'static',
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1.0,
+});
+
+export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/instrumentation.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/instrumentation.ts
new file mode 100644
index 000000000000..964f937c439a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/instrumentation.ts
@@ -0,0 +1,13 @@
+import * as Sentry from '@sentry/nextjs';
+
+export async function register() {
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
+ await import('./sentry.server.config');
+ }
+
+ if (process.env.NEXT_RUNTIME === 'edge') {
+ await import('./sentry.edge.config');
+ }
+}
+
+export const onRequestError = Sentry.captureRequestError;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/next.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/next.config.mjs
new file mode 100644
index 000000000000..b1717b717341
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/next.config.mjs
@@ -0,0 +1,13 @@
+// Deliberately `.mjs`: Next loads it through Node's own ESM loader rather than compiling it, which is the only
+// config format that exercises `@sentry/nextjs/config` as a plain-Node ESM consumer.
+// See https://github.com/getsentry/sentry-javascript/issues/22791
+import { withSentryConfig } from '@sentry/nextjs/config';
+
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ trailingSlash: true,
+};
+
+export default withSentryConfig(nextConfig, {
+ silent: true,
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/package.json b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/package.json
new file mode 100644
index 000000000000..e91dc0f8d767
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "nextjs-16-trailing-slash-static",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml .tmp_dev_server_logs",
+ "start": "next start",
+ "test:prod": "TEST_ENV=production playwright test",
+ "test:build": "pnpm install && pnpm build",
+ "test:assert": "pnpm test:prod"
+ },
+ "dependencies": {
+ "@sentry/nextjs": "file:../../packed/sentry-nextjs-packed.tgz",
+ "@sentry/core": "file:../../packed/sentry-core-packed.tgz",
+ "import-in-the-middle": "^2",
+ "next": "16.2.11",
+ "react": "19.1.0",
+ "react-dom": "19.1.0",
+ "require-in-the-middle": "^8"
+ },
+ "devDependencies": {
+ "@playwright/test": "~1.56.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "@types/node": "^20",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "typescript": "^5"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/playwright.config.mjs
new file mode 100644
index 000000000000..38548e975851
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/playwright.config.mjs
@@ -0,0 +1,25 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+const testEnv = process.env.TEST_ENV;
+
+if (!testEnv) {
+ throw new Error('No test env defined');
+}
+
+const getStartCommand = () => {
+ if (testEnv === 'development') {
+ return 'pnpm next dev -p 3030 2>&1 | tee .tmp_dev_server_logs';
+ }
+
+ if (testEnv === 'production') {
+ return 'pnpm next start -p 3030';
+ }
+
+ throw new Error(`Unknown test env: ${testEnv}`);
+};
+
+const config = getPlaywrightConfig({
+ startCommand: getStartCommand(),
+ port: 3030,
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/sentry.edge.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/sentry.edge.config.ts
new file mode 100644
index 000000000000..898ff48d9d6f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/sentry.edge.config.ts
@@ -0,0 +1,11 @@
+import * as Sentry from '@sentry/nextjs';
+
+Sentry.init({
+ // This app is the static counterpart of `nextjs-16-trailing-slash`, which streams spans.
+ // Do not port it: it exists to keep the static trace lifecycle covered end to end.
+ traceLifecycle: 'static',
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1.0,
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/sentry.server.config.ts
new file mode 100644
index 000000000000..898ff48d9d6f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/sentry.server.config.ts
@@ -0,0 +1,11 @@
+import * as Sentry from '@sentry/nextjs';
+
+Sentry.init({
+ // This app is the static counterpart of `nextjs-16-trailing-slash`, which streams spans.
+ // Do not port it: it exists to keep the static trace lifecycle covered end to end.
+ traceLifecycle: 'static',
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.NEXT_PUBLIC_E2E_TEST_DSN,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1.0,
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/start-event-proxy.mjs
new file mode 100644
index 000000000000..5ff551125b03
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/start-event-proxy.mjs
@@ -0,0 +1,14 @@
+import * as fs from 'fs';
+import * as path from 'path';
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json')));
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'nextjs-16-trailing-slash-static',
+ envelopeDumpPath: path.join(
+ process.cwd(),
+ `event-dumps/next-16-trailing-slash-v${packageJson.dependencies.next}-${process.env.TEST_ENV}.dump`,
+ ),
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/tests/trailing-slash-parameterization.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/tests/trailing-slash-parameterization.test.ts
new file mode 100644
index 000000000000..e6e627329102
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/tests/trailing-slash-parameterization.test.ts
@@ -0,0 +1,153 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+// These tests verify that pageload transactions are correctly named when
+// trailingSlash: true is enabled in next.config.mjs, even when a catch-all
+// route exists. See: https://github.com/getsentry/sentry-javascript/issues/19241
+//
+// This is the static counterpart of `nextjs-16-trailing-slash`. That app asserts the same
+// routes on streamed spans; this one keeps the transaction-based path covered.
+
+test('should create a correctly named pageload transaction for a static route', async ({ page }) => {
+ const transactionPromise = waitForTransaction('nextjs-16-trailing-slash-static', async transactionEvent => {
+ return transactionEvent.transaction === '/static-page' && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto(`/static-page`);
+
+ const transaction = await transactionPromise;
+
+ expect(transaction).toMatchObject({
+ contexts: {
+ trace: {
+ data: {
+ 'sentry.op': 'pageload',
+ 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation',
+ 'sentry.segment.name.source': 'route',
+ 'url.template': '/static-page',
+ },
+ op: 'pageload',
+ origin: 'auto.pageload.nextjs.app_router_instrumentation',
+ },
+ },
+ transaction: '/static-page',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ });
+});
+
+test('should create a correctly named pageload transaction for a parameterized route', async ({ page }) => {
+ const transactionPromise = waitForTransaction('nextjs-16-trailing-slash-static', async transactionEvent => {
+ return (
+ transactionEvent.transaction === '/parameterized/:param' && transactionEvent.contexts?.trace?.op === 'pageload'
+ );
+ });
+
+ await page.goto(`/parameterized/some-value`);
+
+ const transaction = await transactionPromise;
+
+ expect(transaction).toMatchObject({
+ contexts: {
+ trace: {
+ data: {
+ 'sentry.op': 'pageload',
+ 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation',
+ 'sentry.segment.name.source': 'route',
+ },
+ op: 'pageload',
+ origin: 'auto.pageload.nextjs.app_router_instrumentation',
+ },
+ },
+ transaction: '/parameterized/:param',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ });
+});
+
+test('should create a correctly named pageload transaction for a static nested route under parameterized', async ({
+ page,
+}) => {
+ const transactionPromise = waitForTransaction('nextjs-16-trailing-slash-static', async transactionEvent => {
+ return (
+ transactionEvent.transaction === '/parameterized/static' && transactionEvent.contexts?.trace?.op === 'pageload'
+ );
+ });
+
+ await page.goto(`/parameterized/static`);
+
+ const transaction = await transactionPromise;
+
+ expect(transaction).toMatchObject({
+ contexts: {
+ trace: {
+ data: {
+ 'sentry.op': 'pageload',
+ 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation',
+ 'sentry.segment.name.source': 'route',
+ 'url.template': '/parameterized/static',
+ },
+ op: 'pageload',
+ origin: 'auto.pageload.nextjs.app_router_instrumentation',
+ },
+ },
+ transaction: '/parameterized/static',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ });
+});
+
+test('should create a correctly named pageload transaction for the catch-all route', async ({ page }) => {
+ const transactionPromise = waitForTransaction('nextjs-16-trailing-slash-static', async transactionEvent => {
+ return transactionEvent.transaction === '/:slug*' && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto(`/some/unmatched/path`);
+
+ const transaction = await transactionPromise;
+
+ expect(transaction).toMatchObject({
+ contexts: {
+ trace: {
+ data: {
+ 'sentry.op': 'pageload',
+ 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation',
+ 'sentry.segment.name.source': 'route',
+ },
+ op: 'pageload',
+ origin: 'auto.pageload.nextjs.app_router_instrumentation',
+ },
+ },
+ transaction: '/:slug*',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ });
+});
+
+test('should create a correctly named pageload transaction for the home page', async ({ page }) => {
+ const transactionPromise = waitForTransaction('nextjs-16-trailing-slash-static', async transactionEvent => {
+ return transactionEvent.transaction === '/' && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto(`/`);
+
+ const transaction = await transactionPromise;
+
+ expect(transaction).toMatchObject({
+ contexts: {
+ trace: {
+ data: {
+ 'sentry.op': 'pageload',
+ 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation',
+ 'sentry.segment.name.source': 'route',
+ 'url.template': '/',
+ },
+ op: 'pageload',
+ origin: 'auto.pageload.nextjs.app_router_instrumentation',
+ },
+ },
+ transaction: '/',
+ transaction_info: { source: 'route' },
+ type: 'transaction',
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/tsconfig.json b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/tsconfig.json
new file mode 100644
index 000000000000..cc9ed39b5aa2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-16-trailing-slash-static/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts", "**/*.mts"],
+ "exclude": ["node_modules"]
+}