Next.js SDK
Install @errtap/next and capture errors on both sides of a Next.js app — the server via instrumentation.ts, the browser via instrumentation-client.ts.
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,
});
}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 # used by the uploaderThe uploader reads
ERRTAP_RELEASE, thenNEXT_PUBLIC_ERRTAP_RELEASE, thenVERCEL_GIT_COMMIT_SHA. Browser instrumentation also falls back to Vercel's automaticNEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA, so no manual release variable is required on Vercel. If the 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.
Env
| Variable | Where | Notes |
|---|---|---|
ERRTAP_DSN | Server | URL DSN or bare key for Node instrumentation + sourcemap upload |
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 |