ErrTapDocs

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:

  1. The build emits sourcemaps. Turn them on in next.config.mjs:

    const nextConfig = {
      productionBrowserSourceMaps: true,
    };
  2. The maps are uploaded under a release. @errtap/next ships an uploader that posts every .js.map to /ingest/sourcemaps and then deletes the maps from the build so your source isn't served publicly. Run it after next build:

    // package.json
    "scripts": {
      "build": "next build",
      "postbuild": "errtap-upload-sourcemaps"
    }
  3. 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 secret

    Create 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, then NEXT_PUBLIC_ERRTAP_RELEASE, then VERCEL_GIT_COMMIT_SHA. The SDK itself has no release fallback, so pass the release to init explicitly. The snippet above falls back to Vercel's automatic NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA, which matches the uploader's VERCEL_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 (no at); 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 loads

See Performance & Web Vitals.

User 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

VariableWhereNotes
ERRTAP_DSNServerURL DSN or bare key for Node instrumentation; also tells the uploader the host
ERRTAP_AUTH_TOKENBuild onlyUpload token for sourcemaps. Secret — never NEXT_PUBLIC_
NEXT_PUBLIC_ERRTAP_DSNBrowserSame or separate project DSN for the client SDK
NEXT_PUBLIC_ERRTAP_RELEASEBrowser + buildRelease 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

FieldTypeDescription
dsnstringURL DSN (https://et_…@host) or bare key (requires endpoint)
endpointstringOverride ingest URL; optional when dsn is a URL DSN
environmentstringe.g. production, preview
releasestringTies errors to a deploy — pair with release tracking
tagsRecord<string, unknown>Attached to every event
vitalsbooleanClient only. Report Web Vitals (default true)
vitalsSampleRatenumberClient only. Fraction of page loads that report vitals (default 1)
exitOnFatalbooleanServer only. Defaults to false so Next owns the process