-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Description
Is there an existing issue for this?
- I have checked for existing issues https://github.com/getsentry/sentry-javascript/issues
- I have reviewed the documentation https://docs.sentry.io/
- I am using the latest SDK release https://github.com/getsentry/sentry-javascript/releases
How do you use Sentry?
Sentry Saas (sentry.io)
Which SDK are you using?
@sentry/nextjs
SDK Version
10.14.0
Framework Version
Next 15.5.2
Link to Sentry event
No response
Reproduction Example/SDK Setup
GitHub Issue: Sentry + Next.js 15 + next-intl Web Vitals Issue
Issue Title: Critical: Next.js 15 + next-intl App Router creates unusable Sentry Web Vitals - all English routes collapse to /:locale
, navigation not tracked
Repository: https://github.com/getsentry/sentry-javascript/issues
Description
I'm experiencing a critical issue with Sentry Web Vitals in a Next.js 15 App Router application using next-intl
with localePrefix: "as-needed"
. The transaction naming is completely broken, making performance monitoring unusable.
Environment
- Sentry SDK Version:
@sentry/[email protected]
- Next.js Version:
15.5.2
- next-intl Version:
^4.3.7
- Configuration: App Router with
app/[locale]/
structure
Critical Issues
1. All English routes collapse to /:locale
When browsing in English (default locale, no URL prefix):
- Visiting
/
creates transaction:/:locale
- Visiting
/foo
creates transaction:/:locale
- Visiting
/bar
creates transaction:/:locale
- All different pages show as the same route in Sentry
2. Arabic routes get parameterized patterns
When browsing in Arabic (with /ar
prefix):
- Visiting
/ar/foo
creates transaction:/:locale/foo
- Should be normalized to
/foo
for proper grouping
3. Duplicate transactions for root page
- Visiting
/
(English) sometimes creates BOTH:- Transaction:
/
(correct) - Transaction:
/:locale
(incorrect duplicate)
- Transaction:
4. Next.js navigation completely broken
- Locale switching (e.g., from
/
to/ar
) doesn't create Web Vitals transactions <Link>
navigation between pages doesn't trigger Web Vitals- Only direct page loads/refreshes create performance data
Current Configuration
next.config.ts
import {withSentryConfig} from "@sentry/nextjs";
import { NextConfig } from 'next';
import createNextIntlPlugin from 'next-intl/plugin';
const nextConfig: NextConfig = {
output: "standalone",
transpilePackages: ["@t3-oss/env-nextjs", "@t3-oss/env-core"],
htmlLimitedBots: /.*/,
productionBrowserSourceMaps: true,
};
const withNextIntl = createNextIntlPlugin({
requestConfig: './i18n/request.ts',
experimental: {
createMessagesDeclaration: ["./messages/en.json", "./messages/ar.json"],
},
}
);
export default withSentryConfig(withNextIntl(nextConfig), {
org: "fyler",
project: "javascript-nextjs-5v",
authToken: process.env.SENTRY_AUTH_TOKEN,
silent: !process.env.CI,
widenClientFileUpload: true,
disableLogger: true,
release: { setCommits: { auto: true } },
sourcemaps: {
disable: false,
assets: [
".next/static/**/*.js",
".next/static/**/*.js.map",
".next/server/**/*.js",
".next/server/**/*.js.map",
".next/edge-runtime-webpack.js",
".next/edge-runtime-webpack.js.map",
".next/instrumentation.js",
".next/instrumentation.js.map",
".next/middleware.js",
".next/middleware.js.map"
],
ignore: ["**/node_modules/**"],
deleteSourcemapsAfterUpload: false,
},
});
instrumentation-client.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NODE_ENV,
integrations: [Sentry.replayIntegration()],
tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
replaysSessionSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
replaysOnErrorSampleRate: 1.0,
});
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
sentry.server.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
});
sentry.edge.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.05 : 1.0,
});
instrumentation.ts
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;
What I've Tried
I'm aware of issue #4677 but most solutions there are deprecated in the latest SDK versions. I've attempted numerous approaches:
Deprecated/Non-working approaches from that discussion:
beforeNavigate
- removed from SDKIntegrations.BrowserTracing
- deprecated syntax- Manual
startTransaction
- deprecated API - Various route normalization hooks - don't prevent the core issue
Modern attempts that also failed:
beforeStartSpan
inbrowserTracingIntegration
beforeSendTransaction
filtering- Setting
disableManifestInjection: true
- Comprehensive transaction name normalization
- Dropping problematic transactions entirely
Expected Behavior
Transaction names should be:
/
for root page (regardless of locale)/foo
for foo page (regardless of locale)/bar
for bar page (regardless of locale)
With locale preserved as tags: i18n.locale: en/ar
Navigation should trigger Web Vitals:
<Link>
clicks between pages- Locale switching
- Programmatic navigation
Actual Behavior
English browsing:
/ → Transaction: /:locale (sometimes also /)
/foo → Transaction: /:locale
/bar → Transaction: /:locale
Arabic browsing:
/ar → Transaction: /:locale (should be /)
/ar/foo → Transaction: /:locale/foo (should be /foo)
/ar/bar → Transaction: /:locale/bar (should be /bar)
Navigation: No Web Vitals data for any client-side navigation.
Impact
This makes Sentry completely unusable for performance monitoring because:
- Cannot distinguish between pages - all English routes appear as
/:locale
- No navigation tracking - only page loads generate data
- Fragmented data - same logical pages have different transaction names per locale
- Cannot measure user journeys - no data for typical SPA navigation
Root Cause
The issue seems to be that Next.js 15 App Router with [locale]
dynamic segments confuses Sentry's automatic instrumentation, which:
- Creates transactions based on file system routes (
/:locale
) instead of actual URLs - Doesn't properly handle
next-intl
'slocalePrefix: "as-needed"
routing - Fails to track client-side navigation in i18n contexts
Questions
- Is there a working solution for Next.js 15 + App Router + next-intl?
- Should we avoid
localePrefix: "as-needed"
entirely when using Sentry? - Are there plans to fix i18n support in the Next.js SDK?
- Is there a way to completely override Sentry's automatic route detection?
This appears to be a fundamental compatibility issue between Sentry's Next.js integration and modern i18n patterns. Any guidance would be greatly appreciated.
Additional Context
- This issue affects production applications using common i18n patterns
- The problem makes Web Vitals monitoring completely unusable
- Similar issues exist but most solutions are deprecated in current SDK versions
- This seems like a critical compatibility gap that should be addressed
Labels to add when creating the issue:
bug
nextjs
performance
i18n
web-vitals
app-router
Steps to Reproduce
Steps to Reproduce
Prerequisites
- Node.js 18+
- Sentry account with a Next.js project created
- Basic understanding of Next.js App Router
Step 1: Create Next.js 15 App with App Router
npx create-next-app@latest sentry-i18n-bug --typescript --tailwind --eslint --app --no-src-dir
cd sentry-i18n-bug
Step 2: Install Required Dependencies
npm install @sentry/[email protected] next-intl@^4.3.7 rtl-detect@^1.1.2
Step 3: Set Up File Structure
Create the following file structure:
app/
├── [locale]/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── hola/
│ │ └── page.tsx
│ └── products/
│ └── page.tsx
├── globals.css
├── favicon.ico
├── global-error.tsx
└── not-found.tsx
i18n/
├── routing.ts
└── request.ts
messages/
├── en.json
└── ar.json
middleware.ts
instrumentation.ts
instrumentation-client.ts
sentry.server.config.ts
sentry.edge.config.ts
next.config.ts
.env.local
Step 4: Create Configuration Files
4.1 Create i18n/routing.ts
import { defineRouting } from 'next-intl/routing';
export const routing = defineRouting({
locales: ['en', 'ar'],
defaultLocale: 'en',
localePrefix: 'as-needed', // This is the key setting that causes the issue
});
4.2 Create i18n/request.ts
import { routing } from './routing';
import { getRequestConfig } from 'next-intl/server';
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
});
4.3 Create messages/en.json
{
"HomePage": {
"title": "Welcome to our website",
"description": "This is the English version"
},
"HolaPage": {
"title": "Hello Page",
"description": "This is the hello page in English"
},
"ProductsPage": {
"title": "Products",
"description": "Our products in English"
}
}
4.4 Create messages/ar.json
{
"HomePage": {
"title": "مرحباً بكم في موقعنا",
"description": "هذه هي النسخة العربية"
},
"HolaPage": {
"title": "صفحة مرحبا",
"description": "هذه صفحة الترحيب بالعربية"
},
"ProductsPage": {
"title": "المنتجات",
"description": "منتجاتنا بالعربية"
}
}
4.5 Create middleware.ts
import createMiddleware from 'next-intl/middleware';
import { routing } from './i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)']
};
Step 5: Create App Router Pages
5.1 Create app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { routing } from '@/i18n/routing';
import '../globals.css';
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export default async function RootLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const messages = await getMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider messages={messages}>
<nav style={{ padding: '1rem', borderBottom: '1px solid #ccc' }}>
<a href={`/${locale === 'en' ? '' : locale}`}>Home</a> |{' '}
<a href={`/${locale === 'en' ? '' : locale}${locale === 'en' ? '' : '/'}hola`}>Hola</a> |{' '}
<a href={`/${locale === 'en' ? '' : locale}${locale === 'en' ? '' : '/'}products`}>Products</a>
<div style={{ marginTop: '0.5rem' }}>
Language:
<a href="/" style={{ marginLeft: '0.5rem' }}>EN</a> |
<a href="/ar" style={{ marginLeft: '0.5rem' }}>AR</a>
</div>
</nav>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
5.2 Create app/[locale]/page.tsx
import { useTranslations } from 'next-intl';
export default function HomePage() {
const t = useTranslations('HomePage');
return (
<div style={{ padding: '2rem' }}>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
<p>Current URL: <code>{typeof window !== 'undefined' ? window.location.pathname : 'Server'}</code></p>
</div>
);
}
5.3 Create app/[locale]/hola/page.tsx
import { useTranslations } from 'next-intl';
export default function HolaPage() {
const t = useTranslations('HolaPage');
return (
<div style={{ padding: '2rem' }}>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
<p>Current URL: <code>{typeof window !== 'undefined' ? window.location.pathname : 'Server'}</code></p>
</div>
);
}
5.4 Create app/[locale]/products/page.tsx
import { useTranslations } from 'next-intl';
export default function ProductsPage() {
const t = useTranslations('ProductsPage');
return (
<div style={{ padding: '2rem' }}>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
<p>Current URL: <code>{typeof window !== 'undefined' ? window.location.pathname : 'Server'}</code></p>
</div>
);
}
Step 6: Set Up Sentry Configuration Files
6.1 Create next.config.ts
import { withSentryConfig } from '@sentry/nextjs';
import { NextConfig } from 'next';
import createNextIntlPlugin from 'next-intl/plugin';
const nextConfig: NextConfig = {
// Add any Next.js config options here
};
const withNextIntl = createNextIntlPlugin({
requestConfig: './i18n/request.ts',
});
export default withSentryConfig(withNextIntl(nextConfig), {
org: "your-sentry-org",
project: "your-sentry-project",
authToken: process.env.SENTRY_AUTH_TOKEN,
silent: true,
});
6.2 Create instrumentation-client.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NODE_ENV,
integrations: [Sentry.replayIntegration()],
tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
replaysSessionSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
replaysOnErrorSampleRate: 1.0,
});
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
6.3 Create sentry.server.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
});
6.4 Create sentry.edge.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.05 : 1.0,
});
6.5 Create instrumentation.ts
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;
Step 7: Set Up Environment Variables
Create .env.local
:
# Get these from your Sentry project settings
NEXT_PUBLIC_SENTRY_DSN=https://[email protected]/your-project-id
SENTRY_DSN=https://[email protected]/your-project-id
SENTRY_AUTH_TOKEN=your-auth-token-here
Step 8: Build and Run the Application
npm run build
npm run start
Step 9: Reproduce the Bug
9.1 Test English Browsing (Default Locale)
- Open browser to
http://localhost:3000/
- Navigate to
http://localhost:3000/hola
- Navigate to
http://localhost:3000/products
- Check Sentry dashboard after 5-10 minutes
Expected: Transactions should be /
, /hola
, /products
Actual: All show as /:locale
transaction
9.2 Test Arabic Browsing (Non-Default Locale)
- Open browser to
http://localhost:3000/ar
- Navigate to
http://localhost:3000/ar/hola
- Navigate to
http://localhost:3000/ar/products
- Check Sentry dashboard after 5-10 minutes
Expected: Transactions should be /
, /hola
, /products
Actual: Shows as /:locale
, /:locale/hola
, /:locale/products
9.3 Test Navigation Issues
- Open browser to
http://localhost:3000/
- Click on navigation links (don't use direct URL navigation)
- Switch languages using the EN/AR links
- Check Sentry dashboard
Expected: Each navigation should create Web Vitals data
Actual: Only page loads/refreshes create data, no navigation tracking
Expected Result
Expected Sentry Transaction Names:
- Root page:
/
(regardless of locale) - Hola page:
/hola
(regardless of locale) - Products page:
/products
(regardless of locale) - Locale preserved as tags:
i18n.locale: en
ori18n.locale: ar
Actual Result
Actual Sentry Transaction Names:
- English routes:
/:locale
(all pages show as same transaction) - Arabic routes:
/:locale
,/:locale/hola
,/:locale/products
- Sometimes duplicate transactions for same page
- No Web Vitals data for client-side navigation
Additional Context
Additional Notes
- The issue is most pronounced with
localePrefix: "as-needed"
- Changing to
localePrefix: "always"
may reduce the issue but breaks URL structure requirements in my project. - The problem affects both development and production builds
- Console logging in Sentry hooks may show normalization attempts, but final dashboard (inside insights-> Frontend -> Web Vitals) still shows wrong names
This reproduction case should demonstrate the exact issues described in the bug report.
Metadata
Metadata
Assignees
Projects
Status