npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@newinstance/bugwatch

v0.1.4

Published

BugWatch SDK for capturing logs, errors, and context from JavaScript and TypeScript apps.

Readme


Table of contents


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
├── ./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/bugwatch
pnpm add @newinstance/bugwatch
yarn add @newinstance/bugwatch
bun add @newinstance/bugwatch

That 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

  1. Sign in to the BugWatch merchant dashboard at www.newinstance.cloud.
  2. Open the project you want to monitor (or create a new one).
  3. Go to Project settings → API keys.
  4. Copy the key — it looks like sk_test_abcdef1234:secret… (test environment) or sk_live_… (production).

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=production

Never 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 your x-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:

  1. Create a client: const client = createClient({ projectKey }) (or use the BugWatch singleton).
  2. 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 timeafter 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! });

// Registers an onPreResponse lifecycle hook that captures Boom error responses.
// Pass `{ getUser }` to auto-attach the authenticated user — Hapi puts it on
// request.auth.credentials, and onPreResponse runs after the auth pipeline.
await server.register(
	createBugWatchHapiPlugin(client, {
		getUser: (request) => request.auth?.credentials,
	}),
);

Hapi drives its own request lifecycle rather than nesting middleware, so this adapter reads the user synchronously at the capture point (onPreResponse) instead of via AsyncLocalStorage.

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)

Use installBrowserErrorHandlers to capture window errors and unhandled promise rejections automatically, without a framework-specific adapter:

// src/bugwatch.ts  (imported once in your entry file)
import { createClient } from "@newinstance/bugwatch";
import { installBrowserErrorHandlers } from "@newinstance/bugwatch/browser";

const client = createClient({ sessionUrl: "/bugwatch/session" });
installBrowserErrorHandlers(client);

export { client }; // share with the rest of your app

installBrowserErrorHandlers is idempotent — calling it twice is safe. It returns an uninstall function if you need to remove the handlers later (e.g., in a test).

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). Hapi drives its own request lifecycle, so its adapter reads identity synchronously at the capture point. Every other Node adapter — express, koa, fastify, nest, and the next route-handler wrapper — opens an ALS scope.

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, 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, Hapi (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 setRequestUser in an auth middleware so you can see the isolation explicitly. In most apps the one-line getUser adapter option (above) replaces that middleware entirely — reach for setRequestUser when 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 / Nest, and Next inside a handler) accept setRequestUser from @newinstance/bugwatch/node; Hono and Hapi accept 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 setRequestUser early (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. The getUser option 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.

  • setRequestUser returns false. This means the call happened outside any ALS request scope — most likely bugWatchExpressRequestHandler / bugWatchKoa / bugWatchFastify / bugWatchNestMiddleware was 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(...)) without await-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 the opts.context callback in bugWatchHono. Calling setRequestUser or setRequestTag from @newinstance/bugwatch/node in 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);

// 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 === 60

Log 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" });

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 with setRequestUser from @newinstance/bugwatch/node instead — 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,
});

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.


Trace correlation (OpenTelemetry)

If your app uses OpenTelemetry, 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/api

If @opentelemetry/api is not installed the provider silently returns an empty object (no-op) — the SDK never crashes over a missing optional dependency.

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);

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.


Full configuration reference

Pass any of these options to BugWatch.init(...) or createClient(...):

| Option | Type | Default | Description | | ---------------------------- | ------------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------- | | projectKey | string | — | Central API key <keyId>:<secret>. Required on the server. Never use in the browser. | | sessionUrl | string | — | Browser only. URL on YOUR backend that returns { token, expiresAt }. Use instead of projectKey. | | release | string | — | Deployed version string. Appears on every event. | | environment | string | — | Informational label (e.g. "production"). Authoritative env is key-bound. | | enabled | boolean | true | Set false to disable all capture (e.g. in local dev). | | debug | boolean | false | Print SDK-internal logs to console. | | sampleRate | number | 1 | Fraction of events to keep (0–1). | | sensitiveFields | string[] | [] | Extra key names to redact (merged with built-in defaults). | | beforeSend | (event) => event \| null \| Promise<...> | — | Called before queuing; return null to drop. | | traceContextProvider | () => { traceId?, spanId? } | — | Called per-event to inject trace IDs (use otelTraceContextProvider() for OTel). | | captureUnhandledErrors | boolean | true | Auto-capture uncaughtException (node) / window error (browser). | | captureUnhandledRejections | boolean | true | Auto-capture unhandledRejection (node) / unhandledrejection (browser). | | maxQueueSize | number | 1000 | Max events in the in-memory queue; oldest are dropped on overflow. | | batchSize | number | 50 | Events per ingest HTTP request (server hard cap: 5000). | | flushInterval | number (ms) | 5000 | Background flush cadence. Set 0 to disable the timer (manual flush only). | | requestTimeout | number (ms) | 15000 | HTTP request timeout per ingest call. | | retry.maxAttempts | number | 3 | Max delivery attempts (first attempt + retries). | | retry.initialDelay | number (ms) | 200 | Delay before first retry. | | retry.maxDelay | number (ms) | 5000 | Cap on retry delay (with jitter). | | retry.backoffMultiplier | number | 2 | Exponential multiplier per retry. |


Testing

Use InMemoryTransport from @newinstance/bugwatch/testing to capture events in your tests without any network calls:

// payment.test.ts
import { BugWatchClient } from "@newinstance/bugwatch";
import { InMemoryTransport } from "@newinstance/bugwatch/testing";

const transport = new InMemoryTransport();
const client = new BugWatchClient({ projectKey: "sk_test_x:y" }, { transport });

// Run code that captures events:
client.captureException(new Error("card declined"));
await client.flush();

// Assert:
const event = transport.events[0];
console.assert(event?.exception?.type === "Error");
console.assert(event?.exception?.value === "card declined");

// Reset between tests:
transport.reset();

InMemoryTransport also has a find helper for targeted assertions:

const evt = transport.find((e) => e.exception?.type === "TimeoutError");

Verification — did events arrive?

After sending a test event, confirm it arrived in the BugWatch dashboard:

  1. Run your app or the minimal example below and trigger a capture.
  2. Open the BugWatch merchant dashboard at www.newinstance.cloud.
  3. Go to your project → Issues or Logs tab.
  4. The event should appear within a few seconds (the SDK flushes on a 5-second timer by default; call await BugWatch.flush() to force immediate delivery).

If events do not appear:

  • Enable debug: true and check the console for [bugwatch] output.
  • Verify projectKey is set and not accidentally undefined.
  • For browser apps, verify the session endpoint (/bugwatch/session) returns { token, expiresAt }.
  • Check for network errors in the browser DevTools Network tab or Node logs.

Production checklist

Before going live:

  • [ ] projectKey is read from an environment variable — not hardcoded in source.
  • [ ] projectKey is in server-side code only — never in browser bundles.
  • [ ] Browser apps use sessionUrl and the session-mint endpoint is deployed.
  • [ ] release is set to the deployed version (git tag, semver, etc.).
  • [ ] debug: false in production (the default).
  • [ ] enabled is not false in production.
  • [ ] await BugWatch.close() is called on graceful shutdown (SIGTERM handler) to flush queued events.
  • [ ] installNodeErrorHandlers(client) is called on server apps to catch uncaught exceptions.
  • [ ] beforeSend or sensitiveFields covers any domain-specific sensitive fields your app uses.
  • [ ] sampleRate is set appropriately if you have very high log volume.
  • [ ] Your .env file (containing the real key) is in .gitignore and never committed.
  • [ ] Key rotation procedure is documented — know how to rotate the key if it leaks.

Troubleshooting

Events are not appearing in the dashboard

  1. Enable debug: true — look for [bugwatch] flush and [bugwatch] transport send log lines.
  2. Check that projectKey is not undefined. Add a startup assertion:
    if (!process.env.BUGWATCH_KEY) throw new Error("BUGWATCH_KEY is not set");
  3. Make sure BugWatch.init() is called before any captures.
  4. In serverless environments (Lambda, Vercel, Next.js route handlers), always await client.flush() before the function returns, or use withBugWatchRouteHandler which does this automatically.

Events arrive but are missing context (no user, no tags)

  • setUser/setTag/setContext must be called after init and before the capture.
  • In multi-request server apps, set the per-request user with setRequestUser (from @newinstance/bugwatch/node) inside the request scope opened by the framework handler — not the global BugWatch.setUser() — otherwise concurrent requests overwrite each other's user. See Per-request user.

The session endpoint returns 401 in the browser

  • Confirm the endpoint is deployed and accessible from the browser's origin.
  • Confirm projectKey on the server is valid (not expired or revoked).
  • Enable debug: true in the browser client — the SDK logs session fetch failures.

TypeScript type errors on the hint argument of captureException

The hint parameter accepts level, tags, user, traceId, and spanId. All fields are optional:

BugWatch.captureException(err, { level: 50, tags: { orderId: "o_1" } });

The package is ESM-only — my project uses CommonJS

The package is pure ESM ("type": "module"). In Node.js, use import (ESM) or dynamic await import(...). If your project is CommonJS, either convert to ESM (recommended for Node 20+) or use a bundler (webpack, esbuild, Rollup) that handles ESM → CJS transformation.

Pino: only a subset of log fields appear as tags

The Pino adapter forwards string, number, boolean, and bigint values as tags. Objects and arrays are skipped to keep events flat. To forward nested data, stringify it first:

logger.info({ userId: "u_1", payload: JSON.stringify(complexObject) }, "event");

Events are being sampled out — I want to keep all errors

Use beforeSend to bypass sampling for errors:

BugWatch.init({
	projectKey: process.env.BUGWATCH_KEY!,
	sampleRate: 0.1, // only 10% of events by default
	beforeSend(event) {
		if (event.level >= 50) return event; // always keep error + fatal
		if (Math.random() > 0.1) return null; // sample the rest at 10%
		return event;
	},
});

(Note: sampleRate is applied before beforeSend. The pattern above overrides it by always returning error-level events from beforeSend. Adjust to taste.)


Complete examples

Example 1 — minimal (install → init → verify)

This is the fastest path to a confirmed working integration. Run it in a new Node.js project:

mkdir bw-test && cd bw-test
npm init -y
npm install @newinstance/bugwatch

Create test.mjs:

// test.mjs
import { BugWatch } from "@newinstance/bugwatch";

// 1. Initialize with your key.
BugWatch.init({
	projectKey: process.env.BUGWATCH_KEY, // export BUGWATCH_KEY=sk_test_...:...
	debug: true, // see [bugwatch] lines in the console
	release: "[email protected]",
});

// 2. Identify the user (optional).
BugWatch.setUser({ id: "test-user-1", email: "[email protected]" });

// 3. Capture a test exception.
try {
	throw new Error("BugWatch integration test");
} catch (err) {
	BugWatch.captureException(err, { tags: { step: "verification" } });
}

// 4. Flush immediately (do not wait for the 5-second timer).
await BugWatch.flush();

console.log("Done — check the dashboard for the event.");

// 5. Graceful shutdown.
await BugWatch.close();

Run it:

export BUGWATCH_KEY="sk_test_yourKeyId:yourSecret"
node test.mjs

You should see [bugwatch] debug lines in the terminal. Open the BugWatch dashboard → your project → Issues or Logs — the event titled "BugWatch integration test" should appear within seconds.

Example 2 — realistic Express app

This example shows a complete production-style setup: environment variables, init with release, request and error middleware, logger adapter, user identification, tags, and graceful shutdown.

// src/server.ts
import express, {
	type Request,
	type Response,
	type NextFunction,
} from "express";
import { BugWatch, createLogger } from "@newinstance/bugwatch";
import {
	bugWatchExpressRequestHandler,
	bugWatchExpressErrorHandler,
	bugWatchBrowserSessionHandler,
} from "@newinstance/bugwatch/express";
import { installNodeErrorHandlers } from "@newinstance/bugwatch/node";

// ─── 1. Initialize BugWatch at startup ────────────────────────────────────
if (!process.env.BUGWATCH_KEY)
	throw new Error("BUGWATCH_KEY env var is required");

BugWatch.init({
	projectKey: process.env.BUGWATCH_KEY,
	release: process.env.APP_VERSION ?? "dev",
	environment: process.env.NODE_ENV ?? "development",
});

// Capture uncaught Node.js exceptions and unhandled promise rejections.
installNodeErrorHandlers(BugWatch.getClient()!);

// ─── 2. Create