
Sentry Nestjs Sdk
- 2k installs
- 243 repo stars
- Updated July 27, 2026
- getsentry/sentry-for-ai
sentry-nestjs-sdk is an agent skill that Full Sentry SDK setup for NestJS. Use when asked to "add Sentry to NestJS", "install @sentry/nestjs", "setup Sentry in N.
About
All Skills SKILL_TREE md SDK Setup sentry sdk setup SKILL md NestJS SDK Opinionated wizard that scans your NestJS project and guides you through complete Sentry setup User asks to add Sentry to NestJS or setup Sentry in a NestJS app User wants error monitoring tracing profiling logging metrics or crons in NestJS User mentions sentry nestjs or Sentry NestJS User wants to monitor NestJS controllers services guards microservices or background jobs Note SDK versions and APIs below reflect sentry nestjs 10 x NestJS 8 11 supported Always verify against docs sentry io platforms node guides nestjs https docs sentry io platforms node guides nestjs before implementing Run these commands to understand the project before making recommendations The sentry nestjs sdk agent skill provides documented workflows prerequisites triggers and safety guidance from its SKILL md source Agents load it when user requests match the description and follow step by step instructions without inventing capabilities It integrates with standard agent tooling for the tasks inputs outputs and failure modes described in
- description: Full Sentry SDK setup for NestJS. Use when asked to "add Sentry to NestJS", "install @sentry/nestjs", "setu
- > [All Skills](../../SKILL_TREE.md) > [SDK Setup](../sentry-sdk-setup/SKILL.md) > NestJS SDK
- Opinionated wizard that scans your NestJS project and guides you through complete Sentry setup.
- Follow sentry-nestjs-sdk SKILL.md steps and documented constraints.
- Follow sentry-nestjs-sdk SKILL.md steps and documented constraints.
Sentry Nestjs Sdk by the numbers
- 1,997 all-time installs (skills.sh)
- +50 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #99 of 1,453 DevOps & CI/CD skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
sentry-nestjs-sdk capabilities & compatibility
- Capabilities
- description: full sentry sdk setup for nestjs. u · > [all skills](../../skill_tree.md) > [sdk setup · opinionated wizard that scans your nestjs projec · follow sentry nestjs sdk skill.md steps and docu
- Use cases
- orchestration
What sentry-nestjs-sdk says it does
description: Full Sentry SDK setup for NestJS. Use when asked to "add Sentry to NestJS", "install @sentry/nestjs", "setup Sentry in NestJS", or configure error monitoring, tracing, profiling, logging,
> [All Skills](../../SKILL_TREE.md) > [SDK Setup](../sentry-sdk-setup/SKILL.md) > NestJS SDK
Opinionated wizard that scans your NestJS project and guides you through complete Sentry setup.
npx skills add https://github.com/getsentry/sentry-for-ai --skill sentry-nestjs-sdkAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2k |
|---|---|
| repo stars | ★ 243 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | getsentry/sentry-for-ai ↗ |
When should an agent use sentry-nestjs-sdk and what problem does it solve?
Full Sentry SDK setup for NestJS. Use when asked to "add Sentry to NestJS", "install @sentry/nestjs", "setup Sentry in NestJS", or configure error monitoring, tracing, profiling, logging, metrics, cro
Who is it for?
Developers invoking sentry-nestjs-sdk as documented in the skill source.
Skip if: Skip when requirements fall outside sentry-nestjs-sdk documented scope.
When should I use this skill?
Full Sentry SDK setup for NestJS. Use when asked to "add Sentry to NestJS", "install @sentry/nestjs", "setup Sentry in NestJS", or configure error monitoring, tracing, profiling, logging, metrics, cro
What you get
Outputs aligned with the sentry-nestjs-sdk SKILL.md workflow and stated deliverables.
- Configured Sentry SDK
- Tracing and profiling hooks
- Production monitoring dashboard
By the numbers
- Supports Express, Fastify, GraphQL, microservices, WebSockets, and background jobs
- Licensed under Apache-2.0 as part of the sentry-sdk-setup skill tree
Files
All Skills > SDK Setup > NestJS SDK
Sentry NestJS SDK
Opinionated wizard that scans your NestJS project and guides you through complete Sentry setup.
Invoke This Skill When
- User asks to "add Sentry to NestJS" or "setup Sentry" in a NestJS app
- User wants error monitoring, tracing, profiling, logging, metrics, or crons in NestJS
- User mentions
@sentry/nestjsor Sentry + NestJS - User wants to monitor NestJS controllers, services, guards, microservices, or background jobs
Note: SDK versions and APIs below reflect @sentry/nestjs 10.x (NestJS 8–11 supported).Always verify against docs.sentry.io/platforms/node/guides/nestjs/ before implementing.
---
Phase 1: Detect
Run these commands to understand the project before making recommendations:
# Confirm NestJS project
grep -E '"@nestjs/core"' package.json 2>/dev/null
# Check NestJS version
node -e "console.log(require('./node_modules/@nestjs/core/package.json').version)" 2>/dev/null
# Check existing Sentry
grep -i sentry package.json 2>/dev/null
ls src/instrument.ts 2>/dev/null
grep -r "Sentry.init\|@sentry" src/main.ts src/instrument.ts 2>/dev/null
# Check for existing Sentry DI wrapper (common in enterprise NestJS)
grep -rE "SENTRY.*TOKEN|SentryProxy|SentryService" src/ libs/ 2>/dev/null
# Check for config-class-based init (vs env-var-based)
grep -rE "class SentryConfig|SentryConfig" src/ libs/ 2>/dev/null
# Check if SentryModule.forRoot() is already registered in a shared module
grep -rE "SentryModule\.forRoot|SentryProxyModule" src/ libs/ 2>/dev/null
# Detect HTTP adapter (default is Express)
grep -E "FastifyAdapter|@nestjs/platform-fastify" package.json src/main.ts 2>/dev/null
# Detect GraphQL
grep -E '"@nestjs/graphql"|"apollo-server"' package.json 2>/dev/null
# Detect microservices
grep '"@nestjs/microservices"' package.json 2>/dev/null
# Detect WebSockets
grep -E '"@nestjs/websockets"|"socket.io"' package.json 2>/dev/null
# Detect task queues / scheduled jobs
grep -E '"@nestjs/bull"|"@nestjs/bullmq"|"@nestjs/schedule"|"bullmq"|"bull"' package.json 2>/dev/null
# Detect databases
grep -E '"@prisma/client"|"typeorm"|"mongoose"|"pg"|"mysql2"' package.json 2>/dev/null
# Detect AI libraries
grep -E '"openai"|"@anthropic-ai"|"langchain"|"@langchain"|"@google/generative-ai"|"ai"' package.json 2>/dev/null
# Check for companion frontend
ls -d ../frontend ../web ../client ../ui 2>/dev/nullWhat to note:
- Is
@sentry/nestjsalready installed? If yes, check ifinstrument.tsexists andSentry.init()is called — may just need feature config. - Sentry DI wrapper detected? → The project wraps Sentry behind a DI token (e.g.
SENTRY_PROXY_TOKEN) for testability. Use the injected proxy for all runtime Sentry calls (startSpan,captureException,withIsolationScope) instead of importing@sentry/nestjsdirectly in controllers, services, and processors. Onlyinstrument.tsshould import@sentry/nestjsdirectly. - Config class detected? → The project uses a typed config class for
Sentry.init()options (e.g. loaded from YAML or@nestjs/config). Any new SDK options must be added to the config type — do not hardcode values that should be configurable per environment. - `SentryModule.forRoot()` already registered? → If it's in a shared module (e.g. a Sentry proxy module), do not add it again in
AppModule— this causes duplicate interceptor registration. - Express (default) or Fastify adapter? Express is fully supported; Fastify works but has known edge cases.
- GraphQL detected? →
SentryGlobalFilterhandles it natively. - Microservices detected? → Recommend RPC exception filter.
- Task queues /
@nestjs/schedule? → Recommend crons. - AI libraries? → Auto-instrumented, zero config.
- Prisma? → Requires manual
prismaIntegration(). - Companion frontend? → Triggers Phase 4 cross-link.
---
Phase 2: Recommend
Based on what you found, present a concrete proposal. Don't ask open-ended questions — lead with a recommendation:
Always recommended (core coverage):
- ✅ Error Monitoring — captures unhandled exceptions across HTTP, GraphQL, RPC, and WebSocket contexts
- ✅ Tracing — auto-instruments middleware, guards, pipes, interceptors, filters, and route handlers
Recommend when detected:
- ✅ Profiling — production apps where CPU performance matters (
@sentry/profiling-node) - ✅ Logging — structured Sentry Logs + optional console capture
- ✅ Crons —
@nestjs/schedule, Bull, or BullMQ detected - ✅ Metrics — business KPIs or SLO tracking
- ✅ AI Monitoring — OpenAI/Anthropic/LangChain/etc. detected (auto-instrumented, zero config)
Recommendation matrix:
| Feature | Recommend when... | Reference |
|---|---|---|
| Error Monitoring | Always — non-negotiable baseline | ${SKILL_ROOT}/references/error-monitoring.md |
| Tracing | Always — NestJS lifecycle is auto-instrumented | ${SKILL_ROOT}/references/tracing.md |
| Profiling | Production + CPU-sensitive workloads | ${SKILL_ROOT}/references/profiling.md |
| Logging | Always; enhanced for structured log aggregation | ${SKILL_ROOT}/references/logging.md |
| Metrics | Custom business KPIs or SLO tracking | ${SKILL_ROOT}/references/metrics.md |
| Crons | @nestjs/schedule, Bull, or BullMQ detected | ${SKILL_ROOT}/references/crons.md |
| AI Monitoring | OpenAI/Anthropic/LangChain/etc. detected | ${SKILL_ROOT}/references/ai-monitoring.md |
Propose: _"I recommend Error Monitoring + Tracing + Logging. Want Profiling, Crons, or AI Monitoring too?"_
---
Phase 3: Guide
Install
# Core SDK (always required — includes @sentry/node)
npm install @sentry/nestjs
# With profiling support (optional)
npm install @sentry/nestjs @sentry/profiling-node⚠️ Do NOT install `@sentry/node` alongside `@sentry/nestjs` —@sentry/nestjsre-exports everything from@sentry/node. Installing both causes duplicate registration.
Three-File Setup (Required)
NestJS requires a specific three-file initialization pattern because the Sentry SDK must patch Node.js modules (via OpenTelemetry) before NestJS loads them.
Before creating new files, check Phase 1 results:
>
- If instrument.ts already exists → modify it, don't create a new one.- If a config class drives Sentry.init() → read options from the config instead of hardcoding env vars.- If a Sentry DI wrapper exists → use it for runtime calls instead of importing @sentry/nestjs directly in services/controllers.Step 1: Create src/instrument.ts
import * as Sentry from "@sentry/nestjs";
// Optional: add profiling
// import { nodeProfilingIntegration } from "@sentry/profiling-node";
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.SENTRY_ENVIRONMENT ?? "production",
release: process.env.SENTRY_RELEASE,
// Data collection (SDK ≥ 10.57.0 — replaces deprecated sendDefaultPii)
dataCollection: {
// To disable sending user data and HTTP bodies, uncomment the lines below. For more info visit:
// https://docs.sentry.io/platforms/javascript/guides/nestjs/configuration/options/#dataCollection
// userInfo: false,
// httpBodies: [],
},
// Tracing — lower to 0.1–0.2 in high-traffic production
tracesSampleRate: 1.0,
// Profiling (requires @sentry/profiling-node)
// integrations: [nodeProfilingIntegration()],
// profileSessionSampleRate: 1.0,
// profileLifecycle: "trace",
// Structured logs (SDK ≥ 9.41.0)
enableLogs: true,
});Config-driven `Sentry.init()`: If Phase 1 found a typed config class (e.g. SentryConfig), read options from it instead of using raw process.env. This is common in NestJS apps that use @nestjs/config or custom config loaders:
import * as Sentry from "@sentry/nestjs";
import { loadConfiguration } from "./config";
const config = loadConfiguration();
Sentry.init({
dsn: config.sentry.dsn,
environment: config.sentry.environment ?? "production",
release: config.sentry.release,
dataCollection: config.sentry.dataCollection ?? {
// To disable sending user data and HTTP bodies, uncomment the lines below. For more info visit:
// https://docs.sentry.io/platforms/javascript/guides/nestjs/configuration/options/#dataCollection
// userInfo: false,
// httpBodies: [],
},
tracesSampleRate: config.sentry.tracesSampleRate ?? 1.0,
profileSessionSampleRate: config.sentry.profilesSampleRate ?? 1.0,
profileLifecycle: "trace",
enableLogs: true,
});When adding new SDK options (e.g. dataCollection, profileSessionSampleRate), add them to the config type so they can be configured per environment.
Step 2: Import instrument.ts FIRST in src/main.ts
// instrument.ts MUST be the very first import — before NestJS or any other module
import "./instrument";
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Enable graceful shutdown — flushes Sentry events on SIGTERM/SIGINT
app.enableShutdownHooks();
await app.listen(3000);
}
bootstrap();Why first? OpenTelemetry must monkey-patchhttp,express, database drivers, and other modules before they load. Any module that loads beforeinstrument.tswill not be auto-instrumented.
Step 3: Register SentryModule and SentryGlobalFilter in src/app.module.ts
import { Module } from "@nestjs/common";
import { APP_FILTER } from "@nestjs/core";
import { SentryModule, SentryGlobalFilter } from "@sentry/nestjs/setup";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
@Module({
imports: [
SentryModule.forRoot(), // Registers SentryTracingInterceptor globally
],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_FILTER,
useClass: SentryGlobalFilter, // Captures all unhandled exceptions
},
],
})
export class AppModule {}What each piece does:
SentryModule.forRoot()— registersSentryTracingInterceptoras a globalAPP_INTERCEPTOR, enabling HTTP transaction namingSentryGlobalFilter— extendsBaseExceptionFilter; captures exceptions across HTTP, GraphQL (rethrowsHttpExceptionwithout reporting), and RPC contexts
⚠️ Do NOT register `SentryModule.forRoot()` twice. If Phase 1 found it already imported in a shared library module (e.g. aSentryProxyModuleorAnalyticsModule), do not add it again inAppModule. Duplicate registration causes every span to be intercepted twice, bloating trace data.
⚠️ Two entrypoints, different imports:
>
-@sentry/nestjs→ SDK init, capture APIs, decorators (SentryTraced,SentryCron,SentryExceptionCaptured)
-@sentry/nestjs/setup→ NestJS DI constructs (SentryModule,SentryGlobalFilter)
>
Never importSentryModulefrom@sentry/nestjs(main entrypoint) — it loads@nestjs/commonbefore OpenTelemetry patches it, breaking auto-instrumentation.
ESM Setup (Node ≥ 18.19.0)
For ESM applications, use --import instead of a file import:
// instrument.mjs
import * as Sentry from "@sentry/nestjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1.0,
});// package.json
{
"scripts": {
"start": "node --import ./instrument.mjs -r ts-node/register src/main.ts"
}
}Or via environment:
NODE_OPTIONS="--import ./instrument.mjs" npm run startException Filter Options
Choose the approach that fits your existing architecture:
Option A: No existing global filter — use SentryGlobalFilter (recommended)
Already covered in Step 3 above. This is the simplest option.
Option B: Existing custom global filter — add @SentryExceptionCaptured() decorator
import { Catch, ExceptionFilter, ArgumentsHost } from "@nestjs/common";
import { SentryExceptionCaptured } from "@sentry/nestjs";
@Catch()
export class YourExistingFilter implements ExceptionFilter {
@SentryExceptionCaptured() // Wraps catch() to auto-report exceptions
catch(exception: unknown, host: ArgumentsHost): void {
// Your existing error handling continues unchanged
}
}Option C: Specific exception type — manual capture
import { ArgumentsHost, Catch } from "@nestjs/common";
import { BaseExceptionFilter } from "@nestjs/core";
import * as Sentry from "@sentry/nestjs";
@Catch(ExampleException)
export class ExampleExceptionFilter extends BaseExceptionFilter {
catch(exception: ExampleException, host: ArgumentsHost) {
Sentry.captureException(exception);
super.catch(exception, host);
}
}Option D: Microservice RPC exceptions
import { Catch, RpcExceptionFilter, ArgumentsHost } from "@nestjs/common";
import { Observable, throwError } from "rxjs";
import { RpcException } from "@nestjs/microservices";
import * as Sentry from "@sentry/nestjs";
@Catch(RpcException)
export class SentryRpcFilter implements RpcExceptionFilter<RpcException> {
catch(exception: RpcException, host: ArgumentsHost): Observable<any> {
Sentry.captureException(exception);
return throwError(() => exception.getError());
}
}Decorators
@SentryTraced(op?) — Instrument any method
import { Injectable } from "@nestjs/common";
import { SentryTraced } from "@sentry/nestjs";
@Injectable()
export class OrderService {
@SentryTraced("order.process")
async processOrder(orderId: string): Promise<void> {
// Automatically wrapped in a Sentry span
}
@SentryTraced() // Defaults to op: "function"
async fetchInventory() { ... }
}@SentryCron(slug, config?) — Monitor scheduled jobs
import { Injectable } from "@nestjs/common";
import { Cron } from "@nestjs/schedule";
import { SentryCron } from "@sentry/nestjs";
@Injectable()
export class ReportService {
@Cron("0 * * * *")
@SentryCron("hourly-report", {
// @SentryCron must come AFTER @Cron
schedule: { type: "crontab", value: "0 * * * *" },
checkinMargin: 2, // Minutes before marking missed
maxRuntime: 10, // Max runtime in minutes
timezone: "UTC",
})
async generateReport() {
// Check-in sent automatically on start/success/failure
}
}Background Job Scope Isolation
Background jobs share the default isolation scope — wrap with Sentry.withIsolationScope() to prevent cross-contamination:
import * as Sentry from "@sentry/nestjs";
import { Injectable } from "@nestjs/common";
import { Cron, CronExpression } from "@nestjs/schedule";
@Injectable()
export class JobService {
@Cron(CronExpression.EVERY_HOUR)
handleCron() {
Sentry.withIsolationScope(() => {
Sentry.setTag("job", "hourly-sync");
this.doWork();
});
}
}Apply withIsolationScope to: @Cron(), @Interval(), @OnEvent(), @Processor(), and any code outside the request lifecycle.
Working with Sentry DI Wrappers
Some NestJS projects wrap Sentry behind a dependency injection token (e.g. SENTRY_PROXY_TOKEN) for testability and decoupling. If Phase 1 detected this pattern, use the injected service for all runtime Sentry calls — do not import @sentry/nestjs directly in controllers, services, or processors.
import { Controller, Inject } from "@nestjs/common";
import { SENTRY_PROXY_TOKEN, type SentryProxyService } from "./sentry-proxy";
@Controller("orders")
export class OrderController {
constructor(
@Inject(SENTRY_PROXY_TOKEN) private readonly sentry: SentryProxyService,
private readonly orderService: OrderService,
) {}
@Post()
async createOrder(@Body() dto: CreateOrderDto) {
return this.sentry.startSpan(
{ name: "createOrder", op: "http" },
async () => this.orderService.create(dto),
);
}
}Where direct `@sentry/nestjs` import is still correct:
instrument.ts— always usesimport * as Sentry from "@sentry/nestjs"forSentry.init()- Standalone scripts and exception filters that run outside the DI container
Verification
Add a test endpoint to confirm events reach Sentry:
import { Controller, Get } from "@nestjs/common";
import * as Sentry from "@sentry/nestjs";
@Controller()
export class DebugController {
@Get("/debug-sentry")
triggerError() {
throw new Error("My first Sentry error from NestJS!");
}
@Get("/debug-sentry-span")
triggerSpan() {
return Sentry.startSpan({ op: "test", name: "NestJS Test Span" }, () => {
return { status: "span created" };
});
}
}Hit GET /debug-sentry and check the Sentry Issues dashboard within seconds.
For Each Agreed Feature
Walk through features one at a time. Load the reference, follow its steps, verify before moving on:
| Feature | Reference file | Load when... |
|---|---|---|
| Error Monitoring | ${SKILL_ROOT}/references/error-monitoring.md | Always (baseline) |
| Tracing | ${SKILL_ROOT}/references/tracing.md | Always (NestJS routes are auto-traced) |
| Profiling | ${SKILL_ROOT}/references/profiling.md | CPU-intensive production apps |
| Logging | ${SKILL_ROOT}/references/logging.md | Structured log aggregation needed |
| Metrics | ${SKILL_ROOT}/references/metrics.md | Custom KPIs / SLO tracking |
| Crons | ${SKILL_ROOT}/references/crons.md | Scheduled jobs or task queues |
| AI Monitoring | ${SKILL_ROOT}/references/ai-monitoring.md | OpenAI/Anthropic/LangChain detected |
For each feature: Read ${SKILL_ROOT}/references/<feature>.md, follow steps exactly, verify it works.
---
Configuration Reference
Key Sentry.init() Options
| Option | Type | Default | Purpose |
|---|---|---|---|
dsn | string | — | SDK disabled if empty; env: SENTRY_DSN |
environment | string | "production" | e.g., "staging"; env: SENTRY_ENVIRONMENT |
release | string | — | e.g., "myapp@1.0.0"; env: SENTRY_RELEASE |
dataCollection | object | See below | Controls what data the SDK collects (SDK ≥ 10.57.0) |
dataCollection.userInfo | boolean | true | Include IP addresses and user context |
dataCollection.httpHeaders | object | See below | Capture HTTP headers for requests/responses |
dataCollection.cookies | `boolean\ | object` | true |
dataCollection.queryParams | `boolean\ | object` | true |
dataCollection.genAI | object | See below | Control AI input/output recording |
sendDefaultPii | boolean | false | Deprecated — use dataCollection.userInfo instead |
tracesSampleRate | number | — | Transaction sample rate; undefined disables tracing |
tracesSampler | function | — | Custom per-transaction sampling (overrides rate) |
tracePropagationTargets | `Array<string\ | RegExp>` | — |
profileSessionSampleRate | number | — | Continuous profiling session rate (SDK ≥ 10.27.0) |
profileLifecycle | `"trace"\ | "manual"` | "trace" |
enableLogs | boolean | false | Send structured logs to Sentry (SDK ≥ 9.41.0) |
ignoreErrors | `Array<string\ | RegExp>` | [] |
ignoreTransactions | `Array<string\ | RegExp>` | [] |
beforeSend | function | — | Hook to mutate or drop error events |
beforeSendTransaction | function | — | Hook to mutate or drop transaction events |
beforeSendLog | function | — | Hook to mutate or drop log events |
debug | boolean | false | Verbose SDK debug output |
maxBreadcrumbs | number | 100 | Max breadcrumbs per event |
`dataCollection` defaults:
httpHeaders: { request: true, response: true }httpBodies: ["incomingRequest", "outgoingRequest", "incomingResponse", "outgoingResponse"]userInfo: truegenAI: { inputs: true, outputs: true }
Environment Variables
| Variable | Maps to | Notes |
|---|---|---|
SENTRY_DSN | dsn | Used if dsn not passed to init() |
SENTRY_RELEASE | release | Also auto-detected from git SHA, Heroku, CircleCI |
SENTRY_ENVIRONMENT | environment | Falls back to "production" |
SENTRY_AUTH_TOKEN | CLI/source maps | For npx @sentry/wizard@latest -i sourcemaps |
SENTRY_ORG | CLI/source maps | Organization slug |
SENTRY_PROJECT | CLI/source maps | Project slug |
Auto-Enabled Integrations
These integrations activate automatically when their packages are detected — no integrations: [...] needed:
| Auto-enabled | Notes |
|---|---|
httpIntegration | Outgoing HTTP calls via http/https/fetch |
expressIntegration | Express adapter (default NestJS) |
nestIntegration | NestJS lifecycle (middleware, guards, pipes, interceptors, handlers) |
onUncaughtExceptionIntegration | Uncaught exceptions |
onUnhandledRejectionIntegration | Unhandled promise rejections |
openAIIntegration | OpenAI SDK (when installed) |
anthropicAIIntegration | Anthropic SDK (when installed) |
langchainIntegration | LangChain (when installed) |
graphqlIntegration | GraphQL (when graphql package present) |
postgresIntegration | pg driver |
mysqlIntegration | mysql / mysql2 |
mongoIntegration | MongoDB / Mongoose |
redisIntegration | ioredis / redis |
Integrations Requiring Manual Setup
| Integration | When to add | Code |
|---|---|---|
nodeProfilingIntegration | Profiling desired | import { nodeProfilingIntegration } from "@sentry/profiling-node" |
prismaIntegration | Prisma ORM used | integrations: [Sentry.prismaIntegration()] |
consoleLoggingIntegration | Capture console output | integrations: [Sentry.consoleLoggingIntegration()] |
localVariablesIntegration | Capture local var values in errors | integrations: [Sentry.localVariablesIntegration()] |
---
Verification
Test that Sentry is receiving events:
// Add a test endpoint (remove before production)
@Get("/debug-sentry")
getError() {
throw new Error("My first Sentry error!");
}Or send a test message without crashing:
import * as Sentry from "@sentry/nestjs";
Sentry.captureMessage("NestJS Sentry SDK test");If nothing appears:
1. Set debug: true in Sentry.init() — prints SDK internals to stdout 2. Verify SENTRY_DSN env var is set in the running process 3. Check that import "./instrument" is the first line in main.ts 4. Confirm SentryModule.forRoot() is imported in AppModule 5. Check DSN format: https://<key>@o<org>.ingest.sentry.io/<project>
---
Phase 4: Cross-Link
After completing NestJS setup, check for a companion frontend missing Sentry:
ls -d ../frontend ../web ../client ../ui 2>/dev/null
cat ../frontend/package.json ../web/package.json 2>/dev/null \
| grep -E '"react"|"svelte"|"vue"|"next"|"nuxt"'If a frontend exists without Sentry, suggest the matching skill:
| Frontend detected | Suggest skill |
|---|---|
| Next.js | sentry-nextjs-sdk |
| React | sentry-react-sdk |
| Svelte / SvelteKit | sentry-svelte-sdk |
| Vue / Nuxt | Use @sentry/vue — see docs.sentry.io/platforms/javascript/guides/vue/ |
| React Native / Expo | sentry-react-native-sdk |
---
Troubleshooting
| Issue | Solution |
|---|---|
| Events not appearing | Set debug: true, verify SENTRY_DSN, check instrument.ts is imported first |
| Malformed DSN error | Format: https://<key>@o<org>.ingest.sentry.io/<project> |
| Exceptions not captured | Ensure SentryGlobalFilter is registered via APP_FILTER in AppModule |
| Auto-instrumentation not working | instrument.ts must be the first import in main.ts — before all NestJS imports |
| Profiling not starting | Requires tracesSampleRate > 0 + profileSessionSampleRate > 0 + @sentry/profiling-node installed |
enableLogs not working | Requires SDK ≥ 9.41.0 |
| No traces appearing | Verify tracesSampleRate is set (not undefined) |
| Too many transactions | Lower tracesSampleRate or use tracesSampler to drop health checks |
| Fastify + GraphQL issues | Known edge cases — see GitHub #13388; prefer Express for GraphQL |
| Background job events mixed | Wrap job body in Sentry.withIsolationScope(() => { ... }) |
| Prisma spans missing | Add integrations: [Sentry.prismaIntegration()] to Sentry.init() |
| ESM syntax errors | Set registerEsmLoaderHooks: false (disables ESM hooks; also disables auto-instrumentation for ESM modules) |
SentryModule breaks instrumentation | Must import from @sentry/nestjs/setup, never from @sentry/nestjs |
| RPC exceptions not captured | Add dedicated SentryRpcExceptionFilter (see Option D in exception filter section) |
| WebSocket exceptions not captured | Use @SentryExceptionCaptured() on gateway handleConnection/handleDisconnect |
@SentryCron not triggering | Decorator order matters — @SentryCron MUST come after @Cron |
| TypeScript path alias issues | Ensure tsconfig.json paths are configured so instrument resolves from main.ts location |
import * as Sentry ESLint error | Many projects ban namespace imports. Use named imports (import { startSpan, captureException } from "@sentry/nestjs") or use the project's DI proxy instead |
profilesSampleRate vs profileSessionSampleRate | profilesSampleRate is deprecated in SDK 10.x. Use profileSessionSampleRate + profileLifecycle: "trace" instead |
| Duplicate spans on every request | SentryModule.forRoot() registered in multiple modules. Ensure it's only called once — check shared/library modules |
Config property not recognized in instrument.ts | When using a typed config class, new SDK options must be added to the config type definition and the project rebuilt before TypeScript recognizes them |
Version Requirements
| Feature | Minimum SDK Version |
|---|---|
@sentry/nestjs package | 8.0.0 |
@SentryTraced decorator | 8.15.0 |
@SentryCron decorator | 8.16.0 |
| Event Emitter auto-instrumentation | 8.39.0 |
SentryGlobalFilter (unified) | 8.40.0 |
Sentry.logger API (enableLogs) | 9.41.0 |
profileSessionSampleRate | 10.27.0 |
| Node.js requirement | ≥ 18 |
Node.js for ESM --import | ≥ 18.19.0 |
| NestJS compatibility | 8.x – 11.x |
AI Monitoring — Sentry NestJS SDK
OpenAI integration: @sentry/nestjs ≥10.53.0+Vercel AI SDK integration: ≥10.53.0+
Anthropic integration: ≥10.53.0+
Google GenAI integration: ≥10.53.0+
⚠️ Tracing must be enabled. AI monitoring piggybacks on tracing infrastructure. tracesSampleRate must be > 0.---
Overview
Sentry AI Agents Monitoring automatically tracks:
- Agent runs and error rates
- LLM calls (model, token counts, estimated cost)
- Tool calls and outputs
- Agent handoffs
- Full prompt/completion data (opt-in)
- Performance bottlenecks across the AI pipeline
All integrations listed below are auto-enabled when the corresponding AI library is detected at startup. Explicit configuration is only needed to customize recordInputs/recordOutputs.
---
Supported AI Libraries
| Library | Integration API | Auto-enabled? | Min SDK Version |
|---|---|---|---|
OpenAI (openai) | openAIIntegration / instrumentOpenAiClient | ✅ Yes | 10.53.0 |
Vercel AI SDK (ai) | vercelAIIntegration | ✅ Yes | 10.53.0 |
Anthropic (@anthropic-ai/sdk) | anthropicAIIntegration / instrumentAnthropicAiClient | ✅ Yes | 10.53.0 |
Google GenAI (@google/generative-ai) | — | ✅ Yes | 10.53.0 |
LangChain (langchain, @langchain/core) | langchainIntegration | ✅ Yes | 10.53.0 |
---
OpenAI Integration
Auto-Enabled Setup
OpenAI is auto-instrumented — no changes to instrument.ts needed:
// instrument.ts
import * as Sentry from "@sentry/nestjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1.0,
streamGenAiSpans: true,
sendDefaultPii: true, // enables recordInputs/recordOutputs by default
integrations: [
Sentry.openAIIntegration(),
],
});Manual Wrapping (Alternative)
If auto-instrumentation doesn't capture your client (e.g., custom transport), wrap it manually:
import OpenAI from "openai";
import * as Sentry from "@sentry/nestjs";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// Wrap once at module level — reuse this client everywhere.
// Input/output recording follows sendDefaultPii unless explicitly overridden.
const client = Sentry.instrumentOpenAiClient(openai);Streaming — Important
For streamed responses, you must pass stream_options: { include_usage: true }. Without this, OpenAI does not include token counts in streamed responses, so Sentry cannot capture usage metrics:
@Injectable()
export class ChatService {
constructor(private readonly openai: OpenAI) {}
async streamChat(messages: Array<{ role: string; content: string }>) {
const stream = await this.openai.chat.completions.create({
model: "gpt-4o",
messages,
stream: true,
stream_options: { include_usage: true }, // ← REQUIRED for token tracking
});
return stream;
}
}OpenAI Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
recordInputs | boolean | true if sendDefaultPii: true | Capture prompts/messages sent to OpenAI |
recordOutputs | boolean | true if sendDefaultPii: true | Capture generated text/responses |
Supported versions: openai ≥4.0.0
---
Vercel AI SDK Integration
Setup
The integration is auto-enabled when the ai package is detected:
// instrument.ts — customize if needed
import * as Sentry from "@sentry/nestjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1.0,
streamGenAiSpans: true,
sendDefaultPii: true,
integrations: [
Sentry.vercelAIIntegration(),
],
});Per-Call Telemetry (Required)
You must pass experimental_telemetry: { isEnabled: true } to every AI SDK function call you want traced:
import { Injectable } from "@nestjs/common";
import { generateText, streamText } from "ai";
import { openai } from "@ai-sdk/openai";
@Injectable()
export class AiService {
async generate(prompt: string) {
const result = await generateText({
model: openai("gpt-4o"),
prompt,
experimental_telemetry: {
isEnabled: true,
functionId: "my-text-generation",
recordInputs: true,
recordOutputs: true,
},
});
return result.text;
}
async *stream(prompt: string) {
const { textStream } = await streamText({
model: openai("gpt-4o"),
prompt,
experimental_telemetry: {
isEnabled: true,
functionId: "my-stream",
},
});
yield* textStream;
}
}Vercel AI SDK Configuration Options
| Option | Type | Default | Min SDK | Description |
|---|---|---|---|---|
recordInputs | boolean | true* | 9.27.0 | Capture inputs. *Defaults to true when sendDefaultPii: true. |
recordOutputs | boolean | true* | 9.27.0 | Capture outputs. *Defaults to true when sendDefaultPii: true. |
Supported versions: ai ≥3.0.0
---
Anthropic Integration
Setup
// instrument.ts — customize if needed
import * as Sentry from "@sentry/nestjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1.0,
streamGenAiSpans: true,
sendDefaultPii: true,
integrations: [
Sentry.anthropicAIIntegration(),
],
});Manual Wrapping
import Anthropic from "@anthropic-ai/sdk";
import * as Sentry from "@sentry/nestjs";
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
// Input/output recording follows sendDefaultPii unless explicitly overridden.
const client = Sentry.instrumentAnthropicAiClient(anthropic);
const response = await client.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello, Claude!" }],
});Supported Anthropic Operations
| Operation | Method |
|---|---|
| Create messages | client.messages.create() |
| Stream messages | client.messages.stream() |
| Count tokens | client.messages.countTokens() |
| Beta messages | client.beta.messages.create() |
Supported versions: @anthropic-ai/sdk ≥0.19.2
---
Token Usage Tracking
Sentry automatically captures token usage following OpenTelemetry GenAI semantic conventions:
| Span Attribute | Description |
|---|---|
gen_ai.request.model | Model name |
gen_ai.usage.input_tokens | Prompt/input token count |
gen_ai.usage.output_tokens | Completion/output token count |
gen_ai.usage.input_tokens.cached | Cached input tokens |
gen_ai.usage.output_tokens.reasoning | Reasoning tokens (e.g., o1 models) |
Cost estimates are sourced from models.dev and OpenRouter. Unrecognized models show no estimate.
---
Prompt/Completion Capture & PII
recordInputs captures prompts sent to the AI API. recordOutputs captures the generated text/completions returned.
Both default to true only when sendDefaultPii: true is set:
Sentry.init({
dsn: process.env.SENTRY_DSN,
sendDefaultPii: true, // ← enables input/output recording by default
tracesSampleRate: 1.0,
streamGenAiSpans: true,
});⚠️ PII warning: Prompts often contain user-supplied text. If users include personal data in prompts, enabling recordInputs will send that data to Sentry. Review your privacy policy before enabling.---
Complete NestJS Example
// instrument.ts
import * as Sentry from "@sentry/nestjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1.0,
streamGenAiSpans: true,
sendDefaultPii: true,
enableLogs: true,
integrations: [
Sentry.openAIIntegration(),
Sentry.vercelAIIntegration(),
Sentry.anthropicAIIntegration(),
],
});// ai.controller.ts
import { Controller, Post, Body } from "@nestjs/common";
import { AiService } from "./ai.service";
@Controller("ai")
export class AiController {
constructor(private readonly aiService: AiService) {}
@Post("chat")
async chat(@Body() body: { prompt: string }) {
return this.aiService.chat(body.prompt);
}
}// ai.service.ts
import { Injectable } from "@nestjs/common";
import OpenAI from "openai";
import * as Sentry from "@sentry/nestjs";
@Injectable()
export class AiService {
private readonly openai: OpenAI;
constructor() {
this.openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
}
async chat(prompt: string): Promise<string> {
const completion = await this.openai.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: prompt }],
});
return completion.choices[0].message.content ?? "";
}
}---
AI Agents Dashboard
Access at Sentry → AI → Agents (or Insights → AI).
| Tab | What you see |
|---|---|
| Overview | Agent runs, error rates, duration, LLM calls, tokens used, tool calls |
| Models | Per-model cost estimates, token breakdown (input/output/cached), duration |
| Tools | Per-tool call counts, error rates, input/output for each invocation |
| Traces | Full pipeline from user request to final response with all spans |
---
Sampling Strategy
If your tracesSampleRate is below 1.0, you may be losing entire agent runs. See the AI sampling guide for tracesSampler patterns that keep 100% of gen_ai-related transactions while sampling other traffic at a lower rate.
---
Conversation Tracking
Link AI spans across turns into a chat-style timeline at Explore > Conversations.
Prerequisites: streamGenAiSpans: true (SDK >=10.53.0) and sendDefaultPii: true must be set — Conversations reconstructs the chat from input/output attributes, so without PII capture the view will be empty.
import * as Sentry from "@sentry/nestjs";
// Set at the start of a conversation
Sentry.setConversationId("conv_abc123");
// All subsequent AI calls carry gen_ai.conversation.id: "conv_abc123"A single conversation can span multiple traces, and a single trace can contain multiple conversations.
Troubleshooting
| Issue | Solution |
|---|---|
| No AI spans appearing | Verify tracesSampleRate > 0; AI monitoring requires tracing |
| Token counts missing in streams | Add stream_options: { include_usage: true } to all OpenAI streaming calls |
recordInputs/recordOutputs not capturing | Set sendDefaultPii: true, or explicitly pass recordInputs: true / recordOutputs: true to the integration |
| Anthropic spans missing | Check SDK version; add anthropicAIIntegration() explicitly |
| Cost estimates not showing | Model name must match models.dev/OpenRouter pricing data; custom models may show no estimate |
| Vercel AI spans not tracked | Pass experimental_telemetry: { isEnabled: true } to every AI SDK call |
| No data in AI Agents dashboard | Ensure traces are being sent; check DSN and tracesSampleRate |
Crons — Sentry NestJS SDK
Minimum SDK:@sentry/nestjs8.16.0+ for@SentryCrondecorator;@sentry/node7.76.0+ forwithMonitor()
Overview
Sentry Crons monitors scheduled jobs by receiving check-ins at job start, success, and failure. Three approaches:
| Approach | Use when |
|---|---|
@SentryCron decorator | NestJS @Cron scheduled tasks — zero boilerplate |
Sentry.withMonitor() | Manual wrapping — Bull/BullMQ processors, arbitrary functions |
Sentry.captureCheckIn() | Full control — heartbeats, conditional status, or two-step patterns |
Prerequisites
Install the NestJS scheduler package:
npm install --save @nestjs/scheduleRegister the module in your app:
// app.module.ts
import { ScheduleModule } from "@nestjs/schedule";
@Module({
imports: [
ScheduleModule.forRoot(),
// ...
],
})
export class AppModule {}Code Examples
@SentryCron decorator with @Cron
@SentryCron must be placed after @Cron in the decorator stack (closer to the method).
import { Injectable, Logger } from "@nestjs/common";
import { Cron } from "@nestjs/schedule";
import { SentryCron } from "@sentry/nestjs";
import type { MonitorConfig } from "@sentry/core";
@Injectable()
export class TasksService {
private readonly logger = new Logger(TasksService.name);
@Cron("0 2 * * *")
@SentryCron("nightly-report") // slug only — monitor must exist in Sentry UI
async handleNightlyReport() {
this.logger.log("Running nightly report...");
await generateReport();
}
}@SentryCron with MonitorConfig — upsert monitor definition (SDK 8.16.0+)
Supply monitorConfig to create or update the monitor automatically on first execution — no Sentry UI setup needed.
import { Injectable } from "@nestjs/common";
import { Cron } from "@nestjs/schedule";
import { SentryCron } from "@sentry/nestjs";
import type { MonitorConfig } from "@sentry/core";
const monitorConfig: MonitorConfig = {
schedule: {
type: "crontab",
value: "0 2 * * *",
},
timezone: "Europe/Vienna",
checkinMargin: 10, // minutes late before MISSED alert
maxRuntime: 30, // minutes after IN_PROGRESS before TIMEOUT
failureIssueThreshold: 3,
recoveryThreshold: 3,
};
@Injectable()
export class TasksService {
@Cron("0 2 * * *")
@SentryCron("nightly-report", monitorConfig)
async handleNightlyReport() {
await generateReport();
}
}Interval schedule
import { MonitorConfig } from "@sentry/core";
const syncConfig: MonitorConfig = {
schedule: {
type: "interval",
value: 2,
unit: "hour", // minute | hour | day | week | month | year
},
checkinMargin: 5,
maxRuntime: 20,
};
@Injectable()
export class SyncService {
@Cron("0 */2 * * *")
@SentryCron("data-sync", syncConfig)
async handleSync() {
await syncData();
}
}Manual wrapping with Sentry.withMonitor()
Use for Bull/BullMQ processors or any function outside of @nestjs/schedule.
import * as Sentry from "@sentry/nestjs";
import { Processor, WorkerHost } from "@nestjs/bullmq";
import { Job } from "bullmq";
@Processor("reports")
export class ReportProcessor extends WorkerHost {
async process(job: Job) {
return Sentry.withMonitor(
"report-queue-processor",
async () => {
await generateReport(job.data);
},
{
schedule: { type: "crontab", value: "0 3 * * *" },
timezone: "UTC",
checkinMargin: 5,
maxRuntime: 60,
},
);
}
}Manual check-ins with captureCheckIn()
For full control over timing, status, or heartbeat patterns.
import * as Sentry from "@sentry/nestjs";
async function runLongJob() {
// 1. Signal job started
const checkInId = Sentry.captureCheckIn(
{
monitorSlug: "data-pipeline",
status: "in_progress",
},
{
schedule: { type: "crontab", value: "0 4 * * *" },
maxRuntime: 120,
},
);
try {
await processData();
// 2a. Signal success
Sentry.captureCheckIn({
checkInId,
monitorSlug: "data-pipeline",
status: "ok",
});
} catch (err) {
// 2b. Signal failure
Sentry.captureCheckIn({
checkInId,
monitorSlug: "data-pipeline",
status: "error",
});
throw err;
}
}Heartbeat pattern for long-running jobs
Send periodic in_progress check-ins to prevent premature TIMEOUT alerts.
import * as Sentry from "@sentry/nestjs";
async function runBatchJob(batches: Batch[]) {
const checkInId = Sentry.captureCheckIn(
{ monitorSlug: "batch-processor", status: "in_progress" },
{ schedule: { type: "crontab", value: "0 1 * * *" }, maxRuntime: 240 },
);
try {
for (const batch of batches) {
await processBatch(batch);
// Send heartbeat to reset TIMEOUT timer
Sentry.captureCheckIn({
checkInId,
monitorSlug: "batch-processor",
status: "in_progress",
});
}
Sentry.captureCheckIn({
checkInId,
monitorSlug: "batch-processor",
status: "ok",
});
} catch (err) {
Sentry.captureCheckIn({
checkInId,
monitorSlug: "batch-processor",
status: "error",
});
throw err;
}
}MonitorConfig Fields
| Field | Type | Required | Description |
|---|---|---|---|
schedule | object | ✅ | { type: "crontab", value: "* * * * *" } or { type: "interval", value: N, unit: "..." } |
timezone | string | No | IANA timezone name, default "UTC" |
checkinMargin | number | No | Minutes late before MISSED alert |
maxRuntime | number | No | Minutes after in_progress before TIMEOUT |
failureIssueThreshold | number | No | Consecutive failures before opening an issue |
recoveryThreshold | number | No | Consecutive successes to resolve an issue |
Best Practices
- Supply
monitorConfigin@SentryCronorwithMonitor()so monitors are created automatically — no Sentry UI setup needed - Decorator order matters:
@Cronmust come before@SentryCron(farther from the method) - For Bull/BullMQ processors, auto-instrumentation is not supported — use
withMonitor()instead - Send
in_progressbefore starting work so TIMEOUT detection begins immediately - For jobs longer than
maxRuntime, send periodicin_progressheartbeats to reset the timer - Sentry enforces a rate limit of 6 check-ins/minute per monitor-environment — excess are dropped silently
Troubleshooting
| Issue | Solution |
|---|---|
| Monitor not created in Sentry | Provide monitorConfig — monitors are not auto-created without it |
| Decorator has no effect | Ensure @SentryCron is below @Cron in the decorator stack |
| MISSED alerts firing too early | Increase checkinMargin to allow for startup latency |
| TIMEOUT alerts on slow jobs | Increase maxRuntime or send periodic in_progress heartbeats |
| Bull/BullMQ check-ins not working | Auto-instrumentation not supported — wrap with Sentry.withMonitor() |
@SentryCron import missing | Import from @sentry/nestjs; MonitorConfig type from @sentry/core |
Error Monitoring — Sentry NestJS SDK
Minimum SDK: @sentry/nestjs ≥8.0.0@SentryTraced()requires ≥8.15.0 ·@SentryCron()requires ≥8.16.0 · Event Emitter auto-instrumentation requires ≥8.39.0
---
How NestJS Error Capture Works
NestJS routes all unhandled exceptions through its exception filter pipeline before they reach the response. This means errors don't bubble up to Node's uncaught exception handler — Sentry only sees them if you hook into that pipeline.
The SDK provides two integration points:
| Mechanism | Use When |
|---|---|
SentryGlobalFilter (via APP_FILTER) | You don't have a custom catch-all filter |
@SentryExceptionCaptured() decorator | You have an existing @Catch() filter you want to keep |
Both internally use isExpectedError() — a duck-typing check that skips HttpException (4xx) and RpcException so only unexpected errors are reported.
---
Exception Filter Setup
Pattern A: SentryGlobalFilter (recommended for most apps)
Register the filter globally in AppModule. It automatically handles HTTP, GraphQL, and RPC contexts.
// app.module.ts
import { Module } from "@nestjs/common";
import { APP_FILTER } from "@nestjs/core";
import { SentryModule } from "@sentry/nestjs/setup";
import { SentryGlobalFilter } from "@sentry/nestjs/setup";
@Module({
imports: [SentryModule.forRoot()],
providers: [
{
provide: APP_FILTER,
useClass: SentryGlobalFilter,
},
],
})
export class AppModule {}Import path matters:SentryGlobalFilterandSentryModulecome from@sentry/nestjs/setup, not@sentry/nestjs. This separation ensures they're loaded afterSentry.init()runs (ininstrument.ts), so OpenTelemetry instrumentation can patch NestJS before it's imported.
Pattern B: Decorate an existing catch-all filter
If you already have a @Catch() filter, add @SentryExceptionCaptured() to its catch method instead of registering SentryGlobalFilter:
import { Catch, ExceptionFilter, ArgumentsHost } from "@nestjs/common";
import { SentryExceptionCaptured } from "@sentry/nestjs";
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
@SentryExceptionCaptured() // ← captures before your handler runs
catch(exception: unknown, host: ArgumentsHost): void {
// your existing error handling logic
// Sentry capture already happened via the decorator
}
}Pattern C: Per-exception-type filter with manual capture
For filters scoped to a specific exception type, call Sentry.captureException() explicitly:
import { Catch, ArgumentsHost, BadRequestException } from "@nestjs/common";
import { BaseExceptionFilter } from "@nestjs/core";
import * as Sentry from "@sentry/nestjs";
@Catch(DatabaseException)
export class DatabaseExceptionFilter extends BaseExceptionFilter {
catch(exception: DatabaseException, host: ArgumentsHost) {
Sentry.captureException(exception, {
tags: { component: "database", query: exception.query },
});
return super.catch(new BadRequestException(exception.message), host);
}
}---
What Is (and Isn't) Captured Automatically
HTTP context
| Error Type | Captured? | Reason |
|---|---|---|
| Unhandled exceptions from controllers | ✅ Yes | SentryGlobalFilter intercepts |
HttpException (4xx errors) | ❌ No | isExpectedError() skips them |
HttpException subclasses (BadRequestException, etc.) | ❌ No | Duck-typed as expected |
Caught + swallowed in service try/catch | ❌ No | Never reaches the filter |
Re-thrown from try/catch | ✅ Yes | Reaches filter as unhandled |
GraphQL context
SentryGlobalFilter detects host.getType<string>() === 'graphql' and adjusts behavior:
HttpException→ re-thrown without capture (expected, NestJS handles formatting)- Any other
Error→ captured and re-thrown (so GraphQL can format the error response) - Non-
Errorobjects → captured and re-thrown
GraphQL errors are always re-thrown so the Apollo/Mercurius error formatter can run. This means they appear in Sentry and in the GraphQL error response.
RPC / Microservices context
SentryGlobalFilter handles RPC but logs a warning recommending a dedicated filter:
IMPORTANT: RpcException should be handled with a dedicated Rpc exception filter, not the generic SentryGlobalFilterFor production microservices, use a dedicated RPC filter:
import { Catch, RpcExceptionFilter, ArgumentsHost } from "@nestjs/common";
import { Observable, throwError } from "rxjs";
import { RpcException } from "@nestjs/microservices";
import * as Sentry from "@sentry/nestjs";
@Catch(RpcException)
export class SentryRpcExceptionFilter implements RpcExceptionFilter<RpcException> {
catch(exception: RpcException, host: ArgumentsHost): Observable<any> {
Sentry.captureException(exception);
return throwError(() => exception.getError());
}
}The Core Rule
"Caught exceptions never reach the filter. If you catch and swallow an error, Sentry never sees it."
// ✅ Automatically captured — reaches SentryGlobalFilter
throw new Error("Unhandled database error");
// ✅ Automatically captured — re-thrown reaches filter
try {
await db.query(sql);
} catch (err) {
throw err; // or: throw new InternalServerErrorException(err.message)
}
// ❌ NOT captured — swallowed before reaching filter
try {
await db.query(sql);
} catch (err) {
return { error: "Query failed" }; // ← must add captureException here
}
// ✅ Manually captured before graceful return
try {
await db.query(sql);
} catch (err) {
Sentry.captureException(err);
return { error: "Query failed" };
}---
Manual Error Capture
Sentry.captureException(error, context?)
Captures an exception immediately, regardless of the filter pipeline.
import * as Sentry from "@sentry/nestjs";
// Basic
Sentry.captureException(new Error("Payment processing failed"));
// With inline context (one-off enrichment — doesn't affect other events)
Sentry.captureException(error, {
level: "fatal",
tags: { component: "payments", provider: "stripe" },
extra: { orderId, customerId },
user: { id: req.user.id, email: req.user.email },
fingerprint: ["payment-failure", String(error.code)],
contexts: {
order: { id: orderId, total: 9900, currency: "USD" },
},
});Sentry.captureMessage(message, levelOrContext?)
Captures a plain message — useful for notable conditions that aren't exceptions.
// With severity level
Sentry.captureMessage("Deprecated API version used", "warning");
// Levels: "fatal" | "error" | "warning" | "log" | "info" | "debug"
// With full context
Sentry.captureMessage("Cache miss rate above threshold", {
level: "warning",
tags: { cache: "redis", key_pattern: "user:*" },
extra: { missRate: 0.42, threshold: 0.20 },
});---
How isExpectedError() Works
The SDK uses duck-typing — not instanceof — to determine if an error is "expected" (should not be reported). This is intentional: importing @nestjs/common in the main entry point would load it before OpenTelemetry can patch it, breaking automatic instrumentation.
// Internal SDK logic (simplified)
function isExpectedError(exception: unknown): boolean {
if (typeof exception !== 'object' || exception === null) return false;
const ex = exception as Record<string, unknown>;
// HttpException: has getStatus(), getResponse(), initMessage()
if (
typeof ex.getStatus === 'function' &&
typeof ex.getResponse === 'function' &&
typeof ex.initMessage === 'function'
) {
return true; // ← skipped, not reported
}
// RpcException: has getError(), initMessage()
if (typeof ex.getError === 'function' && typeof ex.initMessage === 'function') {
return true; // ← skipped, not reported
}
return false; // ← reported to Sentry
}Implication: If you create custom exception classes that mimic these method signatures, they will be treated as expected errors and skipped. Design your exception hierarchy accordingly.
---
Scope Management
The SDK uses Node's AsyncLocalStorage for automatic request isolation — each HTTP request gets its own scope so breadcrumbs and tags from one request don't contaminate another.
Three Scope Levels
| Scope | Lifetime | Use for |
|---|---|---|
| Global | Process lifetime | App-wide metadata (version, build SHA) |
| Isolation | One HTTP request | Per-request user, tags |
| Current | One span | Per-span metadata |
Precedence when merging: Current > Isolation > Global.
Top-Level Setters Write to Isolation Scope
All Sentry.setXxx() shorthand methods write to the isolation scope — safe for per-request data:
// These are equivalent:
Sentry.setTag("request_id", req.id);
Sentry.getIsolationScope().setTag("request_id", req.id);
// Set user (persists for the current request):
Sentry.setUser({ id: req.user.id, email: req.user.email });
// Clear user:
Sentry.setUser(null);Per-Request Enrichment Middleware
The recommended pattern for attaching user context to every request:
// auth.middleware.ts
import { Injectable, NestMiddleware } from "@nestjs/common";
import { Request, Response, NextFunction } from "express";
import * as Sentry from "@sentry/nestjs";
@Injectable()
export class SentryContextMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const user = req.user; // populated by auth guard
if (user) {
Sentry.setUser({
id: String(user.id),
email: user.email,
username: user.username,
});
Sentry.setTag("user.role", user.role);
Sentry.setTag("tenant.id", String(user.tenantId));
}
next();
}
}Register in AppModule:
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(SentryContextMiddleware).forRoutes("*");
}
}withScope — Temporary Isolated Context
Use withScope when you need context on a single capture without affecting other events:
Sentry.withScope((scope) => {
scope.setTag("operation", "bulk-import");
scope.setLevel("warning");
scope.setContext("import", { rowCount: rows.length, filename });
scope.setFingerprint(["bulk-import-failure", filename]);
Sentry.captureException(importError);
});
// ← scope above does NOT appear on subsequent eventsBackground Job Scope Isolation
Background jobs (@Cron, @Interval, @OnEvent, @Processor) share the default isolation scope with HTTP requests. Without isolation, breadcrumbs from a cron job can leak into the next HTTP error event.
Wrap with withIsolationScope():
import * as Sentry from "@sentry/nestjs";
import { Injectable } from "@nestjs/common";
import { Cron, CronExpression } from "@nestjs/schedule";
@Injectable()
export class ReportGenerationService {
@Cron(CronExpression.EVERY_HOUR)
async generateReports() {
Sentry.withIsolationScope(async () => {
Sentry.setTag("job", "report-generation");
Sentry.addBreadcrumb({ message: "Starting report generation", level: "info" });
try {
await this.doGenerate();
} catch (err) {
Sentry.captureException(err);
}
});
}
}Also applies to @Interval(), @OnEvent(), @Processor(), and any other background task handler.
---
Context Enrichment
Tags (searchable, indexed)
Sentry.setTag("page_locale", "de-at");
Sentry.setTags({
"feature.flag": "new_checkout_v2",
"subscription.tier": "enterprise",
"region": "eu-west-1",
});Constraints: key max 32 chars, value max 200 chars, no newlines.
Context (structured, non-searchable)
Sentry.setContext("order", {
id: orderId,
items: cart.length,
total_usd: cart.total,
coupon: couponCode ?? null,
});
// Clear a context:
Sentry.setContext("order", null);Normalized to 3 levels deep by default. The type key is reserved — don't use it.User Identity
// On authenticated request
Sentry.setUser({
id: String(user.id),
email: user.email,
username: user.username,
subscription: user.plan, // arbitrary extra fields accepted
});
// On logout or unauthenticated context
Sentry.setUser(null);Tags vs Context — Decision Guide
| Feature | Searchable? | Best For |
|---|---|---|
| Tags | ✅ Yes | Filtering, grouping, alerting |
| Context | ❌ No | Structured debug info (nested objects) |
| User | ✅ Partially | User attribution and filtering |
---
Breadcrumbs
Breadcrumbs are automatically captured for HTTP requests, database queries, and console output. Add manual breadcrumbs for business-logic milestones:
Sentry.addBreadcrumb({
category: "auth",
message: "User authenticated via OAuth2",
level: "info",
data: { provider: "google", userId: user.id },
});
Sentry.addBreadcrumb({
type: "http",
category: "api.external",
message: "POST /payments/charge",
level: "info",
data: {
url: "https://api.stripe.com/v1/charges",
method: "POST",
status_code: 422,
},
});beforeBreadcrumb — Filter or Mutate
Sentry.init({
beforeBreadcrumb(breadcrumb, hint) {
// Drop verbose DB health-check queries
if (
breadcrumb.category === "db.query" &&
breadcrumb.message?.includes("SELECT 1")
) {
return null;
}
// Truncate large query strings
if (breadcrumb.category === "db.query" && breadcrumb.message) {
breadcrumb.message = breadcrumb.message.slice(0, 200);
}
return breadcrumb;
},
maxBreadcrumbs: 50, // default: 100
});---
beforeSend and Filtering Hooks
beforeSend — Modify or Drop Error Events
Last chance to modify or discard events. Return null to drop the event entirely.
Sentry.init({
dsn: "...",
beforeSend(event, hint) {
const error = hint.originalException;
// Drop known non-actionable errors
if (error instanceof Error && error.message.includes("ECONNRESET")) {
return null;
}
// Scrub PII from user context
if (event.user?.email) {
event.user = { ...event.user, email: "[filtered]" };
}
// Scrub Authorization headers
const headers = event.request?.headers as Record<string, string> | undefined;
if (headers?.["authorization"]) {
headers["authorization"] = "[filtered]";
}
return event;
},
});ignoreErrors — Pattern-Based Filtering
Sentry.init({
ignoreErrors: [
"ECONNRESET",
/^Connection refused$/i,
/^ETIMEDOUT/,
],
});beforeSendTransaction — Filter Performance Events
Sentry.init({
beforeSendTransaction(event) {
// Drop health check transactions
if (event.transaction === "GET /health") return null;
return event;
},
});---
Fingerprinting and Custom Grouping
All events have a fingerprint. Events with the same fingerprint group into the same Sentry issue.
Per-Capture Fingerprinting
// Via captureException context argument
Sentry.captureException(error, {
fingerprint: ["database-connection-error", error.code],
});
// Via withScope
Sentry.withScope((scope) => {
scope.setFingerprint(["payment-failure", "stripe", String(error.statusCode)]);
Sentry.captureException(error);
});beforeSend Fingerprinting
Sentry.init({
beforeSend(event, hint) {
const error = hint.originalException;
// All DB connection errors → one issue:
if (error instanceof DatabaseConnectionError) {
event.fingerprint = ["database-connection-error"];
}
// Extend default grouping (keep stack-trace hash + add dimension):
if (error instanceof ExternalApiError) {
event.fingerprint = [
"{{ default }}",
error.serviceName,
String(error.statusCode),
];
}
return event;
},
});Template Variables
| Variable | Description |
|---|---|
{{ default }} | Sentry's normally computed hash (extend rather than replace) |
{{ transaction }} | Current transaction/route name |
{{ type }} | Exception class name |
---
Event Processors
Unlike beforeSend (one allowed), multiple event processors can be registered:
// Global — runs for every event
Sentry.addEventProcessor((event, hint) => {
event.extra = {
...event.extra,
buildSha: process.env.GIT_COMMIT_SHA,
nodeVersion: process.version,
};
return event;
});
// Scoped — only for a specific capture
Sentry.withScope((scope) => {
scope.addEventProcessor((event) => {
event.tags = { ...event.tags, processed_by: "payment_service" };
return event;
});
Sentry.captureException(paymentError);
});Execution order: All addEventProcessor() callbacks run first, then beforeSend runs last.
---
Configuration Reference
Key Sentry.init() options for error monitoring (in instrument.ts):
| Option | Type | Default | Purpose |
|---|---|---|---|
dsn | string | env SENTRY_DSN | Project identifier; SDK disabled if empty |
environment | string | "production" | Deployment environment tag |
release | string | env SENTRY_RELEASE | App version string |
sampleRate | number | 1.0 | Fraction of error events to send (0.0–1.0) |
sendDefaultPii | boolean | false | Include IPs, cookies, sessions |
attachStacktrace | boolean | false | Add stack traces to captureMessage() |
maxBreadcrumbs | number | 100 | Max breadcrumbs per event |
ignoreErrors | `Array<string \ | RegExp>` | [] |
beforeSend | `(event, hint) => event \ | null` | — |
beforeBreadcrumb | `(breadcrumb, hint?) => breadcrumb \ | null` | — |
includeLocalVariables | boolean | false | Capture stack-frame local variable values |
debug | boolean | false | Enable SDK debug logging |
---
Error Capture Scenario Reference
| Scenario | Auto Captured? | Solution |
|---|---|---|
| Unhandled controller exception | ✅ Yes | SentryGlobalFilter intercepts |
HttpException (4xx, 5xx) | ❌ No | Expected by design; capture manually if needed |
try/catch with graceful return | ❌ No | Sentry.captureException() before return |
try/catch with re-throw | ✅ Yes | Reaches filter as unhandled |
| GraphQL resolver error | ✅ Yes | SentryGlobalFilter captures + re-throws |
| RPC microservice error | ⚠️ Partial | Use dedicated RpcExceptionFilter |
Background job (@Cron, @OnEvent) | ❌ No | Wrap with withIsolationScope() + manual capture |
| WebSocket gateway error | ❌ No | Catch manually in gateway methods |
| Caught + swallowed error | ❌ No | Always call captureException before swallowing |
---
API Quick Reference
// ── Exception Filter Setup ─────────────────────────────────────────────
import { SentryGlobalFilter } from "@sentry/nestjs/setup" // APP_FILTER token
import { SentryExceptionCaptured } from "@sentry/nestjs" // decorator for catch()
// ── Capture ───────────────────────────────────────────────────────────
Sentry.captureException(error)
Sentry.captureException(error, { level, tags, extra, contexts, fingerprint, user })
Sentry.captureMessage("text", "warning")
Sentry.captureMessage("text", { level, tags, extra })
// ── User ──────────────────────────────────────────────────────────────
Sentry.setUser({ id, email, username, ...custom })
Sentry.setUser(null) // clear on logout
// ── Tags (searchable, indexed) ────────────────────────────────────────
Sentry.setTag("key", "value")
Sentry.setTags({ key1: "v1", key2: "v2" })
// ── Context (structured, non-searchable) ─────────────────────────────
Sentry.setContext("name", { key: value })
Sentry.setContext("name", null) // clear
// ── Breadcrumbs ───────────────────────────────────────────────────────
Sentry.addBreadcrumb({ type, category, message, level, data })
// ── Scopes ────────────────────────────────────────────────────────────
Sentry.withScope((scope) => { scope.setTag(...); Sentry.captureException(...) })
Sentry.withIsolationScope((scope) => { ... }) // background jobs
Sentry.getGlobalScope().setTag(...)
Sentry.getIsolationScope().setTag(...) // same as Sentry.setTag()
// ── Fingerprinting ────────────────────────────────────────────────────
scope.setFingerprint(["group-key"])
event.fingerprint = ["{{ default }}", "extra-dimension"] // in beforeSend
// ── Hooks ─────────────────────────────────────────────────────────────
Sentry.init({ beforeSend(event, hint) { return event | null } })
Sentry.init({ beforeSendTransaction(event) { return event | null } })
Sentry.init({ beforeBreadcrumb(breadcrumb, hint) { return breadcrumb | null } })
Sentry.init({ ignoreErrors: ["string", /regex/] })---
Troubleshooting
| Issue | Solution |
|---|---|
HttpException errors not appearing | Expected — by design. Call Sentry.captureException() manually if you want 4xx/5xx reported |
| Unhandled controller errors not appearing | Ensure SentryGlobalFilter is registered via APP_FILTER in AppModule, and SentryModule.forRoot() is in imports |
| Breadcrumbs from cron jobs appearing in HTTP errors | Wrap cron/event handlers with Sentry.withIsolationScope() |
| GraphQL errors not appearing | SentryGlobalFilter handles this automatically — verify it's registered. Check if a custom exception filter intercepts before SentryGlobalFilter runs |
| RPC errors appear with a warning | Use a dedicated @Catch(RpcException) filter and call Sentry.captureException() explicitly |
| User context missing from events | Set Sentry.setUser() in middleware before the request reaches the controller; isolation scope is per-request |
instrument.ts import order error | import "./instrument" must be the very first line of main.ts — before any other imports |
| Events not appearing | Verify DSN, enable debug: true in Sentry.init() to see SDK logs, confirm SentryModule.forRoot() is imported |
| PII appearing in events | Set sendDefaultPii: false (default) and scrub in beforeSend |
Logging — Sentry NestJS SDK
Minimum SDK:@sentry/nestjs9.41.0+ for structured Sentry Logs (enableLogs: true)
Two Logging Systems
| System | Produces | Requires |
|---|---|---|
| Sentry Structured Logs | Searchable log records in Sentry Logs UI | enableLogs: true + Sentry.logger.* |
| Framework integrations | Bridge NestJS/Pino/Winston logs to Sentry Logs | Integration-specific setup |
Configuration
import * as Sentry from "@sentry/nestjs";
Sentry.init({
dsn: "https://<key>@<org>.ingest.sentry.io/<project>",
enableLogs: true, // required — without this, all Sentry.logger.* calls are no-ops
});Code Examples
Sentry Structured Logs — direct API
import * as Sentry from "@sentry/nestjs";
// All six log levels
Sentry.logger.trace("Starting database connection {database}", { database: "users" });
Sentry.logger.debug("Cache miss for user {userId}", { userId: 123 });
Sentry.logger.info("User signed in");
Sentry.logger.warn("Rate limit reached for endpoint {endpoint}", { endpoint: "/api/results" });
Sentry.logger.error("Failed to process payment for order {orderId}", { orderId: "or_2342" });
Sentry.logger.fatal("Database {database} connection pool exhausted", { database: "users" });Available levels: trace, debug, info, warn, error, fatal
Tagged template for parameterized messages
Use Sentry.logger.fmt to create structured, searchable messages where each placeholder becomes an individually queryable attribute in the Sentry Logs UI:
import * as Sentry from "@sentry/nestjs";
Sentry.logger.info(Sentry.logger.fmt`User ${"userId"} signed in from ${"region"}`, {
userId: 42,
region: "eu-west-1",
});NestJS ConsoleLogger integration
To route NestJS's built-in ConsoleLogger output to Sentry Logs, use consoleLoggingIntegration with forceConsole: true:
import * as Sentry from "@sentry/nestjs";
import { consoleLoggingIntegration } from "@sentry/nestjs";
Sentry.init({
dsn: "...",
enableLogs: true,
integrations: [
consoleLoggingIntegration({ forceConsole: true }),
],
});Then use NestJS's built-in logger as usual — all output is captured:
import { Injectable, Logger } from "@nestjs/common";
@Injectable()
export class AppService {
private readonly logger = new Logger(AppService.name);
doSomething() {
this.logger.log("Processing request"); // → Sentry Logs: info
this.logger.warn("Unusual payload size"); // → Sentry Logs: warn
this.logger.error("Payment failed"); // → Sentry Logs: error
}
}Pino integration (SDK 10.18.0+)
npm install pinoimport * as Sentry from "@sentry/nestjs";
import { pinoIntegration } from "@sentry/nestjs";
Sentry.init({
dsn: "...",
enableLogs: true,
integrations: [pinoIntegration()],
});Winston integration
npm install winstonimport * as Sentry from "@sentry/nestjs";
import { createSentryWinstonTransport } from "@sentry/nestjs";
import winston from "winston";
Sentry.init({ dsn: "...", enableLogs: true });
const logger = winston.createLogger({
transports: [
new winston.transports.Console(),
createSentryWinstonTransport({ minLevel: "info" }),
],
});Bunyan is not supported. Use Pino or Winston if you need a framework logger bridge.
beforeSendLog hook — filter and sanitize
import * as Sentry from "@sentry/nestjs";
Sentry.init({
dsn: "...",
enableLogs: true,
beforeSendLog(log) {
// Drop debug logs to reduce volume
if (log.level === "debug") return null;
// Redact sensitive fields
if (log.attributes?.["user.email"]) {
log.attributes["user.email"] = "[redacted]";
}
return log;
},
});Log-to-Trace Correlation
Log entries are automatically correlated to the active trace — no configuration required. When a log is emitted inside an instrumented request or span, Sentry links it to the corresponding transaction in the Traces UI.
Decision Table
| Goal | Tool |
|---|---|
| Searchable structured records in Sentry Logs UI | Sentry.logger.* + enableLogs: true |
Bridge NestJS ConsoleLogger to Sentry Logs | consoleLoggingIntegration({ forceConsole: true }) |
| Bridge Pino to Sentry Logs | pinoIntegration() (SDK 10.18.0+) |
| Bridge Winston to Sentry Logs | createSentryWinstonTransport() |
| Drop or modify a log before sending | beforeSendLog callback |
Troubleshooting
| Issue | Solution |
|---|---|
Sentry.logger.* calls have no effect | Ensure enableLogs: true is set in Sentry.init() |
NestJS ConsoleLogger output not appearing | Add consoleLoggingIntegration({ forceConsole: true }) |
| Pino logs not appearing | Requires @sentry/nestjs 10.18.0+; add pinoIntegration() |
| Too many log records hitting quota | Use beforeSendLog to filter by level or attribute |
Metrics — Sentry NestJS SDK
Minimum SDK: @sentry/nestjs 10.25.0+Overview
Sentry.metrics provides custom counters, gauges, and distributions. Metrics are enabled by default — no extra init() flag needed.
Metric Types
| Type | API | Use for |
|---|---|---|
| Counter | Sentry.metrics.count() | Event occurrences, request counts |
| Distribution | Sentry.metrics.distribution() | Latencies, sizes — supports p50/p90/p95/p99 |
| Gauge | Sentry.metrics.gauge() | Current values (min, max, avg, sum, count — no percentiles) |
Configuration
import './instrument'; // Sentry init must run before anything else
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();No extra flags required — metrics are on by default once Sentry.init() is called.
Optional beforeSendMetric hook:
import * as Sentry from '@sentry/nestjs';
Sentry.init({
dsn: 'https://<key>@<org>.ingest.sentry.io/<project>',
beforeSendMetric(metric) {
if (metric.name === 'noisy-metric') {
return null; // drop this metric
}
metric.attributes['env'] = 'prod'; // add attribute
return metric;
},
});Code Examples
Counter — event occurrences
import * as Sentry from '@sentry/nestjs';
// In a controller
@Controller('orders')
export class OrdersController {
@Post()
async createOrder(@Body() dto: CreateOrderDto) {
Sentry.metrics.count('orders.created', 1, {
attributes: {
type: dto.type,
region: dto.region,
},
});
return this.ordersService.create(dto);
}
}
// Count per route/method
@Get(':id')
async getOrder(@Param('id') id: string) {
Sentry.metrics.count('http.requests', 1, {
attributes: {
route: '/orders/:id',
method: 'GET',
},
});
return this.ordersService.findOne(id);
}Distribution — percentile analysis
Best for latencies, response sizes, durations where p50/p90/p99 matter:
import * as Sentry from '@sentry/nestjs';
@Injectable()
export class OrdersService {
async processOrder(order: Order): Promise<void> {
const start = Date.now();
await this.doProcessing(order);
Sentry.metrics.distribution('orders.processing_time', Date.now() - start, {
unit: 'millisecond',
attributes: {
'order.type': order.type,
region: order.region,
},
});
}
}Database query timing:
@Injectable()
export class UsersRepository {
async findByEmail(email: string) {
const start = Date.now();
const result = await this.db.users.findOne({ email });
Sentry.metrics.distribution('db.query_time', Date.now() - start, {
unit: 'millisecond',
attributes: { table: 'users', operation: 'findOne' },
});
return result;
}
}Gauge — current state
Use for values that fluctuate over time; no percentile support:
import * as Sentry from '@sentry/nestjs';
// Bull/BullMQ queue depth
@Injectable()
export class QueueMonitorService {
constructor(@InjectQueue('email') private emailQueue: Queue) {}
@Cron(CronExpression.EVERY_MINUTE)
async reportQueueDepth() {
const waiting = await this.emailQueue.getWaitingCount();
const active = await this.emailQueue.getActiveCount();
Sentry.metrics.gauge('queue.depth', waiting, {
attributes: { queue: 'email', state: 'waiting' },
});
Sentry.metrics.gauge('queue.depth', active, {
attributes: { queue: 'email', state: 'active' },
});
}
}Business event counting
@Injectable()
export class PaymentsService {
async chargeCard(dto: ChargeDto): Promise<Charge> {
try {
const charge = await this.stripe.charges.create(dto);
Sentry.metrics.count('payments.charged', 1, {
attributes: {
currency: dto.currency,
success: true,
},
});
return charge;
} catch (err) {
Sentry.metrics.count('payments.charged', 1, {
attributes: {
currency: dto.currency,
success: false,
'error.type': err.type ?? 'unknown',
},
});
throw err;
}
}
}Attribute value types
Sentry.metrics.count('api.request', 1, {
attributes: {
endpoint: '/v2/users', // string
method: 'POST',
success: true, // boolean
status_code: 201, // number
latency: 0.042, // number (float)
},
});Unit strings
| Category | Values |
|---|---|
| Time | "nanosecond", "microsecond", "millisecond", "second", "minute", "hour", "day", "week" |
| Data | "bit", "byte", "kilobyte", "megabyte", "gigabyte", "terabyte" |
| Fractions | "ratio", "percent" |
| Dimensionless | "none" (default when omitted) |
beforeSendMetric — metric object schema
| Key | Type | Description |
|---|---|---|
name | string | Metric identifier |
type | string | "counter" / "gauge" / "distribution" |
value | number | Numeric measurement |
unit | `string \ | undefined` |
attributes | `Record<string, string \ | number \ |
timestamp | number | Epoch seconds |
traceId | `string \ | undefined` |
spanId | `string \ | undefined` |
Best Practices
- Keep attribute cardinality low — avoid user IDs, UUIDs, or timestamps as attribute values
- Use
distributionovergaugewhen you need percentile analysis - Prefix metric names with your service name:
"payments.charge_time"not"charge_time" - Use standard unit strings — Sentry renders them in the UI with proper labels
- Each metric consumes up to 2 KB — avoid unbounded attribute value sets
- Metrics are buffered and flushed periodically — not suitable for sub-second alerting
Troubleshooting
| Issue | Solution |
|---|---|
| Metrics not appearing | Verify @sentry/nestjs ≥ 10.25.0; check debug: true output |
| Metric dropped silently | Check beforeSendMetric hook; verify metric name has no special characters |
| High cardinality warning | Reduce attribute values — avoid per-user or per-request identifiers |
| No percentiles in Sentry UI | Switch from gauge to distribution — gauges do not support percentiles |
Profiling — Sentry NestJS SDK
Requires@sentry/profiling-node(version must exactly match@sentry/nestjs)
Installation
npm install @sentry/profiling-node --saveConfiguration
| Option | Purpose |
|---|---|
integrations: [nodeProfilingIntegration()] | Enable the V8 CPU profiler |
profileSessionSampleRate | Fraction of processes/pods to profile (evaluated once at init) |
profileLifecycle | 'trace' = auto-managed; 'manual' = explicit start/stop |
tracesSampleRate | Must be > 0 — profiling requires tracing to be active |
Mode Comparison
Trace lifecycle ('trace') | Manual ('manual') | |
|---|---|---|
| Start trigger | First active span | Sentry.profiler.startProfiler() |
| Stop trigger | Last span ends | Sentry.profiler.stopProfiler() |
| Coverage | All code during active spans | Only between explicit start/stop |
| Use case | General profiling (recommended) | Targeted hot paths |
| Setup | Zero — fully automatic | Manual call sites required |
Code Examples
Trace lifecycle — recommended
Add nodeProfilingIntegration() to the integrations array in instrument.ts:
// instrument.ts (must be the first file loaded — see main skill)
import * as Sentry from "@sentry/nestjs";
import { nodeProfilingIntegration } from "@sentry/profiling-node";
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [nodeProfilingIntegration()],
tracesSampleRate: 1.0,
profileSessionSampleRate: 1.0, // profile 100% of process sessions
profileLifecycle: "trace", // SDK auto-manages profiler lifetime
});All HTTP requests, lifecycle spans, @OnEvent handlers, and custom spans are profiled automatically.
Manual mode — targeted profiling
import * as Sentry from "@sentry/nestjs";
import { nodeProfilingIntegration } from "@sentry/profiling-node";
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [nodeProfilingIntegration()],
tracesSampleRate: 1.0,
profileSessionSampleRate: 1.0,
profileLifecycle: "manual",
});
// Somewhere in application code:
Sentry.profiler.startProfiler();
await expensiveOperation();
Sentry.profiler.stopProfiler();Production fleet sampling
profileSessionSampleRate is decided once at process startup — use it to sample a fraction of pods/containers rather than per-request:
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [nodeProfilingIntegration()],
tracesSampleRate: 0.1, // sample 10% of requests for traces
profileSessionSampleRate: 0.25, // profile 25% of pods/instances
profileLifecycle: "trace",
});Technical Details
- Uses V8's `CpuProfiler` native C++ add-on — ~100 Hz sampling (10 ms interval)
- Precompiled binaries available for:
- macOS x64 / ARM64
- Linux x64 glibc / ARM64 musl
- Windows x64
- Node.js 18, 20, 22, 24
- Not supported in Deno or Bun
Environment Variables
| Variable | Purpose |
|---|---|
SENTRY_PROFILER_BINARY_PATH | Override full path to profiler.node binary |
SENTRY_PROFILER_BINARY_DIR | Override directory containing profiler.node |
SENTRY_PROFILER_LOGGING_MODE | eager (default) or lazy (starts on first use) |
Eager mode (default): Profiler always running — lower latency to first profile, uses CPU between requests. Lazy mode: Starts on first use — lower baseline CPU overhead, small latency on first profile.
Performance Overhead
- 100 Hz sampling has minimal per-sample cost
- Eager mode consumes some CPU even between requests
- Load test before enabling in high-throughput production services
- Start with a low
profileSessionSampleRate(e.g.,0.1) and increase based on observed overhead
Troubleshooting
| Issue | Solution |
|---|---|
| No profiles appearing | Verify tracesSampleRate > 0 and both profileSessionSampleRate + profileLifecycle are set |
| Native binary fails to load | Check Node.js version is 18–24 and platform is supported; set SENTRY_PROFILER_BINARY_PATH if needed |
| Version mismatch error | @sentry/profiling-node version must exactly match @sentry/nestjs |
| Profiler not stopping (manual mode) | Ensure Sentry.profiler.stopProfiler() is called on shutdown / after the target code |
| High CPU in idle | Switch to SENTRY_PROFILER_LOGGING_MODE=lazy or reduce profileSessionSampleRate |
Tracing — Sentry NestJS SDK
Minimum SDK: @sentry/nestjs 8.x (requires Node >= 18.0.0; 18.19.0+ or 19.9.0+ recommended)Configuration
| Option | Type | Default | Purpose |
|---|---|---|---|
tracesSampleRate | number | undefined | Fraction of transactions to trace (0.0–1.0); omit to disable tracing |
tracesSampler | function | undefined | Per-transaction sampling function; overrides tracesSampleRate |
tracePropagationTargets | `(string \ | RegExp)[]` | all origins |
profileSessionSampleRate | number | undefined | Fraction of process sessions to profile (0.0–1.0); decided once at init |
profileLifecycle | `'trace' \ | 'manual'` | 'trace' |
beforeSendSpan | function | undefined | Callback to mutate or drop individual spans before sending |
skipOpenTelemetrySetup | boolean | false | Skip automatic OTel provider setup (for custom OTel configurations) |
strictTraceContinuation | boolean | false | Only continue traces from same Sentry org (v10+) |
Architecture
@sentry/nestjs is a thin wrapper over @sentry/node. Its tracing stack:
@sentry/nestjs
├── Sentry.init() → auto-adds nestIntegration() to default integrations
├── nestIntegration() → registers 3 OTel instrumentations:
│ ├── @opentelemetry/instrumentation-nestjs-core → app_creation, request_context, handler spans
│ ├── SentryNestInstrumentation → middleware, guard, pipe, interceptor, filter spans
│ └── SentryNestEventInstrumentation → @OnEvent handler spans
├── SentryModule.forRoot() → registers SentryTracingInterceptor globally
├── SentryTracingInterceptor → sets HTTP transaction names from Express/Fastify route patterns
└── SentryGlobalFilter → captures unhandled exceptions (HTTP, GraphQL, RPC)Sentry is the OpenTelemetry provider — any OTel instrumentation automatically flows into Sentry.
Code Examples
Enable tracing
// instrument.ts — must be loaded FIRST in main.ts, before NestJS imports
import * as Sentry from "@sentry/nestjs";
Sentry.init({
dsn: "https://<key>@<org>.ingest.sentry.io/<project>",
tracesSampleRate: 1.0, // 1.0 = 100% of transactions; reduce in production
});// main.ts
import "./instrument"; // MUST be first — before @nestjs/core or any module
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();// app.module.ts — two distinct entry points
import { Module } from "@nestjs/common";
import { SentryModule } from "@sentry/nestjs/setup"; // /setup entry point
import { APP_FILTER } from "@nestjs/core";
import { SentryGlobalFilter } from "@sentry/nestjs/setup"; // /setup entry point
@Module({
imports: [SentryModule.forRoot()], // registers SentryTracingInterceptor globally
providers: [
{
provide: APP_FILTER,
useClass: SentryGlobalFilter, // captures unhandled HTTP/GraphQL/RPC exceptions
},
],
})
export class AppModule {}`@sentry/nestjs` vs `@sentry/nestjs/setup` — two separate entry points:
>
-@sentry/nestjs—Sentry.init(), decorators, span APIs, all@sentry/nodere-exports
-@sentry/nestjs/setup—SentryModule,SentryTracingInterceptor,SentryGlobalFilter
HTTP request auto-tracing
HTTP tracing requires no extra code. Two mechanisms work together:
1. `nestIntegration` (via @opentelemetry/instrumentation-nestjs-core) creates spans:
app_creation.nestjs— NestJS bootstraprequest_context.nestjs— overall request handlinghandler.nestjs— each route handler
2. `SentryTracingInterceptor` (registered via SentryModule.forRoot()) sets the transaction name from the parameterized route:
- Express:
GET /users/:id(fromreq.route.path) - Fastify:
GET /users/:id(fromreq.routeOptions.url)
Typical span tree for a request:
GET /api/users/:id (transaction name)
└── request_context.nestjs
├── AuthGuard (middleware.nestjs)
├── ParseIntPipe (middleware.nestjs)
├── LoggingInterceptor (middleware.nestjs — before route)
│ └── handler.nestjs
│ └── db query span (auto from pg/mysql/etc.)
└── LoggingInterceptor - Interceptors - After Route (middleware.nestjs)All NestJS lifecycle spans (middleware, guards, pipes, interceptors, filters) share op middleware.nestjs.@SentryTraced decorator
import { SentryTraced } from "@sentry/nestjs";
import { Injectable } from "@nestjs/common";
@Injectable()
export class OrderService {
@SentryTraced("db.query") // op="db.query", name="findOrder" (method name)
async findOrder(id: string) {
return this.orderRepo.findOne({ where: { id } });
}
@SentryTraced() // op="function" (default)
async processOrder(data: CreateOrderDto) {
return this.process(data);
}
}- Span
name= method name (e.g.,"findOrder") - Span
op= decorator argument, defaults to"function" - Works with both sync and async methods
- Copies
reflect-metadatakeys — NestJS DI compatibility preserved
Custom spans with startSpan (auto-ends)
import * as Sentry from "@sentry/nestjs";
import { Injectable } from "@nestjs/common";
@Injectable()
export class PaymentService {
async charge(userId: string, amount: number) {
return Sentry.startSpan(
{ name: "charge-card", op: "payment.charge" },
async (span) => {
span.setAttribute("payment.userId", userId);
span.setAttribute("payment.amount", amount);
const result = await this.stripeService.charge(userId, amount);
span.setAttribute("payment.transactionId", result.id);
return result;
},
);
}
}startSpanManual (callback-style, must call span.end())
return Sentry.startSpanManual(
{ name: "legacy-callback", op: "function" },
(span) => {
legacyLib.doWork((err, result) => {
span.setStatus({ code: err ? 2 : 1 }); // 1=OK, 2=ERROR
span.end();
callback(err, result);
});
},
);startInactiveSpan (detached, no auto-parent)
const span = Sentry.startInactiveSpan({ name: "background-index", op: "task" });
// ... do work independently ...
span.end();Span options reference
| Option | Type | Description |
|---|---|---|
name | string | Required. Span name |
op | string | Operation type (db, http.client, function, queue.process, etc.) |
attributes | `Record<string, string \ | number \ |
startTime | number | Custom start timestamp (Unix seconds) |
parentSpan | Span | Explicit parent (overrides auto-parent from context) |
onlyIfParent | boolean | Skip creating span if no active parent exists |
forceTransaction | boolean | Display as root transaction in Sentry UI |
Accessing and modifying the active span
import * as Sentry from "@sentry/nestjs";
// Read active span
const span = Sentry.getActiveSpan();
if (span) {
span.setAttribute("user.id", userId);
span.setAttributes({ "order.type": "subscription", "order.currency": "USD" });
}
// Update span name (v8.47.0+)
if (span) Sentry.updateSpanName(span, "Refined Operation Name");
// Span status codes: 0=UNSET, 1=OK, 2=ERROR
span?.setStatus({ code: 2 });Nested spans
return Sentry.startSpan(
{ name: "process-checkout", op: "business.logic" },
async () => {
const cart = await Sentry.startSpan(
{ name: "fetch-cart", op: "db.query" },
() => this.cartRepo.findById(cartId),
);
await Sentry.startSpan({ name: "apply-discount", op: "function" }, () =>
this.discountService.apply(cart),
);
return Sentry.startSpan({ name: "create-order", op: "db.query" }, () =>
this.orderRepo.create(cart),
);
},
);Modify all spans globally (beforeSendSpan)
Sentry.init({
dsn: "YOUR_DSN",
beforeSendSpan(span) {
if (span.op === "db.query" && span.description?.includes("password")) {
span.description = "[REDACTED]";
}
// return null to drop the span entirely
return span;
},
});Dynamic sampling with tracesSampler
Sentry.init({
dsn: "YOUR_DSN",
tracesSampler: ({ name, attributes, parentSampled }) => {
// Drop health check endpoints
if (/\/(health|ping|readiness|liveness)/.test(name)) return 0;
// Always capture authentication flows
if (name.includes("/auth/")) return 1;
// Inherit parent's sampling decision (distributed tracing)
if (parentSampled !== undefined) return parentSampled;
// Default 10%
return 0.1;
},
});Event emitter auto-tracing (@OnEvent)
Requires @nestjs/event-emitter >= 2.0.0. Handlers are auto-wrapped — no code changes needed:
import { OnEvent } from "@nestjs/event-emitter";
import { Injectable } from "@nestjs/common";
@Injectable()
export class NotificationListener {
@OnEvent("user.created")
async handleUserCreated(payload: UserCreatedEvent) {
// Auto span: name="event user.created", op="event.nestjs"
// forceTransaction: true → appears as separate root transaction in Sentry UI
// Unhandled exceptions auto-captured (they bypass SentryGlobalFilter)
await this.emailService.sendWelcome(payload.userId);
}
@OnEvent("user.created")
@OnEvent("user.updated")
async handleUserChange(payload: UserEvent) {
// Span: name="event user.created|user.updated"
}
@OnEvent("order.*") // wildcards supported
async handleOrder(payload: OrderEvent) {
await this.orderService.process(payload);
}
}Note: Event spans always use forceTransaction: true — they appear as isolated roottransactions, not child spans of the HTTP request that emitted the event.
GraphQL resolver tracing
GraphQL is auto-traced via graphqlIntegration (enabled by default). No configuration needed:
// Spans auto-created for:
// - Query/mutation/subscription execution
// - Individual resolver fields
// SentryGlobalFilter handles GraphQL exceptions correctly:
// - HttpException → rethrown without capturing (expected)
// - All other errors → captured then rethrown (GraphQL ExternalExceptionFilter needs the rethrow)Microservices — transport support matrix
| Transport | Auto-traced? | Mechanism |
|---|---|---|
| AMQP / RabbitMQ | ✅ | amqplibIntegration — amqp.publish + amqp.process spans, headers auto-injected |
| Kafka (KafkaJS) | ✅ | kafkaIntegration — kafka.send + kafka.process spans, trace context in record headers |
| Redis pub/sub | ⚠️ Partial | redisIntegration traces Redis commands only |
| TCP | ❌ | No OTel instrumentation |
| NATS | ❌ | Community OTel NATS package needed |
| gRPC | ❌ | Community OTel gRPC package needed |
WebSocket gateway tracing (manual)
No dedicated WebSocket auto-tracing exists. SentryTracingInterceptor only handles HTTP contexts:
import {
SubscribeMessage,
WebSocketGateway,
MessageBody,
} from "@nestjs/websockets";
import * as Sentry from "@sentry/nestjs";
@WebSocketGateway(3001)
export class ChatGateway {
@SubscribeMessage("message")
async handleMessage(@MessageBody() payload: { data: any; _sentry?: any }) {
const { sentryTrace, baggage } = payload._sentry ?? {};
return Sentry.continueTrace({ sentryTrace, baggage }, () =>
Sentry.startSpan(
{
name: "ws.chat.message",
op: "websocket.server",
forceTransaction: true,
},
async () => this.chatService.process(payload.data),
),
);
}
}
// Client: attach trace context to every message
const traceData = Sentry.getTraceData();
socket.emit("message", {
data: payload,
_sentry: {
sentryTrace: traceData["sentry-trace"],
baggage: traceData["baggage"],
},
});Bull/BullMQ job tracing (manual)
No dedicated Bull integration — use manual spans in @Process() handlers. Always wrap with withIsolationScope to prevent scope leakage between concurrent jobs.
BullMQ with WorkerHost (recommended for @nestjs/bullmq)
import { Processor, WorkerHost } from "@nestjs/bullmq";
import { Job } from "bullmq";
import * as Sentry from "@sentry/nestjs";
@Processor("email")
export class EmailProcessor extends WorkerHost {
async process(job: Job) {
return Sentry.withIsolationScope(() =>
Sentry.startSpan(
{
name: `email ${job.name}`,
op: "queue.process",
forceTransaction: true,
attributes: {
"messaging.system": "bullmq",
"messaging.destination": "email",
"messaging.message.id": job.id ?? "unknown",
"job.name": job.name,
"job.attemptsMade": job.attemptsMade,
},
},
async () => {
await this.emailService.sendWelcomeEmail(job.data.userId);
},
),
);
}
}Why `withIsolationScope`? BullMQ processes jobs concurrently in the same process. Without isolation,setTag,setUser, and breadcrumbs leak between concurrent jobs.
Bull with @Process() decorator
import { Process, Processor } from "@nestjs/bull";
import { Job } from "bull";
import * as Sentry from "@sentry/nestjs";
@Processor("email")
export class EmailProcessor {
@Process("send-welcome")
async handle(job: Job<{ userId: string; _sentry?: Record<string, string> }>) {
const { _sentry, ...data } = job.data;
return Sentry.withIsolationScope(() =>
Sentry.continueTrace(
{
sentryTrace: _sentry?.["sentry-trace"],
baggage: _sentry?.["baggage"],
},
() =>
Sentry.startSpan(
{
name: "email.send-welcome",
op: "queue.process",
forceTransaction: true,
},
async (span) => {
span.setAttribute("job.id", job.id.toString());
span.setAttribute("job.attemptsMade", job.attemptsMade);
await this.emailService.sendWelcomeEmail(data.userId);
},
),
),
);
}
}Publisher — attach trace context to job data
async queueWelcomeEmail(userId: string) {
return Sentry.startSpan({ name: "email.queue", op: "queue.publish" }, () => {
const traceData = Sentry.getTraceData();
return this.emailQueue.add("send-welcome", { userId, _sentry: traceData });
});
}Kafka / NATS microservice handler tracing
Kafka messages are auto-instrumented by kafkaIntegration (KafkaJS), but NATS and other transports require manual spans. For consistency, wrapping @EventPattern() and @MessagePattern() handlers with explicit spans is recommended for all transports:
import { Controller } from "@nestjs/common";
import { EventPattern, MessagePattern, Payload } from "@nestjs/microservices";
import * as Sentry from "@sentry/nestjs";
@Controller()
export class OrderController {
@EventPattern("order.created")
async handleOrderCreated(@Payload() data: OrderEvent) {
return Sentry.startSpan(
{ name: "handleOrderCreated", op: "kafka", forceTransaction: true },
async () => {
await this.orderService.processCreated(data);
},
);
}
@MessagePattern("order.get")
async getOrder(@Payload() data: { id: string }) {
return Sentry.startSpan({ name: "getOrder", op: "rpc" }, async () => {
return this.orderService.findById(data.id);
});
}
}Use forceTransaction: true for event handlers that should appear as root transactions in Sentry UI.Distributed tracing between services
HTTP services propagate sentry-trace and baggage headers automatically. For custom channels:
// Service A — publish with trace context
async sendToQueue(data: any) {
return Sentry.startSpan({ name: "queue.publish", op: "queue.publish" }, () => {
const traceData = Sentry.getTraceData();
return this.queue.send({
payload: data,
headers: {
"sentry-trace": traceData["sentry-trace"],
"baggage": traceData["baggage"],
},
});
});
}
// Service B — continue trace from received message
async handleMessage(message: any) {
return Sentry.continueTrace(
{
sentryTrace: message.headers["sentry-trace"],
baggage: message.headers["baggage"],
},
() => Sentry.startSpan(
{ name: "queue.process", op: "queue.process" },
() => this.processPayload(message.payload)
)
);
}Limit trace propagation targets
Sentry.init({
dsn: "YOUR_DSN",
tracePropagationTargets: [
"localhost",
"https://api.internal.example.com",
/^https:\/\/microservice-[a-z]+\.internal\./,
// tracePropagationTargets: [] → disable outgoing propagation entirely
],
});Database auto-instrumentation
| Driver / ORM | Auto-enabled | Notes |
|---|---|---|
PostgreSQL (pg) | ✅ | postgresIntegration |
| MySQL | ✅ | mysqlIntegration |
| MySQL2 | ✅ | mysql2Integration |
| MongoDB | ✅ | mongoIntegration |
| Mongoose | ✅ | mongooseIntegration |
| Prisma | ⚠️ Manual | prismaIntegration — add explicitly: integrations: [Sentry.prismaIntegration()] |
| SQL Server (Tedious) | ✅ | tediousIntegration |
| Knex | ❌ | Must add manually |
| TypeORM | ❌ | Use opentelemetry-instrumentation-typeorm community package |
| Sequelize | ❌ | No known integration |
// Knex — must add explicitly:
import { knexIntegration } from "@sentry/node";
Sentry.init({ dsn: "YOUR_DSN", integrations: [knexIntegration()] });Redis auto-instrumentation
redisIntegration is auto-enabled — traces all ioredis and node-redis commands:
name: "SET user:123" op: "db.redis"
name: "GET session:abc" op: "db.redis"No configuration needed.
Using OTel APIs directly
Since Sentry is the OTel provider, OTel spans automatically appear in Sentry:
import { trace, SpanStatusCode } from "@opentelemetry/api";
const tracer = trace.getTracer("my-service", "1.0.0");
tracer.startActiveSpan("process-event", (span) => {
try {
processEvent();
span.setStatus({ code: SpanStatusCode.OK });
} catch (e) {
span.setStatus({ code: SpanStatusCode.ERROR });
throw e;
} finally {
span.end();
}
});
// → Appears in Sentry automatically, no extra configThird-party OTel instrumentations also work without any Sentry-specific setup:
// e.g., community TypeORM OTel instrumentation
import "opentelemetry-instrumentation-typeorm";
// → TypeORM query spans appear in Sentry automaticallyDisable or customize integrations
Sentry.init({
// Disable a specific integration:
integrations: (defaults) => defaults.filter((i) => i.name !== "Kafka"),
});
// Override integration config:
Sentry.init({
integrations: [Sentry.breadcrumbsIntegration({ console: false })],
});
// Add non-default integration:
Sentry.addIntegration(Sentry.captureConsoleIntegration());
// Disable all defaults (uncommon):
Sentry.init({ defaultIntegrations: false });Profiling with @sentry/profiling-node
# Version must exactly match @sentry/nestjs
npm install @sentry/profiling-node// instrument.ts
import * as Sentry from "@sentry/nestjs";
const { nodeProfilingIntegration } = require("@sentry/profiling-node");
Sentry.init({
dsn: "YOUR_DSN",
integrations: [nodeProfilingIntegration()],
tracesSampleRate: 1.0,
profileSessionSampleRate: 1.0, // profile 100% of process sessions
profileLifecycle: "trace", // auto start/stop with spans (recommended)
});profileLifecycle | Start | Stop | Use case |
|---|---|---|---|
"trace" (default) | First active span | Last span ends | General profiling — zero config |
"manual" | Sentry.profiler.startProfiler() | Sentry.profiler.stopProfiler() | Targeted hot paths |
`profileSessionSampleRate` is process-level — decided once at startup, not per-request.
Use 0.1 to profile 10% of pods in a fleet without overhead on the rest.Auto-Instrumented Integrations
Framework & HTTP (all auto-enabled)
| Integration | What is traced |
|---|---|
nestIntegration | Middleware, guards, pipes, interceptors, filters, @OnEvent handlers |
httpIntegration | Incoming HTTP requests + outgoing http/https calls |
nativeNodeFetchIntegration | Outgoing fetch() calls |
requestDataIntegration | HTTP request data attached to error events |
Databases (all auto-enabled)
mongoIntegration, mongooseIntegration, mysqlIntegration, mysql2Integration, postgresIntegration, prismaIntegration, tediousIntegration
Cache & Queues (all auto-enabled)
redisIntegration (ioredis + node-redis), amqplibIntegration (AMQP/RabbitMQ), kafkaIntegration (KafkaJS)
AI / LLM (all auto-enabled)
openAIIntegration, anthropicAIIntegration, googleGenAIIntegration, langChainIntegration, vercelAiIntegration
Must be added manually
knexIntegration, dataloaderIntegration, supabaseIntegration, captureConsoleIntegration
What Is and Isn't Auto-Traced
Auto-traced (no code changes needed)
| Feature | Mechanism |
|---|---|
| HTTP requests + transaction naming | nestIntegration + SentryTracingInterceptor |
| Middleware, guard, pipe, interceptor, filter spans | SentryNestInstrumentation (patches @Injectable/@Catch) |
@OnEvent handler spans | SentryNestEventInstrumentation (patches @OnEvent) |
| GraphQL queries/mutations/resolvers | graphqlIntegration |
| AMQP/RabbitMQ + Kafka messages | amqplibIntegration + kafkaIntegration |
| Redis, MongoDB, Mongoose, MySQL, PG | Auto-integrations |
| Outgoing HTTP (axios, fetch, http) | httpIntegration + nativeNodeFetchIntegration |
| Any OTel instrumentation | Auto-forwarded via OTel bridge |
Requires manual instrumentation
| Feature | API |
|---|---|
| Custom business logic spans | Sentry.startSpan(), startSpanManual(), startInactiveSpan() |
| Method-level tracing | @SentryTraced() decorator |
| Cron job monitoring | @SentryCron() decorator |
| Exception filter error capture | @SentryExceptionCaptured() decorator |
| WebSocket gateway tracing | continueTrace() + startSpan() in message handler |
| TCP/NATS/gRPC microservices | Manual startSpan() + continueTrace() |
| Bull/BullMQ job tracing | withIsolationScope() + startSpan() in process() / @Process() |
| Non-HTTP distributed tracing | getTraceData() + continueTrace() |
| TypeORM / Sequelize tracing | Community OTel packages or manual spans |
| Node.js profiling | @sentry/profiling-node + nodeProfilingIntegration() |
Best Practices
- Always import
instrument.tsas the very first import inmain.ts— before@nestjs/coreor any app module - Use
tracesSamplerinstead oftracesSampleRatein production — drop health checks, adjust per-route, honour distributed decisions - Set
tracePropagationTargetsto avoid leakingsentry-traceheaders to third-party services - Prefer
startSpan()(auto-ends) overstartSpanManual()— forgettingspan.end()silently drops the span - Add
sentry-traceandbaggageto your CORS allowlist when tracing browser-to-backend flows - Pin
@sentry/profiling-nodeto the exact same version as@sentry/nestjs - Use
profileSessionSampleRateto profile a fraction of pods rather than every pod — the decision is per-process, not per-request - Always wrap background job handlers (
@Process(),WorkerHost.process(),@Cron(),@OnEvent()) withwithIsolationScope()beforestartSpan()— without isolation, concurrent jobs share scope state - If the project uses a DI wrapper for Sentry (e.g.
SENTRY_PROXY_TOKEN), use the injected service forstartSpan,captureException, etc. — onlyinstrument.tsshould import@sentry/nestjsdirectly - When using a config class for
Sentry.init(), add new SDK options to the config type rather than hardcoding them — this keeps options configurable per environment
Troubleshooting
| Issue | Solution |
|---|---|
| No transactions appearing | Verify tracesSampleRate > 0 or tracesSampler returns non-zero |
Transaction names show raw URL (e.g., /users/123) instead of pattern | SentryModule.forRoot() not imported; or instrument.ts loaded after @nestjs/core |
| Middleware/guard/pipe spans missing | nestIntegration not registered; ensure instrument.ts is first import |
@OnEvent spans not appearing | @nestjs/event-emitter < 2.0.0; or instrument.ts loaded after event emitter |
| Distributed traces broken across services | Check sentry-trace and baggage headers pass through proxies/API gateways |
| DB spans missing | Driver loaded before instrument.ts; reorder imports |
| Profiler crashes at startup | @sentry/profiling-node version doesn't match @sentry/nestjs |
| Event spans appear as isolated transactions | Expected — @OnEvent uses forceTransaction: true by design |
| RPC exceptions not captured or app crashes | Use a dedicated @Catch(RpcException) filter; SentryGlobalFilter logs a warning for RPC |
| OTel instrumentation spans not appearing | Ensure the OTel package is loaded after instrument.ts |
| BullMQ jobs share tags/user/breadcrumbs | Wrap process() body with Sentry.withIsolationScope(() => ...) |
profilesSampleRate not working | Deprecated in SDK 10.x — use profileSessionSampleRate + profileLifecycle: "trace" |
SentryModule.forRoot() registered twice | Only register once — if a shared library module already imports it, skip in AppModule |
import * as Sentry blocked by ESLint | Use named imports or the project's DI proxy; namespace imports trigger no-restricted-syntax rules |
Related skills
How it compares
Use sentry-nestjs-sdk for NestJS-specific guided setup instead of generic Sentry docs when adapters and job types vary.
FAQ
What is sentry-nestjs-sdk?
Full Sentry SDK setup for NestJS. Use when asked to "add Sentry to NestJS", "install @sentry/nestjs", "setup Sentry in NestJS", or configure error monitoring, tracing, profiling, l
When should I use sentry-nestjs-sdk?
Full Sentry SDK setup for NestJS. Use when asked to "add Sentry to NestJS", "install @sentry/nestjs", "setup Sentry in NestJS", or configure error monitoring, tracing, profiling, l
Is sentry-nestjs-sdk safe to install?
Review the Security Audits panel on this page before production use.