ErrTapDocs

Browser SDK

Install @errtap/browser to auto-capture unhandled errors and promise rejections, send logs and user feedback, and report Core Web Vitals from real visitors.

Install

npm i @errtap/browser

Initialise

import { init, captureException, captureMessage } from '@errtap/browser';

init({
  dsn: 'https://et_<key>@<host>/<project>',
  environment: 'production',
  release: '1.4.3',          // source maps, release health, regressions
  tags: { team: 'checkout' } // attached to every event
});

init hooks window.onerror and unhandledrejection automatically, and starts measuring Web Vitals. Calling it again updates the configuration without adding duplicate listeners. Capture handled errors yourself:

try { risky(); } catch (e) {
  captureException(e, { tags: { flow: 'checkout' } });
}
captureMessage('payment fallback used', { level: 'warning' });

Options

OptionTypeDefaultDescription
dsnstringrequiredURL DSN (https://et_<key>@<host>/<project>), or a bare et_… key with endpoint
endpointstringderived from the DSNFull error-ingest URL override, e.g. https://<host>/ingest/error
environmentstring'production'Attached to every event, log and vital
releasestringnoneUsed for source maps, release health and regression detection
tagsRecord<string, unknown>noneAttached to every event and log
vitalsbooleantrueReport Core Web Vitals once per page view
vitalsSampleRatenumber1Fraction of page views (0 to 1) that report vitals

An invalid or missing DSN turns the SDK into a no-op rather than throwing.

endpoint only sets where errors go. Logs and vitals always go to /ingest/log and /ingest/vitals on the endpoint's origin, ignoring any path prefix (such as a proxy mounted at /errtap), and captureFeedback sends nothing unless the endpoint ends in /ingest/error.

API

captureException(error, options?)

Reports a caught error (anything thrown is accepted). options accepts:

FieldTypeDescription
tagsRecord<string, unknown>Key/value tags merged with init-level tags
contextRecord<string, unknown>Arbitrary extra context
user{ id?, email?, … }Who hit the error; id or email drives the users-affected count
levelstringerror (default), warning, info, fatal
fingerprintstringOverrides automatic grouping key
releasestringOverrides the release set at init
urlstringDefaults to the current page

captureMessage(message, options?)

Reports a string message. Accepts the same options as captureException.

logger

Send structured logs with call-specific context:

import { logger } from '@errtap/browser';

await logger.info('checkout started', { orderId: 'ord_1' });

Available levels are debug, info, warn/warning, and error. Pass user, request, or other dynamic context on each capture/log call; the SDK does not keep mutable global user/context state.

User feedback

Ask users what they were doing when something broke, and ErrTap attaches it to the error:

import { captureException, captureFeedback } from '@errtap/browser';

try {
  await checkout();
} catch (error) {
  captureException(error);
  showFeedbackDialog(async ({ message, email }) => {
    // linked to the error above via lastEventId(); pass eventId to link a different one
    await captureFeedback({ message, email });
  });
}

Feedback shows on the issue page (What users said) and in the project's User feedback list. It's kept for your plan's retention window and doesn't count toward your event quota. The same call exists in @errtap/node; Sentry SDKs' captureFeedback works too.

Web Vitals

From 0.5.0 the SDK measures LCP, INP, CLS, FCP and TTFB with PerformanceObserver and sends them in one request when the page is first hidden. The URL is sent without its query string or hash. Results appear on the project's Performance page.

init({ dsn, vitals: false });          // off
init({ dsn, vitalsSampleRate: 0.1 });  // 10% of page views

One report is sent per hard page load; single-page-app route changes are not separate page views. Each report counts as one event toward your quota. See Performance & Web Vitals for thresholds and the dashboard.

Delivery

  • Sends use fetch with keepalive, so reports made during unload still go out.
  • Network errors, timeouts and 5xx are retried twice. On 429 the SDK pauses for Retry-After and drops new events meanwhile.
  • Identical errors within 60 seconds are sent once, and each page sends at most 60 errors and 60 logs per minute, so a render loop can't burn your quota.

Release & environment

Set release and environment at init. ErrTap uses release for:

  • Regression detection — an issue resolved while 1.4.2 was the latest registered release reopens if it reappears in 1.4.3, once 1.4.3 is registered as a later deploy. See Regression behaviour.
  • Source map lookup — uploaded maps are keyed by release so frames are de-minified before grouping.
init({
  dsn: 'https://et_<key>@<host>/<project>',
  release: '1.4.3',
  environment: 'production',
});

Source maps (de-minify stack traces)

Production bundles are minified, so raw frames read like app.min.js:1:35060. ErrTap de-minifies them at ingest when it has your build's source maps for that release.

  1. Have your bundler emit source maps (e.g. Vite build.sourcemap: true, webpack devtool: 'source-map').

  2. After each build, POST the .js.map files to /ingest/sourcemaps under the same release you pass to init(), then delete them so they aren't served publicly:

    curl -X POST https://your-host/ingest/sourcemaps \
      -H "Authorization: Bearer $ERRTAP_AUTH_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"release":"1.4.3","files":[{"filename":"app.min.js","map":"<contents of app.min.js.map>"}]}'

    filename is the basename of the minified .js (not the .map) — that's the key symbolication matches against. Batches are capped at 50 files / 10MB.

    Uploads authenticate with an upload token (Project settings → Upload tokens), not the DSN: the DSN is public, and a sourcemap decides what every stack trace in that release says. Keep the token in CI secrets.

On Next.js, @errtap/next ships an errtap-upload-sourcemaps command that does all of this for you — see the Next.js guide.