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/browserInitialise
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
| Option | Type | Default | Description |
|---|---|---|---|
dsn | string | required | URL DSN (https://et_<key>@<host>/<project>), or a bare et_… key with endpoint |
endpoint | string | derived from the DSN | Full error-ingest URL override, e.g. https://<host>/ingest/error |
environment | string | 'production' | Attached to every event, log and vital |
release | string | none | Used for source maps, release health and regression detection |
tags | Record<string, unknown> | none | Attached to every event and log |
vitals | boolean | true | Report Core Web Vitals once per page view |
vitalsSampleRate | number | 1 | Fraction 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:
| Field | Type | Description |
|---|---|---|
tags | Record<string, unknown> | Key/value tags merged with init-level tags |
context | Record<string, unknown> | Arbitrary extra context |
user | { id?, email?, … } | Who hit the error; id or email drives the users-affected count |
level | string | error (default), warning, info, fatal |
fingerprint | string | Overrides automatic grouping key |
release | string | Overrides the release set at init |
url | string | Defaults 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 viewsOne 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
fetchwithkeepalive, so reports made during unload still go out. - Network errors, timeouts and
5xxare retried twice. On429the SDK pauses forRetry-Afterand 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.2was the latest registered release reopens if it reappears in1.4.3, once1.4.3is 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.
-
Have your bundler emit source maps (e.g. Vite
build.sourcemap: true, webpackdevtool: 'source-map'). -
After each build, POST the
.js.mapfiles to/ingest/sourcemapsunder the samereleaseyou pass toinit(), 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>"}]}'filenameis 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/nextships anerrtap-upload-sourcemapscommand that does all of this for you — see the Next.js guide.