BugBoardDocs

NestJS

NestJS wants the client as a provider — injectable, mockable, and shut down through the framework's lifecycle hooks rather than a raw process listener.

1. Install

npm i bugboard
BUGBOARD_KEY_ID=bbk_xxxxxxxx
BUGBOARD_SIGNING_SECRET=bb_sec_xxxxxxxx

2. Register the client as a provider

// src/bugboard/bugboard.module.ts
import { Global, Module } from '@nestjs/common';
import { createClient, type BugBoardClient } from 'bugboard';

export const BUGBOARD = Symbol('BUGBOARD');

@Global()
@Module({
    providers: [
        {
            provide: BUGBOARD,
            useFactory: (): BugBoardClient =>
                createClient({
                    keyId: process.env.BUGBOARD_KEY_ID,
                    signingSecret: process.env.BUGBOARD_SIGNING_SECRET,
                    environment: process.env.NODE_ENV,
                }),
        },
    ],
    exports: [BUGBOARD],
})
export class BugBoardModule {}

@Global() means you import the module once in AppModule and inject BUGBOARD anywhere. Because the provider is a singleton, there is exactly one queue and one shutdown hook.

3. Report from an exception filter

The natural reporting point:

// src/bugboard/bugboard-exception.filter.ts
import { ArgumentsHost, Catch, HttpException, Inject } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import type { BugBoardClient } from 'bugboard';
import { BUGBOARD } from './bugboard.module';

@Catch()
export class BugBoardExceptionFilter extends BaseExceptionFilter {
    constructor(@Inject(BUGBOARD) private readonly bugboard: BugBoardClient) {
        super();
    }

    catch(exception: unknown, host: ArgumentsHost) {
        const status =
            exception instanceof HttpException ? exception.getStatus() : 500;

        if (status >= 500) {
            const req = host.switchToHttp().getRequest();
            this.bugboard.criticalHigh(
                `Unhandled error: ${req.method} ${req.route?.path}`,
                exception,
                ['nestjs'],
            );
        }

        super.catch(exception, host);
    }
}

Two things to keep: the status >= 500 guard (4xx are client mistakes, not your bugs) and req.route?.path — the route pattern, so repeated failures deduplicate into one card instead of one per id.

Injecting anywhere else works the same way:

@Injectable()
export class CheckoutService {
    constructor(@Inject(BUGBOARD) private readonly bugboard: BugBoardClient) {}

    async charge(order: Order) {
        try {
            await this.payments.charge(order);
        } catch (err) {
            this.bugboard.criticalHigh('Payment capture failed', err, [
                'payments',
            ]);
            throw err;
        }
    }
}

4. Flush on shutdown

import { Inject, Injectable, type OnApplicationShutdown } from '@nestjs/common';

@Injectable()
export class BugBoardShutdown implements OnApplicationShutdown {
    constructor(@Inject(BUGBOARD) private readonly bugboard: BugBoardClient) {}

    async onApplicationShutdown(): Promise<void> {
        await this.bugboard.flush();
    }
}

This requires app.enableShutdownHooks() in main.ts. It is what delivers reports across a SIGTERM — which a containerized service receives on every deploy, and where the SDK's automatic beforeExit hook does not fire.

If you deploy NestJS to a serverless target instead of a long-running container, flushing is mandatory on every invocation — see Serverless & edge.

Testing

Because the client is injected behind a token, tests override it with a spy — no module mocking:

const module = await Test.createTestingModule({
    providers: [CheckoutService, { provide: BUGBOARD, useValue: fakeClient() }],
}).compile();

function fakeClient() {
    return {
        critical: vi.fn(),
        criticalHigh: vi.fn(),
        major: vi.fn(),
        flush: vi.fn().mockResolvedValue(undefined),
    } as unknown as BugBoardClient;
}
expect(bugboard.criticalHigh).toHaveBeenCalledWith(
    'Payment capture failed',
    expect.any(Error),
    ['payments'],
);

This is the payoff for injecting the client rather than importing a shared module deep in the call stack.

Next steps