Error Tracking — Sentry & Distributed Error Context
Why Error Tracking Matters
Without error tracking:
User: "The app is broken"
Dev: "Works on my machine"
→ Hours spent reproducing
With error tracking:
Alert fires: "TypeError: Cannot read 'id' of undefined — 47 occurrences"
Stack trace: src/handlers/user.ts:82 ← UserService.findById ← GET /api/users/:id
Breadcrumbs: [login] → [fetch /users] → [click Edit] → ERROR
Context: user_id=123, version=2.4.1, browser=Safari 17, route=/dashboard
→ Fix in 10 minutesSentry Setup — Node.js
bashnpm install @sentry/node @sentry/profiling-nodetypescript// src/instrument.ts — import FIRST before anything else
import * as Sentry from '@sentry/node';
import { nodeProfilingIntegration } from '@sentry/profiling-node';
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
release: process.env.SENTRY_RELEASE ?? 'unknown',
// Performance monitoring
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
// Profile 10% of sampled transactions
profilesSampleRate: 0.1,
integrations: [
nodeProfilingIntegration(),
Sentry.httpIntegration(), // auto-instruments http module
Sentry.expressIntegration(), // auto-instruments Express
Sentry.prismaIntegration(), // auto-instruments Prisma queries
],
// Don't send errors for these
ignoreErrors: [
'ResizeObserver loop limit exceeded',
/^Network Error/,
],
beforeSend(event, hint) {
// Filter or modify events before sending
if (event.user?.email?.endsWith('@internal.example.com')) {
return null; // Don't send internal test errors
}
return event;
},
});typescript// src/app.ts — import instrument.ts first!
import './instrument';
import express from 'express';
import * as Sentry from '@sentry/node';
const app = express();
// Sentry request handler (must be FIRST middleware)
app.use(Sentry.expressRequestHandler());
app.use(express.json());
app.use(routes);
// Sentry error handler (must be LAST, after all routes)
app.use(Sentry.expressErrorHandler());
// Optional: custom error response after Sentry catches it
app.use((err, req, res, next) => {
res.status(500).json({
error: 'Internal server error',
eventId: res.sentry, // Sentry event ID — show to user for support
});
});Sentry Setup — Next.js
bashnpx @sentry/wizard@latest -i nextjs
# Auto-configures: sentry.client.config.ts, sentry.server.config.ts,
# sentry.edge.config.ts, next.config.js instrumentationtypescript// sentry.server.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 0.1,
// Automatically instruments: fetch, DB queries, Server Actions
});typescript// app/global-error.tsx — catches React render errors
'use client';
import * as Sentry from '@sentry/nextjs';
import NextError from 'next/error';
import { useEffect } from 'react';
export default function GlobalError({ error }: { error: Error }) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);
return (
<html>
<body>
<NextError statusCode={0} />
</body>
</html>
);
}Manual Error Capture
typescriptimport * as Sentry from '@sentry/node';
// Capture exception
try {
await processPayment(orderId);
} catch (err) {
Sentry.captureException(err, {
tags: {
payment_provider: 'stripe',
order_id: orderId,
},
extra: {
order_data: order,
},
level: 'error',
});
throw err; // re-throw if you want it to propagate
}
// Capture a message (non-exception)
Sentry.captureMessage('Payment took >5s', {
level: 'warning',
tags: { payment_id: paymentId },
});
// Set user context (for all errors from this user)
Sentry.setUser({
id: user.id,
email: user.email,
username: user.name,
});
// Add breadcrumb (trail of events leading to error)
Sentry.addBreadcrumb({
category: 'payment',
message: `Starting payment for order ${orderId}`,
level: 'info',
data: { amount: order.total },
});
// Scoped context (doesn't pollute global)
Sentry.withScope((scope) => {
scope.setTag('background_job', 'email-sender');
scope.setExtra('job_payload', payload);
Sentry.captureException(err);
});Performance Monitoring & Tracing
typescript// Manual transaction/spans
const transaction = Sentry.startTransaction({
name: 'process-order',
op: 'job',
});
Sentry.getCurrentScope().setSpan(transaction);
try {
const validateSpan = transaction.startChild({
op: 'validate',
description: 'Validate order data',
});
await validateOrder(order);
validateSpan.finish();
const chargeSpan = transaction.startChild({
op: 'payment',
description: 'Charge credit card',
});
await chargeCard(order);
chargeSpan.finish();
transaction.setStatus('ok');
} catch (err) {
transaction.setStatus('internal_error');
Sentry.captureException(err);
throw err;
} finally {
transaction.finish();
}Distributed Tracing
When a request spans multiple services, Sentry connects the traces.
typescript// Service A (API gateway) — outgoing HTTP request
const response = await fetch('http://users-service/api/user/123', {
headers: {
// Sentry auto-injects trace headers when using httpIntegration:
// sentry-trace: trace-id-value
// baggage: sentry-trace-id=..., sentry-transaction=...
},
});
// Service B (users-service) — picks up trace from headers
// Sentry.init with httpIntegration automatically continues the trace
// Both services show as connected spans in Sentry's performance viewSentry Trace View:
GET /api/orders/123 (api-gateway) 250ms
└── GET /users/456 (users-service) 80ms
└── SELECT users WHERE id=456 (postgres) 12ms
└── GET /products/789 (products-service) 140ms
└── GET products:789 (redis) 2ms [CACHE HIT]Source Maps — Making Stack Traces Readable
Without source maps, minified stack traces are unreadable.
Without source maps:
TypeError at a.b.c:1:4823
With source maps:
TypeError at src/services/UserService.ts:82:15
→ UserService.findByIdUpload Source Maps to Sentry
bash# Next.js — automatic with @sentry/nextjs
# Webpack — use Sentry webpack plugin
npm install @sentry/webpack-plugin --save-devtypescript// next.config.ts
import { withSentryConfig } from '@sentry/nextjs';
const nextConfig = { /* ... */ };
export default withSentryConfig(nextConfig, {
org: 'my-org',
project: 'my-project',
authToken: process.env.SENTRY_AUTH_TOKEN,
// Upload source maps to Sentry, delete from deploy
sourcemaps: {
deleteSourcemapsAfterUpload: true, // don't expose source maps publicly
},
// Automatically set release version
release: { name: process.env.VERCEL_GIT_COMMIT_SHA },
});yaml# GitHub Actions — set release
- name: Build
env:
SENTRY_RELEASE: ${{ github.sha }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: npm run build
- name: Create Sentry release
uses: getsentry/action-release@v1
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: my-org
SENTRY_PROJECT: my-project
with:
environment: production
version: ${{ github.sha }}Error Grouping & Alerting
Fingerprinting (Custom Grouping)
typescriptSentry.init({
beforeSend(event) {
// Custom fingerprint — group database errors by query type
if (event.exception?.values?.[0]?.type === 'DatabaseError') {
event.fingerprint = [
'database-error',
event.tags?.query_type ?? 'unknown',
];
}
return event;
},
});Alert Rules (in Sentry UI or code)
yaml# Example alert config (Sentry issue alerts):
- name: "High error rate"
conditions:
- type: event_frequency
value: 100
interval: 1h
actions:
- type: slack
channel: "#alerts-production"
- name: "New issue in production"
conditions:
- type: first_seen_event
filters:
- type: tagged_event
key: environment
value: production
actions:
- type: pagerdutyStructured Logging + Error Tracking Together
typescriptimport * as Sentry from '@sentry/node';
import pino from 'pino';
const logger = pino({
level: 'info',
// Add Sentry trace ID to every log line
mixin() {
const span = Sentry.getActiveSpan();
const traceId = span ? Sentry.spanToTraceHeader(span) : undefined;
return { traceId };
},
});
// Middleware to log + track
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info({
method: req.method,
path: req.path,
status: res.statusCode,
duration,
userId: req.user?.id,
});
if (res.statusCode >= 500) {
Sentry.captureMessage(`${req.method} ${req.path} → ${res.statusCode}`, {
level: 'error',
tags: { status_code: String(res.statusCode) },
});
}
});
next();
});Alternatives to Sentry
| Tool | Type | Strengths |
|---|---|---|
| Sentry | Error + Performance | Most popular, great DX, free tier |
| Datadog APM | Full observability | Metrics + traces + logs unified |
| New Relic | Full observability | Good for legacy/enterprise |
| Honeycomb | Traces + events | Great for distributed systems |
| Rollbar | Error tracking | Simple, good Node.js support |
| Bugsnag | Error tracking | Good for mobile + web |
| OpenTelemetry | Standard protocol | Vendor-neutral, integrates with all |
Interview Questions
Q: What information should you capture with an error? At minimum: stack trace, error message, timestamp, environment, release version. For web apps: user ID, browser/OS, URL, HTTP method. For API errors: request headers (minus auth), request body (sanitized), response status. Breadcrumbs — the trail of events leading up to the error. Never log PII (passwords, SSNs, credit cards) in error context.
Q: What are source maps and why do you delete them after upload?
Source maps map minified/compiled code back to original source — making stack traces readable (user.ts:82 instead of bundle.min.js:1:48293). You upload them to Sentry so Sentry can un-minify traces server-side. Then delete them from the deployed assets — if publicly accessible, they expose your source code (security risk).
Q: How does distributed tracing work in Sentry?
Each service propagates sentry-trace and baggage HTTP headers. When Service A calls Service B, Sentry instruments the outgoing fetch and injects these headers. Service B's Sentry SDK reads the headers and continues the same trace. In Sentry's UI, you see a unified trace waterfall showing time spent in each service/query — essential for finding the slow part in a multi-service request.
Q: How do you avoid alert fatigue from error tracking? Set appropriate thresholds (alert on spike, not every occurrence), group similar errors by fingerprint, mute known non-actionable errors, configure severity levels (warning vs error vs fatal), route to different channels (PagerDuty for SEV1, Slack for warnings). Review and close resolved issues regularly. Track error budget — if <0.1% of requests error, don't page at night.