Skip to content

Error tracking

Next.js boilerplate with Sentry

Stack traces with the release and route attached, scrubbed of user data first.

Error and performance monitoring wired the way Next.js 16 expects it. instrumentation.ts covers the server and edge runtimes, and instrumentation-client.ts covers the browser. onRequestError captures Server Component and Route Handler failures. A global-error boundary catches the ones React would otherwise swallow. The part that is not boilerplate is src/lib/observability/scrub.ts. Every event passes through it before it leaves the process. Request bodies are dropped, not filtered. Console breadcrumbs are discarded. Users are identified by opaque id only. captureHandled() attaches a stable fingerprint, so caught errors group by cause, not by whichever helper wrapped them.

What Sentry adds to the agent layer: 2 rules · 2 skills · 5 solution docs · 1 MCP server

Maintained by @raviMITNext.js on Vercel

From the manifest

Should you pick Sentry?

Pick it if

Any app where a user saying "it broke" needs to become a stack trace with the release, the route and the browser attached. Strongest for a small team with no on-call rota: Sentry groups, deduplicates and tells you which deploy introduced the regression.

Watch out for

  • It is a data-protection decision, not just a tool choice. Errors carry request context by default, and Sentry 11's dataCollection defaults send cookies, request bodies and query strings. This battery turns those off and puts a scrubbing layer in front, but the responsibility stays yours.
  • Noise is the failure mode. An unconfigured project fills up with browser extension errors, chunk-load 404s after every deploy and aborted fetches. The generated ignoreErrors list covers the usual suspects. New ones arrive with every browser release.
Show 3 more
  • Source maps need a build-time token with write access to your org. Keep it a Vercel build variable. Never put it in client code or the runtime environment.
  • The SDK adds weight to the client bundle, more with Session Replay. Replay is off here because it records the DOM, and the DOM holds what your users typed.
  • Tracing at a high sample rate gets expensive fast on a busy app, and spans are less actionable than errors. Start at 10% and lower it.

What it costs

Free Developer plan: 1 user, 5,000 errors and 5M spans a month. Team starts at $26/month billed yearly, with unlimited users and 50,000 errors. Past the quota, events are dropped unless you set a pay-as-you-go budget. Sampling and noise filters stretch the quota.

Prices change. Check with Sentry before you commit.

registry/tested.yaml

Tested with Sentry

Each pair was installed, typechecked, linted, built and booted together.

Database
NeonSupabase
Admin panel
Admin panel
Customer support
Crisp

What it adds

What Sentry adds to the repo

Read straight from the sentry manifest, so it is exactly what lands in your repo.

Environment variables

  • NEXT_PUBLIC_SENTRY_DSNRequiredPublic, reaches the browser

    The project's ingest endpoint. Public by design: it identifies a project and authorises nothing but sending events, so it belongs in the client bundle. Copy it from Settings > Projects > your project > Client Keys (DSN). An invalid DSN disables the SDK silently, which is why bun run verify parses it.

    Placeholder
    https://examplePublicKey@o0.ingest.sentry.io/0
  • SENTRY_ORGRequired

    Your organisation slug, as it appears in the dashboard URL. Read by the build plugin when it uploads source maps.

    Placeholder
    your-org-slug
  • SENTRY_PROJECTRequired

    The project slug, from the same URL. Together with SENTRY_ORG it decides where source maps and releases are uploaded.

    Placeholder
    your-project-slug
  • SENTRY_AUTH_TOKENRequired

    BUILD-TIME SECRET. This token can write to your Sentry organisation. It is used by next build to upload source maps and create the release, and by the local sentry:issues script: the application never reads it at runtime. Never prefix it with NEXT_PUBLIC_, never import it from application code, and never commit it. On Vercel, set it as an environment variable for every environment that builds; that is enough, and it is deliberately absent from the runtime REQUIRED_ENV list so a server without it still boots. Scope it to project:releases (plus project:read and event:read if you want the issues script).

    Placeholder
    sntrys_replace_me
  • SENTRY_DEBUG_LOCALOptional

    Set to "1" to make the SDK send from a local dev run. Off by default: every config here is enabled only in production, so a debugging session cannot bury a real production issue under errors you are deliberately causing. Turn it on once to prove the wiring, then off again.

    Placeholder
    0

Dependencies

  • @sentry/nextjs^11.0.0

Scripts

  • bun run sentry:issues

    bun scripts/sentry-issues.ts

MCP server

  • sentry

    URL
    https://mcp.sentry.dev/mcp

Files it writes

15 files, at these exact paths.

  • scripts/1 file
    • sentry-issues.ts
  • src/5 files
    • app/1 file
      • global-error.tsx
    • lib/2 files
      • observability/2 files
        • scrub.ts
        • sentry.ts
    • instrumentation-client.ts
    • instrumentation.ts
  • tests/1 file
    • unit/1 file
      • scrub.test.ts
  • variants/6 files
    • auth-better-auth/2 files
      • slots/1 file
        • providers.tsx
      • src/1 file
        • components/1 file
          • observability/1 file
            • sentry-identity.tsx
    • auth-clerk/2 files
      • slots/1 file
        • providers.tsx
      • src/1 file
        • components/1 file
          • observability/1 file
            • sentry-identity.tsx
    • auth-supabase/2 files
      • slots/1 file
        • providers.tsx
      • src/1 file
        • components/1 file
          • observability/1 file
            • sentry-identity.tsx
  • sentry.edge.config.ts
  • sentry.server.config.ts

Stack slots it fills

The stack declares these injection points; this battery supplies the fragment, so the provider tree, the env check and the schema stay one file each instead of many.

  • @slot env-required
  • @slot legal-processors
  • @slot next-config-wrappers
  • @slot verify-checks

The differentiator

What Sentry teaches your agent

Other starter kits stop at the package. This is the part an agent reads: where it may work, what it must never do there, and the problems someone already solved.

Rules (2)

Loaded when the agent opens a matching file.

Every captured exception carries a stable fingerprint hint

Loads onsrc/**sentry.server.config.tssentry.edge.config.ts.claude/rules/sentry-capture.md
Capture through the helper, not the SDK
  • Use captureHandled(error, { fingerprint, tags }) from src/lib/observability/sentry.ts for anything you catch. It sets the fingerprint, the level and the mechanism consistently, and returns the event id so you can show the user a reference.
  • Bare Sentry.captureException(error) is for the boundaries the framework owns: global-error.tsx, error.tsx, onRequestError. Everywhere else, a bare capture is a future issue nobody can group.
  • Unhandled errors need no capture at all. The SDK already reports them, and catching an error only to re-report it hides the original stack.
Fingerprints describe the cause, not the occurrence
  • A fingerprint is a constant string: "stripe.webhook.signature-invalid", "search.upstream-timeout", "pdf.parse-failed".
  • It must contain no ids, no URLs with path parameters, no timestamps, no user input. A variable fingerprint creates one issue per occurrence, which is the same as having no error tracking.
  • Use dotted segments: subsystem, then failure. It reads well in a list and sorts sensibly.
  • Keep "{{ default }}" as the first element (the helper does this) so Sentry still splits genuinely different stacks within one class.
Why this rule exists

Sentry groups by stack trace. A shared helper (a fetchJson, a retry wrapper, a database client) means dozens of unrelated failures share their top frames and arrive as one issue titled after the helper. You get a single issue with 40,000 events and no way to tell which subsystem is broken. The fingerprint hint restores the grouping that the abstraction destroyed.

Capture at the boundary, once
  • Report where you handle the error, not where it is thrown and not at every level in between. A catch that captures and rethrows produces two events for one failure.
  • If you catch and continue, capture. If you catch and rethrow, do not.
  • Never swallow silently. An empty catch {} is invisible in production, and no amount of monitoring will find it.
  • Server Actions and Route Handlers should return a useful response and capture. Show the returned event id to the user: "reference a1b2c3d4" turns an unreproducible report into a lookup.
Levels mean something
  • fatal: the process or the request is dead.
  • error: the user's action failed. The default.
  • warning: degraded but handled: a retry succeeded, a fallback was used.
  • info: do not send it to Sentry. That is a log line.
Do not reconfigure the SDK inline

Sentry.init is called in exactly three places: sentry.server.config.ts, sentry.edge.config.ts and src/instrumentation-client.ts. Never call it again from application code, and never change beforeSend, dataCollection or the sample rates from inside a request: the change is global to the process and affects every other request it serves.

No personal data in breadcrumbs, tags or extra

Loads onevery file.claude/rules/sentry-no-pii.md

Sentry receives everything you attach to an event and stores it in a system with a wider access list than your database, in a jurisdiction you did not choose, for a retention period you did not set. Treat every field as "published to a third party", because that is what it is.

Never send
  • A raw request body. Not scrubbed, not truncated, not "just the fields we need". A body is arbitrary user content and the set of fields grows without anyone revisiting this decision. scrubErrorEvent in src/lib/observability/scrub.ts drops request.data outright: do not add it back.
  • Email addresses, names, phone numbers, addresses, dates of birth. Including inside a message string: throw new Error(\No user for ${email}\) puts an email in the issue title, where it is indexed and searchable.
  • Cookies, Authorization headers, session ids, tokens, API keys.
  • Card numbers, bank details, national ids: anywhere, in any form.
  • The contents of a user's document, message or upload, including model prompts and completions.
Never enable
  • A looser dataCollection (or dropping DATA_COLLECTION from a config). Sentry 11's defaults attach cookies, request bodies, query strings, database values and the client IP to every event. If someone loosens it for a debugging session, put it back in the same session. (sendDefaultPii was the Sentry 10 name for this switch; 11 removed it.)
  • Session Replay without masking. It records the DOM, which is a video of what your user typed. It is off in this configuration on purpose; enabling it is a decision with a privacy review attached, not a config tweak.
Send instead
  • An opaque user id and nothing else: Sentry.setUser({ id }), or identifyUser(id) from src/lib/observability/sentry.ts. That is enough to answer "how many people hit this", which is the only question the user field needs to answer.
  • Counts, enums, booleans, durations, status codes. { itemCount: 47 } instead of the items. { role: "admin" } instead of the user record.
  • Opaque references to a row you can look up yourself: an order id is fine when order ids are not derived from personal data.
  • A stable fingerprint hint describing the class of failure.
Tags are the sharpest edge

Tags are indexed and searchable. A tag containing an email turns the error tracker into a queryable directory of your users, and it is the field people reach for first because it is the most convenient. Tags must be low-cardinality: provider, plan, route_kind, outcome. If a tag can take more than a few hundred distinct values, it does not belong in a tag.

Breadcrumbs are collected automatically: fetch URLs, navigations, console calls. Console breadcrumbs carry whatever was logged, which on a bad day is a whole user record from a debugging console.log someone left in.

scrubBreadcrumb drops console breadcrumbs entirely and redacts URLs. Do not relax that to "make debugging easier". Add the specific breadcrumb you need with trail() instead, with primitive data values only.

If it already happened

Deleting an event does not undelete it from every copy. Assume the data is gone: rotate any leaked credential immediately, then fix the source, then use Sentry's data-scrubbing settings as a second line. Do not stop after the settings change: server-side scrubbing runs after the data has crossed the network.

Skills (2)

Invoked by name.

  • /scrub-pii

    Audit what this app actually sends to Sentry, extend the scrubbing layer for a new field or shape, and respond when something sensitive has already been sent.

    .claude/skills/scrub-pii/SKILL.md

  • /triage-errors

    Work the Sentry issue list: rank by users affected, separate regressions from background noise, find the deploy that caused it, and fix or suppress with a reason.

    .claude/skills/triage-errors/SKILL.md

Solution docs (5)

Written before you hit the problem. Each one ships in the repo at docs/solutions/ and is published here as a cookbook page.

How it fits

What Sentry needs, and what it goes well with

The resolver enforces this before it generates anything, and names every addition it makes.

Requires

Nothing. Sentry stands on its own.

Pairs well with

Nothing extra. Add any tested battery alongside Sentry.

Cannot be combined with

No hard conflicts.

Build a repo with Sentry

Free and MIT. The builder opens with Sentry picked. You download the zip right away, and we email you the link too.

Presets

Presets that already include Sentry

A tested selection with its own file tree and its own generated CLAUDE.md. Start from one instead of from blank.