# Koa

`@newinstance/bugwatch` ships a Koa middleware in the `@newinstance/bugwatch/koa` subpath. It opens a per-request `AsyncLocalStorage` scope, captures anything thrown downstream, and rethrows so Koa's own error handling is unchanged.

## Install

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

That is the only install. Every framework adapter lives in the same package as a subpath export. Requires Node.js >= 20.19.0 and Koa >= 2. The package ships dual ESM and CommonJS builds.

## 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 → DSN keys](/bugwatch) and create a key.
3. The key looks like `sk_live_abc123:your-secret`. Put it in `BUGWATCH_KEY` in your environment, never in client code.

Use the project's **DSN key**, not an organization-level API key. Org keys are not bound to one project and are rejected at ingest.

## Init and mount

```ts
import Koa from "koa";
import { createClient } from "@newinstance/bugwatch";
import { bugWatchKoa } from "@newinstance/bugwatch/koa";

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

// FIRST in the chain.
app.use(bugWatchKoa(client, { getUser: (ctx) => ctx.state.user }));
```

You can also use the `BugWatch` global singleton (`BugWatch.init({ ... })`) and pass `BugWatch` where the examples pass `client`. Both expose the same methods.

## Why mount it first

Koa middleware is an onion. `bugWatchKoa` wraps everything registered after it in an `AsyncLocalStorage` scope and in a `try/catch` around `await next()`. Anything mounted *before* it runs outside both, so:

- errors thrown by earlier middleware are never captured;
- `setRequestUser` / `setRequestTag` / `setRequestContext` called from earlier middleware return `false` and do nothing.

Mount it as the very first `app.use(...)`, before body parsers, auth, loggers, and routers.

## What is captured automatically

When a downstream middleware or route handler throws (or rejects), the middleware calls `captureException` with:

- `level: 50` (error)
- `tags.method` = `ctx.method`
- `tags.route` = `ctx.url` (the raw request URL, including the query string)
- `tags.status` = `String(ctx.status)`

then triggers a flush and **rethrows the original error**. It never swallows the error and never writes a response, so Koa's `ctx.onerror` and any custom error middleware behave exactly as before. If the capture itself fails, the failure is discarded and the original error is still rethrown.

The request scope also adds `method` and `url` tags to every capture made inside the request, including bare `client.captureException(...)` / `client.captureLog(...)` calls in your own code.

Note on `tags.status`: it reads `ctx.status` at the moment the error surfaces, before Koa maps the error onto the response. If your handler threw before setting a status, that is Koa's default `404`, not the `500` the client eventually receives.

## What is not captured

- **Errors you catch yourself.** A `try/catch` in your route means nothing reaches the middleware. Call `client.captureException(err)` inside your catch block; it inherits the request scope.
- **Errors in middleware mounted before `bugWatchKoa`.**
- **Non-error responses.** Returning `ctx.status = 500` without throwing captures nothing.
- **Process-level crashes.** Add `installNodeErrorHandlers(client)` from `@newinstance/bugwatch/node` for uncaught exceptions (captured at level 60) and unhandled rejections (level 50). It is idempotent and returns an uninstall function.

Because `ctx.throw(400, ...)` throws, 4xx client errors are captured as errors too. Filter them with `beforeSend` at init if you only want 5xx.

## Attaching the user

**`getUser` (recommended).** Pass it once when mounting. It is stored as a lazy resolver and called at capture time, not at request start, so it reads `ctx.state.user` after your auth middleware has populated it:

```ts
app.use(bugWatchKoa(client, { getUser: (ctx) => ctx.state.user }));
```

`getUser` may return `null` or `undefined` for anonymous requests.

**`setRequestUser`.** Use it when identity is derived rather than a plain field. It writes only the current request's ALS scope and returns `false` if called outside a request scope. An explicit `setRequestUser` wins over `getUser`.

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

app.use(async (ctx, next) => {
	const id = ctx.headers["x-user-id"];
	if (typeof id === "string") setRequestUser({ id });
	await next();
});
```

Never call `client.setUser(...)` inside a request handler. That writes the process-global scope shared by every concurrent request, so request B overwrites request A's user. Reserve it for single-identity processes such as workers and CLIs.

## Per-request tags and context

`setRequestTag` and `setRequestContext` from `@newinstance/bugwatch/node` behave the same way: current request only, isolated from concurrent requests, `false` when called outside a request scope.

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

router.post("/payments", async (ctx) => {
	const { paymentId, cart } = ctx.request.body as { paymentId: string; cart: unknown[] };
	setRequestTag("paymentId", paymentId);
	setRequestContext("cart", { items: cart.length, currency: "GBP" });
	ctx.body = { ok: true };
});
```

## Distributed tracing

The middleware reads the inbound `traceparent` header off `ctx.headers` and binds its trace ID and span ID to the request scope, so every capture and log in the request joins the caller's trace. If no `traceparent` arrives, a fresh trace ID is synthesized for the request, so a request is always traceable end to end even when it originates the trace.

Inside a route, `withSpan` times an operation and `wrapFetch` propagates the trace to downstream services:

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

const tracedFetch = wrapFetch(client);

router.get("/cart", async (ctx) => {
	ctx.body = await 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 },
	);
});
```

Full details, including `startSpan`, span links, and OpenTelemetry correlation, are in the Distributed tracing section of the JavaScript & TypeScript overview.

## Flush and close

Events and spans are queued and flushed on a background timer (5 seconds by default). The Koa middleware fires a flush after capturing an error but does not await it, so a process that exits immediately can lose the event.

- `await client.flush()` forces immediate delivery without shutting the client down.
- `await client.close()` performs a final flush and stops the client. Call it from your `SIGTERM` handler.

Minting browser session tokens from Koa: use 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/app.ts
import Koa, { type Context, type Next } from "koa";
import Router from "@koa/router";
import bodyParser from "koa-bodyparser";
import { createClient } from "@newinstance/bugwatch";
import { bugWatchKoa } from "@newinstance/bugwatch/koa";
import {
	installNodeErrorHandlers,
	setRequestTag,
} 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: "payments-api",
	release: process.env.APP_VERSION,
	environment: process.env.NODE_ENV,
});
installNodeErrorHandlers(client);

const app = new Koa();
const router = new Router();

// 1. FIRST: opens the ALS request scope, reads traceparent, catches and rethrows.
//    getUser is lazy, so it sees ctx.state.user set by the auth middleware below.
app.use(bugWatchKoa(client, { getUser: (ctx) => ctx.state.user }));

app.use(bodyParser());

// 2. Auth runs inside the scope.
app.use(async (ctx: Context, next: Next) => {
	const id = ctx.headers["x-user-id"];
	if (typeof id === "string") ctx.state.user = { id };
	await next();
});

// 3. Thrown errors are captured and rethrown.
router.post("/payments", async (ctx: Context) => {
	const { paymentId } = ctx.request.body as { paymentId: string };
	setRequestTag("paymentId", paymentId);
	await processPayment(paymentId);
	ctx.body = { ok: true };
});

// 4. Handled errors need an explicit capture.
router.get("/health", async (ctx: Context) => {
	try {
		await pingDatabase();
		ctx.body = { ok: true };
	} catch (err) {
		client.captureException(err, { tags: { check: "database" } });
		ctx.status = 503;
		ctx.body = { ok: false };
	}
});

app.use(router.routes()).use(router.allowedMethods());
// Captured event: user={id}, tags={method, url, route, status, paymentId}, trace from traceparent.
// Concurrent requests each get their own ALS scope, so identity never leaks between them.

const server = app.listen(3000);

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

async function processPayment(_id: string) {
	/* ... */
}
async function pingDatabase() {
	/* ... */
}
```

## Troubleshooting

**No events at all.** Set `debug: true` at init and watch for `[bugwatch]` delivery-failure diagnostics. Confirm `BUGWATCH_KEY` is defined and that the client is created before any capture.

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

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

**Errors from some middleware are missing.** That middleware is mounted before `bugWatchKoa`. Move `bugWatchKoa` to the top of the chain.

**No user on events.** Either `getUser` was not passed, or auth writes somewhere other than `ctx.state.user` (adjust the `getUser` callback), or you used `client.setUser` instead of `setRequestUser`.

**`setRequestUser` / `setRequestTag` returns `false`.** The call is outside the ALS request scope: `bugWatchKoa` is not mounted, is mounted too late, or the code path is not descended from the request (for example a module-level initializer).

**`tags.status` says 404 for a 500 response.** Expected. The tag is `ctx.status` at throw time, before Koa maps the error onto the response. Set `ctx.status` before throwing if you want a specific value.

**Events lost on shutdown or in serverless.** `await client.flush()` before returning, and `await client.close()` on `SIGTERM`.
