Home

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/node

The 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 onRequestError hook,
  • anything you pass to bugtape.captureException() or bugtape.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 handlers

Errors 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

SentNever sent
Error type and a redacted messageVariable values
File, function, line and column of each frameSource code lines
cause errors, each redactedRequest 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' becomes No rate for '[redacted]'
  • numbers, amounts and percentages: margin 4.25% for acct 18233 becomes margin [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

OptionDefaultMeaning
apiKeyBUGTAPE_KEYCapture key
serviceBUGTAPE_SERVICE, DD_SERVICE, appName of this app or service
releaseBUGTAPE_RELEASE, DD_VERSION, then your CI or host's commitVersion or commit (see Release)
environmentBUGTAPE_ENVIRONMENT, DD_ENV, NODE_ENVe.g. production
endpointBUGTAPE_ENDPOINT, https://app.bugtape.ai/v1/ingestIngest URL
redacttrueSet false only for data you know is safe
extraRedactions[]Extra [pattern, replacement] pairs
beforeSendnoneChange or drop each report
sendMessagestruefalse sends the error type without its message
captureUncaughttrueReport uncaught exceptions and unhandled rejections
debugfalseLog 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.