Hono
BugWatch with Hono (including edge runtimes)link
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.
Installlink
npm install @newinstance/bugwatchOne 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: BugWatch → your project → Settings → Data Source Name (DSN) keys. 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)link
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.
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). 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 middlewarelink
Mount bugWatchHono first, before your routes:
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:
- Awaits
next(). - If a nested handler rethrew straight through
next(), it captures and rethrows (the error is never swallowed). - Otherwise it checks
c.error. Real Hono dispatch does not rethrow throughnext(): it routes a thrown error toonError/the default handler and records it onc.error. The middleware captures from there. - 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 scopeslink
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,setRequestContextare 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 getUserlink
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:
client.captureException(err, {
user: c.get("user"),
tags: { transferId, route: "/transfers" },
});Inbound traceparentlink
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 edgelink
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.
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
withSpanusage short-lived and tightly wrapped around one operation. It restores the previous context when the callback settles. - Do not rely on an enclosing
withSpanto 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 usespan.traceparent(), or build a header withbuildTraceparent(...)fromclient.getTraceContext()(theBugWatch.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 edgelink
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:
// 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 examplelink
// 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.
Troubleshootinglink
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).