
Sentry Node Sdk
- 2.6k installs
- 243 repo stars
- Updated July 27, 2026
- getsentry/sentry-for-ai
sentry-node-sdk is a Sentry skill for Node.js, Bun, and Deno error monitoring, tracing, logs, crons, and AI monitoring setup.
About
Sentry Node.js, Bun, and Deno SDK is an opinionated setup wizard for server-side JavaScript and TypeScript runtimes covering error monitoring, tracing, logging, profiling, crons, metrics, and AI monitoring. Phase one detects runtime, framework packages, module system, existing instrument files, logging libraries, cron schedulers, AI libraries, and OpenTelemetry usage via package.json and filesystem scans. Phase two recommends concrete features instead of open-ended questions, choosing @sentry/node, @sentry/bun, or @sentry/deno with ESM --import instrument.mjs or CJS require patterns. Framework-specific handlers cover Express, Fastify, Koa, Hapi, Connect, Bun.serve, and Deno.serve, with cross-links to NestJS and Next.js sibling skills when detected. The skill merges into existing instrument files rather than overwriting, routes OpenTelemetry users to OTLP integration, and suggests Sentry Logs when winston or pino is present. Crons monitoring activates when node-cron, bull, or similar schedulers appear; AI monitoring when OpenAI, Anthropic, or LangChain packages are found. Disable-model-invocation metadata marks it as a guided SDK setup skill under the sentry-sdk-setup parent tree.
- Detect runtime, framework, ESM versus CJS, and existing Sentry packages.
- Instrument.js merge instead of overwrite for existing setups.
- Express, Fastify, Koa, Bun.serve, and Deno.serve handler wiring.
- Optional logs, crons, profiling, metrics, and AI monitoring features.
- OpenTelemetry OTLP path when native tracing already exists.
Sentry Node Sdk by the numbers
- 2,624 all-time installs (skills.sh)
- +53 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #63 of 1,453 DevOps & CI/CD skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
sentry-node-sdk capabilities & compatibility
- Capabilities
- runtime and framework detection phase · sdk install and instrument file setup · framework specific error handler registration · logs crons profiling and ai feature recommendati · opentelemetry otlp integration path · companion frontend cross link detection
- Works with
- sentry · openai · anthropic
- Use cases
- devops · api development
- Pricing
- Freemium
What sentry-node-sdk says it does
Merge into it rather than overwrite
npx skills add https://github.com/getsentry/sentry-for-ai --skill sentry-node-sdkAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.6k |
|---|---|
| repo stars | ★ 243 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | getsentry/sentry-for-ai ↗ |
How do I add Sentry to a Node, Bun, or Deno backend with the right instrument preload and framework handlers?
Add Sentry error monitoring, tracing, logging, profiling, crons, and AI monitoring to Node.js, Bun, or Deno backends.
Who is it for?
Backend developers adding Sentry to Express, Fastify, Bun.serve, or Deno.serve applications.
Skip if: Skip for NestJS-specific setup or Next.js three-runtime apps; use sibling Sentry skills instead.
When should I use this skill?
User asks to add Sentry to Node.js, Bun, Deno, or configure instrument.js and tracing.
What you get
Installed SDK, instrument file, framework error handlers, and enabled monitoring features matched to the stack.
- Configured Sentry SDK initialization
- Error monitoring and tracing instrumentation
By the numbers
- Supports 3 server runtimes: Node.js, Bun, and Deno
- Configures 7 observability areas: errors, tracing, logging, profiling, metrics, crons, and AI monitoring
Files
All Skills > SDK Setup > Node.js / Bun / Deno SDK
Sentry Node.js / Bun / Deno SDK
Opinionated wizard that scans your project and guides you through complete Sentry setup for server-side JavaScript and TypeScript runtimes: Node.js, Bun, and Deno.
Invoke This Skill When
- User asks to "add Sentry to Node.js", "Bun", or "Deno"
- User wants to install or configure
@sentry/node,@sentry/bun, or@sentry/deno - User wants error monitoring, tracing, logging, profiling, crons, metrics, or AI monitoring for a backend JS/TS app
- User asks about
instrument.js,--import ./instrument.mjs,bun --preload, ornpm:@sentry/deno - User wants to monitor Express, Fastify, Koa, Hapi, Connect, Bun.serve(), or Deno.serve()
NestJS? Use `sentry-nestjs-sdk` instead — it uses @sentry/nestjs with NestJS-native decorators and filters.Next.js? Use `sentry-nextjs-sdk` instead — it handles the three-runtime architecture (browser, server, edge).
Note: SDK versions below reflect current Sentry docs at time of writing (@sentry/node≥10.42.0,@sentry/bun≥10.42.0,@sentry/deno≥10.42.0).
Always verify against docs.sentry.io/platforms/javascript/guides/node/ before implementing.
---
Phase 1: Detect
Run these commands to identify the runtime, framework, and existing Sentry setup:
# Detect runtime
bun --version 2>/dev/null && echo "Bun detected"
deno --version 2>/dev/null && echo "Deno detected"
node --version 2>/dev/null && echo "Node.js detected"
# Detect existing Sentry packages
cat package.json 2>/dev/null | grep -E '"@sentry/'
cat deno.json deno.jsonc 2>/dev/null | grep -i sentry
# Detect Node.js framework
cat package.json 2>/dev/null | grep -E '"express"|"fastify"|"@hapi/hapi"|"koa"|"@nestjs/core"|"connect"'
# Detect Bun-specific frameworks
cat package.json 2>/dev/null | grep -E '"elysia"|"hono"'
# Detect Deno frameworks (deno.json imports)
cat deno.json deno.jsonc 2>/dev/null | grep -E '"oak"|"hono"|"fresh"'
# Detect module system (Node.js)
cat package.json 2>/dev/null | grep '"type"'
ls *.mjs *.cjs 2>/dev/null | head -5
# Detect existing instrument file
ls instrument.js instrument.mjs instrument.ts instrument.cjs 2>/dev/null
# Detect logging libraries
cat package.json 2>/dev/null | grep -E '"winston"|"pino"|"bunyan"'
# Detect cron / scheduling
cat package.json 2>/dev/null | grep -E '"node-cron"|"cron"|"agenda"|"bull"|"bullmq"'
# Detect AI / LLM usage
cat package.json 2>/dev/null | grep -E '"openai"|"@anthropic-ai"|"@langchain"|"@vercel/ai"|"@google/generative-ai"'
# Detect OpenTelemetry tracing
cat package.json 2>/dev/null | grep -E '"@opentelemetry/sdk-node"|"@opentelemetry/sdk-trace-node"|"@opentelemetry/sdk-trace-base"'
grep -rn "NodeTracerProvider\|trace\.getTracer\|startActiveSpan" \
--include="*.ts" --include="*.js" --include="*.mjs" 2>/dev/null | head -5
# Check for companion frontend
ls frontend/ web/ client/ ui/ 2>/dev/null
cat package.json 2>/dev/null | grep -E '"react"|"vue"|"svelte"|"next"'What to determine:
| Question | Impact |
|---|---|
| Which runtime? (Node.js / Bun / Deno) | Determines package, init pattern, and preload flag |
| Node.js: ESM or CJS? | ESM requires --import ./instrument.mjs; CJS uses require("./instrument") |
| Framework detected? | Determines which error handler to register |
@sentry/* already installed? | Skip install, go straight to feature config |
instrument.js / instrument.mjs already exists? | Merge into it rather than overwrite |
| Logging library detected? | Recommend Sentry Logs |
| Cron / job scheduler detected? | Recommend Crons monitoring |
| AI library detected? | Recommend AI Monitoring |
| OpenTelemetry tracing detected? | Use OTLP path instead of native tracing |
| Companion frontend found? | Trigger Phase 4 cross-link |
---
Phase 2: Recommend
Present a concrete recommendation based on what you found. Don't ask open-ended questions — lead with a proposal:
Route from OTel detection:
- OTel tracing detected (
@opentelemetry/sdk-nodeor@opentelemetry/sdk-trace-nodeinpackage.json, orNodeTracerProviderin source) → use OTLP path:otlpIntegration()via@sentry/node-core/light; do not settracesSampleRate; Sentry links errors to OTel traces automatically
Recommended (core coverage):
- ✅ Error Monitoring — always; captures unhandled exceptions, promise rejections, and framework errors
- ✅ Tracing — automatic HTTP, DB, and queue instrumentation via OpenTelemetry
Optional (enhanced observability):
- ⚡ Logging — structured logs via
Sentry.logger.*; recommend whenwinston/pino/bunyanor log search is needed - ⚡ Profiling — continuous CPU profiling (Node.js only; not available on Bun or Deno); not available with OTLP path
- ⚡ AI Monitoring — OpenAI, Anthropic, LangChain, Vercel AI SDK; recommend when AI/LLM calls detected
- ⚡ Crons — detect missed or failed scheduled jobs; recommend when node-cron, Bull, or Agenda is detected
- ⚡ Metrics — custom counters, gauges, distributions; recommend when custom KPIs needed
- ⚡ Runtime Metrics — automatic collection of memory, CPU, and event loop metrics;
nodeRuntimeMetricsIntegration()(Node.js) /bunRuntimeMetricsIntegration()(Bun)
Recommendation logic:
| Feature | Recommend when... |
|---|---|
| Error Monitoring | Always — non-negotiable baseline |
| OTLP Integration | OTel tracing detected — replaces native Tracing |
| Tracing | Always for server apps — HTTP spans + DB spans are high-value; skip if OTel tracing detected |
| Logging | App uses winston, pino, bunyan, or needs log-to-trace correlation |
| Profiling | Node.js only — performance-critical service; native addon compatible; skip if OTel tracing detected (requires tracesSampleRate, incompatible with OTLP) |
| AI Monitoring | App calls OpenAI, Anthropic, LangChain, Vercel AI, or Google GenAI |
| Crons | App uses node-cron, Bull, BullMQ, Agenda, or any scheduled task pattern |
| Metrics | App needs custom counters, gauges, or histograms |
| Runtime Metrics | Any Node.js or Bun service wanting automatic memory/CPU/event-loop visibility |
OTel tracing detected: "I see OpenTelemetry tracing in the project. I recommend Sentry's OTLP integration for tracing (via your existing OTel setup) + Error Monitoring + Sentry Logging [+ Metrics/Crons/AI Monitoring if applicable]. Shall I proceed?"
No OTel: "I recommend setting up Error Monitoring + Tracing. Want me to also add Logging or Profiling?"
---
Phase 3: Guide
Runtime: Node.js
Option 1: Wizard (Recommended for Node.js)
You need to run this yourself — the wizard opens a browser for login and requires interactive input that the agent can't handle. Copy-paste into your terminal:
>
```
npx @sentry/wizard@latest -i node
```
>
It handles login, org/project selection, SDK installation, instrument.js creation, and package.json script updates.>
Once it finishes, come back and skip to [Verification](#verification).
If the user skips the wizard, proceed with Option 2 (Manual Setup) below.
---
Option 2: Manual Setup — Node.js
Install
npm install @sentry/node --save
# or
yarn add @sentry/node
# or
pnpm add @sentry/nodeCreate the Instrument File
CommonJS (`instrument.js`):
// instrument.js — must be loaded before all other modules
const Sentry = require("@sentry/node");
Sentry.init({
dsn: process.env.SENTRY_DSN ?? "___DSN___",
sendDefaultPii: true,
// 100% in dev, lower in production
tracesSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,
// Capture local variable values in stack frames
includeLocalVariables: true,
enableLogs: true,
});ESM (`instrument.mjs`):
// instrument.mjs — loaded via --import flag before any other module
import * as Sentry from "@sentry/node";
Sentry.init({
dsn: process.env.SENTRY_DSN ?? "___DSN___",
sendDefaultPii: true,
tracesSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,
includeLocalVariables: true,
enableLogs: true,
});Start Your App with Sentry Loaded First
CommonJS — add require("./instrument") as the very first line of your entry file:
// app.js
require("./instrument"); // must be first
const express = require("express");
// ... rest of your appESM — use the --import flag so Sentry loads before all other modules (Node.js 18.19.0+ required):
node --import ./instrument.mjs app.mjsAdd to package.json scripts:
{
"scripts": {
"start": "node --import ./instrument.mjs server.mjs",
"dev": "node --import ./instrument.mjs --watch server.mjs"
}
}Or via environment variable (useful for wrapping existing start commands):
NODE_OPTIONS="--import ./instrument.mjs" npm startFramework Error Handlers
Register the Sentry error handler after all routes so it can capture framework errors:
Express:
const express = require("express");
const Sentry = require("@sentry/node");
const app = express();
// ... your routes
// Add AFTER all routes — captures 5xx errors by default
Sentry.setupExpressErrorHandler(app);
// Optional: capture 4xx errors too
// Sentry.setupExpressErrorHandler(app, {
// shouldHandleError(error) { return error.status >= 400; },
// });
app.listen(3000);Fastify:
const Fastify = require("fastify");
const Sentry = require("@sentry/node");
const fastify = Fastify();
// Add BEFORE routes (unlike Express!)
Sentry.setupFastifyErrorHandler(fastify);
// ... your routes
await fastify.listen({ port: 3000 });Koa:
const Koa = require("koa");
const Sentry = require("@sentry/node");
const app = new Koa();
// Add as FIRST middleware (catches errors thrown by later middleware)
Sentry.setupKoaErrorHandler(app);
// ... your other middleware and routes
app.listen(3000);Hapi (async — must await):
const Hapi = require("@hapi/hapi");
const Sentry = require("@sentry/node");
const server = Hapi.server({ port: 3000 });
// ... your routes
// Must await — Hapi registration is async
await Sentry.setupHapiErrorHandler(server);
await server.start();Connect:
const connect = require("connect");
const Sentry = require("@sentry/node");
const app = connect();
// Add BEFORE routes (like Fastify and Koa)
Sentry.setupConnectErrorHandler(app);
// ... your middleware and routes
require("http").createServer(app).listen(3000);NestJS — has its own dedicated skill with full coverage:
Use the [`sentry-nestjs-sdk`](../sentry-nestjs-sdk/SKILL.md) skill instead.
NestJS uses a separate package (@sentry/nestjs) with NestJS-native constructs:SentryModule.forRoot(),SentryGlobalFilter,@SentryTraced,@SentryCrondecorators,
and GraphQL/Microservices support. Load that skill for complete NestJS setup.
Vanilla Node.js `http` module — wrap request handler manually:
const http = require("http");
const Sentry = require("@sentry/node");
const server = http.createServer((req, res) => {
Sentry.withIsolationScope(() => {
try {
// your handler
res.end("OK");
} catch (err) {
Sentry.captureException(err);
res.writeHead(500);
res.end("Internal Server Error");
}
});
});
server.listen(3000);Framework error handler summary:
| Framework | Function | Placement | Async? |
|---|---|---|---|
| Express | setupExpressErrorHandler(app) | After all routes | No |
| Fastify | setupFastifyErrorHandler(fastify) | Before routes | No |
| Koa | setupKoaErrorHandler(app) | First middleware | No |
| Hapi | setupHapiErrorHandler(server) | Before server.start() | Yes |
| Connect | setupConnectErrorHandler(app) | Before routes | No |
| NestJS | → Use `sentry-nestjs-sdk` | Dedicated skill | — |
---
Runtime: Bun
No wizard available for Bun. Manual setup only.
Install
bun add @sentry/bunCreate instrument.ts (or instrument.js)
// instrument.ts
import * as Sentry from "@sentry/bun";
Sentry.init({
dsn: process.env.SENTRY_DSN ?? "___DSN___",
sendDefaultPii: true,
tracesSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,
enableLogs: true,
});Start Your App with --preload
bun --preload ./instrument.ts server.tsAdd to package.json:
{
"scripts": {
"start": "bun --preload ./instrument.ts server.ts",
"dev": "bun --watch --preload ./instrument.ts server.ts"
}
}Bun.serve() — Auto-Instrumentation
@sentry/bun automatically instruments Bun.serve() via JavaScript Proxy. No extra setup is required — just initialize with --preload and your Bun.serve() calls are traced:
// server.ts
const server = Bun.serve({
port: 3000,
fetch(req) {
return new Response("Hello from Bun!");
},
});Framework Error Handlers on Bun
Bun can run Express, Fastify, Hono, and Elysia. Use the same @sentry/bun import and the @sentry/node error handler functions (re-exported by @sentry/bun):
import * as Sentry from "@sentry/bun";
import express from "express";
const app = express();
// ... routes
Sentry.setupExpressErrorHandler(app);
app.listen(3000);Bun Feature Support
| Feature | Bun Support | Notes |
|---|---|---|
| Error Monitoring | ✅ Full | Same API as Node |
| Tracing | ✅ Via @sentry/node OTel | Most auto-instrumentations work |
| Logging | ✅ Full | enableLogs: true + Sentry.logger.* |
| Profiling | ❌ Not available | @sentry/profiling-node uses native addons incompatible with Bun |
| Metrics | ✅ Full | Sentry.metrics.* |
| Runtime Metrics | ✅ Full | bunRuntimeMetricsIntegration() — memory, CPU, event loop (no event loop delay percentiles) |
| Crons | ✅ Full | Sentry.withMonitor() |
| AI Monitoring | ✅ Full | OpenAI, Anthropic integrations work |
---
Runtime: Deno
No wizard available for Deno. Manual setup only.
Requires Deno 2.0+. Deno 1.x is not supported.
Use `npm:` specifier. The deno.land/x/sentry registry is deprecated.Install via deno.json (Recommended)
{
"imports": {
"@sentry/deno": "npm:@sentry/deno@10.42.0"
}
}Or import directly with the npm: specifier:
import * as Sentry from "npm:@sentry/deno";Initialize — Add to Entry File
// main.ts — Sentry.init() must be called before any other code
import * as Sentry from "@sentry/deno";
Sentry.init({
dsn: Deno.env.get("SENTRY_DSN") ?? "___DSN___",
sendDefaultPii: true,
tracesSampleRate: Deno.env.get("DENO_ENV") === "development" ? 1.0 : 0.1,
enableLogs: true,
});
// Your application code follows
Deno.serve({ port: 8000 }, (req) => {
return new Response("Hello from Deno!");
});Unlike Node.js and Bun, Deno does not have a--preloador--importflag. Sentry must be the firstimportin your entry file.
Required Deno Permissions
The SDK requires network access to reach your Sentry ingest domain:
deno run \
--allow-net=o<ORG_ID>.ingest.sentry.io \
--allow-read=./src \
--allow-env=SENTRY_DSN,SENTRY_RELEASE \
main.tsFor development, --allow-all works but is not recommended for production.
Deno Cron Integration
Deno provides native cron scheduling. Use denoCronIntegration for automatic monitoring:
import * as Sentry from "@sentry/deno";
import { denoCronIntegration } from "@sentry/deno";
Sentry.init({
dsn: Deno.env.get("SENTRY_DSN") ?? "___DSN___",
integrations: [denoCronIntegration()],
});
// Cron is automatically monitored
Deno.cron("daily-cleanup", "0 0 * * *", () => {
// cleanup logic
});Deno Feature Support
| Feature | Deno Support | Notes |
|---|---|---|
| Error Monitoring | ✅ Full | Unhandled exceptions + captureException |
| Tracing | ✅ Custom OTel | Automatic spans for Deno.serve() and fetch |
| Logging | ✅ Full | enableLogs: true + Sentry.logger.* |
| Profiling | ❌ Not available | No profiling addon for Deno |
| Metrics | ✅ Full | Sentry.metrics.* |
| Runtime Metrics | ❌ Not available | No runtime metrics integration for Deno |
| Crons | ✅ Full | denoCronIntegration() + Sentry.withMonitor() |
| AI Monitoring | ✅ Partial | Vercel AI SDK integration works; OpenAI/Anthropic via npm: |
---
OTLP Integration (OTel-First Projects — Node.js Only)
Use this path only when OpenTelemetry tracing was detected in Phase 1
(e.g.,@opentelemetry/sdk-nodeor@opentelemetry/sdk-trace-nodeinpackage.json).
For projects without an existing OTel setup, use the standard @sentry/node path above.The OTLP integration uses @sentry/node-core/light — a lightweight Sentry SDK that does not bundle its own OpenTelemetry. Instead, it hooks into the user's existing OTel TracerProvider and exports spans to Sentry via OTLP.
When to Use
| Scenario | Recommended path |
|---|---|
| New project, no existing OTel | Standard @sentry/node (above) — includes built-in OTel |
| Existing OTel setup, want Sentry tracing | @sentry/node-core/light + otlpIntegration() |
| Existing OTel setup, sending to own Collector | @sentry/node-core/light + otlpIntegration({ collectorUrl }) |
Install
npm install @sentry/node-core @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/sdk-trace-base
# or
yarn add @sentry/node-core @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/sdk-trace-base
# or
pnpm add @sentry/node-core @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/sdk-trace-baseThe @opentelemetry/* packages are peer dependencies. If the project already has them installed, skip duplicates.Initialize
// instrument.mjs — load via --import flag before any other module
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import * as Sentry from '@sentry/node-core/light';
import { otlpIntegration } from '@sentry/node-core/light/otlp';
// Register the user's OTel TracerProvider first
const provider = new NodeTracerProvider();
provider.register();
Sentry.init({
dsn: process.env.SENTRY_DSN ?? '___DSN___',
sendDefaultPii: true,
enableLogs: true,
// Do NOT set tracesSampleRate — OTel controls sampling
integrations: [
otlpIntegration({
// Export OTel spans to Sentry via OTLP (default: true)
setupOtlpTracesExporter: true,
}),
],
});With a custom Collector endpoint:
Sentry.init({
dsn: process.env.SENTRY_DSN ?? '___DSN___',
integrations: [
otlpIntegration({
collectorUrl: 'http://localhost:4318/v1/traces',
}),
],
});Start Your App
Same --import pattern as the standard Node.js setup:
node --import ./instrument.mjs app.mjsKey Differences from Standard @sentry/node
| Aspect | @sentry/node (standard) | @sentry/node-core/light (OTLP) |
|---|---|---|
| OTel bundled | ✅ Yes — built-in TracerProvider | ❌ No — uses your existing provider |
| Tracing control | tracesSampleRate in Sentry.init() | OTel SDK controls sampling |
| Auto-instrumentation | ✅ Built-in (HTTP, DB, etc.) | ❌ You manage OTel instrumentations |
| Profiling | ✅ Available | ❌ Not compatible |
| Error ↔ trace linking | ✅ Automatic | ✅ Automatic (via otlpIntegration) |
| Package size | Larger (includes OTel) | Smaller (light mode) |
---
For Each Agreed Feature
Load the corresponding reference file and follow its steps:
| Feature | Reference file | Load when... |
|---|---|---|
| Error Monitoring | references/error-monitoring.md | Always (baseline) — captures, scopes, enrichment, beforeSend |
| OTLP Integration | See OTLP Integration above | OTel tracing detected — replaces native Tracing |
| Tracing | references/tracing.md | OTel auto-instrumentation, custom spans, distributed tracing, sampling; skip if OTel tracing detected |
| Logging | references/logging.md | Structured logs, Sentry.logger.*, log-to-trace correlation |
| Profiling | references/profiling.md | Node.js only — CPU profiling, Bun/Deno gaps documented; skip if OTel tracing detected |
| Metrics | references/metrics.md | Custom counters, gauges, distributions |
| Runtime Metrics | See inline below | Automatic memory, CPU, and event loop metrics for Node.js and Bun |
| Crons | references/crons.md | Scheduled job monitoring, node-cron, Bull, Agenda, Deno.cron |
| AI Monitoring | Load sentry-setup-ai-monitoring skill | OpenAI, Anthropic, LangChain, Vercel AI, Google GenAI |
For each feature: read the reference file, follow its steps exactly, and verify before moving on.
Runtime Metrics
Automatically collect Node.js and Bun runtime health metrics (memory, CPU utilization, event loop delay/utilization, uptime) at a configurable interval. Metrics appear in Sentry's Metrics product under the node.runtime.* / bun.runtime.* namespace.
Node.js — add nodeRuntimeMetricsIntegration() to your instrument.js:
const Sentry = require("@sentry/node");
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [
Sentry.nodeRuntimeMetricsIntegration(),
// Optional: change collection interval (default 30 000 ms)
// Sentry.nodeRuntimeMetricsIntegration({ collectionIntervalMs: 60_000 }),
],
});Metrics collected by default: node.runtime.mem.rss, node.runtime.mem.heap_used, node.runtime.mem.heap_total, node.runtime.cpu.utilization, node.runtime.event_loop.delay.p50, node.runtime.event_loop.delay.p99, node.runtime.event_loop.utilization, node.runtime.process.uptime.
Bun — add bunRuntimeMetricsIntegration() to your instrument.ts:
import * as Sentry from "@sentry/bun";
import { bunRuntimeMetricsIntegration } from "@sentry/bun";
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [
bunRuntimeMetricsIntegration(),
// Optional: change collection interval (default 30 000 ms)
// bunRuntimeMetricsIntegration({ collectionIntervalMs: 60_000 }),
],
});Metrics collected: same as Node.js except no event loop delay percentiles (unavailable in Bun). Prefixed with bun.runtime.*.
---
Verification
After setup, verify Sentry is receiving events:
// Add temporarily to your entry file or a test route, then remove
import * as Sentry from "@sentry/node"; // or @sentry/bun / @sentry/deno
Sentry.captureException(new Error("Sentry test error — delete me"));Or trigger an unhandled exception:
// In a route handler or startup — will be captured automatically
throw new Error("Sentry test error — delete me");Then check your Sentry Issues dashboard — the error should appear within ~30 seconds.
Verification checklist:
| Check | How |
|---|---|
| Error captured | Throw in a handler, verify in Sentry Issues |
| Tracing working | Check Performance tab — should show HTTP spans |
includeLocalVariables working | Stack frame in Sentry should show variable values |
| Source maps working | Stack trace shows readable file names, not minified |
---
Config Reference
Sentry.init() Core Options
| Option | Type | Default | Notes |
|---|---|---|---|
dsn | string | — | Required. Also from SENTRY_DSN env var |
tracesSampleRate | number | — | 0–1; required to enable tracing; do not set when using OTLP path |
sendDefaultPii | boolean | false | Include IP, request headers, user info |
includeLocalVariables | boolean | false | Add local variable values to stack frames (Node.js) |
enableLogs | boolean | false | Enable Sentry Logs product (v9.41.0+) |
environment | string | "production" | Also from SENTRY_ENVIRONMENT env var |
release | string | — | Also from SENTRY_RELEASE env var |
debug | boolean | false | Log SDK activity to console |
enabled | boolean | true | Set false in tests to disable sending |
sampleRate | number | 1.0 | Fraction of error events to send (0–1) |
shutdownTimeout | number | 2000 | Milliseconds to flush events before process exit |
nativeNodeFetchIntegration() Options
Configures outgoing fetch/undici span capture. Since @opentelemetry/instrumentation-undici@0.22.0, response headers like content-length are no longer captured automatically — use headersToSpanAttributes to opt in:
Sentry.init({
integrations: [
Sentry.nativeNodeFetchIntegration({
headersToSpanAttributes: {
requestHeaders: ["x-request-id"],
responseHeaders: ["content-length", "content-type"],
},
}),
],
});| Option | Type | Default | Notes |
|---|---|---|---|
breadcrumbs | boolean | true | Record breadcrumbs for outgoing fetch requests |
headersToSpanAttributes.requestHeaders | string[] | — | Request header names to capture as span attributes |
headersToSpanAttributes.responseHeaders | string[] | — | Response header names to capture as span attributes |
otlpIntegration() Options (@sentry/node-core/light/otlp)
For OTel-first projects using @sentry/node-core/light. Import: import { otlpIntegration } from '@sentry/node-core/light/otlp'.
| Option | Type | Default | Purpose |
|---|---|---|---|
setupOtlpTracesExporter | boolean | true | Auto-configure OTLP exporter to send spans to Sentry; set false if you already export to your own Collector |
collectorUrl | string | undefined | OTLP HTTP endpoint of an OTel Collector (e.g., http://localhost:4318/v1/traces); when set, spans are sent to the collector instead of the DSN-derived Sentry endpoint |
Graceful Shutdown
Flush buffered events before process exit — important for short-lived scripts and serverless:
process.on("SIGTERM", async () => {
await Sentry.close(2000); // flush with 2s timeout
process.exit(0);
});Environment Variables
| Variable | Purpose | Runtime |
|---|---|---|
SENTRY_DSN | DSN (alternative to hardcoding in init()) | All |
SENTRY_ENVIRONMENT | Deployment environment | All |
SENTRY_RELEASE | Release version string (auto-detected from git) | All |
SENTRY_AUTH_TOKEN | Source map upload token | Build time |
SENTRY_ORG | Org slug for source map upload | Build time |
SENTRY_PROJECT | Project slug for source map upload | Build time |
NODE_OPTIONS | Set --import ./instrument.mjs for ESM | Node.js |
Source Maps (Node.js)
Readable stack traces in production require source map upload. Use @sentry/cli or the webpack/esbuild/rollup plugins:
npm install @sentry/cli --save-dev# Create a Sentry auth token at sentry.io/settings/auth-tokens/
# Set in .env.sentry-build-plugin (gitignore this file):
SENTRY_AUTH_TOKEN=sntrys_eyJ...Add upload step to your build:
{
"scripts": {
"build": "tsc && sentry-cli sourcemaps inject ./dist && sentry-cli sourcemaps upload ./dist"
}
}---
Phase 4: Cross-Link
After completing backend setup, check for companion services:
# Frontend companion
ls frontend/ web/ client/ ui/ 2>/dev/null
cat package.json 2>/dev/null | grep -E '"react"|"vue"|"svelte"|"next"'
# Other backend services
ls ../go.mod ../requirements.txt ../Gemfile 2>/dev/nullIf a frontend, framework-specific SDK, or other backend is found, suggest the matching skill:
Dedicated JavaScript framework skills (prefer these over generic node-sdk):
| Detected | Prefer skill | Why |
|---|---|---|
NestJS (@nestjs/core in package.json) | `sentry-nestjs-sdk` | Uses @sentry/nestjs with NestJS-native decorators, filters, and GraphQL support |
Next.js (next in package.json) | `sentry-nextjs-sdk` | Three-runtime architecture (browser, server, edge), withSentryConfig, source map upload |
Frontend companions:
| Detected | Suggest |
|---|---|
React app (react in package.json) | `sentry-react-sdk` |
| Svelte/SvelteKit | `sentry-svelte-sdk` |
Other backend companions:
| Detected | Suggest |
|---|---|
Go backend (go.mod) | `sentry-go-sdk` |
Python backend (requirements.txt, pyproject.toml) | `sentry-python-sdk` |
Ruby backend (Gemfile) | `sentry-ruby-sdk` |
Connecting frontend and backend with the same DSN or linked projects enables distributed tracing — stack traces that span your browser, API server, and database in a single trace view.
---
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| Events not appearing | instrument.js loaded too late | Ensure it's the first require() / loaded via --import or --preload |
| Tracing spans missing | tracesSampleRate not set | Add tracesSampleRate: 1.0 to Sentry.init() |
| ESM instrumentation not working | Missing --import flag | Run with node --import ./instrument.mjs; import "./instrument.mjs" inside app is not sufficient |
@sentry/profiling-node install fails on Bun | Native addon incompatible | Profiling is not supported on Bun — remove @sentry/profiling-node |
| Deno: events not sent | Missing --allow-net permission | Run with --allow-net=o<ORG_ID>.ingest.sentry.io |
Deno: deno.land/x/sentry not working | Deprecated and frozen at v8.55.0 | Switch to npm:@sentry/deno specifier |
includeLocalVariables not showing values | Integration not activated or minified code | Ensure includeLocalVariables: true in init; check source maps |
| NestJS: errors not captured | Wrong SDK or missing filter | Use `sentry-nestjs-sdk` — NestJS needs @sentry/nestjs, not @sentry/node |
Hapi: setupHapiErrorHandler timing issue | Not awaited | Must await Sentry.setupHapiErrorHandler(server) before server.start() |
| Shutdown: events lost | Process exits before flush | Add await Sentry.close(2000) in SIGTERM/SIGINT handler |
| Stack traces show minified code | Source maps not uploaded | Configure @sentry/cli source map upload in build step |
| No traces appearing (OTLP) | Missing @opentelemetry/* packages or otlpIntegration not added | Verify @opentelemetry/sdk-trace-node is installed; add otlpIntegration() to integrations; do not set tracesSampleRate |
| OTLP: errors not linked to traces | otlpIntegration not registered | Ensure otlpIntegration() is in the integrations array — it registers the propagation context that links errors to OTel traces |
| Profiling not starting (OTLP) | Profiling requires tracesSampleRate | Profiling is not compatible with the OTLP path; use the standard @sentry/node setup instead |
AI Monitoring - Sentry Node.js SDK
Minimum SDK: @sentry/node >=10.53.0. OpenAI, Anthropic, LangChain, LangGraph, Google GenAI, Vercel AI SDK auto-instrument.Prerequisites
Tracing must be enabled - AI spans require an active trace:
Sentry.init({
dsn: "...",
tracesSampleRate: 1.0,
streamGenAiSpans: true,
sendDefaultPii: true,
});Integration Matrix
| Integration | Min Library | Auto-Enabled | Status |
|---|---|---|---|
OpenAI (openai) | openai 4.0+ | Yes | Stable |
Anthropic (@anthropic-ai/sdk) | 0.19.2+ | Yes | Stable |
Vercel AI SDK (ai) | ai 3.0+ | Yes* | Stable |
LangChain (@langchain/core) | 0.1.0+ | Yes | Stable |
LangGraph (@langchain/langgraph) | 0.1.0+ | Yes | Stable |
Google GenAI (@google/genai) | 1.0+ | Yes | Stable |
*Vercel AI SDK requires experimental_telemetry: { isEnabled: true } on every call.
PII Control
sendDefaultPii | recordInputs | Prompts captured? |
|---|---|---|
false (default) | true | No |
true | true (default) | Yes |
true | false | No |
Set sendDefaultPii: true in Sentry.init() to let supported integrations default recordInputs/recordOutputs to true. Use integration-level options to opt out or override specific integrations.
Configuration Examples
Auto-enabled integrations
import * as Sentry from "@sentry/node";
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1.0,
streamGenAiSpans: true,
sendDefaultPii: true, // required to capture prompts/outputs
});
// OpenAI, Anthropic, LangChain, LangGraph, Google GenAI activate automaticallyExplicit configuration with recordInputs/recordOutputs override
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1.0,
streamGenAiSpans: true,
sendDefaultPii: true,
integrations: [
Sentry.openAIIntegration(),
Sentry.vercelAIIntegration(),
],
});Vercel AI SDK per-call telemetry (required)
await generateText({
model: openai("gpt-4.1"),
prompt: "Hello",
experimental_telemetry: { isEnabled: true, recordInputs: true, recordOutputs: true },
});Browser / Next.js client-side (manual wrapping required)
import OpenAI from "openai";
import * as Sentry from "@sentry/nextjs"; // or @sentry/browser
const openai = Sentry.instrumentOpenAiClient(new OpenAI());Manual Instrumentation - gen_ai.* Spans
Use when the library isn't supported, or for wrapping custom AI logic.
gen_ai.request - LLM call
await Sentry.startSpan({
op: "gen_ai.request",
name: "chat claude-sonnet-4-6",
attributes: { "gen_ai.request.model": "claude-sonnet-4-6" },
}, async (span) => {
span.setAttribute("gen_ai.request.messages", JSON.stringify(messages));
const result = await myClient.chat(messages);
span.setAttribute("gen_ai.usage.input_tokens", result.usage.inputTokens);
span.setAttribute("gen_ai.usage.output_tokens", result.usage.outputTokens);
return result;
});gen_ai.invoke_agent - Agent lifecycle
await Sentry.startSpan({
op: "gen_ai.invoke_agent",
name: "invoke_agent Weather Agent",
attributes: { "gen_ai.agent.name": "Weather Agent", "gen_ai.request.model": "claude-sonnet-4-6" },
}, async (span) => {
const result = await myAgent.run(task);
span.setAttribute("gen_ai.usage.input_tokens", result.totalInputTokens);
span.setAttribute("gen_ai.usage.output_tokens", result.totalOutputTokens);
return result;
});gen_ai.execute_tool - Tool/function call
await Sentry.startSpan({
op: "gen_ai.execute_tool",
name: "execute_tool get_weather",
attributes: {
"gen_ai.tool.name": "get_weather",
"gen_ai.tool.type": "function",
"gen_ai.tool.input": JSON.stringify({ location: "Paris" }),
},
}, async (span) => {
const result = await getWeather("Paris");
span.setAttribute("gen_ai.tool.output", JSON.stringify(result));
return result;
});Span Attribute Reference
Common attributes
| Attribute | Type | Required | Description |
|---|---|---|---|
gen_ai.request.model | string | Yes | Model identifier (e.g., claude-sonnet-4-6, gemini-2.5-flash) |
gen_ai.operation.name | string | No | Human-readable operation label |
gen_ai.agent.name | string | No | Agent name (for agent spans) |
Content attributes (PII-gated — only when sendDefaultPii: true + recordInputs/recordOutputs: true)
| Attribute | Type | Description |
|---|---|---|
gen_ai.request.messages | string | JSON-stringified message array |
gen_ai.request.available_tools | string | JSON-stringified tool definitions |
gen_ai.response.text | string | JSON-stringified response array |
gen_ai.response.tool_calls | string | JSON-stringified tool call array |
Span attributes only accept primitives - arrays/objects must be JSON-stringified.
Token usage attributes
| Attribute | Type | Description |
|---|---|---|
gen_ai.usage.input_tokens | int | Total input tokens (including cached) |
gen_ai.usage.input_tokens.cached | int | Subset served from cache |
gen_ai.usage.input_tokens.cache_write | int | Tokens written to cache (Anthropic) |
gen_ai.usage.output_tokens | int | Total output tokens (including reasoning) |
gen_ai.usage.output_tokens.reasoning | int | Subset for chain-of-thought (o3, etc.) |
gen_ai.usage.total_tokens | int | Sum of input + output |
Cached and reasoning tokens are subsets of totals, not additive. Incorrect reporting produces wrong cost calculations.
Agent Workflow Hierarchy
Transaction
└── gen_ai.invoke_agent "Weather Agent"
├── gen_ai.request "chat claude-sonnet-4-6"
├── gen_ai.execute_tool "get_weather"
├── gen_ai.request "chat claude-sonnet-4-6" ← follow-up
└── gen_ai.execute_tool "format_report"Streaming
| Integration | Streaming | Token counts in streams |
|---|---|---|
| OpenAI | Yes | Requires stream_options: { include_usage: true } |
| Anthropic | Yes | Automatic |
| Vercel AI SDK | Yes | Automatic (with experimental_telemetry) |
| LangChain | Yes | Tracked |
Manual gen_ai.* | Yes | Set token counts after stream completes |
Unsupported Providers
| Provider | Workaround |
|---|---|
| Cohere | Manual gen_ai.* spans |
| AWS Bedrock | Manual gen_ai.* spans |
| Mistral | Manual gen_ai.* spans |
| Groq | Manual gen_ai.* spans |
Sampling Strategy
If tracesSampleRate < 1.0, see the AI sampling guide.
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/node";
// Set at the start of a conversation
Sentry.setConversationId("conv_abc123");
// All subsequent AI calls carry gen_ai.conversation.id: "conv_abc123"
await openai.chat.completions.create({
model: "gpt-5.5",
messages: [{ role: "user", content: "Hello" }],
});
// Later turns in the same conversation are linked automatically
await openai.chat.completions.create({
model: "gpt-5.5",
messages: [
{ role: "user", content: "Hello" },
{ role: "assistant", content: "Hi there!" },
{ role: "user", content: "What's the weather?" },
],
});A single conversation can span multiple traces (e.g., page refresh), and a single trace can contain multiple conversations.
Troubleshooting
| Issue | Solution |
|---|---|
| No AI spans appearing | Verify tracesSampleRate > 0; check SDK >=10.53.0 |
| Token counts missing in streams | Add stream_options: { include_usage: true } (OpenAI) |
| Vercel AI spans not tracked | Add experimental_telemetry: { isEnabled: true } per call |
| Browser OpenAI not traced | Use Sentry.instrumentOpenAiClient() - auto-instrumentation is server-only |
| Prompts not captured | Set sendDefaultPii: true, or explicitly pass recordInputs: true / recordOutputs: true to the integration |
| AI Agents Dashboard empty | Ensure traces are being sent; check DSN and tracesSampleRate |
| Wrong cost calculations | Cached/reasoning tokens are subsets of totals, not additions |
| Conversations view empty | Ensure streamGenAiSpans: true, sendDefaultPii: true, and a conversation ID is set via Sentry.setConversationId() |
Crons / Job Monitoring — Sentry Node.js SDK
Minimum SDK:@sentry/node≥7.51.1 (captureCheckIn,withMonitor)
instrumentNodeCron/instrumentCron: ≥7.92.0
instrumentNodeSchedule: ≥7.93.0failureIssueThreshold/recoveryThreshold: ≥8.7.0
isolateTraceinMonitorConfig: ≥10.28.0
Status: ✅ Generally Available
---
Overview
Sentry Crons (job monitoring) tracks whether scheduled tasks run on time, succeed, and complete within expected durations. Sentry will alert when a job:
- Misses its scheduled start time (checkin margin exceeded)
- Takes too long to complete (maxRuntime exceeded)
- Fails (status
"error")
---
Core API
Sentry.captureCheckIn(checkIn, monitorConfig?) → string
// Three check-in shapes:
// 1. Heartbeat — single-shot, no duration tracking
Sentry.captureCheckIn({ monitorSlug: "my-job", status: "ok" });
Sentry.captureCheckIn({ monitorSlug: "my-job", status: "error" });
// 2. In-progress — signals job started, returns ID for completion
const checkInId = Sentry.captureCheckIn({
monitorSlug: "my-job",
status: "in_progress",
});
// 3. Finished — completes an in-progress check-in
Sentry.captureCheckIn({
checkInId,
monitorSlug: "my-job",
status: "ok", // or "error"
duration: 12.3, // optional, in seconds
});Sentry.withMonitor(slug, callback, monitorConfig?) → T
Wraps a sync or async function. Automatically sends in_progress, then ok on success or error on throw. Records duration automatically.
// Simple form
await Sentry.withMonitor("my-job", async () => {
await runJob();
});
// With monitor config
await Sentry.withMonitor("my-job", async () => {
await runJob();
}, {
schedule: { type: "crontab", value: "0 * * * *" },
checkinMargin: 5,
maxRuntime: 30,
timezone: "America/New_York",
});---
Check-In Status Values
| Status | Meaning |
|---|---|
"in_progress" | Job has started; Sentry waiting for completion |
"ok" | Job completed successfully |
"error" | Job failed; triggers incident in Sentry |
---
Usage Patterns
Heartbeat (simplest — detect missed runs only)
try {
await runMyJob();
Sentry.captureCheckIn({ monitorSlug: "my-cron-job", status: "ok" });
} catch (err) {
Sentry.captureCheckIn({ monitorSlug: "my-cron-job", status: "error" });
throw err;
}Start/Finish (tracks in-progress state and duration)
const checkInId = Sentry.captureCheckIn({
monitorSlug: "my-cron-job",
status: "in_progress",
});
const startTime = Date.now();
try {
await runMyJob();
Sentry.captureCheckIn({
checkInId,
monitorSlug: "my-cron-job",
status: "ok",
duration: (Date.now() - startTime) / 1000, // seconds
});
} catch (err) {
Sentry.captureCheckIn({
checkInId,
monitorSlug: "my-cron-job",
status: "error",
duration: (Date.now() - startTime) / 1000,
});
throw err;
}withMonitor (recommended — handles all status logic)
await Sentry.withMonitor("my-cron-job", async () => {
await runMyJob();
});---
Monitor Configuration (Upsert)
Pass a MonitorConfig to create or update the monitor from code — no manual setup in Sentry UI needed. On first check-in the monitor is created; subsequent calls update it.
interface MonitorConfig {
schedule: CrontabSchedule | IntervalSchedule; // REQUIRED
checkinMargin?: number; // minutes: grace period before "missed" alert
maxRuntime?: number; // minutes: max execution before timeout alert
timezone?: string; // IANA timezone, e.g. "America/New_York"
failureIssueThreshold?: number; // consecutive failures before creating issue (≥8.7.0)
recoveryThreshold?: number; // consecutive OKs before resolving issue (≥8.7.0)
isolateTrace?: boolean; // new trace per monitor run (≥10.28.0)
}Pass as the second argument to captureCheckIn() or third argument to withMonitor().
---
Schedule Types
Crontab schedule:
{ type: "crontab", value: "* * * * *" } // every minute
{ type: "crontab", value: "0 * * * *" } // every hour
{ type: "crontab", value: "0 4 * * *" } // daily at 4:00am
{ type: "crontab", value: "0 9 * * MON-FRI" } // weekdays at 9am
{ type: "crontab", value: "0 0 1 * *" } // first of every month
{ type: "crontab", value: "*/15 * * * *" } // every 15 minutesInterval schedule:
{ type: "interval", value: 1, unit: "hour" }
{ type: "interval", value: 30, unit: "minute" }
{ type: "interval", value: 7, unit: "day" }
{ type: "interval", value: 1, unit: "week" }
// unit: "year" | "month" | "week" | "day" | "hour" | "minute"---
Full Upsert Example
const monitorConfig: Sentry.MonitorConfig = {
schedule: {
type: "crontab",
value: "0 4 * * *", // daily at 4am
},
checkinMargin: 5, // alert if not started within 5 min
maxRuntime: 30, // alert if running > 30 min
timezone: "America/New_York",
failureIssueThreshold: 3, // create issue after 3 consecutive failures
recoveryThreshold: 2, // resolve issue after 2 consecutive successes
isolateTrace: true, // SDK ≥10.28.0
};
await Sentry.withMonitor("daily-report-job", generateDailyReport, monitorConfig);---
Scheduler Library Integrations
node-cron (SDK ≥7.92.0)
import cron from "node-cron";
import * as Sentry from "@sentry/node";
const cronWithCheckIn = Sentry.cron.instrumentNodeCron(cron);
// `name` option is REQUIRED — becomes the monitor slug
cronWithCheckIn.schedule(
"* * * * *",
() => { /* task */ },
{ name: "my-cron-job" },
);cron package — CronJob (SDK ≥7.92.0)
import { CronJob } from "cron";
import * as Sentry from "@sentry/node";
// Monitor slug is bound at instrumentation time
const CronJobWithCheckIn = Sentry.cron.instrumentCron(CronJob, "my-cron-job");
// Constructor form
const job = new CronJobWithCheckIn("* * * * *", () => { /* task */ });
// Static factory form
const job2 = CronJobWithCheckIn.from({
cronTime: "* * * * *",
onTick: () => { /* task */ },
});node-schedule (SDK ≥7.93.0)
import * as schedule from "node-schedule";
import * as Sentry from "@sentry/node";
const scheduleWithCheckIn = Sentry.cron.instrumentNodeSchedule(schedule);
// First argument must be the job name (monitor slug)
scheduleWithCheckIn.scheduleJob(
"my-cron-job", // monitor slug — REQUIRED as first arg
"* * * * *", // cron expression
() => { /* task */ },
);Agenda (no official helper — use withMonitor)
import Agenda from "agenda";
import * as Sentry from "@sentry/node";
const agenda = new Agenda({ db: { address: "mongodb://localhost/agenda" } });
agenda.define("send-newsletter", async (job) => {
await Sentry.withMonitor(
"send-newsletter",
async () => { await sendNewsletterEmails(); },
{
schedule: { type: "crontab", value: "0 8 * * MON" },
maxRuntime: 60,
timezone: "UTC",
},
);
});Bull / BullMQ (no official helper — use withMonitor)
import { Worker } from "bullmq";
import * as Sentry from "@sentry/node";
const worker = new Worker("report-queue", async (job) => {
await Sentry.withMonitor(
"report-queue-processor",
async () => { await processReportJob(job.data); },
{
schedule: { type: "interval", value: 5, unit: "minute" },
maxRuntime: 10,
checkinMargin: 2,
},
);
});---
Library Integration Signatures
// node-cron
function instrumentNodeCron<T>(
lib: Partial<NodeCron> & T,
monitorConfig?: Pick<MonitorConfig, "isolateTrace">,
): T;
// cron package
function instrumentCron<T>(
lib: T & CronJobConstructor,
monitorSlug: string, // REQUIRED — single slug for all jobs from this constructor
): T;
// node-schedule
function instrumentNodeSchedule<T>(
lib: T & NodeSchedule,
): T;---
Serverless / Lambda
Check-ins are lost if the process terminates before flush. Always flush explicitly:
export const handler = async (event: any) => {
const checkInId = Sentry.captureCheckIn({
monitorSlug: "lambda-scheduled-job",
status: "in_progress",
});
try {
await doWork(event);
Sentry.captureCheckIn({
checkInId,
monitorSlug: "lambda-scheduled-job",
status: "ok",
});
} catch (err) {
Sentry.captureCheckIn({
checkInId,
monitorSlug: "lambda-scheduled-job",
status: "error",
});
throw err;
} finally {
// CRITICAL — flush before Lambda container freezes
await Sentry.flush(2000);
}
};For AWS Lambda, prefer @sentry/aws-serverless — it handles flushing automatically.
---
Deno: Native Cron Integration
Deno provides a built-in Deno.cron() API. Use denoCronIntegration to automatically monitor all native Deno crons:
import * as Sentry from "@sentry/deno";
Sentry.init({
dsn: Deno.env.get("SENTRY_DSN"),
integrations: [
Sentry.denoCronIntegration(),
],
});
// Automatically monitored — no manual check-ins needed
Deno.cron("daily-cleanup", "0 0 * * *", async () => {
await cleanupOldRecords();
});
Deno.cron("hourly-sync", "0 * * * *", async () => {
await syncExternalData();
});The integration intercepts Deno.cron() calls and wraps them with automatic in_progress → ok/error check-ins. The monitor slug is the first argument to Deno.cron().
Deno Deploy: Deno.cron() runs natively on Deno Deploy. The integration works in both local Deno and Deno Deploy environments.Node.js and Bun:denoCronIntegrationis only available in@sentry/deno. For Node.js, use thenode-cron,cron, ornode-schedulelibrary helpers above.
---
Rate Limits
- Maximum 6 check-ins per minute per monitor + environment combination
- Each environment (production, staging, etc.) counts separately
- Dropped check-ins appear in Sentry's Usage Stats page
---
Troubleshooting
| Problem | Likely Cause | Fix |
|---|---|---|
| Monitor not created in Sentry | No MonitorConfig passed | Pass schedule in monitorConfig (upsert) |
| Check-ins not arriving | Process exits before flush | Add await Sentry.flush(2000) before exit |
withMonitor status wrong | Callback doesn't throw on failure | Ensure your job throws on error |
node-cron job not tracked | Missing name option | Add { name: "slug" } to cron.schedule() options |
instrumentNodeSchedule slug not set | First arg is cron expression | First arg to scheduleJob() must be the job name |
instrumentCron tracking wrong job | Multiple jobs from one constructor | Each constructor call is for one slug; create multiple if needed |
Duration always undefined | Using heartbeat pattern | Switch to start/finish or withMonitor for duration tracking |
Missing failureIssueThreshold | SDK < 8.7.0 | Upgrade to ≥8.7.0 |
isolateTrace not recognized | SDK < 10.28.0 | Upgrade to ≥10.28.0 |
| Rate limit errors in logs | > 6 check-ins/min per monitor | Reduce check-in frequency or consolidate environments |
Error Monitoring — Sentry Node.js SDK
Minimum SDK: @sentry/node ≥8.0.0NestJS integration: @sentry/nestjs ≥8.0.0Bun integration:@sentry/bun≥8.0.0 (thin wrapper over@sentry/node)
Deno integration: npm:@sentry/deno (Deno 2+)---
The Instrument-First Rule
@sentry/node patches modules at import time via OpenTelemetry. The instrument file must be loaded before everything else — before your framework, before your database driver, before any HTTP client.
// instrument.js — loaded first
const Sentry = require("@sentry/node");
Sentry.init({
dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
release: "my-app@1.2.3",
environment: process.env.NODE_ENV ?? "production",
tracesSampleRate: 1.0,
sendDefaultPii: true,
});// app.js
require("./instrument"); // MUST be line 1
const express = require("express");
// ...ESM (Node 18.19+ / 19.9+):
// instrument.mjs
import * as Sentry from "@sentry/node";
Sentry.init({ dsn: "...", tracesSampleRate: 1.0 });# Launch with --import
node --import ./instrument.mjs app.mjs
# or:
NODE_OPTIONS="--import ./instrument.mjs" npm start---
What Is Captured Automatically
| Error Type | Captured? | Mechanism |
|---|---|---|
| Uncaught exceptions | ✅ Yes | process.on("uncaughtException") |
| Unhandled promise rejections | ✅ Yes | process.on("unhandledRejection") |
| Framework errors (Express, Fastify, Koa, Hapi, Connect) | ✅ Yes | Error handler middleware (see below) |
| NestJS non-HttpExceptions | ✅ Yes | SentryGlobalFilter |
| Caught + re-thrown errors | ✅ Yes | Bubbles to global handler |
| Caught + swallowed errors | ❌ No | Must call captureException manually |
HttpException in NestJS (4xx) | ❌ No | Treated as control flow by design |
The Core Rule
"If you catch an error and don't re-throw it, Sentry never sees it."
// ✅ Auto-captured — unhandled, bubbles up
throw new Error("Unhandled");
// ✅ Auto-captured — re-thrown
try {
await doSomething();
} catch (err) {
throw err;
}
// ❌ NOT captured — swallowed by graceful return
try {
await doSomething();
} catch (err) {
return res.status(500).json({ error: "Failed" }); // ← add captureException!
}
// ✅ Manually captured
try {
await doSomething();
} catch (err) {
Sentry.captureException(err);
return res.status(500).json({ error: "Failed" });
}---
Framework Error Handler Placement
Critical: placement rules differ per framework. Getting this wrong silently misses errors.
| Framework | Function | Placement | Async? |
|---|---|---|---|
| Express | setupExpressErrorHandler(app) | AFTER routes | No |
| Fastify | setupFastifyErrorHandler(app) | BEFORE routes | No |
| Koa | setupKoaErrorHandler(app) | FIRST middleware | No |
| Hapi | setupHapiErrorHandler(server) | Before routes | YES — must `await` |
| Connect | setupConnectErrorHandler(app) | BEFORE routes | No |
| NestJS | SentryGlobalFilter + SentryModule.forRoot() | AppModule providers | No |
---
Express
Error handler goes after all routes, before your own error handler.
require("./instrument");
const express = require("express");
const Sentry = require("@sentry/node");
const app = express();
app.use(express.json());
// ── Routes ──────────────────────────────────────────────────────
app.get("/", (req, res) => res.json({ ok: true }));
app.post("/orders", async (req, res, next) => {
try {
const result = await processOrder(req.body.orderId);
res.json(result);
} catch (err) {
next(err); // pass to error handlers
}
});
// ↓ Sentry AFTER routes, BEFORE your error handler
Sentry.setupExpressErrorHandler(app);
// Optional: capture only specific status codes
// Sentry.setupExpressErrorHandler(app, {
// shouldHandleError(error) {
// return !error.status || parseInt(String(error.status)) >= 500;
// },
// });
// ── Your error handler (runs after Sentry) ──────────────────────
app.use((err, req, res, next) => {
res.status(err.status || 500).json({ error: "Internal Server Error" });
});
app.listen(3000);---
Fastify
Error handler goes before routes. Internally registers a Fastify plugin using onError lifecycle hook.
require("./instrument");
const Fastify = require("fastify");
const Sentry = require("@sentry/node");
const app = Fastify({ logger: true });
// ↓ Sentry BEFORE routes
Sentry.setupFastifyErrorHandler(app);
// Optional: customize which errors are captured
// Sentry.setupFastifyErrorHandler(app, {
// shouldHandleError(error, request, reply) {
// return reply.statusCode >= 500;
// },
// });
app.get("/", async (request, reply) => ({ hello: "world" }));
app.get("/debug-sentry", async () => {
throw new Error("Test Fastify error!");
});
app.listen({ port: 3000 });---
Koa
Error handler goes as the first `app.use()` call, before any route.
require("./instrument");
const Koa = require("koa");
const Router = require("@koa/router");
const Sentry = require("@sentry/node");
const app = new Koa();
const router = new Router();
// ↓ Sentry FIRST middleware
Sentry.setupKoaErrorHandler(app);
router.get("/", async (ctx) => { ctx.body = { ok: true }; });
router.get("/debug-sentry", async () => { throw new Error("Test Koa error!"); });
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000);Note:setupKoaErrorHandlerhas noshouldHandleErroroption — it captures all errors.
---
Hapi
setupHapiErrorHandler is async — you must await it. Internally registers a Hapi lifecycle extension on onPreResponse.
require("./instrument");
const Sentry = require("@sentry/node");
const Hapi = require("@hapi/hapi");
const init = async () => {
const server = Hapi.server({ port: 3000, host: "localhost" });
// ↓ MUST be awaited!
await Sentry.setupHapiErrorHandler(server);
server.route({
method: "GET",
path: "/debug-sentry",
handler: () => { throw new Error("Test Hapi error!"); },
});
await server.start();
console.log("Server running on %s", server.info.uri);
};
init();Caution: Forgetting await silently skips Sentry registration. No error is thrown.---
Connect
Error handler goes before routes.
require("./instrument");
const connect = require("connect");
const Sentry = require("@sentry/node");
const app = connect();
// ↓ Sentry BEFORE routes
Sentry.setupConnectErrorHandler(app);
app.use("/", (req, res, next) => {
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({ hello: "world" }));
});
app.use("/debug-sentry", (req, res, next) => {
throw new Error("Test Connect error!");
});
// Your own error handler (after Sentry)
app.use((err, req, res, next) => {
res.statusCode = err.status || 500;
res.end("Internal Server Error");
});
require("http").createServer(app).listen(3000);---
NestJS
NestJS has a dedicated skill: [`sentry-nestjs-sdk`](../sentry-nestjs-sdk/SKILL.md)
>
NestJS uses a separate package (@sentry/nestjs) with NestJS-native error handlingviaSentryGlobalFilter,SentryModule.forRoot(),@SentryExceptionCaptureddecorator,
and GraphQL/Microservices support. Load that skill for complete NestJS error monitoring
setup including HttpException filtering, custom filters, and background job isolation.---
Vanilla Node.js (http Module)
No framework integration needed — rely on the global uncaughtException / unhandledRejection handlers plus manual captureException for caught errors.
require("./instrument");
const http = require("http");
const Sentry = require("@sentry/node");
const server = http.createServer(async (req, res) => {
try {
const data = await handleRequest(req);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(data));
} catch (err) {
Sentry.captureException(err, {
tags: { path: req.url, method: req.method },
});
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Internal Server Error" }));
}
});
server.listen(3000);---
captureException — Full API
function captureException(
exception: unknown,
captureContext?: CaptureContext | ((scope: Scope) => Scope)
): string; // returns EventId
type CaptureContext = Scope | Partial<ScopeContext> | ((scope: Scope) => Scope);
interface ScopeContext {
user?: User;
level?: "fatal" | "error" | "warning" | "log" | "info" | "debug";
extra?: Record<string, unknown>;
tags?: Record<string, Primitive>;
contexts?: Record<string, Record<string, unknown>>;
fingerprint?: string[];
}// Basic
Sentry.captureException(new Error("Something broke"));
// With inline CaptureContext
Sentry.captureException(error, {
level: "fatal",
tags: { order_id: orderId, payment_method: "stripe" },
user: { id: req.user.id, email: req.user.email },
extra: { requestBody: req.body, retryCount: 3 },
fingerprint: ["order-processing-failure", orderId],
contexts: {
order: { id: orderId, total: 99.99, currency: "USD" },
},
});
// Scope callback form — most flexible
Sentry.captureException(error, (scope) => {
scope.setTag("component", "payment");
scope.setLevel("error");
scope.setTransactionName("POST /orders");
return scope; // must return scope
});
// Non-Error values are accepted (but stack traces may be synthetic)
Sentry.captureException("something broke");
Sentry.captureException({ code: "AUTH_FAILED", userId: 42 });
// Capture and use the returned event ID
const eventId = Sentry.captureException(err);
res.status(500).json({ error: "Something went wrong", eventId });---
captureMessage
function captureMessage(
message: string,
captureContext?: CaptureContext | SeverityLevel
): string; // returns EventId
type SeverityLevel = "fatal" | "error" | "warning" | "log" | "info" | "debug";// Basic
Sentry.captureMessage("Payment gateway timeout");
// With severity level shorthand
Sentry.captureMessage("Disk usage above 90%", "warning");
Sentry.captureMessage("Cache miss rate critical", "fatal");
Sentry.captureMessage("User signed up", "info");
// With full context
Sentry.captureMessage("Rate limit exceeded", {
level: "warning",
tags: { service: "api-gateway", region: "us-east-1" },
user: { id: req.user.id },
extra: { requestsPerMinute: 1500, limit: 1000 },
fingerprint: ["rate-limit", req.headers["x-client-id"]],
});---
Scope Management (v8+ — Hub Removed)
In SDK v8, Hub is removed. Use the three scope types directly.
| Scope | Accessor | Lifetime | Use For |
|---|---|---|---|
| Global | Sentry.getGlobalScope() | Process lifetime | App version, region, build ID |
| Isolation | Sentry.getIsolationScope() | Per HTTP request (auto-forked) | User identity, request metadata |
| Current | Sentry.getCurrentScope() | Narrow/temporary | Per-operation context |
Priority (current wins): Current > Isolation > Global
// All Sentry.setXXX() top-level methods write to the ISOLATION scope
Sentry.setTag("key", "value");
// identical to:
Sentry.getIsolationScope().setTag("key", "value");
// Global scope — survives the lifetime of the process
Sentry.getGlobalScope().setTag("server_region", "eu-west-1");
Sentry.getGlobalScope().setContext("runtime", {
name: "node",
version: process.version,
});withScope — Temporary Per-Capture Context
Primary tool for adding context to a single capture without contaminating other events.
Sentry.withScope((scope) => {
scope.setTag("payment_method", "stripe");
scope.setFingerprint(["stripe-payment-error"]);
scope.setLevel("error");
scope.setUser({ id: req.user.id });
scope.setContext("cart", { items: req.body.items.length });
scope.addBreadcrumb({ category: "payment", message: "Attempt #3", level: "info" });
Sentry.captureException(stripeError);
// All above ONLY applies to this one capture
});withIsolationScope — Full Isolation (Background Jobs)
Use for background jobs, workers, and queue processors where you need a completely clean scope.
Sentry.withIsolationScope(async (scope) => {
scope.setUser({ id: job.userId });
scope.setTag("job_type", job.type);
scope.setTag("job_id", job.id);
await processJob(job); // all events inside are fully isolated from other jobs
});Scope Decision Guide
| Goal | API |
|---|---|
| Data on ALL events (app version, build ID) | Sentry.getGlobalScope().setTag(...) |
| Current request data | Sentry.setTag(...) (writes to isolation scope) |
| One specific capture only | Sentry.withScope((scope) => { ... }) |
| Background job / worker | Sentry.withIsolationScope(async (scope) => { ... }) |
| Inline on a single event | Second arg to captureException(err, { tags: {...} }) |
---
Context Enrichment
setTag / setTags — Indexed, Searchable
Tags are indexed — use them for filtering, grouping, and alerting. Key: max 32 chars, [a-zA-Z0-9_.:−]. Value: max 200 chars.
Sentry.setTag("db_region", "us-east-1");
Sentry.setTag("feature_flag_new_ui", true);
Sentry.setTags({
service: "checkout",
version: "2.1.4",
region: "eu",
});setContext — Structured, Non-Searchable
Attaches structured data visible in the issue detail view. Not indexed. Normalized to 3 levels deep. The type key is reserved — don't use it.
Sentry.setContext("order", {
id: orderId,
total: 99.99,
currency: "USD",
coupon: "SAVE20",
});
Sentry.setContext("database", {
host: "postgres.internal",
query_duration_ms: 4523,
active_connections: 19,
});
Sentry.setContext("order", null); // clear itsetUser — User Identity
// On login (writes to isolation scope — safe per-request)
Sentry.setUser({
id: user.id,
email: user.email,
username: user.displayName,
subscription_tier: "pro", // custom fields accepted
});
// On logout
Sentry.setUser(null);
// Express middleware pattern — set per-request
app.use((req, res, next) => {
if (req.user) {
Sentry.setUser({ id: req.user.id, email: req.user.email });
}
next();
});setExtra / setExtras — Arbitrary Data
Non-indexed supplementary data. Prefer setContext for structured objects.
Sentry.setExtra("server_memory_mb", process.memoryUsage().heapUsed / 1024 / 1024);
Sentry.setExtras({
uptime: process.uptime(),
node_version: process.version,
});Tags vs Context vs Extra
| Feature | Searchable? | Indexed? | Best For |
|---|---|---|---|
| Tags | ✅ Yes | ✅ Yes | Filtering, grouping, alerting |
| Context | ❌ No | ❌ No | Structured debug info (nested objects) |
| Extra | ❌ No | ❌ No | Arbitrary debug values |
| User | ✅ Partially | ✅ Yes | User attribution and filtering |
---
Breadcrumbs
Automatic Breadcrumbs (Zero Config)
| Type | What's Captured |
|---|---|
http | Outgoing HTTP requests (URL, method, status code) |
console | console.log, warn, error calls |
db | Database queries (via OTel auto-instrumentation) |
Manual Breadcrumbs
Sentry.addBreadcrumb({
category: "auth",
message: `User ${user.email} authenticated`,
level: "info",
data: { method: "oauth2", provider: "google" },
});
Sentry.addBreadcrumb({
type: "http",
category: "http",
data: {
method: "POST",
url: "https://api.stripe.com/v1/charges",
status_code: 402,
},
level: "warning",
});
Sentry.addBreadcrumb({
type: "query",
category: "db.query",
message: "SELECT * FROM orders WHERE user_id = ?",
data: { db: "postgres", duration_ms: 42 },
});Breadcrumb Properties
| Key | Type | Values |
|---|---|---|
type | string | "default" \ |
category | string | Dot-notation: "auth", "db.query", "job.start" |
message | string | Human-readable description |
level | string | "fatal" \ |
timestamp | number | Unix timestamp (auto-set if omitted) |
data | object | Arbitrary key/value data |
---
beforeSend and Filtering Hooks
beforeSend — Modify or Drop Error Events
Last chance to modify or drop events. Runs after all event processors. Return null to drop. Only one `beforeSend` is allowed — use addEventProcessor for multiple processors.
Sentry.init({
beforeSend(event, hint) {
const err = hint.originalException;
// Drop in development
if (process.env.NODE_ENV === "development") return null;
// Drop specific error types
if (err instanceof ConnectionResetError) return null;
if (err?.message?.includes("ECONNRESET")) return null;
// Scrub PII from user object
if (event.user) {
delete event.user.email;
delete event.user.ip_address;
}
// Scrub sensitive keys from request body
if (event.request?.data) {
try {
const body = JSON.parse(event.request.data as string);
delete body.password;
delete body.token;
event.request.data = JSON.stringify(body);
} catch {}
}
// Custom fingerprint from error properties
if (err instanceof ApiError) {
event.fingerprint = ["api-error", String(err.statusCode), err.endpoint];
}
// Filter noisy breadcrumbs
if (event.breadcrumbs?.values) {
event.breadcrumbs.values = event.breadcrumbs.values.filter(
(bc) => !bc.data?.url?.includes("/health")
);
}
return event;
},
// Drop specific transaction/span events
beforeSendTransaction(event) {
if (event.transaction === "GET /health") return null;
if (event.transaction === "GET /ping") return null;
return event;
},
});beforeBreadcrumb — Filter or Mutate Breadcrumbs
Sentry.init({
beforeBreadcrumb(breadcrumb, hint) {
// Drop health-check HTTP requests
if (
breadcrumb.type === "http" &&
breadcrumb.data?.url?.includes("/health")
) {
return null;
}
// Redact auth tokens from URLs
if (breadcrumb.type === "http" && breadcrumb.data?.url) {
try {
const url = new URL(breadcrumb.data.url);
url.searchParams.delete("token");
url.searchParams.delete("api_key");
breadcrumb.data.url = url.toString();
} catch {}
}
return breadcrumb;
},
maxBreadcrumbs: 50, // default: 100
});ignoreErrors — Pattern-Based Filtering
Sentry.init({
ignoreErrors: [
"Non-Error exception captured",
/^ECONNRESET/,
/^ETIMEDOUT/,
/^socket hang up/,
],
ignoreTransactions: [
"GET /health",
"GET /ping",
"GET /metrics",
/^GET \/internal\//,
],
});---
Fingerprinting and Custom Grouping
All events have a fingerprint array. Events with the same fingerprint group into the same Sentry issue.
Per-Capture Fingerprinting
Sentry.captureException(error, {
fingerprint: ["payment-declined", req.body.payment_method],
});withScope Fingerprinting
Sentry.withScope((scope) => {
scope.setFingerprint([req.method, req.path, String(err.statusCode)]);
Sentry.captureException(err);
});beforeSend Fingerprinting (Global Rules)
Sentry.init({
beforeSend(event, hint) {
const err = hint.originalException;
// All DB connection errors → one group
if (err instanceof DatabaseConnectionError) {
event.fingerprint = ["database-connection-error"];
}
// Group ApiErrors by status + endpoint
if (err instanceof ApiError) {
event.fingerprint = ["api-error", String(err.statusCode), err.endpoint];
}
// Extend Sentry's default algorithm (keep stack-trace hash + add dimension)
if (err?.code) {
event.fingerprint = ["{{ default }}", err.code];
}
return event;
},
});Template Variables
| Variable | Description |
|---|---|
{{ default }} | Sentry's normally computed hash — extend rather than replace |
{{ transaction }} | Current transaction name |
{{ function }} | Top function in stack trace |
{{ type }} | Exception type name |
---
Event Processors
Unlike beforeSend (one allowed), multiple event processors can be registered. Order is not guaranteed. beforeSend always runs last.
// Runs on every event — enrich with deploy metadata
Sentry.addEventProcessor((event, hint) => {
event.tags = {
...event.tags,
git_sha: process.env.GIT_SHA ?? "unknown",
deployed_by: process.env.DEPLOY_USER ?? "unknown",
};
return event;
});
// Drop events with a custom ignore flag
Sentry.addEventProcessor((event, hint) => {
if ((hint.originalException as any)?.ignore_in_sentry === true) return null;
return event;
});
// Scope-level processor (only inside withScope callback)
Sentry.withScope((scope) => {
scope.addEventProcessor((event) => {
event.tags = { ...event.tags, batch_job: "true" };
return event;
});
Sentry.captureException(new Error("job failed"));
});`addEventProcessor` vs `beforeSend`:
addEventProcessor | beforeSend | |
|---|---|---|
| Count | Unlimited | One only |
| Order | Undefined (before beforeSend) | Always last |
| Async | ✅ Yes | ✅ Yes |
| Drop events | Return null | Return null |
---
requestDataIntegration — Per-Request Data
Auto-enabled. Attaches HTTP request data to all events during a request. Each framework auto-forks an isolation scope per request via OpenTelemetry AsyncLocalStorage — concurrent requests stay separate.
| Field | Captured | Notes |
|---|---|---|
url | ✅ Always | Full request URL |
method | ✅ Always | GET, POST, etc. |
headers | ✅ Always | Auth header scrubbed automatically |
query_string | ✅ Always | URL query params |
data (body) | ⚠️ Opt-in | Requires sendDefaultPii: true |
cookies | ⚠️ Opt-in | Requires sendDefaultPii: true |
ip_address | ⚠️ Opt-in | Requires sendDefaultPii: true |
Sentry.init({
sendDefaultPii: true,
integrations: [
Sentry.requestDataIntegration({
include: {
cookies: true,
data: true, // request body
headers: true,
ip: true,
query_string: true,
url: true,
user: { id: true, username: true, email: false },
},
}),
],
});---
Error Chains
linkedErrorsIntegration is auto-enabled and follows the standard Error.cause chain.
// Standard Error.cause — captured automatically
try {
await connectToDatabase();
} catch (dbError) {
throw new Error("Failed to process order", { cause: dbError });
// Sentry captures BOTH errors as a chain
}
// Configure depth (default: 5)
Sentry.init({
integrations: [
Sentry.linkedErrorsIntegration({ key: "cause", limit: 5 }),
],
});extraErrorDataIntegration — Custom Error Properties
Captures non-standard properties on Error subclasses:
Sentry.init({
integrations: [Sentry.extraErrorDataIntegration({ depth: 3 })],
});
class HttpError extends Error {
constructor(message, response) {
super(message);
this.statusCode = response.status; // captured in extras
this.responseBody = response.body; // captured in extras
this.endpoint = response.url; // captured in extras
}
}---
Lifecycle: Flush Before Shutdown
@sentry/node batches events and sends asynchronously. Always flush before process exit to avoid losing the last events.
// Graceful shutdown — HTTP server
process.on("SIGTERM", async () => {
server.close(async () => {
await Sentry.flush(2000); // wait up to 2s for queue to drain
process.exit(0);
});
});
// Serverless (Lambda, Cloud Functions) — close disables SDK after flush
export const handler = async (event) => {
try {
return await processEvent(event);
} catch (err) {
Sentry.captureException(err);
await Sentry.close(2000); // flush + disable before function freezes
throw err;
}
};---
Sentry.init() — Error-Relevant Options
Sentry.init({
// Identity
dsn?: string; // also: SENTRY_DSN env var
release?: string; // "my-app@1.2.3+abc123"
environment?: string; // default: "production"
serverName?: string; // hostname
enabled?: boolean; // default: true
// Sampling
sampleRate?: number; // 0.0–1.0 error event sample rate
tracesSampleRate?: number; // 0.0–1.0 transaction sample rate
// Data limits
maxBreadcrumbs?: number; // default: 100
maxValueLength?: number; // truncate long strings
normalizeDepth?: number; // default: 3
// Privacy
sendDefaultPii?: boolean; // default: false — enables body/cookies/IP
// Filtering
ignoreErrors?: Array<string | RegExp>;
ignoreTransactions?: Array<string | RegExp>;
// Hooks
beforeSend?: (event: Event, hint: EventHint) => Event | null;
beforeSendTransaction?: (event: Event, hint: EventHint) => Event | null;
beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | null;
// Node-specific
enableLogs?: boolean; // default: false — Sentry.logger.*
attachStacktrace?: boolean; // add stack traces to captureMessage
includeLocalVariables?: boolean; // include local vars in stack frames
onFatalError?: (error: Error) => void;
shutdownTimeout?: number; // default: 2000ms
});---
Bun
@sentry/bun is a thin wrapper over @sentry/node. The API is identical — use --preload instead of require("./instrument") first.
// instrument.ts
import * as Sentry from "@sentry/bun";
Sentry.init({
dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
tracesSampleRate: 1.0,
sendDefaultPii: true,
});bun --preload ./instrument.ts server.tsFor Bun.serve():
import * as Sentry from "@sentry/bun";
const server = Bun.serve({
port: 3000,
fetch(request) {
return new Response("Hello from Bun!");
},
error(error) {
Sentry.captureException(error); // manual — no setupErrorHandler for Bun.serve
return new Response("Internal Server Error", { status: 500 });
},
});Profiling:@sentry/profiling-nodeuses a native addon — incompatible with Bun's runtime. OmitnodeProfilingIntegration()in Bun apps.
---
Deno
// instrument.ts
import * as Sentry from "npm:@sentry/deno";
Sentry.init({
dsn: "https://examplePublicKey@o0.ingest.sentry.io/0",
tracesSampleRate: 1.0,
});// server.ts
import "../instrument.ts";
import * as Sentry from "npm:@sentry/deno";
Deno.serve({ port: 3000 }, async (request) => {
try {
return new Response("Hello from Deno!");
} catch (error) {
Sentry.captureException(error);
return new Response("Internal Server Error", { status: 500 });
}
});Requirements: Deno 2+. Run with--allow-net --allow-env --allow-read. NosetupExpressErrorHandlerequivalent — usetry/catch+captureException.
---
Quick Reference
// Init
Sentry.init({ dsn: "...", tracesSampleRate: 1.0 });
// Capture
Sentry.captureException(new Error("oops"));
Sentry.captureException(err, { tags: { source: "api" }, level: "fatal" });
Sentry.captureMessage("Something happened", "warning");
// Context (→ isolation scope, auto-forked per request)
Sentry.setUser({ id: 1, email: "user@example.com" });
Sentry.setTag("region", "us-east");
Sentry.setTags({ service: "checkout", version: "2.0" });
Sentry.setContext("cart", { items: 3, total: 49.99 });
Sentry.addBreadcrumb({ category: "auth", message: "login", level: "info" });
// Scoped capture (temporary context, one event only)
Sentry.withScope((scope) => {
scope.setTag("temp_tag", "only-this-event");
scope.setFingerprint(["my-custom-group"]);
scope.setLevel("warning");
Sentry.captureException(err);
});
// Background job isolation
await Sentry.withIsolationScope(async (scope) => {
scope.setUser({ id: job.userId });
await processJob(job);
});
// Global (all events, process lifetime)
Sentry.getGlobalScope().setTag("app", "my-api");
// Framework error handlers — placement matters!
Sentry.setupExpressErrorHandler(app); // Express: AFTER routes
Sentry.setupFastifyErrorHandler(app); // Fastify: BEFORE routes
Sentry.setupKoaErrorHandler(app); // Koa: FIRST middleware
await Sentry.setupHapiErrorHandler(server); // Hapi: BEFORE routes, MUST await
Sentry.setupConnectErrorHandler(app); // Connect: BEFORE routes
// Shutdown
await Sentry.flush(2000);---
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| Errors not appearing in Sentry | instrument.js loaded too late | Ensure it's the first require() or loaded via --import / --preload before app code |
| Express errors not captured | setupExpressErrorHandler placed before routes | Move it after all route definitions |
| Fastify errors not captured | setupFastifyErrorHandler placed after routes | Move it before route definitions (opposite of Express) |
| Hapi error handler silently fails | setupHapiErrorHandler not awaited | Must await Sentry.setupHapiErrorHandler(server) — it's the only async handler |
NestJS HttpException not captured | Intentional — SentryGlobalFilter skips control flow exceptions | Create a custom filter extending SentryGlobalFilter and override catch() to capture HttpException if desired |
setUser() leaks between requests | Using global scope for user data | Use Sentry.setUser() (isolation scope) — it's auto-forked per request by framework integrations |
withScope changes persisting | Wrong scope layer | withScope creates a temporary current scope — changes don't survive the callback. Use setTag() for request-lifetime data |
beforeSend returning wrong type | Not returning event or null | beforeSend must return the event object or null to drop — undefined causes silent failures |
| Breadcrumbs not showing | maxBreadcrumbs: 0 | Check init config — default is 100; set to desired max |
| Duplicate error events | Multiple capture paths | Ensure only one handler captures each error — e.g., don't both re-throw and call captureException |
| Stack traces show minified code | Source maps not uploaded | Configure @sentry/cli sourcemap upload in your build pipeline |
Logging — Sentry Node.js SDK
Minimum SDK: @sentry/node ≥9.41.0 (stable GA)First experimental: ≥9.10.0 (via _experiments.enableLogs)Console multi-arg parsing: ≥10.13.0
Consola reporter: ≥10.12.0
Scope attributes on logs: ≥10.32.0
Status: ✅ Generally Available
---
Overview
Sentry Logs are high-cardinality structured log entries that link directly to traces and errors. They let you answer why something broke, not just what broke.
Key characteristics:
- Sent as structured data — each attribute is individually searchable in Sentry UI
- Automatically linked to the active trace (if tracing is enabled)
- Buffered and batched (max 100 per buffer) — no per-log network overhead
- NOT a replacement for a logging library; designed to complement one
---
Initialization
enableLogs: true is required. Logging is disabled by default.
import * as Sentry from "@sentry/node";
Sentry.init({
dsn: process.env.SENTRY_DSN,
enableLogs: true, // REQUIRED — default: false
beforeSendLog: (log) => { // optional filter/transform
if (log.level === "debug") return null; // null = drop this log
return log;
},
});---
Logger API
All six methods live at Sentry.logger.*:
Sentry.logger.trace(message, attributes?, options?)
Sentry.logger.debug(message, attributes?, options?)
Sentry.logger.info(message, attributes?, options?)
Sentry.logger.warn(message, attributes?, options?)
Sentry.logger.error(message, attributes?, options?)
Sentry.logger.fatal(message, attributes?, options?)| Method | Severity # | When to Use |
|---|---|---|
trace | 1 | Fine-grained debugging, hot paths |
debug | 5 | Development diagnostics, variable dumps |
info | 9 | Normal operations, milestones, events |
warn | 13 | Potential issues, degraded state |
error | 17 | Failures that need attention |
fatal | 21 | Critical failures, service down |
Full TypeScript signature (same shape for all six methods):
function info(
message: ParameterizedString, // string or fmt`` tagged template
attributes?: Record<string, unknown>, // string | number | boolean values
options?: { scope?: Scope }, // optional scope override
): void;---
Basic Usage
Sentry.logger.trace("Entering function", { fn: "processOrder" });
Sentry.logger.debug("Cache lookup", { key: "user:123", hit: false });
Sentry.logger.info("Order created", { orderId: "order_456", total: 99.99 });
Sentry.logger.warn("Rate limit approaching", { current: 95, max: 100 });
Sentry.logger.error("Payment failed", { reason: "card_declined", userId: 42 });
Sentry.logger.fatal("Database unavailable", { host: "primary-db", port: 5432 });---
Parameterized Messages — Sentry.logger.fmt
Use the fmt tagged template literal to create parameterized messages. Interpolated values are extracted as individually searchable attributes in Sentry.
const userId = "user_123";
const productName = "Widget Pro";
Sentry.logger.info(
Sentry.logger.fmt`User ${userId} purchased ${productName}`,
);
// Stored in Sentry as:
// sentry.message.template → "User '%s' purchased '%s'"
// sentry.message.parameter.0 → "user_123"
// sentry.message.parameter.1 → "Widget Pro"You can combine fmt with additional attributes:
Sentry.logger.info(
Sentry.logger.fmt`Order ${orderId} placed by ${userId}`,
{ total: 149.99, itemCount: 3, region: "us-west-2" },
);fmt is an alias for Sentry.parameterize() internally. The returned string carries hidden __sentry_template_string__ and __sentry_template_values__ properties used by the SDK for serialization.
---
Structured Attributes
The second argument is a plain object. Values must be string, number, or boolean.
Sentry.logger.info("API request completed", {
userId: user.id,
userTier: user.plan, // "free" | "pro" | "enterprise"
endpoint: "/api/orders",
method: "POST",
statusCode: 200,
durationMs: 234,
orderValue: 149.99,
isBeta: true,
retryCount: 0,
});Attributes become filterable columns in the Sentry Logs view.
---
Scope-Based Attributes (SDK ≥10.32.0)
Set attributes on a scope once and they are automatically attached to every log emitted within that scope.
// Global scope — applies to all logs for the app's lifetime
Sentry.getGlobalScope().setAttributes({
service: "checkout-service",
version: "2.1.0",
region: "us-west-2",
});
// Isolation scope — unique per HTTP request (auto-created by HTTP integrations)
Sentry.getIsolationScope().setAttributes({
org_id: user.orgId,
user_tier: user.tier,
request_id: req.id,
});
// Current scope — single operation block
Sentry.withScope((scope) => {
scope.setAttribute("operation", "payment-processing");
scope.setAttribute("payment_method", "stripe");
Sentry.logger.info("Processing payment", { amount: 99.99 });
// → includes all scope attributes + the explicit { amount }
});---
Auto-Attached Attributes
The SDK automatically attaches these to every log:
| Attribute Key | Value |
|---|---|
sentry.environment | environment from Sentry.init() |
sentry.release | release from Sentry.init() |
sentry.sdk.name | e.g., "sentry.javascript.node" |
sentry.sdk.version | e.g., "10.42.0" |
server.address | Server hostname / server_name |
user.id | Current scope user ID (if set) |
user.name | Current scope username (if set) |
user.email | Current scope user email (if set) |
sentry.message.template | Parameterized template (when using fmt) |
sentry.message.parameter.N | Positional interpolated values |
---
Console Integration
Capture console.* calls as Sentry logs using the built-in integration (SDK ≥9.41.0):
Sentry.init({
dsn: process.env.SENTRY_DSN,
enableLogs: true,
integrations: [
Sentry.consoleLoggingIntegration({
levels: ["log", "warn", "error"],
// Default levels: ['debug','info','warn','error','log','trace','assert']
}),
],
});
// These now send to Sentry Logs automatically:
console.log("User action:", "checkout"); // → severity: info
console.warn("Memory pressure"); // → severity: warn
console.error("Unhandled rejection"); // → severity: errorMulti-argument parsing (args become message.parameter.N attributes) requires SDK ≥10.13.0.
---
Consola Integration (SDK ≥10.12.0)
import { createConsola } from "consola";
import * as Sentry from "@sentry/node";
const logger = createConsola();
logger.addReporter(Sentry.createConsolaReporter());
logger.info("This goes to Sentry Logs");
logger.error("This too");---
beforeSendLog Hook
Filter or transform logs before they are sent. Return null to drop:
Sentry.init({
dsn: process.env.SENTRY_DSN,
enableLogs: true,
beforeSendLog: (log) => {
// Drop debug logs in production
if (process.env.NODE_ENV === "production" && log.level === "debug") {
return null;
}
// Scrub sensitive fields
if (log.attributes?.credit_card) {
log.attributes.credit_card = "[REDACTED]";
}
// Add computed attributes
log.attributes = {
...log.attributes,
processed_at: Date.now(),
};
return log;
},
});The log object shape: { level, message, attributes, severityNumber }.
---
Third-Party Logger Bridges
Sentry does not provide official first-party transports for Winston, Pino, Bunyan, or Morgan. Use the patterns below to forward logs to Sentry.logger.*.
Winston:
import winston from "winston";
import * as Sentry from "@sentry/node";
const levelMap: Record<string, keyof typeof Sentry.logger> = {
silly: "trace", verbose: "debug", debug: "debug",
http: "info", info: "info", warn: "warn", error: "error",
};
const sentryTransport = new winston.transports.Stream({
stream: {
write: (message: string) => {
const parsed = JSON.parse(message);
const fn = levelMap[parsed.level] ?? "info";
(Sentry.logger[fn] as Function)(parsed.message, parsed.meta ?? {});
},
},
});
const logger = winston.createLogger({
format: winston.format.json(),
transports: [new winston.transports.Console(), sentryTransport],
});Pino:
import pino from "pino";
import * as Sentry from "@sentry/node";
const PINO_TO_SENTRY: Record<number, keyof typeof Sentry.logger> = {
10: "trace", 20: "debug", 30: "info",
40: "warn", 50: "error", 60: "fatal",
};
const dest = pino.destination({
write(chunk: string) {
const log = JSON.parse(chunk);
const fn = PINO_TO_SENTRY[log.level] ?? "info";
const { msg, level, time, pid, hostname, ...attrs } = log;
(Sentry.logger[fn] as Function)(msg, attrs);
},
});
const logger = pino({ level: "trace" }, dest);---
Trace Linking
Logs emitted during an active span automatically include sentry.trace.parent_span_id, linking them to the active trace. From a log in Sentry you can navigate to its parent trace; from a trace you can see all logs emitted during that request.
---
Log Buffering and Flushing
Logs are buffered in memory (max 100 per buffer: MAX_LOG_BUFFER_SIZE = 100). The buffer flushes automatically when full or on client.close().
For serverless or short-lived processes, flush explicitly before exit:
await Sentry.flush(2000); // flush with 2s timeout
await Sentry.close(2000); // flush + close all transports---
Troubleshooting
| Problem | Likely Cause | Fix |
|---|---|---|
| Logs not appearing in Sentry | enableLogs not set | Add enableLogs: true to Sentry.init() |
Sentry.logger is undefined | SDK < 9.41.0 | Upgrade to ≥9.41.0 |
| Attributes not searchable | Using complex objects | Use only string, number, boolean values |
| Console logs not captured | Missing integration | Add consoleLoggingIntegration() to integrations |
| Logs cut off in serverless | Buffer not flushed | Call await Sentry.flush(2000) before function returns |
fmt values not parameterized | Using string interpolation | Use tagged template: ` fmtmsg ${val} not "msg " + val` |
| Logs missing trace link | No active span | Enable tracing with tracesSampleRate |
beforeSendLog not firing | enableLogs: false | Logs are dropped before the hook if logging is disabled |
| Scope attributes missing from logs | SDK < 10.32.0 | Upgrade to ≥10.32.0 for scope attribute inheritance |
| Consola reporter not working | SDK < 10.12.0 | Upgrade to ≥10.12.0 |
Metrics — Sentry Node.js SDK
Minimum SDK:@sentry/node≥10.25.0 (stableSentry.metrics.*API)
enableMetricstop-level option: ≥10.24.0 (default:true)
beforeSendMetric hook: ≥10.24.0Scope attributes on metrics: ≥10.33.0
---
Overview
Sentry Metrics let you track counters, current values, and value distributions. They appear in Sentry alongside related errors and can be correlated with traces.
Key characteristics:
- Metrics are enabled by default — no configuration required for basic use
- Buffered in memory (max 1000 entries) and sent periodically
- High-cardinality attributes degrade backend performance — keep attribute cardinality low
- Use
Sentry.logger.*(not metrics) when you need per-user or per-request detail
---
Initialization
Metrics are on by default. Opt out if needed:
import * as Sentry from "@sentry/node";
Sentry.init({
dsn: process.env.SENTRY_DSN,
// Metrics are enabled by default — no config needed
// To disable entirely:
// enableMetrics: false,
beforeSendMetric: (metric) => { // optional filter/transform
if (metric.name === "debug_metric") return null;
return metric;
},
});---
Metrics API
Three methods. No increment() or set() — those do not exist in v10:
Sentry.metrics.count(name, value?, options?)
Sentry.metrics.gauge(name, value, options?)
Sentry.metrics.distribution(name, value, options?)Full TypeScript signatures:
interface MetricOptions {
unit?: string; // see unit table below
attributes?: Record<string, unknown>; // filterable dimensions
scope?: Scope; // optional scope override
}
function count(name: string, value?: number, options?: MetricOptions): void;
// value defaults to 1
function gauge(name: string, value: number, options?: MetricOptions): void;
function distribution(name: string, value: number, options?: MetricOptions): void;---
Metric Types
| Method | Underlying Type | Use For |
|---|---|---|
count | counter | Event frequency — requests, errors, signups |
gauge | gauge | Current snapshot value — queue depth, CPU % |
distribution | distribution | Value histograms/ranges — latencies, file sizes |
// count — how many times something happened
Sentry.metrics.count("user.signups", 1);
Sentry.metrics.count("api.errors", 1, { attributes: { endpoint: "/checkout" } });
// gauge — what the current state is
Sentry.metrics.gauge("queue.depth", 42);
Sentry.metrics.gauge("memory.usage", 512, { unit: "megabyte" });
// distribution — spread of a measured value
Sentry.metrics.distribution("api.latency", 187.5, { unit: "millisecond" });
Sentry.metrics.distribution("payload.size", 1024, { unit: "byte" });---
Units
Pass a unit string in MetricOptions. Used for display formatting in Sentry.
| Category | Unit Values |
|---|---|
| Time | millisecond, second, minute, hour, day, week |
| Storage | bit, byte, kilobyte, megabyte, gigabyte, terabyte, petabyte |
| Fractions | ratio, percent |
| None | none (or omit unit) |
Sentry.metrics.distribution("api.latency", 187.5, { unit: "millisecond" });
Sentry.metrics.distribution("job.duration", 3.2, { unit: "second" });
Sentry.metrics.gauge("memory.usage", 512, { unit: "megabyte" });
Sentry.metrics.gauge("cache.hit_rate", 0.87, { unit: "ratio" });
Sentry.metrics.gauge("disk.usage_pct", 72.4, { unit: "percent" });
Sentry.metrics.count("user.logins"); // omit unit for plain counts---
Attributes (Tags)
Use attributes in MetricOptions to add filterable/groupable dimensions.
Size limit: 2 KB per metric envelope. Metrics exceeding this are dropped.
Sentry.metrics.count("api.requests", 1, {
attributes: {
endpoint: "/api/orders",
method: "POST",
status_code: 200,
user_tier: "pro",
region: "us-west-2",
version: "v2",
},
});
Sentry.metrics.distribution("db.query_time", 45.3, {
unit: "millisecond",
attributes: {
table: "orders",
operation: "SELECT",
index_used: true,
rows_scanned: 1240,
},
});---
Cardinality
Keep attribute values bounded. High-cardinality attributes (per-user IDs, request UUIDs) cause performance issues in Sentry's metrics backend.
// ❌ HIGH CARDINALITY — avoid as metric attributes
Sentry.metrics.count("page.view", 1, {
attributes: { user_id: "uuid-abc-123" }, // millions of unique values
});
// ✅ LOW CARDINALITY — bounded enums and sets
Sentry.metrics.count("page.view", 1, {
attributes: {
page: "/dashboard",
user_tier: "pro", // bounded enum
ab_variant: "control", // bounded enum
region: "us-east-1", // bounded set
},
});Use Sentry.logger.* for per-user or per-request data — logs handle high cardinality gracefully.
---
Scope-Based Attributes (SDK ≥10.33.0)
Set attributes on a scope and they auto-attach to all metrics emitted within it:
// Global scope — applies to all metrics app-wide
Sentry.getGlobalScope().setAttributes({
service: "payments",
deploy_env: "production",
});
// Per-request scope
Sentry.withScope((scope) => {
scope.setAttribute("step", "checkout");
scope.setAttribute("user_tier", "enterprise");
// Both metrics inherit the scope attributes above
Sentry.metrics.count("checkout.attempts", 1);
Sentry.metrics.gauge("cart.value", 249.99);
});---
Auto-Attached Default Attributes
| Attribute | Value | Context |
|---|---|---|
sentry.environment | From Sentry.init({ environment }) | Always |
sentry.release | From Sentry.init({ release }) | Always |
sentry.sdk.name | SDK identifier | Always |
sentry.sdk.version | e.g., "10.42.0" | Always |
user.id, user.name, user.email | If user is set in scope | When set |
server.address | Server hostname | Server-side |
---
beforeSendMetric Hook
Filter or modify metrics before transmission. Return null to drop:
Sentry.init({
dsn: process.env.SENTRY_DSN,
beforeSendMetric: (metric) => {
// metric: { name, value, type, unit?, attributes? }
// Drop internal debug metrics
if (metric.name.startsWith("_internal.")) {
return null;
}
// Normalize metric names
metric.name = metric.name.toLowerCase().replace(/[^a-z0-9_.]/g, "_");
// Add global context
metric.attributes = {
...metric.attributes,
host: process.env.HOSTNAME,
deploy_id: process.env.DEPLOY_ID,
};
return metric;
},
});---
Flushing
Metrics are buffered (max MAX_METRIC_BUFFER_SIZE = 1000) and flushed periodically. For serverless or short-lived scripts, flush explicitly:
await Sentry.flush(2000); // flush + 2s timeout
await Sentry.close(2000); // flush + close all transports---
Troubleshooting
| Problem | Likely Cause | Fix |
|---|---|---|
| Metrics not appearing | SDK < 10.24.0 | Upgrade to ≥10.24.0 |
Sentry.metrics is undefined | SDK < 10.25.0 | Upgrade to ≥10.25.0 |
| Metrics silently dropped | Attribute envelope > 2 KB | Reduce number or size of attributes |
increment() not found | Renamed in v10 | Use count() instead |
set() not found | Removed in v10 | No equivalent; use count() with bounded attributes |
| Scope attributes missing | SDK < 10.33.0 | Upgrade to ≥10.33.0 |
| Metrics lost in serverless | Buffer not flushed | Call await Sentry.flush(2000) before function returns |
| High-cardinality issues | Unbounded attribute values | Keep attributes to bounded enums/sets |
Profiling — Sentry Node.js SDK
Node.js profiling:@sentry/profiling-node— must match your@sentry/nodeversion exactly
Bun and Deno are NOT supported. The profiler is a native C++ addon (node-gyp) that only runs in Node.js.---
Overview
Profiling captures V8 CPU call stacks at ~100 samples/second alongside your traces. Profiles attach to spans, giving you flame graphs to identify hot paths directly from a slow trace.
Profiling is Node.js only:
| Runtime | Profiling | Notes |
|---|---|---|
| Node.js ≥18 | ✅ | Full support via @sentry/profiling-node |
| Bun | ❌ | Native addon not supported |
| Deno | ❌ | Native addon not supported |
---
How Profiling Relates to Tracing
Profiles attach to spans — they require tracing to be enabled:
1. tracesSampleRate / tracesSampler decides whether a request is traced at all 2. profileSessionSampleRate decides whether the session opts into profiling 3. A profile is only collected when both sampling decisions are "yes"
tracesSampleRate: 0.1 + profileSessionSampleRate: 0.5
→ ~5% of requests will have both a trace AND a profile attachedIn trace lifecycle mode, you can drill from a slow span in the Performance UI directly into a flame graph:
Trace: "POST /api/checkout" (850ms)
├── "validateCart" (45ms) → [Profile attached] → shows DB driver hot paths
├── "processPayment" (620ms)
└── "updateInventory" (185ms) → [Profile attached] → shows ORM overhead---
Installation
npm install @sentry/profiling-node --save⚠️ Version pinning is required.@sentry/profiling-nodemust exactly match your@sentry/nodeversion. Mismatched versions cause silent failures or startup crashes.
# Both must be the same version
npm install @sentry/node@latest @sentry/profiling-node@latest---
SDK Configuration
Trace Mode (Recommended)
Profiles auto-attach to all sampled spans with no additional code:
import * as Sentry from "@sentry/node";
import { nodeProfilingIntegration } from "@sentry/profiling-node";
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [
nodeProfilingIntegration(),
],
tracesSampleRate: 1.0,
// Session-level sampling: decision made once at process startup
profileSessionSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
// "trace" = profiles auto-attach to every sampled span
profileLifecycle: "trace",
});Manual Mode
Start and stop profiling around specific code paths:
import * as Sentry from "@sentry/node";
import { nodeProfilingIntegration } from "@sentry/profiling-node";
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [nodeProfilingIntegration()],
tracesSampleRate: 1.0,
profileSessionSampleRate: 1.0,
profileLifecycle: "manual",
});
// Explicit start/stop around critical code:
Sentry.profiler.startProfiler();
await heavyComputation();
Sentry.profiler.stopProfiler();---
Continuous Profiling
For long-running processes, batch jobs, or background workers that don't map cleanly to request spans, use the profiler API directly:
import * as Sentry from "@sentry/node";
import { nodeProfilingIntegration } from "@sentry/profiling-node";
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [nodeProfilingIntegration()],
});
// Start profiling at process startup
Sentry.profiler.startProfiler();
// Profile is chunked automatically into ~60-second intervals and uploaded
// All spans created during this window have profile data attached
// Stop when done (e.g. graceful shutdown)
process.on("SIGTERM", () => {
Sentry.profiler.stopProfiler();
process.exit(0);
});Profile Chunk Architecture
The V2 profiler divides data into 60-second chunks that upload automatically:
Process lifetime:
┌─── 60 seconds ───┬─── 60 seconds ───┬─── 60 seconds ───┐
│ Chunk 1 │ Chunk 2 │ Chunk 3 │
│ Spans: [...] │ Spans: [...] │ Spans: [...] │
│ Sent at 60s │ Sent at 120s │ Sent at 180s │
└──────────────────┴──────────────────┴──────────────────┘This means there's no 30-second max like in the legacy V1 API — the profiler runs as long as your process does.
---
Profiler API Reference
Sentry.profiler.startProfiler()
Starts the V8 CpuProfiler. Call this once at process startup or before the code you want to profile.
Sentry.profiler.startProfiler();- No-ops gracefully if already running
- Takes effect immediately
- Overhead is low; safe to call at startup in production
Sentry.profiler.stopProfiler()
Stops the profiler and flushes remaining profile data to Sentry.
Sentry.profiler.stopProfiler();- Flushes the current in-progress chunk
- Call on graceful shutdown to avoid losing the last partial chunk
Manual Start/Stop Pattern
// Profile a specific batch job, not the entire process
async function runNightlyBatch() {
Sentry.profiler.startProfiler();
try {
await Sentry.startSpan({ op: "batch", name: "nightly-sync" }, async () => {
await syncUsers();
await syncOrders();
await syncInventory();
});
} finally {
Sentry.profiler.stopProfiler();
}
}---
Configuration Reference
| Parameter | Type | Description |
|---|---|---|
profileSessionSampleRate | 0.0–1.0 | Session-level sampling. Decision made once at process startup. |
profileLifecycle | `"trace" \ | "manual"` |
nodeProfilingIntegration() | integration | Enables V8 CpuProfiler. Must be in integrations array. |
profileSessionSampleRate Semantics
The profiling sampling decision is made once per process startup — not per request.
A "profiling session" either opts in or opts out for its entire lifetime. Within a profiling session, every traced span gets a profile attached (in trace mode).
// 10% of Node.js processes will profile all their requests
profileSessionSampleRate: 0.1profileLifecycle Modes
| Mode | Trigger | Best for |
|---|---|---|
"trace" | Auto-attached to every sampled span | Broad production coverage, web servers |
"manual" | startProfiler() / stopProfiler() | Batch jobs, specific hot paths, CLI tools |
---
Supported Platforms
Precompiled native binaries are available for:
| OS | Architecture | Node.js |
|---|---|---|
| macOS | x64 (Intel) | 18–24 |
| macOS | ARM64 (Apple Silicon) | 18–24 |
| Linux (glibc) | x64 | 18–24 |
| Linux (glibc) | ARM64 | 18–24 |
| Linux (musl/Alpine) | x64 | 18–24 |
| Linux (musl/Alpine) | ARM64 | 18–24 |
| Windows | x64 | 18–24 |
❌ FreeBSD, 32-bit systems, Bun, and Deno are not supported.
The native addon requires Node.js — it cannot run in other runtimes.
Alpine Linux / Docker
The musl libc variant is included automatically. If you see missing binary errors on Alpine:
# Ensure you're installing native dependencies
RUN npm install --include=optionalOr rebuild from source:
RUN apk add --no-cache python3 make g++ && npm rebuild @sentry/profiling-node---
Environment Variables
# Override profiler binary path (for custom builds or non-standard environments)
SENTRY_PROFILER_BINARY_PATH=/custom/path/sentry_cpu_profiler.node
# Override binary directory
SENTRY_PROFILER_BINARY_DIR=/path/to/dir
# Profiler logging mode:
# "eager" (default) — faster startProfiler calls, slightly more CPU overhead
# "lazy" — lower CPU overhead, slightly slower startProfiler
SENTRY_PROFILER_LOGGING_MODE=lazy node server.js---
Production Recommendations
Sentry.init({
integrations: [nodeProfilingIntegration()],
tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
profileSessionSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
profileLifecycle: "trace",
});Performance impact notes:
- Sampling rate (~100Hz): The V8 CpuProfiler adds CPU overhead. Test with realistic load before deploying
profileSessionSampleRate: 1.0to high-traffic production. - Memory: Each 60-second chunk uses ~10–20 MB of buffer. Capped at 50 chunks (~100 MB max).
- Network: One profiling upload per 60-second window per process.
"For high-throughput environments, we recommend testing prior to deployment to ensure that your service's performance characteristics maintain expectations." — Sentry docs
For high-traffic servers, start conservative:
// Start at 1–5% and increase after measuring overhead
profileSessionSampleRate: 0.01---
Complete Setup Example
// instrument.ts (loaded before app code)
import * as Sentry from "@sentry/node";
import { nodeProfilingIntegration } from "@sentry/profiling-node";
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [
nodeProfilingIntegration(),
],
tracesSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,
profileSessionSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,
profileLifecycle: "trace",
});// app.ts
import "./instrument"; // Must be first import
import express from "express";
import * as Sentry from "@sentry/node";
const app = express();
app.get("/api/users", async (req, res) => {
// Automatically traced + profiled (in profiling sessions)
const users = await db.query("SELECT * FROM users");
res.json(users);
});
Sentry.setupExpressErrorHandler(app);
app.listen(3000);---
Troubleshooting
| Issue | Solution |
|---|---|
| No profiles appearing in Sentry | Verify @sentry/profiling-node version exactly matches @sentry/node version (npm ls @sentry/profiling-node) |
Cannot find module '@sentry/profiling-node' | Run npm install @sentry/profiling-node and confirm it's in dependencies (not devDependencies) |
| Native addon fails to load | Check you're on Node.js ≥18; check OS/arch is in the supported platforms table |
| Profiles not linked to spans | Confirm profileLifecycle: "trace" is set and tracesSampleRate > 0; both are required |
| High CPU usage | Lower profileSessionSampleRate; use SENTRY_PROFILER_LOGGING_MODE=lazy |
| Alpine/musl Linux binary error | Run npm rebuild @sentry/profiling-node after installing build tools (apk add python3 make g++) |
| Profiling works locally but not in Docker | Ensure npm install --include=optional runs in the Docker build; musl variant must be present |
| Flame graphs show minified names | Upload source maps via authToken in Sentry config; use NODE_OPTIONS=--enable-source-maps |
| Last ~60s of data lost on shutdown | Call Sentry.profiler.stopProfiler() in your SIGTERM/SIGINT handler before process.exit() |
| Bun or Deno profiling doesn't work | Native addon only supports Node.js — profiling is not available in Bun or Deno |
Related skills
How it compares
Use sentry-node-sdk for guided server-side Sentry setup across Node.js, Bun, and Deno; pick frontend Sentry skills for browser or React SDK integration.
FAQ
Which package for Bun versus Node?
Use @sentry/bun for Bun and @sentry/node for Node.js with runtime-appropriate preload flags.
What if instrument.js already exists?
Merge Sentry init into the existing instrument file instead of overwriting it.
When use the NestJS skill instead?
When @nestjs/core is detected, switch to sentry-nestjs-sdk for native decorators and filters.
Is Sentry Node Sdk safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.