# Node.js

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

```bash
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

```js
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()`.

```js
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

```js
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:

```ts
// 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](/docs/quickstart/) 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'` 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.

```js
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](/docs/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

```bash
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.
