- Published on
Error Tracking in Production — Sentry, Source Maps, and the Alerts That Actually Matter
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
Introduction
Your users hit an error and close the app. You find out three weeks later when they mention it on Twitter. Sentry catches errors in real-time, groups them intelligently, and wakes you at the right times. This post covers setting up Sentry SDK with context injection, uploading source maps in CI so stack traces are readable, release tracking to know which deploy broke things, and alert routing that distinguishes between "page me now" and "log this for later."
- Sentry SDK Setup with Context
- Enriching Errors with Context
- Source Map Upload in CI
- Release Tracking
- Performance Monitoring with Transactions
- Alert Routing and Routing Rules
- Error Grouping and Fingerprinting
- Scrubbing PII from Events
- Error Volume vs. Unique Errors
- Checklist
- Conclusion
Sentry SDK Setup with Context
Install and initialize Sentry:
npm install @sentry/node @sentry/tracing
Configure in your application:
// src/sentry.ts
import * as Sentry from '@sentry/node';
import { nodeProfilingIntegration } from '@sentry/profiling-node';
export function initSentry() {
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
integrations: [
new Sentry.Integrations.Http({ breadcrumbs: true, tracing: true }),
new Sentry.Integrations.OnUncaughtException(),
new Sentry.Integrations.OnUnhandledRejection(),
nodeProfilingIntegration(),
],
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
profilesSampleRate: 0.1,
release: process.env.COMMIT_SHA, // Set from CI/CD
denyUrls: [
/extensions\//i,
/^chrome:\/\//i,
/bot|crawler/i,
],
ignoreErrors: [
// Ignore noisy errors
/random plugin/i,
/top\.GLOBALS/,
'NetworkError',
'TimeoutError',
],
maxBreadcrumbs: 50,
maxValueLength: 1024,
});
}
Add global error handler:
// src/error-handler.ts
import express from 'express';
import * as Sentry from '@sentry/node';
export function setupErrorHandling(app: express.Application) {
// Request handler captures request info
app.use(Sentry.Handlers.requestHandler());
// Tracing middleware
app.use(Sentry.Handlers.tracingHandler());
// Your routes
// Error handler must be last
app.use(Sentry.Handlers.errorHandler());
// Custom error handler for graceful error response
app.use(
(err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
Sentry.captureException(err, {
contexts: {
request: {
url: req.url,
method: req.method,
headers: req.headers,
},
},
});
res.status(500).json({
error: 'Internal server error',
errorId: (res as any).sentry,
});
}
);
}
Enriching Errors with Context
Attach user and request context to errors:
// src/middleware/sentry-context.ts
import express from 'express';
import * as Sentry from '@sentry/node';
export function attachSentryContext(
req: express.Request,
res: express.Response,
next: express.NextFunction
) {
if (req.user) {
Sentry.setUser({
id: req.user.id,
email: req.user.email,
username: req.user.username,
ip_address: req.ip,
});
}
Sentry.setTag('request_id', req.id);
Sentry.setTag('api_version', req.get('api-version') || '1.0');
Sentry.setContext('request', {
method: req.method,
url: req.url,
headers: {
'user-agent': req.get('user-agent'),
'accept-language': req.get('accept-language'),
},
query: req.query,
});
Sentry.setContext('environment', {
node_version: process.version,
npm_version: process.env.npm_version,
deployment_region: process.env.AWS_REGION,
});
next();
}
Capture additional context in business logic:
// src/services/order.ts
import * as Sentry from '@sentry/node';
export async function processOrder(orderId: string) {
Sentry.setContext('order', {
id: orderId,
status: 'processing',
items_count: 0,
total: 0,
});
try {
const order = await fetchOrder(orderId);
Sentry.setContext('order', {
id: order.id,
status: order.status,
items_count: order.items.length,
total: order.total,
customer_id: order.customerId,
payment_method: order.paymentMethod,
});
const payment = await processPayment(order);
Sentry.setContext('payment', {
transaction_id: payment.transactionId,
amount: payment.amount,
processing_time_ms: payment.processingTime,
success: payment.success,
});
return order;
} catch (error) {
// Error will include all context
throw error;
}
}
Source Map Upload in CI
Upload source maps so stack traces point to original code:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Create Sentry release
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: my-org
SENTRY_PROJECT: api-service
run: |
npm install -D @sentry/cli
# Create release
npx sentry-cli releases create \
--org $SENTRY_ORG \
--project $SENTRY_PROJECT \
${{ github.sha }}
# Upload source maps
npx sentry-cli releases files \
--org $SENTRY_ORG \
--project $SENTRY_PROJECT \
upload-sourcemaps \
--release ${{ github.sha }} \
--dist ${{ github.sha }} \
dist/
# Mark release as complete
npx sentry-cli releases finalize \
--org $SENTRY_ORG \
--project $SENTRY_PROJECT \
${{ github.sha }}
- name: Deploy
run: npm run deploy
Configure in your build process:
// webpack.config.js
import SentryWebpackPlugin from '@sentry/webpack-plugin';
export default {
mode: 'production',
output: {
filename: '[name].[contenthash].js',
sourceMapFilename: '[name].[contenthash].js.map',
},
devtool: 'source-map',
plugins: [
new SentryWebpackPlugin({
authToken: process.env.SENTRY_AUTH_TOKEN,
org: 'my-org',
project: 'api-service',
release: process.env.COMMIT_SHA,
dist: process.env.COMMIT_SHA,
sourceMaps: {
assets: ['dist/**'],
ignore: ['node_modules'],
},
cleanArtifacts: true,
urlPrefix: 'app:///',
}),
],
};
Release Tracking
Know which deploy introduced each error:
// src/sentry-setup.ts
import * as Sentry from '@sentry/node';
export function initSentryWithRelease() {
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
release: `api-service@${process.env.VERSION}`, // e.g., api-service@1.2.3
dist: process.env.BUILD_ID, // e.g., abc123def456
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
],
});
// Track deployment
if (process.env.NODE_ENV === 'production') {
Sentry.captureMessage('Deployment successful', 'info', {
contexts: {
deployment: {
version: process.env.VERSION,
build_id: process.env.BUILD_ID,
deployed_at: new Date().toISOString(),
deployed_by: process.env.DEPLOYED_BY || 'ci',
},
},
});
}
}
Link deployment to Sentry release:
# In your CI/CD deploy step
curl https://sentry.io/api/0/organizations/my-org/releases/$VERSION/deploys/ \
-H 'Authorization: Bearer $SENTRY_AUTH_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"environment": "production",
"name": "Production deployment",
"url": "https://github.com/my-org/repo/releases/tag/'$VERSION'",
"dateStarted": "'$(date -u +'%Y-%m-%dT%H:%M:%SZ')'",
"dateFinished": "'$(date -u +'%Y-%m-%dT%H:%M:%SZ')'",
"status": "success"
}'
Performance Monitoring with Transactions
Monitor request performance:
// src/tracing.ts
import * as Sentry from '@sentry/node';
import express from 'express';
export function setupPerformanceMonitoring(app: express.Application) {
app.use((req, res, next) => {
const transaction = Sentry.startTransaction({
name: `${req.method} ${req.path}`,
op: 'http.server',
source: 'url',
});
res.on('finish', () => {
transaction.setHttpStatus(res.statusCode);
transaction.end();
});
next();
});
}
// Monitor database queries
export function captureDbQuery(query: string, duration: number, params?: any[]) {
const span = Sentry.getActiveTransaction()?.startChild({
description: query,
op: 'db.query',
data: {
'db.rows_affected': 0,
'db.statement': query,
},
});
if (span) {
span.end();
}
// Alert if query is slow
if (duration > 1000) {
Sentry.captureMessage(`Slow database query: ${duration}ms`, 'warning', {
contexts: {
query: {
duration_ms: duration,
statement: query,
},
},
});
}
}
Alert Routing and Routing Rules
Configure different alerts for different issues:
// src/alert-routing.ts
import * as Sentry from '@sentry/node';
export async function setupAlertRouting() {
// Critical errors → PagerDuty
Sentry.init({
integrations: [
{
name: 'pagerduty-integration',
setup(client) {
client.on('beforeSend', (event) => {
if (isCriticalError(event)) {
triggerPagerDutyIncident(event);
}
return event;
});
},
},
],
});
}
function isCriticalError(event: Sentry.ErrorEvent): boolean {
const exceptions = event.exception?.values || [];
// Database connection errors → critical
if (
exceptions.some((e) =>
e.value?.includes('connection refused')
)
) {
return true;
}
// Payment processing errors → critical
if (event.message?.includes('payment_failed')) {
return true;
}
// Authentication errors → critical
if (
exceptions.some((e) =>
e.value?.includes('authentication_failed')
)
) {
return true;
}
// 5xx errors on main paths → critical
if (event.tags?.['http.status_code'] === '500') {
const path = event.request?.url;
if (path?.includes('/api/orders') || path?.includes('/api/checkout')) {
return true;
}
}
return false;
}
async function triggerPagerDutyIncident(event: Sentry.ErrorEvent) {
await fetch('https://events.pagerduty.com/v2/enqueue', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
routing_key: process.env.PAGERDUTY_ROUTING_KEY,
event_action: 'trigger',
payload: {
summary: event.message || 'Critical error in production',
severity: 'critical',
source: 'api-service',
custom_details: {
sentry_event_id: event.event_id,
sentry_url: `https://sentry.io/my-org/api-service/issues/${event.event_id}/`,
error_type: event.exception?.values?.[0]?.type,
},
},
}),
});
}
Configure Sentry alert routing in the dashboard:
# Alert rule: Critical database errors
name: Critical Database Error
conditions:
- filter:
match: error
condition:
- attribute: error.value
match: regex
value: 'connection refused|timeout|pool exhausted'
- filter:
match: error
condition:
- attribute: tags.severity
match: equals
value: critical
actions:
- service: pagerduty
integration: my-integration
action: trigger_incident
- service: slack
integration: alerts-critical
action: send_message
notify_every: 60 # Deduplicate similar errors
Error Grouping and Fingerprinting
Control how Sentry groups errors:
// src/error-fingerprinting.ts
import * as Sentry from '@sentry/node';
export function captureError(error: Error, context: any) {
Sentry.captureException(error, {
fingerprint: (event) => {
// Group by error type and API endpoint
const exception = event.exception?.values?.[0];
const path = event.request?.url;
if (exception?.type === 'ValidationError') {
return [exception.type, path];
}
// Group 404s by endpoint
if (event.tags?.['http.status_code'] === '404') {
return ['404', path];
}
// Group timeout errors by service
if (exception?.value?.includes('timeout')) {
return ['timeout', context.service];
}
// Default fingerprint (stack trace based)
return ['{{ default }}'];
},
contexts: context,
});
}
Scrubbing PII from Events
Prevent sensitive data from reaching Sentry:
// src/sentry-pii-scrubber.ts
import * as Sentry from '@sentry/node';
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
],
beforeSend(event) {
// Remove credit card numbers
const ccRegex = /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g;
if (event.request?.url) {
event.request.url = event.request.url.replace(ccRegex, '[REDACTED]');
}
if (event.message) {
event.message = event.message.replace(ccRegex, '[REDACTED]');
}
// Remove email addresses from exception messages
const emailRegex = /[\w\.-]+@[\w\.-]+\.\w+/g;
if (event.exception) {
for (const exc of event.exception.values || []) {
if (exc.value) {
exc.value = exc.value.replace(emailRegex, '[REDACTED_EMAIL]');
}
}
}
// Remove Authorization headers
if (event.request?.headers) {
delete event.request.headers['authorization'];
delete event.request.headers['x-api-key'];
delete event.request.headers['cookie'];
}
// Remove password fields from context
if (event.contexts?.request?.data) {
const data = event.contexts.request.data;
if (typeof data === 'object') {
delete data.password;
delete data.token;
delete data.secret;
}
}
return event;
},
});
Error Volume vs. Unique Errors
Distinguish signal from noise:
// Suppress high-volume, low-priority errors
Sentry.init({
beforeSend(event) {
// Ignore browser extension errors (1000s per day)
if (event.request?.url?.includes('chrome-extension://')) {
return null;
}
// Sample noisy user errors (keep 10%)
if (event.tags?.['error_type'] === 'network' && Math.random() > 0.1) {
return null;
}
// Keep all critical errors
if (event.tags?.['severity'] === 'critical') {
return event;
}
return event;
},
tracePropagationTargets: [
'localhost',
/^\//,
// Skip external API errors (noisy)
],
});
Checklist
- Initialize Sentry with DSN
- Add global error handler
- Attach user and request context
- Upload source maps in CI
- Track releases and deployments
- Configure performance monitoring
- Set up alert routing to PagerDuty
- Define custom error fingerprinting
- Scrub PII from events
- Monitor error volume and deduplicate
Conclusion
Error tracking is your early warning system. Sentry catches bugs before customers do. Source maps make stack traces useful. Release tracking pinpoints which deploy broke things. Alert routing prevents alert fatigue. Set it up on day one, not when you're firefighting. Your 2 AM self will thank you.