@newinstance/bugwatch
v0.1.4
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
- Trace correlation (OpenTelemetry)
- 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
├── ./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 Project settings → API keys.
- Copy the key — it looks like
sk_test_abcdef1234:secret…(test environment) orsk_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=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! });
// 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 appinstallBrowserErrorHandlers 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
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 / 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
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);
// 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" });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,
});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/apiIf @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:
- Run your app or the minimal example below and trigger a capture.
- Open the BugWatch merchant dashboard at www.newinstance.cloud.
- Go to your project → Issues or Logs tab.
- 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: trueand check the console for[bugwatch]output. - Verify
projectKeyis set and not accidentallyundefined. - 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:
- [ ]
projectKeyis read from an environment variable — not hardcoded in source. - [ ]
projectKeyis in server-side code only — never in browser bundles. - [ ] Browser apps use
sessionUrland the session-mint endpoint is deployed. - [ ]
releaseis set to the deployed version (git tag, semver, etc.). - [ ]
debug: falsein production (the default). - [ ]
enabledis notfalsein 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. - [ ]
beforeSendorsensitiveFieldscovers any domain-specific sensitive fields your app uses. - [ ]
sampleRateis set appropriately if you have very high log volume. - [ ] Your
.envfile (containing the real key) is in.gitignoreand never committed. - [ ] Key rotation procedure is documented — know how to rotate the key if it leaks.
Troubleshooting
Events are not appearing in the dashboard
- Enable
debug: true— look for[bugwatch] flushand[bugwatch] transport sendlog lines. - Check that
projectKeyis notundefined. Add a startup assertion:if (!process.env.BUGWATCH_KEY) throw new Error("BUGWATCH_KEY is not set"); - Make sure
BugWatch.init()is called before any captures. - In serverless environments (Lambda, Vercel, Next.js route handlers), always
await client.flush()before the function returns, or usewithBugWatchRouteHandlerwhich does this automatically.
Events arrive but are missing context (no user, no tags)
setUser/setTag/setContextmust be called afterinitand 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 globalBugWatch.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
projectKeyon the server is valid (not expired or revoked). - Enable
debug: truein 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/bugwatchCreate 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.mjsYou 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 