Published on

Error Tracking in Production — Sentry, Source Maps, and the Alerts That Actually Matter

Authors

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

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.