NestJS

NestJS

Register createReplayStackNestInterceptor and createReplayStackNestExceptionFilter from @replaystack/sdk/nestjs globally so every HTTP controller is covered—same approach as replaystack-nestjs-example.

Runnable reference

The replaystack-nestjs-example app mirrors the Express demo routes (/ok, Bearer POST /fail, /fail/type). Uses @replaystack/sdk@^1.0.7 with interceptor + exception filter (not Express-style middleware). Express version: replaystack-express-example.

Recommended: SDK factories

Import Nest factories from @replaystack/sdk/nestjs and the shared client from @replaystack/sdk. Provide your client once—typically a small replaystack.instance.ts—then wire APP_INTERCEPTOR and APP_FILTER.

replaystack.instance.ts
import { createReplayStackClient, type ReplayStack } from "@replaystack/sdk";

const apiKey = process.env.REPLAYSTACK_API_KEY?.trim();
if (!apiKey) {
  console.error("Missing REPLAYSTACK_API_KEY. Copy .env.example to .env.");
  process.exit(1);
}

/** Shared singleton — import in AppModule providers below. */
export const replayStack: ReplayStack = createReplayStackClient({
  apiKey,
  endpoint: process.env.REPLAYSTACK_ENDPOINT,
  serviceName: process.env.REPLAYSTACK_SERVICE_NAME ?? "nestjs-api",
  environment: process.env.NODE_ENV ?? "development",
  appVersion: process.env.APP_VERSION,
  commitHash: process.env.COMMIT_HASH,
  captureSuccess: true,
});
app.module.ts
import { Module } from "@nestjs/common";
import { APP_FILTER, APP_INTERCEPTOR } from "@nestjs/core";
import {
  createReplayStackNestExceptionFilter,
  createReplayStackNestInterceptor,
} from "@replaystack/sdk/nestjs";
import { replayStack } from "./replaystack.instance";

function endpointPath(url: string | undefined): string {
  if (!url) return "";
  try {
    const p = url.startsWith("http") ? new URL(url).pathname : url;
    return p.split("?")[0] ?? "";
  } catch {
    return url.split("?")[0] ?? "";
  }
}

@Module({
  imports: [],
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: createReplayStackNestInterceptor({
        client: replayStack,
        captureRequestBody: true,
        captureResponseBody: true,
        captureHeaders: true,
        shouldCapture: ({ endpoint }) => endpointPath(endpoint) !== "/health",
      }),
    },
    {
      provide: APP_FILTER,
      useClass: createReplayStackNestExceptionFilter({
        client: replayStack,
        captureRequestBody: true,
        captureResponseBody: true,
        captureHeaders: true,
      }),
    },
  ],
})
export class AppModule {}
Import Nest helpers from @replaystack/sdk/nestjs (not the package root). SDK v1.0.4+ split entry points; v1.0.7 adds captureLogs (default on), captureFailure, and safer offline-queue defaults—see the SDK CHANGELOG on GitHub.

Client options reference

Only apiKey is required. Everything else is optional—the SDK fills many values from environment variables when you do not pass them explicitly. For ReplayStack Cloud, you usually only need the key—omit endpoint to use the default host https://api.replaystack.co.

Field What it doesIf you omit it
apiKeyRequiredProject key from the ReplayStack dashboard. Keep server-side only.
endpointOptionalAPI base URL (no /api/v1/... path). The SDK posts to /api/v1/ingest/events under this host.REPLAYSTACK_ENDPOINT env, else https://api.replaystack.co
serviceNameOptionalLogical service name in the UI (filters, grouping).REPLAYSTACK_SERVICE_NAME env, or set per event
environmentOptionalLabel for where this process runs (production, staging, …).NODE_ENV, else development
appVersionOptionalRelease or build version shown on events.REPLAYSTACK_APP_VERSION / APP_VERSION env when not set on the client
commitHashOptionalGit/deploy SHA for tying events to a revision.REPLAYSTACK_COMMIT_HASH / COMMIT_HASH env when not set on the client
enabledOptionalTurns all SDK sends off without removing code.true unless REPLAYSTACK_ENABLED=false
timeoutMsOptionalHow long to wait on each ingest HTTP request.2500 (overridable via REPLAYSTACK_TIMEOUT_MS)
retriesOptionalRetries if the ingest request fails transiently.1 (REPLAYSTACK_RETRIES)
sampleRateOptionalRandom sample of events, 0–1. Use to reduce volume on success paths.1 (capture all)
captureSuccessOptionalWhether successful HTTP-style events are sent (failures are still captured).false — set true or REPLAYSTACK_CAPTURE_SUCCESS=true for 2xx traffic (examples often enable this)
captureLogsOptionalAttach application log lines to events (e.g. error log on exceptions).true — set false or REPLAYSTACK_CAPTURE_LOGS=false to disable
logLevelOptionalMinimum log level stored when captureLogs is on.error (REPLAYSTACK_LOG_LEVEL)
maxLogsOptionalMax log lines kept per request context.50
batchFlushIntervalMsOptionalWhen > 0, buffer events and POST to /api/v1/ingest/bulk-events on an interval.0 (disabled; REPLAYSTACK_BATCH_FLUSH_INTERVAL_MS)
batchMaxEventsOptionalMax events per bulk flush batch.20 (REPLAYSTACK_BATCH_MAX_EVENTS)
maxPayloadSizeBytesOptionalTruncates very large JSON bodies/headers before send.512 KiB
maskFieldsOptionalExtra field names to redact in payloads and headers (built-in sensitive list always applies).built-in list always on (authorization, password, passwd, token, access_token, refresh_token, …)
ignoredPathsOptionalURL paths to skip for client-level capture. Express middleware also merges its own defaults (/health, /metrics, /favicon.ico).none
maxBreadcrumbsOptionalMax breadcrumbs kept per request/client context.50
fetchImplOptionalInject fetch for tests or runtimes without global fetch.globalThis.fetch
onErrorOptionalCalled if the SDK fails internally (network, parsing). Does not replace your app error handling.none
offlineQueueMaxOptionalMax prepared events to keep in memory when ingest is down after retries. Oldest dropped when full. 0 = disable queueing.0 — set REPLAYSTACK_OFFLINE_QUEUE_MAX to buffer failed sends in RAM
flushIntervalMsOptionalIf > 0, periodically calls flush() to drain the offline queue when the API recovers.0 / disabled (REPLAYSTACK_FLUSH_INTERVAL_MS)
onQueueDropOptionalCallback when the offline queue exceeds offlineQueueMax and drops the oldest event.none

maskFields: optional extra JSON/header keys to redact. Passwords, tokens, cookies, and card fields are masked even when you omit this option. See Security & masking for the full built-in name list.

Lifecycle and reliability: call flush() to drain the in-memory queue after failed sends. close() stops new capture, cancels periodic flush, then drains. In Node, installReplayStackProcessGuards(client) from @replaystack/sdk registers optional hooks (unhandled rejection, uncaught exception, beforeExit) to flush best-effort—crash capture is not guaranteed.

ReplayStack client

Same options as Express or Next.js: only apiKey is required. The table lists every field; snippets above show the subset used in examples.

Pieces (legacy / advanced)

  • Prefer the Recommended section—no manual interceptor code required.
  • Alternatively, expose the client via a global module (REPLAYSTACK_CLIENT), then paste the reference interceptor and filter classes below only if you need custom capture logic.

DI module (optional)

replaystack.module.ts
import { Module, Global } from "@nestjs/common";
import { createReplayStackClient } from "@replaystack/sdk";

@Global()
@Module({
  providers: [
    {
      provide: "REPLAYSTACK_CLIENT",
      useFactory: () =>
        createReplayStackClient({
          apiKey: process.env.REPLAYSTACK_API_KEY!,
          endpoint: process.env.REPLAYSTACK_ENDPOINT!,
          serviceName: process.env.REPLAYSTACK_SERVICE_NAME || "nestjs-api",
          environment: process.env.NODE_ENV || "development",
          appVersion: process.env.APP_VERSION,
          commitHash: process.env.COMMIT_HASH,
          ignoredPaths: ["/health", "/metrics"],
          sampleRate: 1,
        }),
    },
  ],
  exports: ["REPLAYSTACK_CLIENT"],
})
export class ReplayStackModule {}

Manual interceptor (advanced)

Reference implementation—in production use createReplayStackNestInterceptor whenever possible so you inherit SDK fixes automatically.

replaystack.interceptor.ts
import {
  CallHandler,
  ExecutionContext,
  Inject,
  Injectable,
  NestInterceptor,
} from "@nestjs/common";
import { Observable, tap, catchError, throwError } from "rxjs";
import type { ReplayStack } from "@replaystack/sdk";

/** Advanced: manual capture. Prefer createReplayStackNestInterceptor from @replaystack/sdk/nestjs in production. */
@Injectable()
export class ReplayStackInterceptor implements NestInterceptor {
  constructor(
    @Inject("REPLAYSTACK_CLIENT")
    private readonly replayStack: ReplayStack
  ) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    const startedAt = Date.now();
    const http = context.switchToHttp();
    const req = http.getRequest();

    return next.handle().pipe(
      tap(async (responseBody) => {
        await this.replayStack.captureEvent({
          eventType: "api",
          method: req.method,
          endpoint: req.originalUrl || req.url,
          requestHeaders: req.headers,
          requestPayload: req.body,
          responsePayload: responseBody,
          status: "success",
          statusCode: 200,
          executionTimeMs: Date.now() - startedAt,
          sourceIp: req.ip,
          userAgent: req.headers["user-agent"],
        });
      }),
      catchError((error) => throwError(() => error))
    );
  }
}
Read the real HTTP status from the response in production; the sample pins statusCode to 200 inside tap solely to illustrate fields ReplayStack accepts.

Manual exception filter

Prefer createReplayStackNestExceptionFilter. Keeps ingestion before responding.

replaystack-exception.filter.ts
import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
  Inject,
} from "@nestjs/common";
import type { ReplayStack } from "@replaystack/sdk";

@Catch()
export class ReplayStackExceptionFilter implements ExceptionFilter {
  constructor(
    @Inject("REPLAYSTACK_CLIENT")
    private readonly replayStack: ReplayStack
  ) {}

  async catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const req = ctx.getRequest();
    const res = ctx.getResponse();

    const statusCode =
      exception instanceof HttpException ? exception.getStatus() : 500;

    const error = exception as Error;

    await this.replayStack.captureException(error, {
      eventType: "api",
      method: req.method,
      endpoint: req.originalUrl || req.url,
      requestHeaders: req.headers,
      requestPayload: req.body,
      responsePayload: {
        message: error.message || "Internal Server Error",
      },
      statusCode,
      executionTimeMs: 0,
      sourceIp: req.ip,
      userAgent: req.headers["user-agent"],
    });

    res.status(statusCode).json({
      statusCode,
      message: error.message || "Internal Server Error",
    });
  }
}

Register globally (manual classes)

Only needed if you opted into the handwritten interceptor/filter above alongside the Nest DI module pattern.

app.module.ts
import { Module } from "@nestjs/common";
import { APP_FILTER, APP_INTERCEPTOR } from "@nestjs/core";
import { ReplayStackModule } from "./replaystack.module";
import { ReplayStackInterceptor } from "./replaystack.interceptor";
import { ReplayStackExceptionFilter } from "./replaystack-exception.filter";

@Module({
  imports: [ReplayStackModule],
  providers: [
    { provide: APP_INTERCEPTOR, useClass: ReplayStackInterceptor },
    { provide: APP_FILTER, useClass: ReplayStackExceptionFilter },
  ],
})
export class AppModule {}