From sentry-pack
Designs production-grade Sentry architecture for multi-service orgs: project topology, team alerts, centralized Node/TS config, distributed tracing, source maps.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin sentry-packThis skill is limited to using the following tools:
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.
Guides Sentry SDK selection and setup for error tracking, performance monitoring in monoliths, microservices, serverless, event-driven, SPAs, mobile, and hybrid architectures.
Creates and configures Sentry projects across staging/production environments via REST API. Extracts DSNs and generates SDK snippets for onboarding services to error monitoring.
Sets up full Sentry SDK for Node.js, Bun, and Deno runtimes with error monitoring, tracing, logging, profiling, metrics, crons, and AI monitoring for server-side JS/TS apps.
Share bugs, ideas, or general feedback.
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.
@sentry/node v8+ installed (npm install @sentry/node @sentry/profiling-node)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.
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
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
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))
);
}
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:
#!/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'] },
})],
};
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));
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;
}
}
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
lib/sentry.ts enforcing PII scrubbing, sample rates, and noise filters across all services| 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 |
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));
@sentry/browser) for frontend error-to-session correlationcount() by service, level)