Most Next.js apps go to production with no idea what's actually failing for users. console.error doesn't reach you, Vercel logs have minified stack traces, and you find out about errors when a user reports them. Sentry fixes this — but the App Router setup has enough gotchas that a "quick install" turns into an afternoon.
Installation
npx @sentry/wizard@latest -i nextjs
The wizard creates the three config files, wraps next.config.ts, generates instrumentation.ts and global-error.tsx, and sets up source map upload.
Three Config Files
Sentry initializes separately for each Next.js runtime:
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
integrations: [Sentry.replayIntegration({ maskAllText: true })],
enabled: process.env.NODE_ENV === 'production',
})
// sentry.server.config.ts
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 0.1,
enabled: process.env.NODE_ENV === 'production',
})
// sentry.edge.config.ts (for middleware)
Sentry.init({ dsn: process.env.SENTRY_DSN, tracesSampleRate: 0.1 })
instrumentation.ts
// instrumentation.ts (at root, not inside app/)
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')
}
}
Error Boundaries in App Router
error.tsx — route segment errors
'use client'
import { useEffect } from 'react'
import * as Sentry from '@sentry/nextjs'
export default function DashboardError({
error, reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// error.digest correlates client error with server log
Sentry.captureException(error, { tags: { digest: error.digest } })
}, [error])
return (
<div>
<h2>Something went wrong</h2>
<p>Reference: {error.digest}</p>
<button onClick={reset}>Try again</button>
</div>
)
}
global-error.tsx — root layout errors
error.tsx doesn't catch root layout errors. global-error.tsx does — and it must include <html> and <body>:
'use client'
export default function GlobalError({ error, reset }: { error: Error, reset: () => void }) {
useEffect(() => { Sentry.captureException(error) }, [error])
return (
<html><body>
<h1>Application Error</h1>
<button onClick={reset}>Reload</button>
</body></html>
)
}
Server Actions Instrumentation
Server Actions aren't automatically captured. Wrap them explicitly:
'use server'
import * as Sentry from '@sentry/nextjs'
export const createPost = Sentry.withServerActionInstrumentation(
'createPost',
{ recordResponse: true },
async (formData: FormData) => {
await db.insert(posts).values({
title: formData.get('title') as string,
})
return { success: true }
}
)
User Context
// After authentication
Sentry.setUser({ id: session.user.id, email: session.user.email })
Sentry.setTag('plan', 'pro')
Sentry.setContext('subscription', { seats: 25 })
// On logout
Sentry.setUser(null)
Source Maps
Without source maps: at n (chunks/pages.js:1:4523) — useless.
With source maps: at DashboardPage (app/dashboard/page.tsx:47:12) — actionable.
# .env.local + Vercel env vars
SENTRY_AUTH_TOKEN=your-token # Settings → API Keys in Sentry dashboard
SENTRY_ORG=your-org-slug
SENTRY_PROJECT=your-project-slug
withSentryConfig in next.config.ts uploads source maps automatically during next build.
Sampling Strategy
tracesSampler: (ctx) => {
if (ctx.parentSampled) return 1 // always trace if parent does
const url = ctx.location?.href ?? ''
if (url.includes('/checkout')) return 0.5
if (url.includes('/api/webhooks')) return 1.0
return 0.05 // default: 5%
},
Quick Reference
// Capture with context
Sentry.captureException(error, { tags: { action: 'checkout' } })
// User context
Sentry.setUser({ id: user.id, email: user.email })
// Custom span
await Sentry.startSpan({ name: 'db.query', op: 'db.query' }, () => db.select()...)
// Server Action wrapper
export const action = Sentry.withServerActionInstrumentation('name', {}, async () => {})
// error.tsx
useEffect(() => { Sentry.captureException(error) }, [error])
Minimum viable setup: run the wizard, set enabled: process.env.NODE_ENV === 'production', add error.digest in error.tsx, set user context after auth. Everything else layers in as the app matures.
Full article at stacknotice.com/blog/sentry-nextjs-complete-guide-2026
Top comments (0)