@newinstance/bugwatch
v0.2.2
Published
BugWatch SDK for capturing logs, errors, and context from JavaScript and TypeScript apps.
Maintainers
Readme
Table of contents
- Overview
- Supported environments
- Requirements
- Install
- Where to get your project key
- Environment variables
- Basic init (server-side)
- Framework adapters
- Per-request context and concurrency
- Capturing errors and messages
- Log forwarding — logger adapters
- Enriching events
- Release and environment
- Sensitive data redaction
- Sampling
- Distributed tracing
- Browser secure flow (CRITICAL reading for front-end apps)
- Debug mode
- Full configuration reference
- Testing
- Verification — did events arrive?
- Production checklist
- Troubleshooting
- Complete examples
- Compatibility
- Upgrade notes
- Links
Overview
One install. Every JS/TS framework and logger. Framework- and logger-specific helpers are exposed as subpath exports from the same package, so you never install a separate plugin package.
@newinstance/bugwatch
├── . → core API (BugWatch singleton + createClient)
├── ./node → Node.js process error handlers + AsyncLocalStorage request scope
├── ./browser → window error handlers (browser)
├── ./otel → OpenTelemetry trace-context bridge
├── ./testing → InMemoryTransport for unit tests
├── ./express → Express request + error middleware + session handler
├── ./fastify → Fastify error hook
├── ./koa → Koa middleware
├── ./hono → Hono middleware (edge-safe)
├── ./hapi → Hapi plugin
├── ./nest → NestJS exception filter
├── ./next → Next.js App Router route-handler wrapper (server)
├── ./next/client → Next.js browser entry (initBrowser, error boundary)
├── ./react → React error boundary component
├── ./vue → Vue 3 plugin
├── ./pino → Pino destination
├── ./winston → Winston transport
├── ./bunyan → Bunyan raw stream
├── ./log4js → log4js appender
└── ./console → console.warn/error capture (patch)Supported environments
| Runtime | Status |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Node.js ≥ 20 | Full — all adapters |
| Bun, Deno | Core + Hono (edge-safe middleware); node adapters require Node-compatible APIs |
| Cloudflare Workers, Vercel Edge, Deno Deploy | Core + ./hono; ./next for serverless handlers; no ./node (no AsyncLocalStorage) |
| Modern browsers (Chrome/Firefox/Safari/Edge) | ./browser + ./react + ./vue via session token (see browser secure flow) |
| Next.js server components and route handlers | ./next on the server; ./browser + ./react in client components |
| AWS Lambda, Netlify Functions | Core + withBugWatchRouteHandler from ./next (flush before function returns) |
Requirements
- Node.js ≥ 20.19.0 (or a compatible runtime listed above)
- TypeScript users: TypeScript ≥ 5 is recommended; strict mode is fully supported
- No other mandatory dependencies — framework/logger adapters use peer dependencies you already have
Install
npm install @newinstance/bugwatchpnpm add @newinstance/bugwatchyarn add @newinstance/bugwatchbun add @newinstance/bugwatchThat is the only install command you need. Framework and logger integrations are included in the same package via subpath exports — no extra packages.
Where to get your project key
- Sign in to the BugWatch merchant dashboard at www.newinstance.cloud.
- Open the project you want to monitor (or create a new one).
- Go to BugWatch → your project → Settings → Data Source Name (DSN) keys, and create a key.
- Copy the key — it looks like
sk_test_abcdef1234:secret…(test environment) orsk_live_…(production).
Use the project's DSN key, not a general API key. Ingest requires a key that is bound to exactly one project — that is what the project's DSN keys are. A key created from the organization's top-level API Keys page is not bound to a single project and will be rejected at ingest with
This API key is not a BugWatch project key(HTTP 400).
The key format is <keyId>:<secret>. Keep the secret part private:
- Server-side code — set the full key in an environment variable and read it at runtime.
- Browser / client-side code — never put the key in browser code; use the browser secure flow instead.
Use one key per environment (test, staging, production). The key binds the environment server-side — you do not need to pass the environment separately for most purposes.
Environment variables
Add these to your .env file (and to your deployment secrets):
# Required — your BugWatch central API key
BUGWATCH_KEY=sk_live_abc123:your-secret-here
# Optional — include the deployed version in every captured event
APP_VERSION=1.0.0
# Optional — NODE_ENV is often already set by your runtime
NODE_ENV=productionNever commit .env files containing real keys. If you use a secrets manager (Doppler, AWS Secrets
Manager, Vault, etc.) load the value from there.
Basic init (server-side)
Call BugWatch.init once, at application startup, before any other code runs. The global singleton
is safe to call from anywhere in your app after that.
// src/instrumentation.ts (or the top of your main entry file)
import { BugWatch } from "@newinstance/bugwatch";
BugWatch.init({
projectKey: process.env.BUGWATCH_KEY!,
release: process.env.APP_VERSION, // optional but recommended
environment: process.env.NODE_ENV, // informational only; real env is key-bound
});What each line does:
projectKey— authenticates your events with the BugWatch ingest API using yourx-api-key.release— tags every event with the deployed version so you can filter by release in the dashboard.environment— added to events as a label; the authoritative environment is the one your API key is registered under.
For advanced use cases (multi-tenant apps, isolated clients per test), use createClient instead:
import { createClient } from "@newinstance/bugwatch";
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });The client object exposes the same methods as BugWatch.* and is passed directly to framework
adapters.
Framework adapters
Every adapter follows the same two-step pattern:
- Create a client:
const client = createClient({ projectKey })(or use theBugWatchsingleton). - Register the adapter according to the framework's own plugin/middleware API.
All adapters never swallow errors — they capture the error, then re-throw (or call next(err)),
so your framework's own error handling continues normally.
Express
// src/app.ts
import express from "express";
import { createClient } from "@newinstance/bugwatch";
import {
bugWatchExpressRequestHandler,
bugWatchExpressErrorHandler,
} from "@newinstance/bugwatch/express";
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
const app = express();
// FIRST — opens a per-request AsyncLocalStorage scope so every capture
// during the request automatically includes method/url/route.
// Pass `{ getUser }` to auto-attach the authenticated user with zero extra code.
app.use(bugWatchExpressRequestHandler(client, { getUser: (req) => req.user }));
// ...your routes...
app.get("/health", (_req, res) => res.json({ ok: true }));
// LAST — captures any thrown error, then calls next(err).
// Must be the final middleware.
app.use(bugWatchExpressErrorHandler(client));Mount order matters: request handler first, error handler last.
Per-request user (concurrency-safe)
Every adapter isolates the user to a single request — concurrent requests can never overwrite each other's identity. There are two ways to attach it:
1. The getUser option (recommended). Give the adapter a one-line extractor and it reads
the user for you. It resolves lazily at capture time — after your auth middleware has run —
so req.user is populated by the time an error is captured:
app.use(
bugWatchExpressRequestHandler(client, {
getUser: (req) => req.user, // e.g. { id, email } set by Passport/JWT auth
}),
);2. setRequestUser (for derived or dynamic identity). Call it from your own middleware once
the request is authenticated — useful when the user isn't simply a field on req, or you want to
shape it. It writes only the current request's scope, and an explicit call always wins over
getUser:
import { setRequestUser } from "@newinstance/bugwatch/node";
// Mount AFTER bugWatchExpressRequestHandler, once the request is authenticated.
app.use((req, _res, next) => {
if (req.user) setRequestUser({ id: req.user.id, email: req.user.email });
next();
});Either way, any captureException / captureMessage during the request is tagged with the user
and clears automatically when the request ends. For the complete explanation — how isolation works
under the hood, full runnable examples for every adapter, and the anti-pattern to avoid — see
Per-request context and concurrency below.
Fastify
import Fastify from "fastify";
import { createClient } from "@newinstance/bugwatch";
import { bugWatchFastify } from "@newinstance/bugwatch/fastify";
const app = Fastify();
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
// Attaches a setErrorHandler hook; preserves Fastify's default error response.
// Pass `{ getUser }` to auto-attach the authenticated user (resolved lazily at capture time).
bugWatchFastify(app, client, { getUser: (req) => req.user });Koa
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! });
// FIRST — opens AsyncLocalStorage scope + catches downstream errors before re-throwing.
// Pass `{ getUser }` to auto-attach the authenticated user (Koa's convention is ctx.state.user).
app.use(bugWatchKoa(client, { getUser: (ctx) => ctx.state.user }));
// ...your other middleware and routes...Hono
Hono works identically on Node.js, Cloudflare Workers, Deno, and Bun. It does not use
AsyncLocalStorage, so it is safe on all runtimes:
import { Hono } from "hono";
import { createClient } from "@newinstance/bugwatch";
import { bugWatchHono } from "@newinstance/bugwatch/hono";
const app = new Hono();
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
// FIRST — captures errors and awaits flush before re-throwing.
// On edge/serverless runtimes the flush ensures delivery before the runtime freezes.
// Pass `{ getUser }` to auto-attach the authenticated user (Hono's convention is c.get("user")).
app.use(bugWatchHono(client, { getUser: (c) => c.get("user") }));
// ...your routes...Hapi
import Hapi from "@hapi/hapi";
import { createClient } from "@newinstance/bugwatch";
import { createBugWatchHapiPlugin } from "@newinstance/bugwatch/hapi";
const server = Hapi.server({ port: 3000 });
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
// Opens a per-request AsyncLocalStorage scope in onRequest, and registers an onPreResponse
// hook that captures Boom error responses.
// Pass `{ getUser }` to auto-attach the authenticated user — Hapi puts it on
// request.auth.credentials, and it is resolved lazily at capture time, after the auth pipeline.
await server.register(
createBugWatchHapiPlugin(client, {
getUser: (request) => request.auth?.credentials,
}),
);Because the plugin opens a request scope, a bare captureException / captureLog — or any
logger call — anywhere inside a Hapi request automatically inherits that request's user, method,
and url, and setRequestUser / setRequestTag work inside handlers:
import { setRequestTag } from "@newinstance/bugwatch/node";
server.route({
method: "POST",
path: "/transfers",
handler: async (request) => {
setRequestTag("transferId", request.payload.transferId);
await processTransfer(request.payload); // a throw here is captured with tag + user
return { ok: true };
},
});Hapi drives its own request lifecycle rather than nesting middleware, so the scope is bound with
enterContext in an onRequest extension rather than by wrapping the request in
runWithContext.
NestJS
// src/main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { createClient } from "@newinstance/bugwatch";
import { BugWatchExceptionFilter } from "@newinstance/bugwatch/nest";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
// A catch-all exception filter: captures, then re-throws so Nest's own handling runs.
app.useGlobalFilters(new BugWatchExceptionFilter(client));
await app.listen(3000);
}
bootstrap();If you prefer the factory function:
import { createBugWatchNestFilter } from "@newinstance/bugwatch/nest";
app.useGlobalFilters(createBugWatchNestFilter(client));To open a per-request scope (so setRequestUser works) and auto-attach the user, also register
the middleware. Nest guards run after middleware, so getUser resolves lazily once the guard has
populated req.user:
import { bugWatchNestMiddleware } from "@newinstance/bugwatch/nest";
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(bugWatchNestMiddleware(client, { getUser: (req) => req.user }))
.forRoutes("*");
}
}Next.js (App Router) and serverless
Use withBugWatchRouteHandler to wrap individual App Router route handlers. It captures any
thrown error, flushes events (critical in serverless — the function must deliver events before it
exits), then re-throws:
// app/api/checkout/route.ts
import { createClient } from "@newinstance/bugwatch";
import { withBugWatchRouteHandler } from "@newinstance/bugwatch/next";
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
export const POST = withBugWatchRouteHandler(client, async (req) => {
// your handler logic — setRequestUser works here (the wrapper opens a request scope)
return Response.json({ ok: true });
});The wrapper opens a per-request AsyncLocalStorage scope around your handler, so
setRequestUser / setRequestTag called inside it are correctly isolated. Because a Route
Handler resolves identity inside the handler (const session = await auth()), the primary path in
Next is to call setRequestUser(session.user) there. A third getUser option is available for
identity your middleware.ts forwards on the request (e.g. a header or cookie), which the
extractor can read: withBugWatchRouteHandler(client, handler, { getUser: (req) => ... }).
This same wrapper works for AWS Lambda handlers, Vercel Functions, Netlify Functions — any function that must flush before returning.
For client components, use @newinstance/bugwatch/browser + @newinstance/bugwatch/react
(see Browser secure flow).
React (error boundary)
The React adapter runs in the browser. Use a sessionUrl, not your secret key (see
Browser secure flow):
// src/App.tsx
import { createClient } from "@newinstance/bugwatch";
import { BugWatchErrorBoundary } from "@newinstance/bugwatch/react";
// sessionUrl points to YOUR backend endpoint that mints a browser session token.
// Never put projectKey here.
const client = createClient({ sessionUrl: "/bugwatch/session" });
export function App() {
return (
<BugWatchErrorBoundary
client={client}
fallback={<div>Something went wrong.</div>}
>
<Routes />
</BugWatchErrorBoundary>
);
}BugWatchErrorBoundary catches render-tree errors (including their component stack) and renders
the fallback element. If you omit fallback it renders nothing when there is an error.
Vue 3 (plugin)
// src/main.ts
import { createApp } from "vue";
import App from "./App.vue";
import { createClient } from "@newinstance/bugwatch";
import { createBugWatchVuePlugin } from "@newinstance/bugwatch/vue";
// sessionUrl → session token from your backend; never put projectKey in the browser.
const client = createClient({ sessionUrl: "/bugwatch/session" });
createApp(App).use(createBugWatchVuePlugin(client)).mount("#app");The plugin hooks into app.config.errorHandler and preserves any handler you already set.
Browser (any frontend framework)
One import, one call. initBrowser handles everything: the SSR guard, an idempotent
singleton, client creation, and installing the window error + unhandled-rejection
handlers. It never throws - any failure (bad config, unreachable session URL)
degrades to a working no-op client and a single console.warn, so a crash reporter
can never crash your app.
// imported once in your entry file - safe at module scope, safe under SSR
import { initBrowser } from "@newinstance/bugwatch";
export const client = initBrowser({ sessionUrl: "/bugwatch/session" });getBrowserClient() returns the singleton from anywhere else. No subpath imports and
no tsconfig changes are needed: since v0.2.0 the package ships typesVersions, so
even "moduleResolution": "node" consumers resolve every subpath's types without
paths aliases. Never alias @newinstance/bugwatch/* to .d.ts files in
compilerOptions.paths - bundlers that honour paths (Next.js) rewrite the runtime
import to the declaration stub and every export becomes undefined. The SDK now
detects that state and logs a named BugWatchResolutionError explaining the fix.
Next.js
Pages Router - one line in pages/_app.tsx:
import { initBrowser } from "@newinstance/bugwatch/next/client";
initBrowser({
sessionUrl: "/bugwatch/session",
release: process.env.NEXT_PUBLIC_RELEASE,
});
export default function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}App Router - init in app/layout.tsx (or any client component that always mounts),
report render errors from app/error.tsx:
"use client";
import { useEffect } from "react";
import { captureRenderError } from "@newinstance/bugwatch/next/client";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
captureRenderError(error, { digest: error.digest });
}, [error]);
return <button onClick={reset}>Try again</button>;
}@newinstance/bugwatch/next/client is browser-safe: it pulls in no Node built-ins,
unlike @newinstance/bugwatch/next (the server-side route-handler wrapper).
Legacy multi-import flow
The pre-0.2.0 flow keeps working but is no longer recommended:
import { createClient } from "@newinstance/bugwatch";
import { installBrowserErrorHandlers } from "@newinstance/bugwatch/browser";
const client = createClient({ sessionUrl: "/bugwatch/session" });
installBrowserErrorHandlers(client);Prefer initBrowser: hand-written init at module scope throws during module
evaluation when anything goes wrong, which can prevent your app from hydrating at
all. installBrowserErrorHandlers is idempotent and returns an uninstall function.
Node (raw, no web framework)
// src/index.ts
import { createClient } from "@newinstance/bugwatch";
import {
installNodeErrorHandlers,
installAsyncScope,
runWithContext,
} from "@newinstance/bugwatch/node";
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
// Capture uncaughtException (level fatal=60) + unhandledRejection (level error=50).
// Idempotent — safe to call multiple times.
installNodeErrorHandlers(client);
// Optional: AsyncLocalStorage request-scope support so any capture inside runWithContext
// automatically attaches request metadata (method, url, route, tags, user).
installAsyncScope(client);
// Wrap per-request async work:
await runWithContext(
{ method: "GET", url: "/orders", requestId: req.id },
async () => {
// Any BugWatch.captureException() call in here auto-attaches the context above.
await processOrder(req);
},
);Per-request context and concurrency
The problem
A Node.js server handles many users' requests concurrently on a single event loop. If you store the "current user" in one shared variable, whichever request wrote last wins — so request A's exception can end up attributed to request B's user. The same race applies to tags and context objects attached globally at request time.
How the SDK separates requests
Every Node adapter (express, koa, fastify, nest, and the next route-handler wrapper)
opens a per-request AsyncLocalStorage scope the moment the request arrives.
AsyncLocalStorage binds a storage cell to the current async call-tree: every await, callback,
and timer spawned inside that tree inherits the same cell, but a different request's tree sees its
own separate cell. There is no shared mutable state.
setRequestUser, setRequestTag, and setRequestContext (from @newinstance/bugwatch/node)
each write into the current request's ALS cell, not the global root scope. When the SDK
builds a capture event it merges the global root scope with the active ALS scope; the request
scope wins on any overlapping field. Because the ALS binding is built into Node's async
scheduler, two requests interleaving on the event loop can never reach each other's cell.
Hono has no AsyncLocalStorage, so its adapter reads identity from Hono's own per-request
context object c (one object per request by construction). Every Node adapter — express,
koa, fastify, hapi, nest, and the next route-handler wrapper — opens an ALS scope.
Hapi binds its scope with enterContext in an onRequest extension, because Hapi drives its
own request lifecycle instead of nesting middleware around a next() call.
The SDK handles isolation; you just set identity the right way for your adapter.
Three ways to attach identity
| Method | Adapters | How |
| ------------------------------------------------------------------------------ | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| getUser adapter option (recommended) | All (express/fastify/koa/hono/hapi/nest/next) | Pass { getUser: (req) => req.user } when registering the adapter. Resolved lazily at capture time — after auth runs — so it needs no extra middleware. |
| ALS request scope via setRequestUser / setRequestTag / setRequestContext | Express, Koa, Fastify, Hapi, Nest, Next (inside a handler) | Call these helpers after the request scope is open. Bare captureException calls in the same request auto-attach the identity. An explicit call wins over getUser. |
| Explicit per-capture { user, tags } hint | Hono (and any adapter as a fallback) | Pass { user, tags } directly to captureException / captureLog. Nothing is shared. |
Full examples per adapter
Each example is self-contained and runnable. Every example ends with a comment showing exactly what lands in BugWatch and a note confirming concurrent requests are isolated.
These examples show
setRequestUserin an auth middleware so you can see the isolation explicitly. In most apps the one-linegetUseradapter option (above) replaces that middleware entirely — reach forsetRequestUserwhen the identity is derived rather than a plain field on the request.
Express
// src/app.ts
import express, {
type Request,
type Response,
type NextFunction,
} from "express";
import { createClient } from "@newinstance/bugwatch";
import {
bugWatchExpressRequestHandler,
bugWatchExpressErrorHandler,
} from "@newinstance/bugwatch/express";
import { setRequestUser, setRequestTag } from "@newinstance/bugwatch/node";
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
const app = express();
app.use(express.json());
// 1. Open a per-request ALS scope (must be first).
app.use(bugWatchExpressRequestHandler(client));
// 2. Auth middleware — runs inside the ALS scope, so setRequestUser writes THIS request only.
app.use((req: Request, _res: Response, next: NextFunction) => {
if (req.headers["x-user-id"]) {
setRequestUser({ id: req.headers["x-user-id"] as string });
setRequestTag("plan", (req.headers["x-plan"] as string) ?? "free");
}
next();
});
// 3. A route that may throw.
app.post("/orders", async (req: Request, res: Response) => {
const orderId = (req.body as { orderId: string }).orderId;
setRequestTag("orderId", orderId); // still inside this request's ALS scope
await processOrder(orderId); // if this throws, the error handler captures it
res.json({ ok: true });
});
// 4. Error handler (last) — still inside the ALS scope, so user + tags are attached.
app.use(bugWatchExpressErrorHandler(client));
// → captured event: user=<x-user-id>, tags={plan, orderId, method, route, status}
// Concurrent request B has its own ALS scope — its user and tags are fully isolated.
async function processOrder(_id: string) {
/* ... */
}
app.listen(3000);Koa
// src/app.ts
import Koa, { type Context, type Next } from "koa";
import Router from "@koa/router";
import { createClient } from "@newinstance/bugwatch";
import { bugWatchKoa } from "@newinstance/bugwatch/koa";
import { setRequestUser, setRequestTag } from "@newinstance/bugwatch/node";
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
const app = new Koa();
const router = new Router();
// 1. Open ALS scope + catch downstream errors (must be first).
app.use(bugWatchKoa(client));
// 2. Auth middleware — inside the ALS scope opened above.
app.use(async (ctx: Context, next: Next) => {
const userId = ctx.headers["x-user-id"] as string | undefined;
if (userId) {
setRequestUser({ id: userId });
setRequestTag("region", (ctx.headers["x-region"] as string) ?? "unknown");
}
await next();
});
// 3. Route.
router.post("/payments", async (ctx: Context) => {
const paymentId = (ctx.request.body as { paymentId: string }).paymentId;
setRequestTag("paymentId", paymentId);
await processPayment(paymentId); // throw captured by bugWatchKoa above
ctx.body = { ok: true };
});
app.use(router.routes());
// → captured event: user=<x-user-id>, tags={region, paymentId, method, route, status}
// Request B runs its own ALS scope in parallel — its data never reaches request A's scope.
async function processPayment(_id: string) {
/* ... */
}
app.listen(3000);Fastify
// src/app.ts
import Fastify from "fastify";
import { createClient } from "@newinstance/bugwatch";
import { bugWatchFastify } from "@newinstance/bugwatch/fastify";
import { setRequestUser, setRequestTag } from "@newinstance/bugwatch/node";
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
const app = Fastify();
// 1. Register BugWatch: opens an ALS scope in onRequest, captures errors in setErrorHandler.
bugWatchFastify(app, client);
// 2. Auth preHandler — the ALS scope is already open (opened in onRequest above),
// so setRequestUser writes 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. Route.
app.post("/webhooks", async (request) => {
const eventId = (request.body as { eventId: string }).eventId;
setRequestTag("webhookEvent", eventId);
await handleWebhook(eventId); // throws → captured by setErrorHandler in bugWatchFastify
return { ok: true };
});
// → captured event: user=<x-user-id>, tags={tier, webhookEvent, method, route, status}
// The onRequest hook fires once per request; its ALS scope is isolated to that request's async tree.
async function handleWebhook(_id: string) {
/* ... */
}
await app.listen({ port: 3000 });Hono
Hono is edge-safe: it has no AsyncLocalStorage. Per-request identity comes from Hono's own
c context object (one per request), threaded through opts.context. For manual captures,
read from c and pass { user, tags } in the hint.
// src/app.ts
import { Hono } from "hono";
import { createClient } from "@newinstance/bugwatch";
import { bugWatchHono } from "@newinstance/bugwatch/hono";
import type { BugWatchUser } from "@newinstance/bugwatch";
// Hono's context variable key for the authenticated user.
const userKey = "user";
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
const app = new Hono<{ Variables: { [userKey]: BugWatchUser } }>();
// 1. Auth middleware — stores the user on Hono's per-request context c (not a global).
app.use(async (c, next) => {
const userId = c.req.header("x-user-id");
if (userId) {
c.set(userKey, { id: userId });
}
await next();
});
// 2. BugWatch middleware — reads identity from c via opts.context (called at capture time).
// Every captured error automatically gets the user/tags from THIS request's c object.
app.use(
bugWatchHono(client, {
context: (c) => ({
user: c.get(userKey),
tags: { env: c.req.header("x-env") ?? "unknown" },
}),
}),
);
// 3. Route — manual capture with explicit user/tags from c (for non-thrown captures).
app.post("/transfers", async (c) => {
const user = c.get(userKey);
const { transferId } = await c.req.json<{ transferId: string }>();
try {
await processTransfer(transferId);
} catch (err) {
// Manual capture: pass user + tags explicitly — no ALS available on edge.
client.captureException(err, {
user,
tags: { transferId, route: "/transfers" },
});
// → captured event: user=<x-user-id>, tags={transferId, route, env}
throw err;
}
return c.json({ ok: true });
});
// Errors thrown from routes without a try/catch are captured by bugWatchHono via c.error —
// opts.context reads c at that moment so each error gets its own request's identity.
// Concurrent request B has its own c object — its user is never reachable from request A.
async function processTransfer(_id: string) {
/* ... */
}
export default app;NestJS
// src/main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module.js";
import { createClient } from "@newinstance/bugwatch";
import { BugWatchExceptionFilter } from "@newinstance/bugwatch/nest";
async function bootstrap() {
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
const app = await NestFactory.create(AppModule);
// 1. Exception filter — captures, then re-throws so Nest's own handling still runs.
app.useGlobalFilters(new BugWatchExceptionFilter(client));
await app.listen(3000);
}
bootstrap();
// src/app.module.ts
import {
Module,
type NestModule,
type MiddlewareConsumer,
} from "@nestjs/common";
import { createClient } from "@newinstance/bugwatch";
import { bugWatchNestMiddleware } from "@newinstance/bugwatch/nest";
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
// 2. Open a per-request ALS scope for every route (must run before guards/interceptors).
consumer.apply(bugWatchNestMiddleware(client)).forRoutes("*");
}
}
// src/auth.guard.ts — a guard runs after the middleware, so the ALS scope is already open.
import {
Injectable,
type CanActivate,
type ExecutionContext,
} from "@nestjs/common";
import { setRequestUser, setRequestTag } from "@newinstance/bugwatch/node";
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(ctx: ExecutionContext): boolean {
const req = ctx
.switchToHttp()
.getRequest<{ headers: Record<string, string> }>();
const userId = req.headers["x-user-id"];
if (userId) {
setRequestUser({ id: userId }); // writes THIS request's ALS scope only
setRequestTag("role", req.headers["x-role"] ?? "user");
}
return true;
}
}
// src/orders.controller.ts
import { Controller, Post, Body } from "@nestjs/common";
import { setRequestTag } from "@newinstance/bugwatch/node";
@Controller("orders")
export class OrdersController {
@Post()
async create(@Body() body: { orderId: string }) {
setRequestTag("orderId", body.orderId); // still inside the same ALS scope
await processOrder(body.orderId); // throws → BugWatchExceptionFilter captures
return { ok: true };
}
}
// → captured event: user=<x-user-id>, tags={role, orderId, method, route, status}
// Request B has its own ALS scope — changing its user never touches request A's scope.
async function processOrder(_id: string) {
/* ... */
}Next.js (App Router)
Next.js App Router route handlers may run on the Node.js runtime or on the edge runtime.
Neither guarantees AsyncLocalStorage continuity across the Next.js internal dispatch.
The safe approach for both runtimes is to pass { user, tags } explicitly on each capture.
Node.js users who need implicit propagation can wrap the handler body in runWithContext
from @newinstance/bugwatch/node, but that only works on the Node runtime.
// app/api/invoices/route.ts
import { type NextRequest } from "next/server";
import { createClient } from "@newinstance/bugwatch";
import { withBugWatchRouteHandler } from "@newinstance/bugwatch/next";
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
// Wraps the handler: captures thrown errors, flushes before the function returns,
// then re-throws. Critical in serverless — events must be delivered before exit.
export const POST = withBugWatchRouteHandler(
client,
async (req: NextRequest) => {
// Read the authenticated user from your session/auth layer.
const userId = req.headers.get("x-user-id") ?? undefined;
const invoiceId = ((await req.json()) as { invoiceId: string }).invoiceId;
try {
await createInvoice(invoiceId);
} catch (err) {
// Pass user + tags explicitly — no shared state, each invocation is independent.
client.captureException(err, {
user: userId ? { id: userId } : undefined,
tags: { invoiceId, route: "/api/invoices" },
});
// → captured event: user=<userId>, tags={invoiceId, route}
throw err; // withBugWatchRouteHandler re-throws after flush
}
return Response.json({ ok: true });
},
);
// Every serverless invocation is a separate function call — concurrency is handled by
// the platform spinning up separate instances, so there is no shared scope to pollute.
// The wrapper already opens a per-request AsyncLocalStorage scope, so you can also just call
// setRequestUser inside the handler and let bare captures pick it up implicitly:
// import { setRequestUser } from "@newinstance/bugwatch/node";
// export const POST = withBugWatchRouteHandler(client, async (req) => {
// const session = await auth();
// setRequestUser({ id: session.user.id });
// await createInvoice(...);
// return Response.json({ ok: true });
// });
async function createInvoice(_id: string) {
/* ... */
}Anti-pattern — global setter per request
// WRONG — do not do this on a server handling concurrent requests:
app.use((req, _res, next) => {
client.setUser({ id: req.headers["x-user-id"] as string }); // ← process-wide setter
next();
});
// client.setUser() writes the GLOBAL root scope, shared by every concurrent request.
// Request B arrives and overwrites the user → request A's next capture is now attributed
// to request B's user. Under load this is a near-certain data attribution bug.The correct approach on any adapter is the getUser option. Failing that, the ALS adapters
(Express / Koa / Fastify / Hapi / Nest, and Next inside a handler) accept setRequestUser from
@newinstance/bugwatch/node; Hono accepts a per-capture { user } hint (or opts.context
in bugWatchHono). Never call client.setUser / BugWatch.setUser inside a request handler on a
concurrent server — reserve it for single-identity processes such as background workers and CLIs.
Edge cases
Identity set before auth completes. If you call
setRequestUserearly (e.g. with a partial identity) and then overwrite it later in the same request, both calls write the same ALS cell — only the last value applies. The scope is cleared automatically when the request's async tree ends. ThegetUseroption sidesteps this entirely: it is resolved lazily at capture time, so it always reads the user as it stands when an event is actually captured, not when the request opened.setRequestUserreturnsfalse. This means the call happened outside any ALS request scope — most likelybugWatchExpressRequestHandler/bugWatchKoa/bugWatchFastify/bugWatchNestMiddlewarewas not registered, or the call is in a code path that is not descended from the request handler (e.g. a top-level module initializer). The SDK does nothing and does not touch the global scope.Background work spawned after the response is sent. If you fire an async task inside a request handler (
void sendEmail(...)) withoutawait-ing it, that task still runs inside the same ALS context as its parent request until the parent's async tree fully resolves. Any captures inside it will pick up the request's identity. This is usually the desired behavior, but be aware that long-running fire-and-forget tasks may outlive the conceptual "request" window.Hono and edge runtimes have no ALS. On Cloudflare Workers, Deno Deploy, and Vercel Edge there is no
AsyncLocalStorage. You must always use explicit{ user, tags }in capture hints or theopts.contextcallback inbugWatchHono. CallingsetRequestUserorsetRequestTagfrom@newinstance/bugwatch/nodein these environments is not possible (the subpath is Node-only and will not be importable on edge runtimes).
Capturing errors and messages
Use these methods anywhere after BugWatch.init() has been called. If called before init they
return a generated event ID and are otherwise no-ops (safe to call early).
import { BugWatch } from "@newinstance/bugwatch";
// Capture an exception — works with Error objects, strings, or any thrown value.
// Returns the event ID string.
try {
await processPayment(order);
} catch (err) {
const eventId = BugWatch.captureException(err);
// optionally include extra context:
BugWatch.captureException(err, {
level: 50, // numeric level (error=50, fatal=60, ...)
tags: { orderId: order.id, gateway: "paystack" },
user: { id: user.id, email: user.email },
});
}
// Capture an informational message (not an error).
// Second argument is the numeric level; defaults to info (30).
BugWatch.captureMessage("Payment completed", 30);
// Optional third argument carries tags, user, and trace hints:
BugWatch.captureMessage("Refund issued", 30, { tags: { orderId: order.id } });
// Cause chains are captured automatically: for errors built with `new Error(msg, { cause })`
// (and AggregateError), the event carries a structured `causes[]` array with each cause's own
// type, message, and stacktrace (up to 3 deep, cycle-safe), alongside the flattened
// "Caused by:" text in the main message. Nothing to configure.
// Low-level: capture a structured log entry.
// level can be a number (10–60) or a severity name string.
BugWatch.captureLog({
level: 40, // warn
message: "Slow DB query",
tags: { duration_ms: "1200", table: "orders" },
user: { id: "u_123" },
});Numeric severity scale (Pino-compatible):
| Level | Name | | ----- | ----- | | 10 | trace | | 20 | debug | | 30 | info | | 40 | warn | | 50 | error | | 60 | fatal |
You can also import named constants:
import { LEVELS } from "@newinstance/bugwatch";
// LEVELS.trace === 10, LEVELS.debug === 20, ..., LEVELS.fatal === 60Log forwarding — logger adapters
Pick the adapter that matches your logging library. All adapters forward to BugWatch in addition to their normal output — they do not replace your existing logger.
Pino
import pino from "pino";
import { createClient } from "@newinstance/bugwatch";
import { createBugWatchPinoDestination } from "@newinstance/bugwatch/pino";
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
const logger = pino(createBugWatchPinoDestination(client));
logger.info({ userId: "u_1" }, "user signed in");
logger.error({ err: new Error("db timeout") }, "database error");Pino's numeric levels (10–60) map directly to BugWatch levels.
Winston
import winston from "winston";
import { createClient } from "@newinstance/bugwatch";
import { createBugWatchWinstonTransport } from "@newinstance/bugwatch/winston";
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
const logger = winston.createLogger({
transports: [
new winston.transports.Console(),
createBugWatchWinstonTransport(client),
],
});
logger.error("payment gateway timeout", { gateway: "stripe" });Winston level names are mapped to BugWatch numerics: error→50, warn→40, info→30, verbose/debug→20, silly/trace→10, fatal→60.
Bunyan
import bunyan from "bunyan";
import { createClient } from "@newinstance/bugwatch";
import { createBugWatchBunyanStream } from "@newinstance/bugwatch/bunyan";
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
const logger = bunyan.createLogger({
name: "my-app",
streams: [
{ stream: process.stdout },
{ level: "info", type: "raw", stream: createBugWatchBunyanStream(client) },
],
});
logger.error({ err: new Error("connection refused") }, "redis down");Bunyan's numeric levels (10–60) map directly to BugWatch levels.
log4js
import log4js from "log4js";
import { createClient } from "@newinstance/bugwatch";
import { bugWatchLog4jsAppender } from "@newinstance/bugwatch/log4js";
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
log4js.configure({
appenders: {
console: { type: "console" },
bugwatch: { type: bugWatchLog4jsAppender(client) },
},
categories: {
default: { appenders: ["console", "bugwatch"], level: "info" },
},
});
const logger = log4js.getLogger();
logger.error("unhandled error in checkout flow");console (patch)
Patches console.warn and console.error to forward to BugWatch. The original console output
is preserved. Reversible:
import { createClient } from "@newinstance/bugwatch";
import { captureConsole } from "@newinstance/bugwatch/console";
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
// Patches console.warn and console.error.
const restore = captureConsole(client);
// Optionally, only patch specific levels:
// const restore = captureConsole(client, { levels: ["error"] });
console.error("payment failed", new Error("card declined"));
// To undo the patch (e.g. in tests):
restore();Native logger (no library required)
If you do not use a logging library, createLogger gives you a structured logger backed directly
by BugWatch. No extra dependencies:
import { BugWatch, createLogger } from "@newinstance/bugwatch";
BugWatch.init({ projectKey: process.env.BUGWATCH_KEY! });
// Call createLogger on the client, or use BugWatch.createLogger() for the singleton.
const log = BugWatch.createLogger();
// Create child loggers that automatically merge bindings into every call's tags.
const paymentLog = log.child({ service: "payment", provider: "paystack" });
paymentLog.info("charge initiated", { amount: 5000, currency: "NGN" });
paymentLog.error(new Error("gateway timeout"), { attempt: 1 });
paymentLog.fatal("unrecoverable state — shutting down");createLogger also accepts a client directly if you use createClient:
import { createClient, createLogger } from "@newinstance/bugwatch";
const client = createClient({ projectKey: process.env.BUGWATCH_KEY! });
const log = createLogger(client, { service: "api" });Local console output
The native logger also prints each call to your console by default — in addition to shipping to BugWatch — so your logs stay visible locally while you develop. It's the only logging path that echoes locally; the console patch and the library adapters keep their own existing output.
- Node, TTY → an aligned, colored, one-line-per-call format. Scalars go inline; nested objects, arrays, and error stacks expand beneath, readably.
- Node, piped / not a TTY → the same layout with no color codes (clean for files,
grep, CI, and log shippers). - Browser → routed to
console.info / warn / errorwith the metadata object attached, so it keeps devtools' native styling and stays expandable.
14:23:01.482 INFO payment processed
amount=1999 currency=USD ok=true userId=42
customer: { id: 'cus_123', tier: 'gold', address: { city: 'Lagos' } }
14:23:01.512 ERROR checkout failed orderId=99
└─ Error: gateway timeout
at charge (checkout.ts:88:11)Turn it off, or tune it, with the console option on init / createClient:
// Fully silent locally — backend shipping is unchanged:
BugWatch.init({ projectKey: process.env.BUGWATCH_KEY!, console: false });
// Fine-grained:
BugWatch.init({
projectKey: process.env.BUGWATCH_KEY!,
console: {
enabled: true, // default: true
level: "info", // only print info and above (default: "trace" — everything)
format: "auto", // "auto" (pretty, color on a TTY) | "pretty" | "json" (one NDJSON line)
colors: true, // force color on/off (default: auto-detect TTY + NO_COLOR)
},
});Use format: "json" for one NDJSON line per call when piping into a collector, or
console: false in production if you already run a log pipeline.
Enriching events
setUser — user identification
Attach a user to all subsequent events. Pass null to clear (e.g., on logout):
// After authentication:
BugWatch.setUser({
id: "u_123",
email: "[email protected]",
username: "alice",
// ip: "1.2.3.4", // optional; auto-populated by the ingest server
});
// On logout:
BugWatch.setUser(null);All fields are optional — pass whichever you have.
Servers handling concurrent requests:
BugWatch.setUser()sets a single process-wide user and is meant for single-identity processes (workers, CLIs). For a per-request user, set it on the request scope withsetRequestUserfrom@newinstance/bugwatch/nodeinstead — see Per-request user. Using the global setter per request can attribute one request's events to another user.
setTag
Add a key-value string tag to all subsequent events:
BugWatch.setTag("region", "eu-west-1");
BugWatch.setTag("tier", "pro");
BugWatch.setTag("feature_flag_checkout_v2", "enabled");Tags are searchable and filterable in the BugWatch dashboard.
setContext
Attach a named object of arbitrary data to all subsequent events. Unlike tags, context values are not indexed for filtering but they do appear on the event detail view:
BugWatch.setContext("payment", {
provider: "paystack",
currency: "NGN",
amountKobo: 50000,
});addBreadcrumb
Record a trail of what happened before an error. Breadcrumbs are kept in a bounded ring (last 50) on the active scope and attached to every captured event, where they render as a timeline on the occurrence view:
BugWatch.addBreadcrumb({
category: "cart",
message: "cart validated",
data: { items: 3 },
});
BugWatch.addBreadcrumb({
category: "http",
message: "POST /charge",
level: 30,
});Fields: category, type, level (numeric), message, data (small object), and an optional
timestamp (ms; defaults to now). Inside a request scope the trail is per-request; outside one it
is process-wide.
setRelease
Set or update the release string at runtime:
BugWatch.setRelease("[email protected]");You can also set this at init time via the release option.
withScope — per-event enrichment
Use withScope when you want to attach extra context to a single capture without affecting future
captures. The callback receives a clone of the active scope; mutations to it do not persist:
BugWatch.withScope((scope) => {
scope.setTag("orderId", order.id);
scope.setUser({ id: user.id });
// Captures inside this callback use the cloned scope.
// Any BugWatch.captureException / captureLog calls here see these tags.
});Note: withScope itself does not capture anything — you must call captureException or
captureLog inside the callback (or use per-call hint arguments on captureException).
Release and environment
Release identifies which deployed version produced an event. Set it at init or call
BugWatch.setRelease("v2.4.1") at any time. Convention: use a version string, git SHA, or
packageJson.version:
import { version } from "./package.json" assert { type: "json" };
BugWatch.init({
projectKey: process.env.BUGWATCH_KEY!,
release: version, // e.g. "1.2.3"
environment: process.env.NODE_ENV,
});Environment (dev / staging / production) is primarily enforced server-side by the API key
you use. The environment option here is an informational label that appears on events in the
dashboard — useful for self-documenting which key goes where.
Sensitive data redaction
The SDK redacts sensitive values before they leave your app. Redaction is non-mutating and happens on a deep clone of each event before it is queued.
Redacted by default (case-insensitive key match):
password, passwd, pwd, token, accesstoken, refreshtoken, idtoken,
authorization, auth, cookie, setcookie, secret, clientsecret, apikey,
privatekey, sessionid, ssn, creditcard, cardnumber, cvv, pin, nin, bvn
Any matched value is replaced with [REDACTED] in the event that reaches the server.
Add your own keys:
BugWatch.init({
projectKey: process.env.BUGWATCH_KEY!,
sensitiveFields: ["accountNumber", "iban", "taxId"],
// Merged with the defaults above — you do not need to repeat the default list.
});Drop or mutate events before they are sent with beforeSend:
BugWatch.init({
projectKey: process.env.BUGWATCH_KEY!,
beforeSend(event) {
// Return null to drop the event entirely:
if (event.tags?.url?.includes("/health")) return null;
// Or mutate and return the event:
if (event.user) {
event.user = { id: event.user.id }; // strip email/username
}
return event;
},
});beforeSend is async-compatible — you can await inside it.
Sampling
To send only a fraction of events (useful for high-volume info/debug logs):
BugWatch.init({
projectKey: process.env.BUGWATCH_KEY!,
sampleRate: 0.25, // send 25% of events; default is 1 (100%)
});Sampling applies uniformly to all events. If you need level-based sampling (e.g., send all errors,
sample info), use beforeSend to implement the logic yourself.
Distributed tracing
The SDK ties errors and logs to end-to-end traces so BugWatch can reconstruct a whole request across services: which service the failure started in, which downstream call broke, and every log line that belongs to the same request. It follows the W3C Trace Context standard, so it interoperates with OpenTelemetry and any other traced system.
Set a serviceName at init so your spans are attributed to the right service on the service map:
BugWatch.init({
projectKey: process.env.BUGWATCH_KEY!,
serviceName: "checkout-api",
release: "[email protected]",
});Automatic trace propagation
Every server adapter (Express, Fastify, Koa, Hono, Hapi, NestJS, Next.js) reads the incoming
traceparent header and attaches its trace ID and span ID to every capture and log inside that
request. Nothing to configure: mounting the adapter's request handler is enough.
If a request arrives WITHOUT a traceparent header, the SDK generates a fresh trace ID for that
request, so every error and log in the request still shares one trace. That means a request is
always fully traceable end to end, even when it is the origin of the trace.
Creating spans: withSpan and startSpan
Spans time an operation and place it on the trace waterfall. withSpan runs a function inside a
span, times it, records a thrown exception on the span (with its stacktrace), marks the span as an
error, and re-throws. All captures and logs inside the callback are automatically linked to it:
const result = await BugWatch.withSpan(
"db.query load-cart",
async (span) => {
span?.setAttr("db.system", "postgresql");
return loadCart(cartId);
},
{ kind: 3 }, // OTel span kind: 1 internal, 2 server, 3 client, 4 producer, 5 consumer
);startSpan gives you manual control when the operation does not fit one callback:
const span = BugWatch.startSpan("job.process refunds", {
kind: 5,
attrs: { "messaging.system": "redis-streams" },
});
try {
await processJob(job);
span?.end();
} catch (err) {
span?.recordException(err);
span?.end();
throw err;
}Attach code.filepath, code.lineno, and code.function attributes and BugWatch shows the source
location on the span detail panel.
Spans inherit the active trace context (inbound traceparent, an enclosing withSpan, or the
per-request trace) and are exported in batches; BugWatch.flush() and BugWatch.close() flush
them together with events. Span creation is server-side (projectKey mode); the browser
sessionUrl mode captures errors and logs but does not create spans.
Tracing outbound HTTP: wrapFetch
wrapFetch returns a fetch that opens a client-kind span for each request, injects the
traceparent header so the downstream service joins your trace, tags the response status, and
marks 5xx responses and network failures as error spans:
import { createClient, wrapFetch } from "@newinstance/bugwatch";
const client = createClient({
projectKey: process.env.BUGWATCH_KEY!,
serviceName: "checkout-api",
});
const tracedFetch = wrapFetch(client);
const res = await tracedFetch("https://payments.internal/charge", {
method: "POST",
});An existing traceparent header on the request is never overwritten. Options:
wrapFetch(client, { fetch, captureErrors }) accepts a custom fetch implementation and, with
captureErrors: true, also reports network failures as BugWatch events (they are always recorded
on the span).
Propagating to other services manually
When you cannot use wrapFetch (a non-fetch HTTP client, a message payload, a queued job), send
the current trace context yourself:
// Returns { traceparent: "00-<traceId>-<spanId>-01" } or {} when there is no active trace.
const headers = {
...BugWatch.traceHeaders(),
"content-type": "application/json",
};
// Inside withSpan, prefer the span's own header so the child links to that exact span:
BugWatch.withSpan("publish refund", (span) => {
queue.publish({ body, traceparent: span?.traceparent() });
});On the receiving side, a BugWatch server adapter picks the header up automatically; outside an adapter, parse and set it manually:
import { parseTraceparent } from "@newinstance/bugwatch";
const ctx = parseTraceparent(message.traceparent);
if (ctx) BugWatch.setTraceContext(ctx.traceId, ctx.spanId);Linking spans across traces (queues and jobs)
A queue consumer usually starts its own trace but should still point back at the producer. Pass the producer's ids as a span link and BugWatch draws the async producer-to-consumer edge on the service map:
const span = BugWatch.startSpan("emails process", {
kind: 5,
links: [
{
traceId: msg.traceId,
spanId: msg.spanId,
attrs: { "messaging.message.id": msg.id },
},
],
});Span limits: 50 attributes (keys and values truncated to 200 chars, non-scalar values dropped),
20 events, 10 links, names truncated to 200 chars, exception-event stacktraces to 8000 chars.
Spans buffer up to 200 entries (oldest dropped) and flush with the event queue. Helper exports
normalizeTraceId and normalizeSpanId validate raw IDs the same way the SDK does.
Trace correlation with OpenTelemetry
If your app already uses OpenTelemetry for tracing, you do not need BugWatch spans. The ./otel
subpath provides a traceContextProvider that reads the current OTel span and injects traceId
and spanId into every BugWatch event automatically:
import { BugWatch } from "@newinstance/bugwatch";
import { otelTraceContextProvider } from "@newinstance/bugwatch/otel";
BugWatch.init({
projectKey: process.env.BUGWATCH_KEY!,
traceContextProvider: otelTraceContextProvider(),
});@opentelemetry/api is an optional peer dependency; install it separately:
npm install @opentelemetry/apiIf @opentelemetry/api is not installed the provider silently returns an empty object (no-op);
the SDK never crashes over a missing optional dependency.
Manual trace context
You can also set trace context manually per-event:
BugWatch.captureException(err, { traceId: "abc123", spanId: "def456" });Or globally:
BugWatch.setTraceContext(currentTraceId, currentSpanId);
// Clear by passing null:
BugWatch.setTraceContext(null, null);
// Read the active context (request scope, enclosing span, or provider):
const { traceId, spanId } = BugWatch.getTraceContext();Browser secure flow (CRITICAL reading for front-end apps)
Browser code is public — anything you ship to the browser can be read by any visitor.
Never put your projectKey (or its secret part) in client-side code. An exposed secret
allows anyone to ingest arbitrary events under your account.
The solution is a browser session token: your backend mints a short-lived token using your secret, and the browser SDK uses that token instead. Your secret never leaves your server.
Step 1 — Mount a session-mint endpoint on YOUR backend
The simplest option is the built-in Express handler:
// Express (one line):
import { bugWatchBrowserSessionHandler } from "@newinstance/bugwatch/express";
app.get(
"/bugwatch/session",
bugWatchBrowserSessionHandler({ projectKey: process.env.BUGWATCH_KEY! }),
);For any other framework, use mintBrowserSession from the core package:
import { mintBrowserSession } from "@newinstance/bugwatch";
// Example: Fastify, Hono, Hapi, Next.js API route, etc.
// POST /bugwatch/session — call this from your own route handler:
const session = await mintBrowserSession({
projectKey: process.env.BUGWATCH_KEY!,
});
// session = { token: "...", expiresAt: 1234567890 }
// Return this JSON to the browser.What this does: your backend calls POST https://api.newinstance.cloud/api/v1/bugwatch/browser-session
with your x-api-key header and receives a short-lived, ingest-only token scoped to your project
and environment. Your secret never leaves your server.
Step 2 — Init the browser SDK with sessionUrl
// Browser / client-side code (React, Vue, Svelte, plain JS, etc.)
import { createClient } from "@newinstance/bugwatch";
import { installBrowserErrorHandlers } from "@newinstance/bugwatch/browser";
// Point at YOUR backend endpoint — no projectKey here.
const client = createClient({ sessionUrl: "/bugwatch/session" });
installBrowserErrorHandlers(client);The SDK automatically fetches a token from sessionUrl on first use, caches it, and refreshes it
before expiry (and on a 401). Events go to
https://api.newinstance.cloud/api/v1/bugwatch/ingest/browser with the
x-bugwatch-session header. Your secret is never in the browser.
What NOT to do
// WRONG — exposes your secret to every browser visitor:
const client = createClient({ projectKey: "sk_live_abc123:my-secret" });If you accidentally expose a key: rotate it immediately in the BugWatch dashboard under Project settings → API keys.
Debug mode
Enable debug: true to print SDK-internal diagnostics to the console. Use this when
events are not arriving or to verify the SDK is initialized correctly:
BugWatch.init({
projectKey: process.env.BUGWATCH_KEY!,
debug: true, // prints [bugwatch] log lines to the console
});Debug output includes: init confirmation, event queued, flush triggered, transport success/failure with HTTP status codes, redaction info. Disable in production.
