Next.js SDK
Install @errtap/next to capture errors on both sides of a Next.js app (server via instrumentation.ts, browser via instrumentation-client.ts), report Web Vitals, and upload source maps.
Install
npm i @errtap/next@errtap/next is two thin wrappers: @errtap/next/server wraps @errtap/node, and
@errtap/next/client wraps @errtap/browser. Initialise both to cover Server Components,
Route Handlers, and everything that runs in the browser.
Server — instrumentation.ts
Next.js calls register() on boot for both the Node.js and Edge runtimes. Edge exposes
a partial process without process.on, so only init on Node and load the server SDK with a
dynamic import:
// instrumentation.ts
export async function register() {
// Edge has no process.on — skip Node SDK there
if (process.env.NEXT_RUNTIME !== 'nodejs') return;
if (!process.env.ERRTAP_DSN) return;
const { init } = await import('@errtap/next/server');
init({
dsn: process.env.ERRTAP_DSN, // https://et_…@host
environment: process.env.NODE_ENV,
release: process.env.VERCEL_GIT_COMMIT_SHA,
});
}
export async function onRequestError(error, request, context) {
if (process.env.NEXT_RUNTIME !== 'nodejs' || !process.env.ERRTAP_DSN) return;
const { onRequestError: captureRequestError } = await import('@errtap/next/server');
await captureRequestError(error, request, context);
}onRequestError is Next's request-error hook for Server Components, Route
Handlers, and Server Actions. @errtap/next/server excludes headers from the
event and sets exitOnFatal: false so Next owns the process.
Browser — instrumentation-client.ts
Requires Next.js 15.3+. The file runs before your app hydrates:
// instrumentation-client.ts
import { init } from '@errtap/next/client';
if (process.env.NEXT_PUBLIC_ERRTAP_DSN) {
init({
dsn: process.env.NEXT_PUBLIC_ERRTAP_DSN,
environment: process.env.NODE_ENV,
// Must match the release your sourcemaps are uploaded under (see Sourcemaps below).
release: process.env.NEXT_PUBLIC_ERRTAP_RELEASE ?? process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
});
}On Next.js before 15.3, call the same init from a Client Component rendered in the root
layout.
The browser side needs a NEXT_PUBLIC_-prefixed env var — DSN keys are write-only, so
exposing one to the client is by design.
Sourcemaps (de-minify browser stack traces)
Production browser bundles are minified, so raw stack traces read like
rD@…/chunks/abc.js:1:35060. ErrTap de-minifies them at ingest when it has the
release's sourcemaps — but only if three things line up:
-
The build emits sourcemaps. Turn them on in
next.config.mjs:const nextConfig = { productionBrowserSourceMaps: true, }; -
The maps are uploaded under a release.
@errtap/nextships an uploader that posts every.js.mapto/ingest/sourcemapsand then deletes the maps from the build so your source isn't served publicly. Run it afternext build:// package.json "scripts": { "build": "next build", "postbuild": "errtap-upload-sourcemaps" } -
The event and the maps share the same release. Set one release value for both the client SDK and the uploader. On Vercel:
NEXT_PUBLIC_ERRTAP_RELEASE=$VERCEL_GIT_COMMIT_SHA # read by init() above ERRTAP_DSN=https://et_…@your-host # tells the uploader which host ERRTAP_AUTH_TOKEN=etu_… # upload token — a secretCreate the upload token under Project settings → Upload tokens. Unlike the DSN it is a secret: set it only in your build environment, never as a
NEXT_PUBLIC_variable. The DSN can't upload maps — it ships in your browser bundle, so anyone could read it.The uploader reads
ERRTAP_RELEASE, thenNEXT_PUBLIC_ERRTAP_RELEASE, thenVERCEL_GIT_COMMIT_SHA. The SDK itself has no release fallback, so pass the release toinitexplicitly. The snippet above falls back to Vercel's automaticNEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA, which matches the uploader'sVERCEL_GIT_COMMIT_SHA, so no manual release variable is needed on Vercel. If the token, DSN or release is missing, the postbuild still deletes generated maps instead of leaving application source publicly accessible; it logs that upload was skipped.
Miss any one and frames stay minified: no release on the event skips symbolication
entirely, and a release with no uploaded maps has nothing to apply. Server errors
(instrumentation.ts) use the same mechanism — tag them with the same release.
Safari and Firefox report frames as
fn@url:line:col(noat); ErrTap symbolicates those too, so mobile-Safari errors de-minify like Chrome's.
Web Vitals
The client init wraps @errtap/browser, so Core Web Vitals (LCP, INP, CLS, FCP, TTFB) are
reported once per page load by default and shown on the project's Performance page.
Client-side route changes are not reported as separate page views.
init({ dsn, vitals: false }); // off
init({ dsn, vitalsSampleRate: 0.25 }); // 25% of page loadsUser feedback
import { captureFeedback } from '@errtap/next/client';
await captureFeedback({ message: 'Checkout froze', email });Feedback links to the last error the browser SDK sent unless you pass eventId. See
User feedback.
Env
| Variable | Where | Notes |
|---|---|---|
ERRTAP_DSN | Server | URL DSN or bare key for Node instrumentation; also tells the uploader the host |
ERRTAP_AUTH_TOKEN | Build only | Upload token for sourcemaps. Secret — never NEXT_PUBLIC_ |
NEXT_PUBLIC_ERRTAP_DSN | Browser | Same or separate project DSN for the client SDK |
NEXT_PUBLIC_ERRTAP_RELEASE | Browser + build | Release tag; must match on the event and the uploaded sourcemaps |
Keep real DSNs in .env.local (gitignored), not in source.
Manual capture
Import from the side you're on:
import { captureException, captureMessage, logger } from '@errtap/next/server';
// or '@errtap/next/client' in Client Components
logger.info('checkout started', { cartId });
try {
await chargeCard();
} catch (err) {
captureException(err as Error, { tags: { flow: 'checkout' } });
throw err;
}Verify
Ingest returns HTTP 202 when accepted (not a sync DB write):
curl -i -X POST https://api.errtap.com/ingest/error \
-H "Authorization: DSN et_<your_key>" \
-H "Content-Type: application/json" \
-d '{"message":"test event","type":"Error","environment":"production"}'Then trigger a real error once through the app so the SDK path is exercised. Events show under Issues.
Options
| Field | Type | Description |
|---|---|---|
dsn | string | URL DSN (https://et_…@host) or bare key (requires endpoint) |
endpoint | string | Override ingest URL; optional when dsn is a URL DSN |
environment | string | e.g. production, preview |
release | string | Ties errors to a deploy — pair with release tracking |
tags | Record<string, unknown> | Attached to every event |
vitals | boolean | Client only. Report Web Vitals (default true) |
vitalsSampleRate | number | Client only. Fraction of page loads that report vitals (default 1) |
exitOnFatal | boolean | Server only. Defaults to false so Next owns the process |