The Gap Between Dev and Prod
In development, errors are visible. In production, they're silent.
A user hits a bug, refreshes, gives up. You never know. Until enough users give up that your retention numbers drop.
Error monitoring closes that gap.
Sentry: The Standard
npm install @sentry/nextjs
# or
npm install @sentry/node
// sentry.server.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
// Filter out noise
ignoreErrors: [
'ResizeObserver loop limit exceeded',
/^Network Error/,
],
beforeSend(event, hint) {
// Don't send errors from bots
const ua = event.request?.headers?.['user-agent'] ?? '';
if (/bot|crawler|spider/i.test(ua)) return null;
return event;
},
});
Capturing Errors with Context
try {
await processPayment(userId, amount);
} catch (error) {
Sentry.withScope((scope) => {
scope.setUser({ id: userId, email: user.email });
scope.setTag('payment.provider', 'stripe');
scope.setContext('payment', { amount, currency: 'USD' });
Sentry.captureException(error);
});
throw error; // re-throw so caller handles it
}
Context transforms a stack trace into an actionable bug report: you know who was affected, what they were doing, and what the state was.
Custom Error Classes
class AppError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number = 500,
public context?: Record<string, unknown>,
) {
super(message);
this.name = 'AppError';
}
}
class PaymentError extends AppError {
constructor(message: string, public stripeError?: Stripe.StripeError) {
super(message, 'PAYMENT_ERROR', 402);
}
}
class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(`${resource} not found`, 'NOT_FOUND', 404, { resource, id });
}
}
// In your error handler
app.use((error: Error, req: Request, res: Response, next: NextFunction) => {
if (error instanceof AppError) {
// Known error — log at warn level, send to Sentry as info
Sentry.captureException(error, { level: 'warning' });
return res.status(error.statusCode).json({ error: error.message, code: error.code });
}
// Unknown error — full alert
Sentry.captureException(error);
res.status(500).json({ error: 'Internal server error' });
});
Source Maps
Without source maps, your stack traces are minified gibberish:
TypeError: Cannot read properties of undefined
at e.t.handleRequest (main.8f3a2.js:1:48291)
With source maps:
TypeError: Cannot read properties of undefined
at PaymentService.handleRequest (src/services/payment.ts:83:12)
// next.config.js
const { withSentryConfig } = require('@sentry/nextjs');
module.exports = withSentryConfig(nextConfig, {
silent: true,
org: 'your-org',
project: 'your-project',
// Upload source maps on build
widenClientFileUpload: true,
hideSourceMaps: true, // don't expose to browser
});
Alerting Without Alert Fatigue
// Sentry alert rules (configure in dashboard):
// 1. New issue → immediate Slack notification
// 2. Issue > 100 events/hour → PagerDuty
// 3. Error rate > 1% of sessions → immediate alert
// 4. Performance regression > 20% → Slack warning
// In code: control alert volume
Sentry.captureException(error, {
// Don't alert for expected errors
level: error instanceof AppError ? 'warning' : 'error',
fingerprint: ['{{ default }}', error.code], // group similar errors
});
Health Checks
// /api/health endpoint
app.get('/health', async (req, res) => {
const checks = await Promise.allSettled([
db.$queryRaw`SELECT 1`,
redis.ping(),
fetch(process.env.EXTERNAL_API_URL + '/health'),
]);
const status = checks.every(c => c.status === 'fulfilled') ? 'ok' : 'degraded';
const statusCode = status === 'ok' ? 200 : 503;
res.status(statusCode).json({
status,
timestamp: new Date().toISOString(),
checks: {
database: checks[0].status,
redis: checks[1].status,
externalApi: checks[2].status,
},
});
});
Structured Logging
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
transport: process.env.NODE_ENV === 'development'
? { target: 'pino-pretty' }
: undefined,
});
// Structured logs are queryable in Datadog/CloudWatch/Loki
logger.info({ userId, orderId, amount }, 'Payment processed');
logger.error({ error: err.message, stack: err.stack, userId }, 'Payment failed');
Unstructured: console.log('Payment failed for user 123')
Structured: { level: 'error', userId: '123', error: 'Card declined', timestamp: '...' }
With structured logs, you can query: "all payment failures in the last hour" in seconds.
The Minimum Viable Monitoring Stack
- Sentry for error tracking (free tier covers most startups)
- Structured logging with pino or winston
- Health check endpoint monitored by UptimeRobot (free)
- Source maps uploaded on deploy
You don't need Datadog and PagerDuty on day one. But you do need to know when users are hitting errors.
Production monitoring, structured logging, and health checks pre-configured: Whoff Agents AI SaaS Starter Kit.
Top comments (0)