# Fastify

Wire the BugWatch JavaScript SDK (`@newinstance/bugwatch`) into a Fastify app in two lines: create a client, register the plugin. You get automatic error capture, per-request identity isolation, and trace propagation.

## Requirements

- Node.js >= 20.19.0
- Fastify >= 4
- The package ships dual ESM and CommonJS builds, so both `import` and `require` work.

## Install

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

No extra plugin package. The Fastify adapter ships as the `@newinstance/bugwatch/fastify` subpath export.

## Get a project key

Sign in at [www.newinstance.cloud](https://www.newinstance.cloud), open your project, then **BugWatch, your project, Settings, Data Source Name (DSN) keys** and create a key. It looks like `sk_live_abc123:secret` (or `sk_test_...`). Put the full value in `BUGWATCH_KEY` and read it at runtime. Use the project's DSN key, not an organization-level API key: org keys are rejected at ingest with `This API key is not a BugWatch project key` (HTTP 400).

## Wire it up

```ts
import Fastify from "fastify";
import { createClient } from "@newinstance/bugwatch";
import { bugWatchFastify } from "@newinstance/bugwatch/fastify";

const client = createClient({
	projectKey: process.env.BUGWATCH_KEY!,
	serviceName: "checkout-api",
	release: process.env.APP_VERSION,
	environment: process.env.NODE_ENV,
});

const app = Fastify();
bugWatchFastify(app, client, { getUser: (req) => (req as any).user });
```

`bugWatchFastify(app, client, options?)` takes exactly one option, `getUser: (req: FastifyRequest) => BugWatchUser | null | undefined`, where `BugWatchUser` is `{ id?, email?, username?, ip? }`. Fastify's `FastifyRequest` has no `user` field by default, so either cast or declare it via module augmentation for your auth plugin.

Note `bugWatchFastify(app, ...)` is a plain function call on the instance, not a Fastify plugin: do not pass it to `app.register(...)`, which would trap it in an encapsulated context.

**Order:** call `bugWatchFastify` on your **root** instance, before you register routes. It installs an `onRequest` hook, and Fastify runs `onRequest` hooks in registration order, so registering first means every later hook, `preHandler`, and handler runs inside the BugWatch request scope.

## What it does, exactly

Two things, and nothing else:

1. **An `onRequest` hook** that opens a per-request `AsyncLocalStorage` scope holding `method`, `url`, the inbound `traceparent` header, and (when you pass `getUser`) a lazy user resolver. Every capture in that request's async tree inherits it.
2. **`app.setErrorHandler(...)`** that captures any error reaching Fastify's error handler with `level: 50` and tags `{ method, route, status }` (`route` is `request.url`, `status` is `reply.statusCode`), triggers a flush, then calls `reply.send(err)` so Fastify produces its normal error response. Capture failures are swallowed internally: a monitor never crashes the host.

It does **not** install process-level handlers. Add those yourself for uncaught exceptions and unhandled rejections.

```ts
import { installNodeErrorHandlers } from "@newinstance/bugwatch/node";
installNodeErrorHandlers(client);
```

## Interaction with your own setErrorHandler (read this)

Fastify allows one error handler per encapsulation context, and **the last `setErrorHandler` call on a given instance wins**. That has real consequences:

- If you call `app.setErrorHandler(...)` on the root instance **after** `bugWatchFastify(app, client)`, your handler replaces BugWatch's and **automatic error capture stops**.
- If you call it **before**, BugWatch's replaces yours and your error formatting is lost. BugWatch's handler ends with `reply.send(err)`, which yields Fastify's default error payload, not your custom shape.
- A child plugin (an encapsulated `fastify.register(...)` scope) with its own `setErrorHandler` wins for its own routes, so errors in that subtree are **not** captured automatically.

If you need custom error responses, keep your handler and capture explicitly inside it. The `onRequest` scope is still open, so the user, tags, and trace context are attached for free:

```ts
bugWatchFastify(app, client, { getUser: (req) => (req as any).user });

// Overrides BugWatch's handler. Capture manually so nothing is lost.
app.setErrorHandler(async (err, request, reply) => {
	client.captureException(err, {
		level: 50,
		tags: {
			method: request.method,
			route: request.url,
			status: String(reply.statusCode),
		},
	});
	await client.flush();
	reply.status(err.statusCode ?? 500).send({ error: "internal_error" });
});
```

## Per-request identity

Two ways, both concurrency safe. Never call `client.setUser()` inside a request handler: that writes the process-global scope and concurrent requests will overwrite each other's user.

**`getUser` (recommended).** Resolved lazily at capture time, after auth has run, so `req.user` is already populated when an event is captured. One line, no extra hook.

**`setRequestUser` (for derived identity).** Call it from a `preHandler` once the request is authenticated. It writes only the current request's ALS cell and an explicit call wins over `getUser`. It returns `false` if called outside a request scope, in which case it does nothing.

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

app.addHook("preHandler", async (request) => {
	const id = request.headers["x-user-id"] as string | undefined;
	if (id) setRequestUser({ id });
});
```

## Per-request tags and context

`setRequestTag(key, value)` and `setRequestContext(key, data)` from `@newinstance/bugwatch/node` behave the same way: current request only, `false` when outside a scope.

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

app.post("/orders", async (request) => {
	setRequestTag("orderId", (request.body as { orderId: string }).orderId);
	setRequestContext("billing", { plan: "pro" });
	return { ok: true };
});
```

`method` and `url` are added as tags automatically by the `onRequest` hook.

## Tracing: inbound traceparent and spans

The `onRequest` hook reads the inbound `traceparent` header and attaches its trace ID and span ID to every capture and log in that request. If the header is absent or unparseable, the SDK **synthesizes a fresh trace ID** for the request (with no parent span), so a request is always traceable end to end even when it originates the trace. Nothing to configure.

Inside a route, `withSpan` times an operation and links captures inside it, and `wrapFetch` opens a client span per outbound call and injects `traceparent` so the downstream service joins your trace:

```ts
import { wrapFetch } from "@newinstance/bugwatch";
const tracedFetch = wrapFetch(client);

app.get("/cart", async () => {
	return client.withSpan(
		"db.query load-cart",
		async (span) => {
			span?.setAttr("db.system", "postgresql");
			const res = await tracedFetch("https://api.example.com/cart");
			return res.json();
		},
		{ kind: 3 },
	);
});
```

Set `serviceName` at init so spans land on the right service in the service map. For `startSpan`, span links, manual `traceHeaders()` propagation, and the OpenTelemetry bridge, see the JavaScript & TypeScript overview's Distributed tracing section.

## Shutdown

Events batch and flush on a timer, so a process that exits immediately can drop queued events. Flush on shutdown:

```ts
process.on("SIGTERM", async () => {
	await app.close();
	await client.close(); // final flush
	process.exit(0);
});
```

Use `client.flush()` when you want immediate delivery without shutting the client down (serverless handlers, tests, verification scripts).

If you also run BugWatch in a browser frontend, mint its session tokens from Fastify with the core `mintBrowserSession({ projectKey })` helper inside a route of your own (the ready-made handler is Express-only); see the Browser page.

## Complete example

```ts
// src/server.ts
import Fastify from "fastify";
import { createClient, wrapFetch } from "@newinstance/bugwatch";
import { bugWatchFastify } from "@newinstance/bugwatch/fastify";
import {
	installNodeErrorHandlers,
	setRequestTag,
	setRequestUser,
} from "@newinstance/bugwatch/node";

if (!process.env.BUGWATCH_KEY) throw new Error("BUGWATCH_KEY is not set");

const client = createClient({
	projectKey: process.env.BUGWATCH_KEY,
	serviceName: "checkout-api",
	release: process.env.APP_VERSION,
	environment: process.env.NODE_ENV,
});
installNodeErrorHandlers(client);

const app = Fastify({ logger: true });
const tracedFetch = wrapFetch(client);

// 1. FIRST: opens the ALS scope in onRequest, captures errors in setErrorHandler.
bugWatchFastify(app, client, {
	getUser: (req) => (req as { user?: { id: string; email: string } }).user,
});

// 2. Auth runs inside the scope opened above, so this request's scope only.
app.addHook("preHandler", async (request) => {
	const userId = request.headers["x-user-id"] as string | undefined;
	if (userId) {
		setRequestUser({ id: userId });
		setRequestTag("tier", (request.headers["x-tier"] as string) ?? "basic");
	}
});

// 3. Routes.
app.get("/health", async () => ({ ok: true }));

app.post("/checkout", async (request) => {
	const { orderId } = request.body as { orderId: string };
	setRequestTag("orderId", orderId);

	return client.withSpan(
		"checkout.charge",
		async (span) => {
			span?.setAttr("order.id", orderId);
			const res = await tracedFetch("https://api.example.com/charge", {
				method: "POST",
				body: JSON.stringify({ orderId }),
			});
			if (!res.ok) throw new Error(`charge failed: ${res.status}`);
			return { ok: true };
		},
		{ kind: 3 },
	);
});
// A throw here reaches BugWatch's setErrorHandler:
// user=<x-user-id>, tags={tier, orderId, method, url, route, status}, trace=inbound or synthesized.
// Concurrent requests each run their own ALS scope; neither can see the other's data.

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

await app.listen({ port: 3000, host: "0.0.0.0" });
```

Verify: start the app, send a request that throws, then open the BugWatch dashboard at [www.newinstance.cloud](https://www.newinstance.cloud) under your project's Issues tab. The error handler already calls flush, so it should appear within seconds.

## Troubleshooting

**Errors are not captured at all.** Something replaced BugWatch's error handler. Check for a later `app.setErrorHandler(...)` on the root instance, or an encapsulated plugin with its own handler covering those routes. See the section above and capture manually inside your handler.

**`setRequestUser` / `setRequestTag` returns `false`.** The call happened outside an ALS request scope: either `bugWatchFastify` was not registered, or the code path is not descended from the request (a module-level initializer, a `setInterval` created at boot). The SDK does nothing and never touches the global scope.

**User is missing on events.** With `getUser`, confirm the field actually exists on `FastifyRequest` at capture time; `req.user` is not standard Fastify and depends on your auth plugin. With `setRequestUser`, confirm the `preHandler` is registered after `bugWatchFastify`.

**`ingest 401 ... Invalid or inactive API key`.** The key is unknown, revoked, or from the wrong environment. Mint a fresh DSN key and use `sk_live_...` for production, `sk_test_...` for test.

**`ingest 400 ... This API key is not a BugWatch project key`.** You used an organization-level API key. Use the project's DSN key.

**Nothing appears in the dashboard.** Init `debug: true` on the client and watch for `[bugwatch]` delivery-failure diagnostics. Confirm `BUGWATCH_KEY` is not `undefined`. On serverless, always `await client.flush()` before the function returns.

**Traces do not join across services.** Use `wrapFetch` for outbound calls, or forward `client.traceHeaders()` manually on non-fetch HTTP clients and queue payloads. An existing `traceparent` header on an outbound request is never overwritten.
