DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Error Monitoring in Production: Beyond console.log

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
Enter fullscreen mode Exit fullscreen mode
// 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;
  },
});
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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' });
});
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

With source maps:

TypeError: Cannot read properties of undefined
  at PaymentService.handleRequest (src/services/payment.ts:83:12)
Enter fullscreen mode Exit fullscreen mode
// 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
});
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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,
    },
  });
});
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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

  1. Sentry for error tracking (free tier covers most startups)
  2. Structured logging with pino or winston
  3. Health check endpoint monitored by UptimeRobot (free)
  4. 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)