BugTape docs
Node.js
Server errors, Express and Next.js, with messages redacted.
Report errors from Node.js servers, workers and Next.js apps. Messages are redacted by pattern before they leave your process. Variable values, request bodies, headers and query strings are never read.
Install
npm install @bugtape/nodeThe package has no dependencies and supports Node.js 20 and later, as ES modules or CommonJS. If your network cannot reach the npm registry, install the same reviewed tarball from BugTape: npm install https://app.bugtape.ai/sdk/node/bugtape-node-0.1.2.tgz.
Start
import * as bugtape from '@bugtape/node'; // or: const bugtape = require('@bugtape/node')
bugtape.init({ service: 'api', release: process.env.GIT_SHA });The capture key is read from the BUGTAPE_KEY environment variable. Use a key from Setup → Node.js in the console (bt_live_… or bt_test_…). Without a key, init logs one warning and reporting stays off.
From then on BugTape reports:
- uncaught exceptions,
- unhandled promise rejections (Node raises these as uncaught exceptions by default),
- errors passed to the Express error handler or the Next.js
onRequestErrorhook, - anything you pass to
bugtape.captureException()orbugtape.captureMessage().
try {
await runExport();
} catch (err) {
bugtape.captureException(err);
throw err;
}Crashes still crash
Without BugTape, an uncaught exception prints the error and exits with code 1 (in a worker thread, the Worker emits error). Adding any uncaughtException listener turns that off, so when BugTape's is the only one, it reports the error, waits up to 2 seconds for delivery, then hands the error back to Node, whose own default runs as before. The printed stack is the original one; only the short header line above it points at the line in BugTape that handed the error back. If your app has its own uncaughtException listener, that listener decides what happens. Loading the package twice (ES module and CommonJS, or two versions) still installs one listener.
Reject with Error objects: a rejection with a plain value has no stack, so all such reports share one location.
If you run Node with --unhandled-rejections=warn or none, or your app listens for unhandledRejection itself, rejections do not reach BugTape automatically. Call bugtape.captureException(reason) in your listener.
Express
app.use(routes);
app.use(bugtape.expressErrorHandler()); // after your routes, before your own error handlersErrors that end in a 5xx are reported with the method and the route pattern, such as POST /api/invoices/:id. 4xx errors such as 404 are skipped. The error is passed on unchanged, so your own error handler still answers the request. When no route matched, no path is sent, because a concrete path can carry names.
Next.js
Next.js 15 and later call onRequestError from instrumentation.ts for errors in pages, route handlers, server actions and middleware:
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') (await import('@bugtape/node')).init({ service: 'web' });
}
export async function onRequestError(...args: Parameters<typeof import('@bugtape/node').captureRequestError>) {
if (process.env.NEXT_RUNTIME === 'nodejs') await (await import('@bugtape/node')).captureRequestError(...args);
}Each report carries the route pattern (/clients/[id]) and the route type. Next's own redirect and not-found signals are skipped. captureRequestError waits up to 2 seconds for delivery, so the report is sent before a serverless function is frozen. The Edge runtime is not supported; the checks above skip it.
For browser errors in the same app, add the browser SDK as well.
What leaves your server
| Sent | Never sent |
|---|---|
| Error type and a redacted message | Variable values |
| File, function, line and column of each frame | Source code lines |
cause errors, each redacted | Request bodies, headers, cookies and query strings |
| Request method and route pattern (Express, Next.js) | Other environment variables |
Your service, release and environment | |
Datadog trace and span ids, when dd-trace is running |
File paths are sent relative to the working directory, so your server's directory layout stays private. The only environment variables read are the BugTape settings in the Options table and, when set, NODE_ENV, DD_SERVICE, DD_ENV and DD_VERSION, which are sent as the environment, service, release and Datadog fields, plus the commit variables your CI or host sets (listed under Release), read only to fill release.
Redaction replaces:
- quoted values:
No rate for 'Acme Pty Ltd'becomesNo rate for '[redacted]' - numbers, amounts and percentages:
margin 4.25% for acct 18233becomesmargin [num]% for acct [num] - SQL statements, email addresses, bearer tokens and JWTs, and URLs with credentials
- cloud storage paths, data file names, ids and IBANs
- values of
…_id=,name=,customer:,token=,password=and similar pairs
Quoted code names are kept where the message is about code: Cannot read properties of undefined (reading 'map') and Cannot find module './db' are sent as they are.
Values you pass in extra are redacted the same way. A value under a key such as client, customer, name, account or amount is replaced whole.
What redaction does not catch
Redaction works by pattern. A value written into a message without quotes or digits is not recognised: ` new Error(no rate for client ${name}) is sent as no rate for client Acme Pty Ltd`. For code like that, pick one:
- add a pattern with
extraRedactions, - check or drop reports with
beforeSend, - or set
sendMessages: false, which sends the error type and location without the message.
bugtape.init({
service: 'api',
extraRedactions: [[/\bACME-\d+\b/, '[account]']],
beforeSend: (report) => (String(report.title).includes('healthcheck') ? null : report),
});beforeSend receives the full JSON payload. Return it (changed or not) to send it, or null to drop it.
Grouping
Repeats of the same error group into one issue. The key is the error type, the redacted message and the code location. The code location is the innermost frame in your own code, not in node_modules or Node itself, and it is sent as a stable address such as node://api/routes/invoices/createInvoice. Redaction removes the changing values, so a timeout for account 18233 and one for account 20411 are the same issue.
Datadog
If your app already runs dd-trace, each report carries the active trace and span ids. Set your Datadog site in Settings → Integrations → Datadog, and the issue in BugTape links straight to the trace. BugTape never imports or starts Datadog itself. See Use BugTape with Datadog.
Options
| Option | Default | Meaning |
|---|---|---|
apiKey | BUGTAPE_KEY | Capture key |
service | BUGTAPE_SERVICE, DD_SERVICE, app | Name of this app or service |
release | BUGTAPE_RELEASE, DD_VERSION, then your CI or host's commit | Version or commit (see Release) |
environment | BUGTAPE_ENVIRONMENT, DD_ENV, NODE_ENV | e.g. production |
endpoint | BUGTAPE_ENDPOINT, https://app.bugtape.ai/v1/ingest | Ingest URL |
redact | true | Set false only for data you know is safe |
extraRedactions | [] | Extra [pattern, replacement] pairs |
beforeSend | none | Change or drop each report |
sendMessages | true | false sends the error type without its message |
captureUncaught | true | Report uncaught exceptions and unhandled rejections |
debug | false | Log BugTape's own delivery problems |
Release
Every report should carry a release, so BugTape can tell a fixed issue from a regression. Without one it records release_missing. When you pass no release and set neither BUGTAPE_RELEASE nor DD_VERSION, the SDK uses the first of these that is set: GITHUB_SHA, VERCEL_GIT_COMMIT_SHA, CF_PAGES_COMMIT_SHA, COMMIT_REF, CI_COMMIT_SHA, CIRCLE_SHA1, GIT_COMMIT, TRAVIS_COMMIT, BUILD_VCS_NUMBER, RENDER_GIT_COMMIT, RAILWAY_GIT_COMMIT_SHA, HEROKU_SLUG_COMMIT. The same CI list is used by the browser SDK; the last three are hosts that set the commit at runtime. The list is exported as COMMIT_ENV_KEYS.
init is safe to call more than once, for example on Next.js dev reloads: a call with the same settings keeps the running client.
Delivery
Reports go out in the background with a 5 second timeout, and an in-flight report never keeps the process alive by itself. BugTape retries once on a network error, a 5xx or a 429, using the same capture id so the server does not count it twice. At most 30 reports a minute are sent. BugTape never throws into your app.
Queued reports are sent before a script exits on its own, and BugTape never holds the exit for more than about 2 seconds, even when the endpoint does not answer. In a serverless handler or a script that calls process.exit, call await bugtape.flush() first. It resolves true when every report since the last flush was accepted, and false on timeout or when one was rejected or could not be sent (debug: true logs why).
Check it
BUGTAPE_KEY=bt_test_… node --input-type=module -e "import * as b from '@bugtape/node'; b.init({ service: 'check' }); b.captureMessage('BugTape Node check');"Then open Issues in the console.