# Express

BugWatch captures errors, logs, and traces from your Express app and sends them to your BugWatch project. This page takes you from an empty project to a fully instrumented server.

## Install

```sh
npm install @newinstance/bugwatch
```

That is the only package you need. The Express integration ships in the same package as the subpath export `@newinstance/bugwatch/express`, and the request scope helpers live in `@newinstance/bugwatch/node`.

Requirements: Node.js 20.19.0 or newer, Express 4 or newer. The package ships dual ESM and CommonJS builds, so both `import` and `require` work.

## Get a project key

1. Sign in at [www.newinstance.cloud](https://www.newinstance.cloud).
2. Open your project, then go to BugWatch, your project, Settings, Data Source Name (DSN) keys.
3. Create a key and copy it. It looks like `sk_live_abc123:your-secret-here` (or `sk_test_...`).

Use the project's DSN key, not an organization-level API key: a key that is not bound to exactly one project is rejected at ingest with `This API key is not a BugWatch project key`.

Put it in an environment variable, never in code:

```sh
BUGWATCH_KEY=sk_live_abc123:your-secret-here
APP_VERSION=1.0.0
```

## Initialize once at startup

Call `BugWatch.init` before any other code runs, at the top of your entry file:

```ts
// src/server.ts
import { BugWatch } from "@newinstance/bugwatch";
import { installNodeErrorHandlers } from "@newinstance/bugwatch/node";

if (!process.env.BUGWATCH_KEY)
	throw new Error("BUGWATCH_KEY env var is required");

BugWatch.init({
	projectKey: process.env.BUGWATCH_KEY,
	serviceName: "checkout-api", // attributes spans on the service map
	release: process.env.APP_VERSION ?? "dev",
	environment: process.env.NODE_ENV ?? "development",
});

// Catches uncaught exceptions (level 60) and unhandled promise rejections (level 50)
// process-wide. Idempotent; returns an uninstall function. getClient() returns
// undefined before init, hence the non-null assertion after BugWatch.init.
installNodeErrorHandlers(BugWatch.getClient()!);
```

If you prefer an explicit client instead of the singleton (multi-tenant apps, isolated clients in tests), use `createClient({ projectKey })`. It exposes the same methods and is passed to the adapters the same way.

## Mount the two middlewares

The Express integration is two middlewares, and the order is the whole design:

```ts
import express from "express";
import {
	bugWatchExpressRequestHandler,
	bugWatchExpressErrorHandler,
} from "@newinstance/bugwatch/express";

const app = express();
app.use(express.json());

// FIRST, before any route or auth middleware.
app.use(bugWatchExpressRequestHandler(BugWatch.getClient()!));

// ...your routes...

// LAST, after every route and every other error handler.
app.use(bugWatchExpressErrorHandler(BugWatch.getClient()!));
```

`bugWatchExpressRequestHandler` opens an `AsyncLocalStorage` scope for the request and calls `next()` inside it. Everything downstream (your middlewares, your routes, their awaits, timers, and callbacks) runs inside that scope, so any capture made during the request inherits the request's method, URL, user, tags, and trace context. Mount it after your routes and those routes run outside the scope, so their captures arrive bare. It never touches the response.

`bugWatchExpressErrorHandler` is an Express error middleware (4 arguments), so Express only reaches it for errors, and only if it is registered after the routes that throw. It captures the error at level 50 with tags `method`, `route` (the request URL), and `status` (the response status code), triggers a flush, and then calls `next(err)`. It never swallows the error and never changes the response, so your own error handler still runs and still decides what the client sees. If you have your own error middleware, put BugWatch's last so it sees everything your handler passes along.

## What is captured automatically, and what is not

Captured with no extra code:

- Errors thrown in a route handler, or passed to `next(err)`, once the error handler is mounted last.
- Request metadata on every capture inside the request: `method` and `url`. (The `route` tag appears on error-handler captures, where it carries the request URL; the request handler runs before Express resolves a route, so it cannot record one.)
- The inbound `traceparent` header, or a fresh trace ID when there is none.
- Uncaught exceptions and unhandled rejections, if you called `installNodeErrorHandlers`.

Not captured automatically:

- Errors you catch yourself and never rethrow. Call `BugWatch.captureException(err)` for those.
- Request or response bodies, headers, and query strings. Attach only what you want with per-request tags or context.
- Non-error responses. A handler that returns a 500 without throwing is invisible to the error middleware.

## Attaching the user

There are two ways, and both are isolated per request, so concurrent requests can never overwrite each other's identity.

**Option 1: the `getUser` option (recommended).** Pass an extractor when registering the request handler. It is resolved lazily at capture time, which means it runs after your auth middleware has populated `req.user`, even though the handler itself is mounted before that middleware:

```ts
app.use(
	bugWatchExpressRequestHandler(BugWatch.getClient()!, {
		getUser: (req) => req.user, // e.g. { id, email } set by Passport or your JWT check
	}),
);
```

Return `null` or `undefined` for anonymous requests.

**Option 2: `setRequestUser`, for derived identity.** Use it when the user is not simply a field on `req`, or when you want to reshape it. Call it from your own middleware, mounted after the BugWatch request handler:

```ts
import { setRequestUser } from "@newinstance/bugwatch/node";

app.use((req, _res, next) => {
	if (req.user) setRequestUser({ id: req.user.id, email: req.user.email });
	next();
});
```

An explicit `setRequestUser` call always wins over `getUser`. It returns `true` when it was applied and `false` when called outside a request scope, in which case it does nothing at all and never touches the process-global scope. Do not use the global `BugWatch.setUser()` per request: that is process-wide, and a concurrent request will overwrite it.

## Per-request tags and context

`setRequestTag` and `setRequestContext` behave exactly like `setRequestUser`: they write only the current request's scope and return `false` outside one.

```ts
import { setRequestTag, setRequestContext } from "@newinstance/bugwatch/node";

app.post("/orders", async (req, res) => {
	setRequestTag("orderId", req.body.orderId);
	setRequestContext("cart", { items: req.body.items.length });
	await processOrder(req.body); // if this throws, the tags are on the event
	res.json({ ok: true });
});
```

Anything set this way is attached to every capture and log for the rest of that request and disappears when the request ends.

## Tracing

The request handler reads the inbound `traceparent` header and attaches its trace ID and span ID to every capture and log in that request, so a failure in your service links to the caller's trace. If the request arrives without a `traceparent`, the SDK synthesizes a fresh trace ID for the request (with no parent span), so every error and log in the request still shares a single trace. Nothing to configure beyond mounting the handler.

Time an operation inside a handler with `withSpan`. It records a thrown exception on the span, marks the span as an error, and rethrows:

```ts
const cart = await BugWatch.withSpan(
	"db.query load-cart",
	async (span) => {
		span?.setAttr("db.system", "postgresql");
		return loadCart(cartId);
	},
	{ kind: 3 }, // OTel span kind: 3 is client
);
```

Trace outbound HTTP with `wrapFetch`. It opens a client span per request, injects the `traceparent` header so the downstream service joins your trace, tags the response status, and marks 5xx and network failures as error spans:

```ts
import { wrapFetch } from "@newinstance/bugwatch";

const tracedFetch = wrapFetch(BugWatch.getClient()!);
const res = await tracedFetch("https://api.example.com/charge", { method: "POST" });
```

See the JavaScript & TypeScript overview's Distributed tracing section for `startSpan`, manual propagation with `traceHeaders()`, queue and job span links, and the OpenTelemetry bridge.

## Minting browser session tokens from Express

Your project key must never reach the browser. If you also run BugWatch in a frontend, mount the built-in route helper on your Express app so the browser can fetch a short-lived, ingest-only token instead:

```ts
import { bugWatchBrowserSessionHandler } from "@newinstance/bugwatch/express";

app.get(
	"/bugwatch/session",
	bugWatchBrowserSessionHandler({ projectKey: process.env.BUGWATCH_KEY! }),
);
```

The handler mints the session server side with your key and responds with the session JSON, or with a 502 and `{ error: "failed to mint BugWatch session" }` if minting fails. In the browser, init with `initBrowser({ sessionUrl: "/bugwatch/session" })` and no project key. The SDK fetches, caches, and refreshes the token for you.

## Flush and close on shutdown

Events and spans are batched and exported on a timer, so a process that exits immediately can drop whatever is still queued. Call `BugWatch.close()` in your shutdown path for a final flush, and `BugWatch.flush()` any time you need immediate delivery:

```ts
process.on("SIGTERM", () => {
	server.close(async () => {
		await BugWatch.close();
		process.exit(0);
	});
});
```

## Complete example

```ts
// src/server.ts
import express, {
	type Request,
	type Response,
	type NextFunction,
} from "express";
import { BugWatch } from "@newinstance/bugwatch";
import {
	bugWatchExpressRequestHandler,
	bugWatchExpressErrorHandler,
	bugWatchBrowserSessionHandler,
} from "@newinstance/bugwatch/express";
import {
	installNodeErrorHandlers,
	setRequestUser,
	setRequestTag,
} from "@newinstance/bugwatch/node";

if (!process.env.BUGWATCH_KEY)
	throw new Error("BUGWATCH_KEY env var is required");

BugWatch.init({
	projectKey: process.env.BUGWATCH_KEY,
	serviceName: "checkout-api",
	release: process.env.APP_VERSION ?? "dev",
	environment: process.env.NODE_ENV ?? "development",
});
installNodeErrorHandlers(BugWatch.getClient()!);

// Optional: the SDK's built-in structured logger (ships logs to BugWatch too).
const log = BugWatch.createLogger().child({ service: "api" });
const app = express();
app.use(express.json());

// 1. FIRST: opens the per-request scope.
app.use(bugWatchExpressRequestHandler(BugWatch.getClient()!));

// 2. Session mint endpoint for the frontend.
app.get(
	"/bugwatch/session",
	bugWatchBrowserSessionHandler({ projectKey: process.env.BUGWATCH_KEY }),
);

// 3. Auth: runs inside the scope, so this writes THIS request only.
app.use((req: Request, _res: Response, next: NextFunction) => {
	const userId = req.headers["x-user-id"] as string | undefined;
	if (userId) setRequestUser({ id: userId });
	next();
});

// 4. Routes.
app.get("/health", (_req: Request, res: Response) => {
	res.json({ ok: true });
});

app.post("/checkout", async (req: Request, res: Response) => {
	const orderId = (req.body as { orderId: string }).orderId;
	setRequestTag("orderId", orderId);
	log.info("checkout initiated", { orderId });

	await BugWatch.withSpan("checkout.process", async (span) => {
		span?.setAttr("order.id", orderId);
		await processOrder(req.body); // a throw here reaches the error handler
	});

	log.info("checkout complete", { orderId });
	res.json({ ok: true });
});

// 5. LAST: captures, flushes, then calls next(err).
app.use(bugWatchExpressErrorHandler(BugWatch.getClient()!));

const PORT = Number(process.env.PORT ?? 3000);
const server = app.listen(PORT, () => {
	log.info(`server listening on port ${String(PORT)}`);
});

process.on("SIGTERM", () => {
	server.close(async () => {
		await BugWatch.close();
		process.exit(0);
	});
});

async function processOrder(_body: unknown): Promise<void> {
	/* ... */
}
```

Verify it: start the app, then `curl -X POST http://localhost:3000/checkout -H "Content-Type: application/json" -d '{"orderId":"o_1"}'` and open the BugWatch dashboard, your project, Issues or Logs. Entries appear within a few seconds.

## Troubleshooting

**Nothing is captured when a route throws.** The error handler is not last. Express only routes an error to a 4-argument error middleware registered after the throwing route, so `bugWatchExpressErrorHandler` must be the final `app.use`, below every route, router, and other error handler.

**Async route errors are missing.** On Express 4, a rejected promise in an `async` handler is not forwarded automatically. Either wrap the handler so the rejection reaches `next(err)`, or catch and call `BugWatch.captureException(err)` yourself. An error caught with no rethrow and no capture is invisible to BugWatch.

**Your own error middleware swallows the error.** If a handler mounted before BugWatch's sends a response without calling `next(err)`, the chain stops there and BugWatch never sees the error. Always call `next(err)` from intermediate error handlers.

**Events arrive with no user or tags.** The identity was set outside the request scope. Confirm `bugWatchExpressRequestHandler` is mounted before the middleware calling `setRequestUser`, `setRequestTag`, or `setRequestContext`. Those helpers return `false` when there is no active scope, which is a quick way to check.

**Events never show up from a short script or a test.** The batch timer never fired before the process exited. `await BugWatch.flush()` after the capture, and `await BugWatch.close()` before exiting. The same applies to serverless handlers: flush before returning.

**`ingest 401: Invalid or inactive API key`, or `400: This API key is not a BugWatch project key`.** The key is revoked, from the wrong environment, or is not a project DSN key. Mint a fresh key under your project's Settings, DSN keys, and use the `sk_live_...` key in production. Set `debug: true` at init and the SDK prints the server's exact reason after the status code.
