# Node.js (Sentry-compatible SDK)

> Use the official Sentry SDK for Node.js with a Belvador DSN and the data-scrubbing hook Belvador provides.

For Node.js servers Belvador uses the **official Sentry SDK for Node.js** with your Belvador DSN. Belvador adds the init options below and its own scrubbing from `@belvador/core/sentry` — the code Belvador's SDKs run — which strips request bodies, cookies and credentials before anything leaves your server.

> [!NOTE]
> `@sentry/node` is Sentry's SDK, not a Belvador package; Belvador does not certify its framework integrations. Belvador runs this exact recipe with `@sentry/node` 11.9.0 against a real node: an uncaught crash, a `{ data, error }` result, and errors kept by `@belvador/spool` while the node was unreachable — through a `SIGKILL` and through an out-of-memory crash — each arrive once. Your node scrubs again before storing anything — see [Data protection](https://belvador.dev/en/docs/privacy).

## Before you start

- A project with platform **Node.js** and its DSN.
- A Node.js version supported by the current `@sentry/node` release (see [Sentry's Node.js guide](https://docs.sentry.io/platforms/javascript/guides/node/)).

## 1. Install

```bash
npm install @sentry/node@11.9.0 @belvador/core@0.6.1
```

`@belvador/core` is installed only for its stable `@belvador/core/sentry` entry: Belvador's scrubbing and `captureResult` for an official Sentry SDK. It loads no Sentry code of its own.

## 2. Create `instrument.mjs`

```js title="instrument.mjs"
import * as Sentry from "@sentry/node";
import { scrubBreadcrumb, scrubEvent } from "@belvador/core/sentry";

Sentry.init({
  dsn: process.env.BELVADOR_DSN ?? "https://<public-key>@ingest.belvador.dev/<project-id>",
  release: process.env.APP_VERSION,
  environment: process.env.NODE_ENV,
  sendDefaultPii: false,
  tracesSampleRate: 0,
  maxBreadcrumbs: 50,
  maxValueLength: 8192,
  // Belvador's scrubbing: request bodies, cookies and credentials never leave the app.
  beforeSend: scrubEvent,
  beforeBreadcrumb: scrubBreadcrumb,
});
```

What the options do:

| Option | Why |
|---|---|
| `dsn` | Reads `BELVADOR_DSN` from the environment, with the project DSN as fallback |
| `release`, `environment` | Your app's version and environment, from `APP_VERSION` and `NODE_ENV` |
| `sendDefaultPii: false` | No IP addresses, cookies or user details added by the SDK |
| `tracesSampleRate: 0` | The Sentry SDK sends no spans; for traces use [OpenTelemetry](https://belvador.dev/en/docs/otel/node) |
| `maxBreadcrumbs: 50`, `maxValueLength: 8192` | Same bounds as Belvador's own SDKs |
| `beforeSend: scrubEvent` | Removes request bodies, cookies, auth headers and client IP headers; masks values under sensitive names and credentials in text |
| `beforeBreadcrumb: scrubBreadcrumb` | The same scrubbing for breadcrumbs (URLs with tokens, logged values) |

## 3. Load it before your app

```bash
node --import ./instrument.mjs server.js
```

The `--import` flag loads the instrumentation first in an ES module app. In a CommonJS app, `require("./instrument")` on the very first line of your entry file instead (rename the file to `instrument.js` and use `require` there).

## Web frameworks

`@sentry/node` instruments common frameworks. Express, Fastify, Koa and others need one extra line to report errors from their error handler — follow the framework section of [Sentry's Node.js guide](https://docs.sentry.io/platforms/javascript/guides/node/) and keep the `init` options above. Belvador has not verified each framework.

## Report errors you handle

```js
import * as Sentry from "@sentry/node";

try {
  await chargeCard(order);
} catch (error) {
  Sentry.captureException(error);
  throw new PaymentFailed(order.id);
}
```

## Database errors that do not throw

Supabase and PostgREST return `{ data, error }` instead of throwing. Bind `captureResult` to your Sentry SDK once:

```js title="db.mjs"
// captureResult on the app's own Sentry SDK (@sentry/node, @sentry/bun or @sentry/deno).
import * as Sentry from "@sentry/node";
import { captureResultWith } from "@belvador/core/sentry";

export const captureResult = captureResultWith(Sentry);
```

Then wrap each call; the result comes back unchanged and the error is reported typed by its Postgres code. The types and options: [Database errors](https://belvador.dev/en/docs/sdk/database-errors).

## On a VPS: keep errors across crashes

Without help, `@sentry/node` keeps unsent errors in memory: a crash, a restart or a deploy while your node is unreachable loses them. Add `@belvador/spool` and use this `instrument.mjs` instead:

```js title="instrument.mjs"
import * as Sentry from "@sentry/node";
import { scrubBreadcrumb, scrubEvent } from "@belvador/core/sentry";
import { withSpool } from "@belvador/spool";

Sentry.init({
  dsn: process.env.BELVADOR_DSN ?? "https://<public-key>@ingest.belvador.dev/<project-id>",
  release: process.env.APP_VERSION,
  environment: process.env.NODE_ENV,
  sendDefaultPii: false,
  tracesSampleRate: 0,
  maxBreadcrumbs: 50,
  maxValueLength: 8192,
  // Belvador's scrubbing: request bodies, cookies and credentials never leave the app.
  beforeSend: scrubEvent,
  beforeBreadcrumb: scrubBreadcrumb,
  // Errors that cannot be sent wait on disk and go out after a restart.
  transport: withSpool(Sentry.makeNodeTransport, {
    dir: process.env.BELVADOR_SPOOL_DIR ?? "/var/lib/my-app/belvador",
  }),
});
```

- Point `BELVADOR_SPOOL_DIR` at a directory that survives restarts and that only your app's user can read — with systemd, `StateDirectory=my-app` gives you `/var/lib/my-app`.
- Errors wait there (at most 64 MiB, 72 hours) and are sent by the next process once your node answers. Tested: errors made while the node was unreachable arrived after the process was killed with `SIGKILL`, and after it died of out-of-memory.
- An out-of-memory crash itself is not reported — the process dies before any code can run — but every error it had already kept is.

Details: [Offline and durable delivery](https://belvador.dev/en/docs/sdk/offline).

## Set it up with an AI agent

Paste this into Claude Code, Cursor or another coding agent. It contains the files above for a server that keeps errors across crashes, and the rules it must follow:

````text
Belvador errors: Node.js (@sentry/node)

Tested with: @sentry/* 11.9.0
Guide: https://belvador.dev/en/docs/sdk/node

Goal
Report this server's errors to Belvador with the official Sentry SDK and Belvador's scrubbing, exactly as in the files below. Keep any other error SDK out: one Sentry.init per process.

Rules
Copy the files as they are and keep every Sentry.init option: sendDefaultPii: false, tracesSampleRate: 0, maxBreadcrumbs: 50, beforeSend: scrubEvent, beforeBreadcrumb: scrubBreadcrumb. Read the DSN from BELVADOR_DSN; do not invent other environment variables. Do not change authentication, routing or business logic. Report errors the code already handles with captureException (report() on Supabase Edge Functions) and wrap Supabase or PostgREST results that return an error instead of throwing with captureResult, without changing what the code does with the result.

```bash
npm install @sentry/node@11.9.0 @belvador/core@0.6.1
```

```bash
npm install @belvador/spool@0.6.1
```

instrument.mjs
```javascript
import * as Sentry from "@sentry/node";
import { scrubBreadcrumb, scrubEvent } from "@belvador/core/sentry";
import { withSpool } from "@belvador/spool";

Sentry.init({
  dsn: process.env.BELVADOR_DSN ?? "https://<public-key>@ingest.belvador.dev/<project-id>",
  release: process.env.APP_VERSION,
  environment: process.env.NODE_ENV,
  sendDefaultPii: false,
  tracesSampleRate: 0,
  maxBreadcrumbs: 50,
  maxValueLength: 8192,
  // Belvador's scrubbing: request bodies, cookies and credentials never leave the app.
  beforeSend: scrubEvent,
  beforeBreadcrumb: scrubBreadcrumb,
  // Errors that cannot be sent wait on disk and go out after a restart.
  transport: withSpool(Sentry.makeNodeTransport, {
    dir: process.env.BELVADOR_SPOOL_DIR ?? "/var/lib/my-app/belvador",
  }),
});
```

db.mjs
```javascript
// captureResult on the app's own Sentry SDK (@sentry/node, @sentry/bun or @sentry/deno).
import * as Sentry from "@sentry/node";
import { captureResultWith } from "@belvador/core/sentry";

export const captureResult = captureResultWith(Sentry);
```

Verify
Start the app locally with BELVADOR_DSN set and trigger one test error (a route or a one-off script that throws). Confirm it appears as an issue in the Belvador project. Do not send test errors to production unless the user says so.

Report
List the files you changed, the packages added with exact versions, the environment variables the app now needs, the test error you saw arrive, and anything you could not verify.
````

## Check that it works

Run a one-off script with the same instrumentation:

```js
import * as Sentry from "@sentry/node";

Sentry.captureException(new Error("Belvador Node.js test"));
await Sentry.flush(2000);
```

The issue appears in the project within a few seconds.

## Limits to know

- Belvador's scrubbing is pattern-based. A secret under an unusual name, or inside a free-text message, can slip through.
- Your node stores error events, attachments, check-ins and Sentry logs, and counts delivery reports. Keep `tracesSampleRate: 0`; tracing data and release-health sessions are counted as **Not supported yet** in the project's intake numbers and dropped. `@sentry/node` 11 sends request sessions and `Sentry.logger` calls by default, so expect a small **Not supported yet** count.
- Scheduled jobs and workers can report runs with `Sentry.withMonitor` — see [Monitors with the official Sentry SDKs](https://belvador.dev/en/docs/monitors/sentry-sdks).
- Windows services: the same recipe applies, but Belvador has not run it on Windows yet.
