Here's a scenario we've lived through: it's 11pm, a user emails saying 'the checkout is broken,' and you have absolutely zero idea what happened. No logs. No stack trace. Just a vague description and the sinking feeling that you've been flying blind for weeks. That's the moment you install Sentry and wonder why you didn't do it on day one.
Sentry for Next.js has gotten genuinely good — but it's also a bit of a maze to configure correctly, especially with the App Router, Server Actions, and edge runtimes all behaving differently. This post is what we wish existed when we were setting it up. No hand-waving, just the actual config that works.
Installation Without the Footguns
The official wizard (npx @sentry/wizard@latest -i nextjs) is actually pretty good and handles most of the boilerplate. Run it. But don't just accept everything blindly — there are a few defaults you'll want to change immediately.
npx @sentry/wizard@latest -i nextjs
# What it creates:
# - sentry.client.config.ts
# - sentry.server.config.ts
# - sentry.edge.config.ts
# - instrumentation.ts (Next.js 14+ way to init Sentry server-side)
# - next.config.ts gets wrapped with withSentryConfig()The wizard sets up three separate config files — one for client, server, and edge. This isn't overkill, it's how Next.js actually runs. Your server components run in Node, edge middleware runs in the V8 isolate, and your client components run in the browser. Sentry needs to know which context it's in.
The Config That Actually Matters
The defaults are fine, but you should tune a few things before you ship. The biggest one: tracesSampleRate. Leaving it at 1.0 in production will absolutely blow through your Sentry quota. We set it to 0.1 (10%) for traces, but keep error capture at 100%.
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// 10% of transactions for performance monitoring
// Errors are always captured regardless of this
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
// Only replay sessions where errors occur
replaysOnErrorSampleRate: 1.0,
// 0.1% of all sessions (replays are expensive quota-wise)
replaysSessionSampleRate: 0.001,
// Don't send events in development
enabled: process.env.NODE_ENV === 'production',
integrations: [
Sentry.replayIntegration({
// Mask all text by default (GDPR-friendly)
maskAllText: true,
blockAllMedia: true,
}),
],
// Ignore noise you don't care about
ignoreErrors: [
'ResizeObserver loop limit exceeded',
'ResizeObserver loop completed with undelivered notifications',
/^Network Error$/,
/ChunkLoadError/,
],
});That ignoreErrors list is worth copying. ResizeObserver errors are browser noise. ChunkLoadErrors usually mean a user has an old tab open after you deployed — annoying, not actionable. Network errors from client side are often just someone's flaky WiFi.
// sentry.server.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.SENTRY_DSN, // Note: no NEXT_PUBLIC_ prefix here
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
enabled: process.env.NODE_ENV === 'production',
// Spotlight shows Sentry events in dev tools locally
spotlight: process.env.NODE_ENV === 'development',
});Use NEXT_PUBLIC_SENTRY_DSN for the client config (it needs to be exposed to the browser) and SENTRY_DSN for server config. They should be the same DSN value — just different env var names for different contexts.
Catching Errors in Server Actions (The Part Nobody Docs Properly)
Server Actions are where error tracking gets interesting. If a Server Action throws, Next.js swallows the error on the client by default — you just get a generic error message. Sentry can catch these on the server side, but you need to make sure your error boundaries and logging are wired up correctly.
// lib/safe-action.ts — a thin wrapper that ensures Sentry captures failures
import * as Sentry from '@sentry/nextjs';
type ActionResult<T> =
| { success: true; data: T }
| { success: false; error: string };
export async function safeAction<T>(
actionFn: () => Promise<T>,
context?: Record<string, unknown>
): Promise<ActionResult<T>> {
try {
const data = await actionFn();
return { success: true, data };
} catch (error) {
// Sentry will auto-capture in instrumentation.ts,
// but we add context manually for better debugging
Sentry.captureException(error, {
extra: context,
tags: {
layer: 'server-action',
},
});
const message =
error instanceof Error ? error.message : 'Something went wrong';
// Never expose raw error messages to the client in production
return {
success: false,
error:
process.env.NODE_ENV === 'production'
? 'An error occurred. We have been notified.'
: message,
};
}
}
// Usage in a Server Action:
export async function createSubscription(userId: string, planId: string) {
return safeAction(
() => stripe.subscriptions.create({ customer: userId, items: [{ price: planId }] }),
{ userId, planId }
);
}The pattern above does two things: it catches errors so Sentry can log them with context, and it sanitizes what gets sent back to the client. Never send raw database errors or stack traces to the browser in production. Users don't need that info, and it's a security leak.
Error Boundaries for App Router Pages
App Router uses error.tsx files for error boundaries. By default, these catch errors but don't report them anywhere. You need to add Sentry capture inside them.
// app/error.tsx
'use client';
import { useEffect } from 'react';
import * as Sentry from '@sentry/nextjs';
interface ErrorPageProps {
error: Error & { digest?: string };
reset: () => void;
}
export default function ErrorPage({ error, reset }: ErrorPageProps) {
useEffect(() => {
// The digest is Next.js's internal error ID
// Useful for correlating with server logs
Sentry.captureException(error, {
extra: { digest: error.digest },
});
}, [error]);
return (
<div>
<h2>Something went wrong</h2>
<p>We've been notified and are looking into it.</p>
{/* Show digest in dev for easier debugging */}
{process.env.NODE_ENV === 'development' && (
<pre>Digest: {error.digest}</pre>
)}
<button onClick={reset}>Try again</button>
</div>
);
}
// Also add global-error.tsx for layout-level errors:
// app/global-error.tsx follows the same pattern
// but must include <html> and <body> tagsOne thing that bit us: the error.tsx component only catches errors in the page segment it belongs to, not in layouts above it. If your root layout throws, you need a global-error.tsx at the app root. It's a slightly different component because it replaces the entire document, including your root layout, so you have to include html and body tags in it.
Source Maps: The Setup That Makes Stack Traces Useful
Without source maps, your Sentry errors will point to minified bundle files. You'll see something like 'Error at e.js:1:4892' which is completely useless. Source maps fix this. The Sentry webpack plugin uploads them during build.
// next.config.ts
import { withSentryConfig } from '@sentry/nextjs';
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// your existing config
};
export default withSentryConfig(nextConfig, {
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
// Silences the Sentry build output — up to you
silent: !process.env.CI,
// Upload source maps, then delete them from the build output
// You DON'T want source maps publicly accessible
widenClientFileUpload: true,
hideSourceMaps: true,
// Disable the default automatic instrumentation of API routes
// if you're using App Router — the instrumentation.ts approach is better
autoInstrumentServerFunctions: false,
// Tree-shake Sentry debug code in production
disableLogger: true,
// Required env vars for source map upload:
// SENTRY_AUTH_TOKEN — generate in Sentry settings
// SENTRY_ORG — your org slug
// SENTRY_PROJECT — your project slug
});The hideSourceMaps: true option is important. It strips source maps from your public build output after uploading them to Sentry. This means your users (and potential attackers) can't read your source code through the browser devtools, but Sentry can still show you meaningful stack traces.
Set SENTRY_AUTH_TOKEN in your CI environment, not in .env. It's a sensitive token with write access to your Sentry org. Commit it and you'll have a bad time.
Adding User Context (So You Know Who Broke What)
By default, Sentry captures errors but doesn't know which user triggered them. Adding user context turns 'unknown error in checkout' into 'user@example.com got this error at 11:43pm'. Much more useful when you're trying to help someone.
// In your auth session provider or root layout
// This sets the user context for all subsequent Sentry events
'use client';
import { useEffect } from 'react';
import * as Sentry from '@sentry/nextjs';
import { useSession } from 'next-auth/react'; // or your auth library
export function SentryUserContext() {
const { data: session } = useSession();
useEffect(() => {
if (session?.user) {
Sentry.setUser({
id: session.user.id,
email: session.user.email ?? undefined,
// Don't include sensitive fields like passwords obviously
});
} else {
// Clear user context on logout
Sentry.setUser(null);
}
}, [session]);
return null; // render-nothing component
}
// In app/layout.tsx:
// <SentryUserContext />
// <SessionProvider>
// {children}
// </SessionProvider>A word on privacy: in Europe (and Romania, where we're based), GDPR applies. Logging user emails in error tracking is personal data. Make sure your privacy policy mentions it, and consider using user IDs instead of emails if you want to be safer. Sentry also lets you configure PII scrubbing, which we'd recommend enabling by default.
Performance: Does Sentry Slow Down Your App?
Short answer: barely, if you configure it right. The Sentry client SDK is about 80-100kb gzipped, which is non-trivial but not catastrophic. The things that actually impact performance are lazy-loading sessions replays (they're large) and high tracesSampleRate.
- Session Replay: Only load it when needed. If replaysSessionSampleRate is 0 and replaysOnErrorSampleRate is low, the replay module may never execute
- tracesSampleRate at 1.0 in production adds overhead to every request — keep it at 0.1 or lower
- The withSentryConfig wrapper adds a small overhead to every build, but you don't feel this at runtime
- Source map uploads only happen at build time — zero runtime cost
- Tunnel route (/api/sentry-tunnel) bypasses ad blockers that block sentry.io — worth setting up if you care about completeness
To set up the tunnel route (so ad blockers don't eat your errors), add tunnelRoute: '/monitoring' to your withSentryConfig options. Sentry will automatically create an API route that proxies events to their servers. This has helped us get a much more complete picture of errors in production.
Alerts That Don't Drive You Insane
The default Sentry alerts will send you an email for every single new error. Within a week you'll have 400 unread emails and you'll turn off all notifications. Instead, set up alerts properly from day one.
- Alert on error frequency, not just occurrence: 'more than 10 occurrences in 10 minutes' is meaningful, 'any new error' is noise
- Create an 'ignored' filter for the errors you listed in ignoreErrors — they shouldn't even create issues
- Use issue priority to triage: P0/P1 issues get Slack alerts, P2/P3 are email only
- Set up a weekly digest instead of per-error emails once you're past the initial setup phase
- Tag errors by environment — you probably only want alerts for production, not staging
We have a single Slack channel for production P0 errors and a weekly digest email for everything else. This means when the Slack channel pings, we actually look at it. Alert fatigue is a real thing and it'll make you less reliable, not more.
The Quick Sanity Check
After setup, verify it actually works before you ship. Sentry ships a test button in their dashboard, but we prefer to add a quick verification route in development.
// app/api/sentry-test/route.ts
// DELETE THIS BEFORE DEPLOYING TO PRODUCTION
import * as Sentry from '@sentry/nextjs';
import { NextResponse } from 'next/server';
export async function GET() {
// Only allow in development
if (process.env.NODE_ENV !== 'development') {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
// Test server-side capture
Sentry.captureMessage('Sentry server-side test', 'info');
// Test exception capture
try {
throw new Error('Test error from /api/sentry-test');
} catch (e) {
Sentry.captureException(e);
}
return NextResponse.json({ ok: true, message: 'Check your Sentry dashboard' });
}Hit this endpoint, then check your Sentry dashboard. If you see the events within 30 seconds, your setup is working. If you don't, check that your DSN is correct and that enabled isn't false in your config. This has saved us from the embarrassment of shipping 'error tracking' that wasn't actually tracking anything.
If you're starting a new Next.js project and want Sentry already wired up correctly from the start, our templates at peal.dev include this full setup — App Router config, source maps, user context, and sane alert defaults — so you're not doing this from scratch every time.
The goal isn't to never have bugs. It's to know about them before your users email you. Sentry done right means you're already investigating by the time someone reports an issue.
One last thing: review your Sentry issues at least once a week, not just when something blows up. The interesting stuff is usually the low-frequency errors that never get bad enough to alert but are affecting a real chunk of users. Those are often the bugs that are actually hurting your retention, and you'd never know without looking.
