# NestJS SDK

`@cohorly/nest` is a standard NestJS dynamic module (`forRoot` / `forRootAsync`) plus an injectable `CohorlyService` wrapping `@cohorly/node`, with an automatic final flush on application shutdown. Supports `@nestjs/common` ^10 and ^11.

## Install

```bash
pnpm add @cohorly/nest
```

## Register the module

```typescript
// app.module.ts
import { Module } from "@nestjs/common";
import { CohorlyModule } from "@cohorly/nest";

@Module({
  imports: [
    CohorlyModule.forRoot({
      token: process.env.COHORLY_TOKEN!,
      host: "https://cohorly-service.velloalabs.com",
      isGlobal: true, // inject CohorlyService anywhere without re-importing
    }),
  ],
})
export class AppModule {}
```

## Inject the service

```typescript
import { Injectable } from "@nestjs/common";
import { CohorlyService } from "@cohorly/nest";

@Injectable()
export class SignupService {
  constructor(private readonly cohorly: CohorlyService) {}

  async signUp(userId: string, email: string) {
    this.cohorly.track("signed_up", { distinct_id: userId, method: "email" });
    this.cohorly.people.set(userId, { $email: email, plan: "free" });
  }
}
```

`CohorlyService` exposes the full mixpanel-node-style
surface: `track`, `trackBatch`,
`import`, `importBatch`, `people.*`,
`alias`, `flush`, and the raw client at
`service.client`.

## Async configuration

```typescript
import { ConfigModule, ConfigService } from "@nestjs/config";
import { CohorlyModule } from "@cohorly/nest";

CohorlyModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  isGlobal: true,
  useFactory: (config: ConfigService) => ({
    token: config.getOrThrow("COHORLY_TOKEN"),
    host: config.get("COHORLY_HOST") ?? "https://cohorly-service.velloalabs.com",
  }),
});
```

All `@cohorly/node` config options are accepted
(`host`, `flushIntervalMs`,
`batchSize`, `debug`, `maxQueueSize`,
`maxRetryDelayMs`, and the error tracking ones: `release`, `dist`,
`serverName`, `captureUncaught`, `flushTimeoutMs`, `beforeSendException`, ...),
plus `token` (required), `isGlobal`, `captureExceptions` and `distinctId`.

## Error tracking

```typescript
CohorlyModule.forRoot({
  token: process.env.COHORLY_TOKEN!,
  release: process.env.RELEASE, // see the Node.js SDK "Releases"
  captureExceptions: true, // global exception filter
  distinctId: (host) => host.switchToHttp().getRequest().user?.id,
  isGlobal: true,
});
```

`captureExceptions: true` (default `false`) registers
`CohorlyExceptionFilter` as a global filter (`APP_FILTER`). It reports server
errors and leaves client errors alone:

| Thrown                                                                                                                | Reported |
| --------------------------------------------------------------------------------------------------------------------- | -------- |
| `HttpException` with status 500 or more                                                                               | Yes      |
| Anything that is not an `HttpException` (Nest answers 500)                                                            | Yes      |
| `HttpException` below 500 (`NotFoundException`, `BadRequestException`, `UnauthorizedException`, ...)                  | No       |
| An error with a numeric `statusCode` below 500 that is not an `HttpException` (`http-errors`, e.g. body-parser's 413) | No       |

The filter reports by the status Nest will answer with. Nest answers an
`http-errors` error with its own `statusCode`, so a body-parser 413 is a
client error like any other 4xx and is not reported.

The response does not change: the filter extends `BaseExceptionFilter` and
hands every exception to Nest's default handling after capturing it. The
event's tags are only `http.method` and `http.route` (the route pattern, e.g.
`/rides/:id`, or the path without the query string); the request body,
headers, cookies and query string are never sent.

The filter supports **HTTP only**. `BaseExceptionFilter` answers through the
HTTP adapter, so in microservice (RPC) and WebSocket contexts this version
is not covered: the filter still captures the error (without tags) and
delegates, and what the client gets is whatever Nest's default handling
does there. Keep your own RPC/WebSocket filters for those transports.

`distinctId` (sync, from the `ArgumentsHost`) attributes the error to a user;
without it, errors go to the SDK's server id. For the rest of the API
(`captureUncaught`, breadcrumbs, source maps, the event format), see the
[Node.js SDK error tracking docs](/sdks/node#error-tracking). `CohorlyService`
also exposes `captureException`, `addBreadcrumb`, `withCohorly` and
`flushWithTimeout`.

To register the filter yourself instead (e.g. next to other global filters),
leave `captureExceptions` off and use
`app.useGlobalFilters(new CohorlyExceptionFilter(app.get(CohorlyService), {}, app.getHttpAdapter()))`.

## Graceful shutdown

`CohorlyService` implements
`OnApplicationShutdown`: when the Nest application closes
it stops the SDK's flush timer and performs a final flush. To
also cover process signals, enable Nest's shutdown hooks:

```typescript
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks();
```
