# Hono

This page covers `@newinstance/bugwatch` in a Hono app on Node.js, Cloudflare Workers, Vercel Edge, Deno and Bun. The Hono adapter is the edge-safe adapter: it uses no `AsyncLocalStorage`, so the same code runs unchanged on every one of those runtimes.

## Install

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

One package. The Hono middleware ships as the `./hono` subpath export, so there is no plugin package to add.

Get your key from the dashboard at [www.newinstance.cloud](https://www.newinstance.cloud): [BugWatch → your project → Settings → Data Source Name (DSN) keys](/bugwatch). It looks like `sk_live_abc123:secret`. Use a project DSN key, not an organization-level API key, and use one key per environment.

## Init (edge-safe)

Import only the core entry (`@newinstance/bugwatch`) and `@newinstance/bugwatch/hono`. **Never import `@newinstance/bugwatch/node` on an edge runtime**: that subpath needs `AsyncLocalStorage` and Node process hooks, and will not be importable on Workers, Vercel Edge or Deno Deploy.

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

export const client = createClient({
	projectKey: BUGWATCH_KEY, // from your runtime's env/bindings, never hardcoded
	serviceName: "checkout-edge",
	release: "checkout-edge@2.1.0",
	environment: "production",
	flushInterval: 0, // edge isolates freeze between requests; flush explicitly instead
});
```

`flushInterval: 0` disables the background flush timer. Edge isolates are frozen between requests, so a timer is not a reliable delivery path there. Deliver explicitly instead (see [Flushing on edge](#flushing-on-edge)). On Node-hosted Hono you can leave the default `5000`.

Read the key from the runtime's own mechanism: `env.BUGWATCH_KEY` on Cloudflare (created inside the fetch handler, or lazily memoized), `process.env.BUGWATCH_KEY` on Node/Bun/Vercel Edge, `Deno.env.get("BUGWATCH_KEY")` on Deno.

## Mounting the middleware

Mount `bugWatchHono` first, before your routes:

```ts
import { Hono } from "hono";
import { bugWatchHono } from "@newinstance/bugwatch/hono";

const app = new Hono();
app.use(bugWatchHono(client, { getUser: (c) => c.get("user") }));
```

What it does on every request:

1. Awaits `next()`.
2. If a nested handler rethrew straight through `next()`, it captures and rethrows (the error is never swallowed).
3. Otherwise it checks `c.error`. Real Hono dispatch does not rethrow through `next()`: it routes a thrown error to `onError`/the default handler and records it on `c.error`. The middleware captures from there.
4. Every capture is wrapped in try/catch internally. A monitor must never crash the host.

Each captured event gets `level: 50` (error) and tags `{ method: c.req.method, route: c.req.path }` automatically.

## Capture without request scopes

The Node adapters (Express, Koa, Fastify, Hapi, Nest) open an `AsyncLocalStorage` request scope, which is what makes `setRequestUser` / `setRequestTag` / `setRequestContext` work: bare `captureException` calls inside the request pick that identity up implicitly.

Hono has none of that, by design, because edge runtimes have no ALS. Instead the adapter is **stateless per capture**: at the moment an error is captured, it builds a fresh hint object and reads everything it needs out of Hono's own `c` context, which is a distinct object per request by construction. Two interleaved requests can never see each other's identity because they never share a cell.

Practical differences versus the Node adapters:

- `setRequestUser`, `setRequestTag`, `setRequestContext` are **not available**. They live in `@newinstance/bugwatch/node`.
- Never use `client.setUser(...)` per request as a substitute. That writes the process-global root scope, shared by every concurrent request, and misattributes users under load. Reserve global setters for single-identity processes (workers, CLIs).
- Manual captures do not inherit request identity automatically. Pass `{ user, tags }` explicitly in the hint.

## opts.context and getUser

```ts
app.use(
	bugWatchHono(client, {
		context: (c) => ({
			user: c.get("user"),
			tags: { env: c.req.header("x-env") ?? "unknown" },
		}),
		// getUser wins over context.user when both are provided; context.tags still merge.
		getUser: (c) => c.get("user"),
	}),
);
```

Both callbacks run lazily, at capture time, so they see the state as it stands when the error happened (after auth middleware has run), not as it stood when the request opened. `context().tags` are merged over the automatic `method`/`route` tags. If `getUser` is supplied it decides the user outright; `context().user` is only consulted when `getUser` is absent.

For manual captures inside a route, read from `c` yourself:

```ts
client.captureException(err, {
	user: c.get("user"),
	tags: { transferId, route: "/transfers" },
});
```

## Inbound traceparent

At capture time the adapter reads the `traceparent` request header via `c.req.header("traceparent")`, parses it with the W3C parser, and puts `traceId` and `spanId` directly into the capture hint. Nothing to configure: mounting the middleware is enough for errors in a Hono service to join the caller's distributed trace.

Hint values take precedence over the client scope and over any `traceContextProvider`. If the request carries no `traceparent`, the hint carries no trace ids and the event falls back to whatever trace context is active on the client (for example an enclosing `withSpan`).

## Spans and wrapFetch on edge

Spans work on edge runtimes. ID generation uses `globalThis.crypto.getRandomValues` with a `Math.random` fallback, and the exporter posts OTLP JSON with `globalThis.fetch`. No Node APIs are involved.

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

const tracedFetch = wrapFetch(client);
const res = await tracedFetch("https://payments.example.com/charge", {
	method: "POST",
});
```

`wrapFetch` opens a client-kind span per request, injects `traceparent` so the downstream service joins the trace (an existing `traceparent` header is never overwritten), tags the response status, and marks 5xx and network failures as error spans.

One honest caveat. `withSpan` sets the active trace context on a scope, and without ALS that scope is the client's **root scope**, which is global to the isolate. Concurrent requests in the same isolate share it. So on edge:

- Keep `withSpan` usage short-lived and tightly wrapped around one operation. It restores the previous context when the callback settles.
- Do not rely on an enclosing `withSpan` to decorate captures from a different request's async work.
- Prefer explicit hints for anything that must be attributed exactly: `client.captureException(err, { traceId, spanId, user, tags })`. For outbound propagation use `span.traceparent()`, or build a header with `buildTraceparent(...)` from `client.getTraceContext()` (the `BugWatch.traceHeaders()` shortcut exists on the global singleton only).

The middleware's own captures are unaffected by this: their trace ids come from the per-request hint, not from the shared scope.

## Flushing on edge

`bugWatchHono` starts a flush after each capture but does not await it. An edge runtime can freeze or discard the isolate the instant the response is returned, so an un-awaited promise is not a delivery guarantee. Make delivery explicit at the outer boundary:

```ts
// Cloudflare Workers / any runtime with an ExecutionContext
const res = await app.fetch(request, env, ctx);
ctx.waitUntil(client.flush());
return res;
```

Where there is no `waitUntil` (Deno Deploy, Bun, plain Node), `await client.flush()` before returning the response. `flush()` drains queued events and any buffered spans together, and never throws. Do not call `client.close()` per request: it clears the flush timer and is meant for process shutdown.

## Complete Cloudflare Worker example

```ts
// src/index.ts
import { Hono } from "hono";
import { createClient, type BugWatchClient, type BugWatchUser } from "@newinstance/bugwatch";
import { bugWatchHono } from "@newinstance/bugwatch/hono";

type Env = { BUGWATCH_KEY: string };
type Vars = { user: BugWatchUser };

let client: BugWatchClient | undefined;
const getClient = (env: Env): BugWatchClient =>
	(client ??= createClient({
		projectKey: env.BUGWATCH_KEY,
		serviceName: "checkout-edge",
		release: "checkout-edge@2.1.0",
		flushInterval: 0, // no background timer on edge; we flush explicitly below
	}));

const app = new Hono<{ Bindings: Env; Variables: Vars }>();

// 1. Auth: store identity on Hono's per-request context object c, never on a global.
app.use(async (c, next) => {
	const userId = c.req.header("x-user-id");
	if (userId) c.set("user", { id: userId });
	await next();
});

// 2. BugWatch: mounted after auth so getUser/context see the resolved user, but before routes.
app.use(async (c, next) =>
	bugWatchHono(getClient(c.env), {
		getUser: (ctx) => ctx.get("user"),
		context: (ctx) => ({ tags: { region: ctx.req.header("cf-ipcountry") ?? "unknown" } }),
	})(c, next),
);

// 3. Uncaught route error: captured from c.error with user, tags, and inbound traceparent.
app.get("/cart/:id", async (c) => {
	const cart = await loadCart(c.req.param("id"));
	if (!cart) throw new Error(`cart ${c.req.param("id")} not found`);
	return c.json(cart);
});

// 4. Handled error: capture explicitly with per-request identity read from c, do not rethrow.
app.post("/transfers", async (c) => {
	const { transferId } = await c.req.json<{ transferId: string }>();
	try {
		await processTransfer(transferId);
	} catch (err) {
		getClient(c.env).captureException(err, {
			user: c.get("user"),
			tags: { transferId, route: "/transfers" },
		});
		return c.json({ ok: false }, 500);
	}
	return c.json({ ok: true });
});

async function loadCart(_id: string): Promise<{ id: string } | null> {
	return null;
}
async function processTransfer(_id: string): Promise<void> {}

export default {
	async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
		const res = await app.fetch(request, env, ctx);
		ctx.waitUntil(getClient(env).flush()); // guarantees delivery before the isolate freezes
		return res;
	},
};
```

For a Node, Bun or Deno Hono server the app body is identical: read the key from the process environment, drop the `Bindings` generic, and `await client.flush()` in place of `ctx.waitUntil`.

## Troubleshooting

**Nothing arrives, no errors logged.** Turn on `debug: true` and watch for `[bugwatch]` delivery-failure diagnostics. On edge the usual cause is a frozen isolate: add `ctx.waitUntil(client.flush())` or `await client.flush()` at the boundary.

**`Cannot resolve node:async_hooks` at build time.** Something imported `@newinstance/bugwatch/node`. That subpath is Node-only. Use the core entry plus `@newinstance/bugwatch/hono` on edge.

**`setRequestUser` is not exported / does nothing.** It is Node-adapter only. On Hono use `getUser`, `opts.context`, or an explicit `{ user }` hint.

**Two events for one error.** A route captured manually and then rethrew, so the middleware captured the same error again from `c.error`. Either capture and return an error response, or let it throw and rely on the middleware.

**Events arrive with no user.** The user is not on `c` yet when the capture happens, or `getUser` reads the wrong key. `getUser` overrides `context().user` even when it returns `undefined`, so do not supply both for the same field.

**Errors are not captured at all.** The middleware must be mounted with `app.use` above the routes it should cover, and it must actually run for that path. An `onError` handler is fine, since Hono records the error on `c.error` regardless.

**Traces are split across services.** Confirm the caller sends `traceparent` and that you use `wrapFetch(client)` (or `span.traceparent()` on the active span) for outbound calls so the next hop inherits the ids.

**`ingest 401: Invalid or inactive API key`** or **`ingest 400: This API key is not a BugWatch project key`.** Use a per-project DSN key from Settings → DSN keys, matching the environment (`sk_live_…` for production).
