# NestJS

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.

## Requirements

- 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`). The `nest` subpath imports `reflect-metadata` itself, so you do not need an extra import for the adapter.

## Install

```sh
npm install @newinstance/bugwatch
```

No 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](https://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 once

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.

```ts
// 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 pieces

### 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.

```ts
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.

```ts
// 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 services

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.

```ts
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 tracing

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:

```ts
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 shutdown

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.

```ts
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 example

```ts
// 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();
```

```ts
// 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("*");
	}
}
```

```ts
// 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
	}
}
```

## Troubleshooting

- **`setRequestUser` / `setRequestTag` returns `false`.** The call is outside an ALS request scope. Usually `bugWatchNestMiddleware` was not registered, was not applied with `forRoutes("*")`, 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`, and `status`; identity and per-request tags come from the middleware's scope.
- **Filter order.** `BugWatchExceptionFilter` is `@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 both `useGlobalFilters` and `APP_FILTER` double-reports every exception.
- **Nothing in the dashboard.** Init with `debug: true` and watch for `[bugwatch]` delivery-failure diagnostics. Verify `BUGWATCH_KEY` is 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, so `onApplicationShutdown` never runs and the queue is never drained.
