Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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*
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function CatchAllPage() {
return <div>Catch-all page</div>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Link from 'next/link';

export default function Page() {
return (
<div>
<h1>Next 16 trailing slash test app</h1>
<ul>
<li>
<Link href="/static-page">Static Page</Link>
</li>
<li>
<Link href="/parameterized/foo">Parameterized</Link>
</li>
<li>
<Link href="/parameterized/static">Parameterized Static</Link>
</li>
</ul>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function ParameterizedPage() {
return <div>Dynamic parameterized page</div>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function ParameterizedStaticPage() {
return <div>Parameterized static page</div>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function StaticPage() {
return <div>Static page</div>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as Sentry from '@sentry/nextjs';

Sentry.init({
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,
sendDefaultPii: true,
});

export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { withSentryConfig } from '@sentry/nextjs';
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
trailingSlash: true,
};

export default withSentryConfig(nextConfig, {
silent: true,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "nextjs-16-trailing-slash",
"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:build-latest": "pnpm install && pnpm add next@latest && pnpm build",
"test:assert": "pnpm test:prod"
},
"dependencies": {
"@sentry/nextjs": "latest || *",
"@sentry/core": "latest || *",
"import-in-the-middle": "^2",
"next": "16.1.5",
"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"
},
"sentryTest": {
"variants": [
{
"build-command": "pnpm test:build-latest",
"label": "nextjs-16-trailing-slash (latest, turbopack)"
}
]
}
}
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/nextjs';

Sentry.init({
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,
sendDefaultPii: true,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/nextjs';

Sentry.init({
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,
sendDefaultPii: true,
});
Original file line number Diff line number Diff line change
@@ -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',
envelopeDumpPath: path.join(
process.cwd(),
`event-dumps/next-16-trailing-slash-v${packageJson.dependencies.next}-${process.env.TEST_ENV}.dump`,
),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
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.ts, even when a catch-all
// route exists. See: https://github.com/getsentry/sentry-javascript/issues/19241

test('should create a correctly named pageload transaction for a static route', async ({ page }) => {
const transactionPromise = waitForTransaction('nextjs-16-trailing-slash', 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.source': 'url',
},
op: 'pageload',
origin: 'auto.pageload.nextjs.app_router_instrumentation',
},
},
transaction: '/static-page',
transaction_info: { source: 'url' },
type: 'transaction',
});
});

test('should create a correctly named pageload transaction for a parameterized route', async ({ page }) => {
const transactionPromise = waitForTransaction('nextjs-16-trailing-slash', 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.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', 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.source': 'url',
},
op: 'pageload',
origin: 'auto.pageload.nextjs.app_router_instrumentation',
},
},
transaction: '/parameterized/static',
transaction_info: { source: 'url' },
type: 'transaction',
});
});

test('should create a correctly named pageload transaction for the catch-all route', async ({ page }) => {
const transactionPromise = waitForTransaction('nextjs-16-trailing-slash', 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.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', 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.source': 'url',
},
op: 'pageload',
origin: 'auto.pageload.nextjs.app_router_instrumentation',
},
},
transaction: '/',
transaction_info: { source: 'url' },
type: 'transaction',
});
});
Original file line number Diff line number Diff line change
@@ -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"]
}
Loading
Loading