sentry-reference-architectureClaude Skill
Manage best-practice Sentry architecture patterns.
| name | sentry-reference-architecture |
| description | Design production-grade Sentry architecture for multi-service organizations. Use when planning Sentry rollout, structuring projects across teams, building shared config modules, or setting up distributed tracing. Trigger: "sentry architecture", "sentry project structure", "sentry reference design", "sentry distributed tracing". |
| allowed-tools | Read, Write, Edit, Bash(npm:*), Bash(npx:*), Glob, Grep |
| version | 1.0.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
| compatible-with | claude-code, codex, openclaw |
| tags | ["saas","sentry","architecture","enterprise","distributed-tracing","microservices"] |
Sentry Reference Architecture
Overview
Enterprise Sentry architecture patterns for multi-service organizations. Covers centralized configuration, project topology, team-based alert routing, distributed tracing, error middleware, source map management, and a production-ready SentryService wrapper.
Prerequisites
- Sentry organization at sentry.io (Business plan+ for team features)
@sentry/nodev8+ installed (npm install @sentry/node @sentry/profiling-node)- Service inventory and team ownership documented
- Node.js 18+ (ESM and native fetch instrumentation)
Instructions
Step 1 — Project Structure Strategy
Pattern A: One Project Per Service (3+ services, recommended)
Organization: acme-corp
├── Team: platform-eng
│ ├── Project: api-gateway (Node/Express)
│ ├── Project: auth-service (Node/Fastify)
│ └── Project: user-service (Node/Express)
├── Team: payments
│ ├── Project: payment-api (Node/Express)
│ └── Project: billing-worker (Node worker)
└── Team: frontend
├── Project: web-app (React/Next.js)
└── Project: mobile-app (React Native)
Benefits: independent quotas, team-scoped alerts, per-service rate limits, isolated release tracking.
Pattern B: Shared Project (< 3 services, single team) — one project with Environment tags (production/staging/dev). Simpler setup; outgrow when alert noise exceeds one team.
Step 2 — Centralized Config Module
Create lib/sentry.ts imported by every service to enforce org-wide defaults:
// lib/sentry.ts import * as Sentry from '@sentry/node'; import { nodeProfilingIntegration } from '@sentry/profiling-node'; export interface SentryServiceConfig { serviceName: string; dsn: string; environment?: string; version?: string; tracesSampleRate?: number; ignoredTransactions?: string[]; } export function initSentry(config: SentryServiceConfig): void { const env = config.environment || process.env.NODE_ENV || 'development'; Sentry.init({ dsn: config.dsn, environment: env, release: `${config.serviceName}@${config.version || 'unknown'}`, serverName: config.serviceName, tracesSampleRate: config.tracesSampleRate ?? (env === 'production' ? 0.1 : 1.0), sendDefaultPii: false, maxBreadcrumbs: 50, integrations: [nodeProfilingIntegration()], ignoreErrors: [ 'ResizeObserver loop completed with undelivered notifications', /Loading chunk \d+ failed/, 'AbortError', ], tracesSampler: ({ name, parentSampled }) => { if (parentSampled !== undefined) return parentSampled; const ignored = config.ignoredTransactions || [ 'GET /health', 'GET /healthz', 'GET /ready', 'GET /metrics', ]; if (ignored.some(p => name.includes(p))) return 0; return config.tracesSampleRate ?? (env === 'production' ? 0.1 : 1.0); }, beforeSend(event) { if (event.request?.headers) { delete event.request.headers['authorization']; delete event.request.headers['cookie']; delete event.request.headers['x-api-key']; } return event; }, initialScope: { tags: { service: config.serviceName, team: process.env.TEAM_NAME || 'unassigned' }, }, }); } export { Sentry };
Bootstrap in each service:
import { initSentry } from '@acme/sentry-config'; initSentry({ serviceName: 'api-gateway', dsn: process.env.SENTRY_DSN! }); // Must run BEFORE other imports that need instrumentation
Step 3 — Error Handling Middleware
Express:
// lib/sentry-middleware.ts import * as Sentry from '@sentry/node'; import type { Request, Response, NextFunction, ErrorRequestHandler } from 'express'; export function sentryRequestHandler() { return (req: Request, _res: Response, next: NextFunction): void => { Sentry.setTag('http.route', req.route?.path || req.path); if (req.user) Sentry.setUser({ id: req.user.id }); next(); }; } export const sentryErrorHandler: ErrorRequestHandler = (err, req, res, _next) => { const status = (err as any).statusCode || 500; Sentry.withScope((scope) => { scope.setLevel(status >= 500 ? 'error' : 'warning'); scope.setTag('http.status_code', String(status)); scope.setContext('request', { method: req.method, url: req.originalUrl }); status >= 500 ? Sentry.captureException(err) : Sentry.captureMessage(err.message, 'warning'); }); res.status(status).json({ error: status >= 500 ? 'Internal server error' : err.message }); }; // Wire: app.use(sentryRequestHandler()) → routes → app.use(sentryErrorHandler)
FastAPI (Python):
import sentry_sdk, os from sentry_sdk.integrations.fastapi import FastApiIntegration def init_sentry(service_name: str) -> None: sentry_sdk.init( dsn=os.environ["SENTRY_DSN"], environment=os.getenv("ENV", "development"), release=f"{service_name}@{os.getenv('SERVICE_VERSION', 'unknown')}", traces_sample_rate=0.1, send_default_pii=False, integrations=[FastApiIntegration(transaction_style="endpoint")], before_send=lambda event, hint: _scrub(event), ) def _scrub(event): headers = event.get("request", {}).get("headers", {}) for k in ["authorization", "cookie", "x-api-key"]: headers.pop(k, None) return event
Step 4 — Distributed Tracing
HTTP (automatic): SDK v8 auto-propagates sentry-trace + baggage headers on fetch/http. All services in the same org link automatically.
Message queues (manual propagation):
// lib/sentry-queue.ts import * as Sentry from '@sentry/node'; export function publishWithTrace<T>(queue: string, payload: T, publish: Function) { return Sentry.startSpan({ name: `queue.publish.${queue}`, op: 'queue.publish' }, async () => { const headers: Record<string, string> = {}; const span = Sentry.getActiveSpan(); if (span) { headers['sentry-trace'] = Sentry.spanToTraceHeader(span); headers['baggage'] = Sentry.spanToBaggageHeader(span) || ''; } await publish(queue, { payload, headers }); }); } export function consumeWithTrace<T>(queue: string, msg: { payload: T; headers: Record<string, string> }, handler: (p: T) => Promise<void>) { return Sentry.continueTrace( { sentryTrace: msg.headers['sentry-trace'], baggage: msg.headers['baggage'] }, () => Sentry.startSpan({ name: `queue.process.${queue}`, op: 'queue.process' }, () => handler(msg.payload)) ); }
Step 5 — Team-Based Alert Routing
Configure in Project Settings > Ownership Rules:
# .sentry/ownership-rules path:src/payments/** #payments-team path:src/auth/** #platform-team url:*/api/v1/payments/* #payments-team tags.service:payment-api #payments-team * #platform-team
Alert tiers:
- P0 Critical: error rate > 50/min OR crash-free < 95% — PagerDuty on-call, 15 min SLA
- P1 Warning: new production issue or regression — Slack #alerts-prod, same-day SLA
- P2 Performance: P95 > 2s or Apdex < 0.7 — Slack #alerts-perf, next sprint
- P3 Info: new staging issue — Slack #alerts-staging, backlog triage
Step 6 — Source Map Uploads (Monorepo)
#!/usr/bin/env bash # scripts/upload-sourcemaps.sh — run in CI after build set -euo pipefail SERVICE="${1:?Usage: upload-sourcemaps.sh <service>}" RELEASE="${SERVICE}@$(git rev-parse --short HEAD)" npx @sentry/cli releases new "$RELEASE" --org "$SENTRY_ORG" --project "$SERVICE" npx @sentry/cli sourcemaps upload --org "$SENTRY_ORG" --project "$SERVICE" \ --release "$RELEASE" --url-prefix "~/" --validate "./services/${SERVICE}/dist/" npx @sentry/cli releases set-commits "$RELEASE" --org "$SENTRY_ORG" --auto npx @sentry/cli releases finalize "$RELEASE" --org "$SENTRY_ORG"
Webpack plugin alternative (auto-uploads on build):
import { sentryWebpackPlugin } from '@sentry/webpack-plugin'; export default { devtool: 'source-map', plugins: [sentryWebpackPlugin({ org: process.env.SENTRY_ORG, project: 'web-app', authToken: process.env.SENTRY_AUTH_TOKEN, sourcemaps: { filesToDeleteAfterUpload: ['./dist/**/*.map'] }, })], };
Step 7 — Custom Integrations
Wrap internal SDK calls with spans for tracing visibility:
import * as Sentry from '@sentry/node'; export function withSpan<T>(op: string, desc: string, fn: () => Promise<T>, attrs?: Record<string, string | number>): Promise<T> { return Sentry.startSpan({ name: desc, op, attributes: attrs }, async (span) => { try { const r = await fn(); span.setStatus({ code: 1, message: 'ok' }); return r; } catch (e) { span.setStatus({ code: 2, message: String(e) }); throw e; } }); } // Usage: await withSpan('payment.charge', 'charge $50', () => gateway.charge(5000, 'usd', custId));
Step 8 — SentryService Wrapper
Production wrapper with singleton, metrics, and graceful shutdown:
// lib/sentry-service.ts import * as Sentry from '@sentry/node'; export class SentryService { private static instance: SentryService | null = null; private initialized = false; private constructor(private config: { serviceName: string; dsn: string; version?: string }) {} static getInstance(config?: { serviceName: string; dsn: string; version?: string }): SentryService { if (!SentryService.instance) { if (!config) throw new Error('Config required on first call'); SentryService.instance = new SentryService(config); } return SentryService.instance; } init(): void { if (this.initialized) return; const { initSentry } = require('./sentry'); initSentry(this.config); this.initialized = true; } captureError(error: Error, ctx?: { tags?: Record<string, string>; extra?: Record<string, unknown>; level?: Sentry.SeverityLevel }): string { return Sentry.withScope((scope) => { if (ctx?.tags) Object.entries(ctx.tags).forEach(([k, v]) => scope.setTag(k, v)); if (ctx?.extra) Object.entries(ctx.extra).forEach(([k, v]) => scope.setExtra(k, v)); if (ctx?.level) scope.setLevel(ctx.level); return Sentry.captureException(error); }); } async trackOperation<T>(name: string, op: string, fn: () => Promise<T>): Promise<T> { return Sentry.startSpan({ name, op }, async (span) => { try { const r = await fn(); span.setStatus({ code: 1, message: 'ok' }); return r; } catch (e) { span.setStatus({ code: 2, message: String(e) }); Sentry.captureException(e); throw e; } }); } async shutdown(timeoutMs = 5000): Promise<void> { await Sentry.close(timeoutMs); SentryService.instance = null; this.initialized = false; } }
Step 9 — Health Check Exclusion
Define health routes BEFORE Sentry middleware so they never create transactions:
app.get('/health', (_req, res) => res.status(200).json({ status: 'ok' })); app.get('/readiness', async (_req, res) => { const dbOk = await checkDatabase(); res.status(dbOk ? 200 : 503).json({ db: dbOk }); }); // Combined with tracesSampler (Step 2) dropping /health, /ready, /metrics
Output
- Centralized
lib/sentry.tsenforcing PII scrubbing, sample rates, and noise filters across all services - Project-per-service topology with team ownership, independent quotas, and per-service releases
- Error middleware for Express and FastAPI with severity-based capture
- Distributed tracing across HTTP (automatic) and message queues (manual propagation helpers)
- Team-based alert routing with 4-tier escalation (P0-P3)
- Source map pipeline with CLI and Webpack plugin for monorepo builds
- SentryService singleton with custom metrics and graceful shutdown
Error Handling
| Error | Cause | Solution |
|---|---|---|
| Traces not linking cross-service | Missing trace headers in non-HTTP transport | Use publishWithTrace/consumeWithTrace for queues |
init() called multiple times | Multiple imports | Use SentryService singleton (idempotent init) |
| Source maps not resolving | Wrong url-prefix | Use --url-prefix "~/" and --validate flag |
| Alerts routing to wrong team | Ownership rules mismatch | Verify path: rules match source tree; add tags.service: fallback |
| Health checks consuming quota | Probes hitting instrumented routes | Define health routes before middleware; use tracesSampler |
| PII leaking | Missing beforeSend scrubbing | Enforce sendDefaultPii: false + header deletion in shared config |
Examples
Bootstrap a service (3 lines):
import { SentryService } from '@acme/sentry-config'; const sentry = SentryService.getInstance({ serviceName: 'user-service', dsn: process.env.SENTRY_DSN! }); sentry.init();
Capture with business context:
sentry.captureError(err, { tags: { 'payment.provider': 'stripe' }, extra: { orderId: order.id, amount: order.total }, level: 'error', });
Track an operation:
const result = await sentry.trackOperation('order.fulfillment', 'business.process', () => fulfillOrder(id));
Resources
- Sentry Node.js SDK v8
- Distributed Tracing
- Ownership Rules
- Source Maps CLI
- Alerting Best Practices
- Sentry Webpack Plugin
Next Steps
- Roll out incrementally — start with one high-traffic service, validate traces and alerts, then onboard remaining services
- Set up Sentry Crons for scheduled job monitoring (ETL, billing workers)
- Enable Session Replay (
@sentry/browser) for frontend error-to-session correlation - Define per-project quota budgets to prevent one noisy service from exhausting org quota
- Build Discover dashboards for cross-service error trends (
count() by service, level)
Similar Claude Skills & Agent Workflows
feishu-wiki
Feishu knowledge base navigation.
write-docs
Write BrowserOS feature documentation.
skill-developer
Create and manage Claude Code skills following Anthropic best practices.
documentation-lookup
This skill should be used when the user asks about libraries, frameworks, API references, or needs code examples.
教程美化方案
使用 VitePress 和 Element Plus 组件美化教程,提升可读性和交互性
material-component-doc
用于 FlowGram 物料库组件文档撰写的专用技能,提供组件文档生成、Story 创建、翻译等功能的指导和自动化支持