NestJS
NestJSlink
Server-side error and log capture for NestJS with @newinstance/bugwatch. Two pieces do the work: a middleware that opens a per-request scope, and a global exception filter that captures whatever your handlers throw.
Requirementslink
- Node.js >= 20.19.0
@nestjs/common>= 9- The package ships dual ESM and CommonJS builds, so it works in both ESM and classic CommonJS Nest projects.
- Standard Nest decorator settings in
tsconfig.json(experimentalDecorators,emitDecoratorMetadata). Thenestsubpath importsreflect-metadataitself, so you do not need an extra import for the adapter.
Installlink
npm install @newinstance/bugwatchNo extra plugin package. Framework integrations ship as subpath exports of the same package.
Get a project DSN key from the BugWatch dashboard at www.newinstance.cloud under BugWatch, your project, Settings, Data Source Name (DSN) keys. It looks like sk_live_abcdef1234:secret. Keep it in an environment variable, never in client code.
Create the client oncelink
Both the middleware and the filter need the same client instance, so create it in its own module-level file and import it from everywhere.
// src/bugwatch.ts
import { createClient } from "@newinstance/bugwatch";
import { installNodeErrorHandlers } from "@newinstance/bugwatch/node";
export const bugwatch = createClient({
projectKey: process.env.BUGWATCH_KEY!,
serviceName: "orders-api",
release: process.env.GIT_SHA,
environment: process.env.NODE_ENV,
});
// Catches uncaughtException / unhandledRejection outside the request lifecycle.
installNodeErrorHandlers(bugwatch);Wire both pieceslink
1. bugWatchNestMiddleware in AppModule
Applied for all routes, it opens an AsyncLocalStorage (ALS) scope for the rest of the request. That scope carries method, url (from originalUrl or url), the inbound traceparent, and an optional lazy user resolver. Because Nest runs middleware before guards, interceptors, pipes, and handlers, everything downstream sits inside the scope.
import { Module, type MiddlewareConsumer, type NestModule } from "@nestjs/common";
import { bugWatchNestMiddleware } from "@newinstance/bugwatch/nest";
import { bugwatch } from "./bugwatch.js";
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(bugWatchNestMiddleware(bugwatch, { getUser: (req) => (req as { user?: { id: string } }).user }))
.forRoutes("*");
}
}getUser is resolved lazily at capture time, not when the request opens, so a guard that populates req.user after the middleware has already run is still picked up.
2. BugWatchExceptionFilter as a global filter
It is a catch-all (@Catch()) filter. On every unhandled exception it captures the error at level 50 with tags method, route, and status (read from exception.getStatus(), else the response statusCode, else 500), triggers a flush, then rethrows the exception so Nest's own exception layer still formats the response. It never swallows an error and never crashes the host: its capture path is wrapped in a try/catch that ignores its own failures.
Register it either way, not both.
// Option A: useGlobalFilters in main.ts
app.useGlobalFilters(new BugWatchExceptionFilter(bugwatch));
// Option B: APP_FILTER provider (keeps registration inside the module graph)
import { APP_FILTER } from "@nestjs/core";
import { createBugWatchNestFilter } from "@newinstance/bugwatch/nest";
@Module({
providers: [{ provide: APP_FILTER, useFactory: () => createBugWatchNestFilter(bugwatch) }],
})
export class AppModule {}createBugWatchNestFilter(client) is just a factory for new BugWatchExceptionFilter(client).
Note: on Nest 11 the catch-all route string is {*splat} rather than "*"; use whichever matches your Nest major.
Per-request user, tags, and context from serviceslink
Inside the scope opened by the middleware, use the request-scoped helpers from @newinstance/bugwatch/node. They write only the current request's ALS cell, so concurrent requests can never overwrite each other's identity.
import { Injectable } from "@nestjs/common";
import { setRequestUser, setRequestTag, setRequestContext } from "@newinstance/bugwatch/node";
@Injectable()
export class OrdersService {
async place(orderId: string, userId: string) {
setRequestUser({ id: userId }); // an explicit call wins over getUser
setRequestTag("orderId", orderId);
setRequestContext("cart", { items: 3 });
await this.charge(orderId); // a throw here is captured with user + tags attached
}
}These work in services, controllers, guards, and interceptors because ALS propagates through the async call tree. Each returns true when applied and false when called outside a request scope (for example a top-level module initializer, or when the middleware was not registered). When it returns false the SDK does nothing and never touches the process-global scope.
Never call client.setUser() inside a request handler. That writes the process-global scope shared by every concurrent request and produces misattributed events under load. Use getUser or setRequestUser.
Distributed tracinglink
The middleware reads the inbound traceparent header and binds its trace ID and span ID to the request scope, so every capture and log in that request joins the caller's trace. If no traceparent arrives, the SDK synthesizes a fresh trace ID for the request, so a request is always traceable end to end even when it originates the trace.
Add spans and traced outbound calls from any service:
import { Injectable } from "@nestjs/common";
import { wrapFetch } from "@newinstance/bugwatch";
import { bugwatch } from "./bugwatch.js";
const tracedFetch = wrapFetch(bugwatch);
@Injectable()
export class PaymentsService {
async charge(orderId: string) {
return bugwatch.withSpan(
"db.query load-order",
async (span) => {
span.setAttr("db.system", "postgresql");
// injects traceparent so the downstream service joins this trace
await tracedFetch("https://payments.example.com/charge", { method: "POST" });
return orderId;
},
{ kind: 3 }, // OTel span kind: 3 = client
);
}
}See the Distributed tracing section of the JavaScript & TypeScript overview for startSpan, span links, traceHeaders(), and the OpenTelemetry bridge.
Flush on shutdownlink
Events are batched and flushed on a timer (flushInterval, 5000 ms by default), so a process that exits immediately can drop queued events. Enable Nest's shutdown hooks and close the client from a lifecycle hook.
import { Injectable, type OnApplicationShutdown } from "@nestjs/common";
import { bugwatch } from "./bugwatch.js";
@Injectable()
export class BugWatchShutdown implements OnApplicationShutdown {
async onApplicationShutdown() {
await bugwatch.close(); // final flush of events and spans
}
}app.enableShutdownHooks() is what makes Nest call this on SIGTERM / SIGINT.
Complete examplelink
// src/main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module.js";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks(); // required for OnApplicationShutdown to fire
await app.listen(3000);
}
void bootstrap();// src/app.module.ts
import {
Module,
Injectable,
type MiddlewareConsumer,
type NestModule,
type OnApplicationShutdown,
} from "@nestjs/common";
import { APP_FILTER } from "@nestjs/core";
import { bugWatchNestMiddleware, createBugWatchNestFilter } from "@newinstance/bugwatch/nest";
import { bugwatch } from "./bugwatch.js";
import { OrdersController } from "./orders.controller.js";
@Injectable()
class BugWatchShutdown implements OnApplicationShutdown {
async onApplicationShutdown() {
await bugwatch.close();
}
}
@Module({
controllers: [OrdersController],
providers: [
BugWatchShutdown,
{ provide: APP_FILTER, useFactory: () => createBugWatchNestFilter(bugwatch) },
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(bugWatchNestMiddleware(bugwatch, { getUser: (req) => (req as { user?: { id: string } }).user }))
.forRoutes("*");
}
}// 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);
throw new Error("payment gateway timeout");
// captured with user, orderId, method, route, status, and the request's trace id,
// then rethrown so Nest returns its usual 500 response body
}
}Troubleshootinglink
setRequestUser/setRequestTagreturnsfalse. The call is outside an ALS request scope. UsuallybugWatchNestMiddlewarewas not registered, was not applied withforRoutes("*"), or the code path is not descended from the request (a module initializer, a timer created at boot).- Errors captured without user or tags. The filter is registered but the middleware is not. The filter alone still reports
method,route, andstatus; identity and per-request tags come from the middleware's scope. - Filter order.
BugWatchExceptionFilteris@Catch()with no argument, so a narrower filter (for example@Catch(HttpException)) that Nest selects for a given exception type handles it instead and BugWatch never sees it. Put BugWatch's capture inside that filter, or let it delegate. Registering the filter through bothuseGlobalFiltersandAPP_FILTERdouble-reports every exception. - Nothing in the dashboard. Init with
debug: trueand watch for[bugwatch]delivery-failure diagnostics. VerifyBUGWATCH_KEYis defined at boot. This API key is not a BugWatch project key(HTTP 400). You used an organization-level API key. Ingest requires a per-project DSN key.- Events lost on redeploy.
app.enableShutdownHooks()is missing, soonApplicationShutdownnever runs and the queue is never drained.