Skip to content
Draft, under reviewUpdated 2026-06-13
On this page

You ship to production blind without this. Wire Sentry in on day one so the first unhandled exception lands in your inbox with a stack trace, a release tag, and the request that caused it, not a vague "it broke" from a user three days later.

Next.js 16 routes all of this through the instrumentation hooks: instrumentation.ts for the server and instrumentation-client.ts for the browser. Sentry plugs into both. You own the wiring; the wizard just bootstraps it.

Decisions

Use the official @sentry/nextjs package and run the wizard once. Do not hand-roll a custom error reporter. Sentry already hooks onRequestError, client errors, and tracing for you; reinventing it wastes your time and misses edge cases like React Server Component digests.

Keep the DSN public (it is safe in the client bundle, it only accepts events). Keep the auth token server-only. Tag every event with a release so you can answer "which deploy broke this" in one click.

Install and run the wizard

The wizard is the fast path. It detects App Router, writes the config files, patches next.config.mjs, and adds source map upload.

npx @sentry/wizard@latest -i nextjs

Answer the prompts: select your project, let it create the example route if you want a smoke test, and accept source maps. It writes:

  • sentry.server.config.ts and sentry.edge.config.ts (server and edge runtime init)
  • instrumentation-client.ts (browser init, the Next 16 client hook)
  • instrumentation.ts (server hook that loads the runtime configs and re-exports onRequestError)
  • a withSentryConfig wrapper in next.config.mjs
  • .env.sentry-build-plugin with your auth token (gitignore this)

Confirm the server instrumentation hook

Next 16 calls register once per server instance and routes server errors through onRequestError. The wizard wires both. Your instrumentation.ts should look like this:

// 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

The runtime split matters: the framework calls register in both the Node.js and Edge runtimes, so conditionally import the matching config or you will load Node-only code into an edge bundle. onRequestError is the Next 16 server-error hook; re-exporting Sentry's captureRequestError is what forwards uncaught server errors, including ones surfaced during Server Component rendering.

Confirm the client instrumentation hook

instrumentation-client.ts runs after the HTML loads but before hydration, which is the right moment to start error capture. Initialize Sentry at module top level and export onRouterTransitionStart so navigations become breadcrumbs.

// instrumentation-client.ts
import * as Sentry from '@sentry/nextjs'
 
Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  environment: process.env.NEXT_PUBLIC_VERCEL_ENV ?? 'development',
  tracesSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,
  replaysSessionSampleRate: 0.0,
})
 
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart

onRouterTransitionStart is the Next 16 client navigation hook. It fires with the target url and a navigationType of 'push' | 'replace' | 'traverse'. Wiring Sentry's handler here means every issue carries the route the user came from.

Handle the DSN correctly

Two different env names, two different scopes. Do not mix them up.

Avoid

// instrumentation-client.ts (browser)
Sentry.init({ dsn: process.env.SENTRY_DSN }) // undefined at runtime

Server-only env vars are not inlined into the client bundle, so the DSN is undefined and you silently capture nothing.

Prefer

// instrumentation-client.ts (browser)
Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN })
 
// sentry.server.config.ts (server): either name works server-side
Sentry.init({ dsn: process.env.SENTRY_DSN })

The DSN is not a secret; it only ingests events. The NEXT_PUBLIC_ prefix is what makes it reach the browser.

The auth token is different: it can publish releases and read project data. Keep it server-only and out of git.

Never expose SENTRY_AUTH_TOKEN to the client or commit .env.sentry-build-plugin. A leaked token lets anyone push releases and read your source maps.

Tag releases and upload source maps

Minified stack traces are useless. The build plugin uploads source maps and stamps a release so Sentry can un-minify and group by deploy. Confirm your next.config.mjs wrapper:

// next.config.mjs
import { withSentryConfig } from '@sentry/nextjs'
 
/** @type {import('next').NextConfig} */
const nextConfig = {
  // your existing config
}
 
export default withSentryConfig(nextConfig, {
  org: 'your-org',
  project: 'your-project',
  authToken: process.env.SENTRY_AUTH_TOKEN,
  silent: !process.env.CI,
  widenClientFileUpload: true,
  // strip uploaded maps from the public bundle so they are not served
  sourcemaps: { deleteSourcemapsAfterUpload: true },
})

On Vercel, set the release in your init configs from the deploy SHA so every event ties back to a commit:

// in each Sentry.init({ ... })
release: process.env.VERCEL_GIT_COMMIT_SHA,

Verify end to end

Add a throwaway server route and a client button that both throw, deploy to a preview, and confirm two issues land in Sentry with readable traces and the right environment and release tags.

// app/sentry-check/route.ts: uncached by default in Next 16
export async function GET() {
  throw new Error('Sentry server check')
}

Hit the route, watch the issue appear, then delete the route. If the trace is minified, your source map upload failed: check that SENTRY_AUTH_TOKEN is present in the build environment and the org/project slugs match.

Tuning sample rates

Start tracesSampleRate at 0.1. Full tracing on a busy app burns your quota fast and tells you little you cannot learn from 10 percent. Keep error capture at 100 percent (it is the default) and only sample performance traces. Turn session replay on only for error sessions (replaysOnErrorSampleRate: 1.0, replaysSessionSampleRate: 0.0) so you get a recording of what broke without recording every healthy visit.

Sample performance traces, never errors. You want every exception and a representative slice of traces.

Sources

  • Next.js docs01-app/02-guides/instrumentation.md
  • Next.js docs01-app/03-api-reference/03-file-conventions/instrumentation.md
  • Next.js docs01-app/03-api-reference/03-file-conventions/instrumentation-client.md