Dev Dissection — Week 11: Monitoring & Scaling

Welcome back to Dev Dissection! Last week we added analytics to understand user behavior. This week, we’re adding the safety net that keeps your app running smoothly in production.

By the end of this lesson, you’ll have professional-grade error monitoring and understand how to spot performance bottlenecks before they become problems.

Prerequisites

  • Your TODO app from previous weeks
  • Basic understanding of Node.js performance concepts
  • A Sentry account (we’ll create this together)

Why Production Monitoring Matters

Your analytics tell you what users want to do. Monitoring tells you what’s preventing them from doing it.

Real-World Impact:

  • Stripe catches API errors before they affect payments
  • Vercel identifies performance regressions before users notice
  • Discord monitors memory usage to prevent crashes during peak times

Without monitoring, you’re fixing problems after users have already left.

Setting Up Sentry: Professional Error Tracking

Step 1: Create Sentry Account

  1. Go to sentry.io
  2. Sign up for free (5,000 errors/month)
  3. Create a new project: “TODO App Production”
  4. Choose “Next.js” as your platform
  5. Copy your DSN from the setup guide

Step 2: Install Sentry SDK

npx @sentry/wizard@latest -i nextjs

Follow the steps to setup for nextjs. It would autogenerate base configurations you need for project.

Step 3: Configure Sentry

Create lib/sentry.ts:

import * as Sentry from '@sentry/nextjs';
import { analytics } from './analytics';

class ErrorTracker {
  // User Context
  static setUser(user: { id: string; email: string; name: string }) {
    Sentry.setUser({
      id: user.id,
      email: user.email,
      username: user.name,
    });
  }

  // API Errors
  static trackAPIError(
    endpoint: string,
    method: string,
    status: number,
    error: string,
    context?: Record<string, unknown>
  ) {
    Sentry.captureException(new Error(`API Error: ${method} ${endpoint}`), {
      tags: {
        type: 'api_error',
        endpoint,
        method,
        status,
      },
      extra: {
        error,
        ...context,
      },
      level: status >= 500 ? 'error' : 'warning',
    });
  }

  // User Action Errors
  static trackUserError(
    action: string,
    error: string,
    context?: Record<string, unknown>
  ) {
    Sentry.captureException(new Error(`User Action Failed: ${action}`), {
      tags: {
        type: 'user_error',
        action,
      },
      extra: {
        error,
        ...context,
      },
      level: 'warning',
    });
  }

  // Performance Issues
  static trackPerformanceIssue(
    operation: string,
    duration: number,
    threshold: number
  ) {
    if (duration > threshold) {
      Sentry.captureMessage(`Slow Operation: ${operation}`, {
        level: 'warning',
        tags: {
          type: 'performance',
          operation,
        },
        extra: {
          duration,
          threshold,
          slowBy: duration - threshold,
        },
      });
    }
  }

  // Custom Business Logic Errors
  static trackBusinessLogicError(
    operation: string,
    error: string,
    severity: 'low' | 'medium' | 'high' = 'medium'
  ) {
    const level =
      severity === 'high'
        ? 'error'
        : severity === 'medium'
        ? 'warning'
        : 'info';

    Sentry.captureException(new Error(`Business Logic Error: ${operation}`), {
      tags: {
        type: 'business_logic',
        operation,
        severity,
      },
      extra: { error },
      level,
    });
  }
}

class PerformanceMonitor {
  private static startTime = Date.now();
  private static memoryThreshold = 50; // MB

  // Memory Usage Tracking
  static checkMemoryUsage() {
    if (typeof window !== 'undefined' && 'memory' in performance) {
      const memory = (
        performance as Performance & {
          memory: {
            usedJSHeapSize: number;
            totalJSHeapSize: number;
            jsHeapSizeLimit: number;
          };
        }
      ).memory;

      const usedMB = memory.usedJSHeapSize / 1024 / 1024;

      if (usedMB > this.memoryThreshold) {
        ErrorTracker.trackPerformanceIssue(
          'memory_usage',
          usedMB,
          this.memoryThreshold
        );
      }

      return {
        used: usedMB,
        total: memory.totalJSHeapSize / 1024 / 1024,
        limit: memory.jsHeapSizeLimit / 1024 / 1024,
      };
    }
    return null;
  }

  // API Response Time Tracking
  static measureAPICall<T>(
    operation: string,
    apiCall: () => Promise<T>
  ): Promise<T> {
    const startTime = Date.now();

    return apiCall()
      .then((result) => {
        const duration = Date.now() - startTime;
        this.trackAPIPerformance(operation, duration, 'success');
        return result;
      })
      .catch((error) => {
        const duration = Date.now() - startTime;
        this.trackAPIPerformance(operation, duration, 'error');
        throw error;
      });
  }

  private static trackAPIPerformance(
    operation: string,
    duration: number,
    status: string
  ) {
    // Log slow operations
    if (duration > 3000) {
      ErrorTracker.trackPerformanceIssue(operation, duration, 3000);
    }

    // Send to analytics
    analytics.trackEvent('API Performance', {
      operation,
      duration,
      status,
      isSlowRequest: duration > 3000,
    });
  }

  // Resource Usage Report
  static getResourceReport() {
    const memory = this.checkMemoryUsage();
    const uptime = Date.now() - this.startTime;

    return {
      memory,
      uptime: Math.floor(uptime / 1000), // seconds
      timestamp: new Date().toISOString(),
    };
  }

  // Periodic Health Check
  static startHealthMonitoring(intervalMinutes: number = 5) {
    if (typeof window === 'undefined') return;

    setInterval(() => {
      const report = this.getResourceReport();

      analytics.trackEvent('Health Check', {
        memoryUsed: report.memory?.used,
        uptime: report.uptime,
      });
    }, intervalMinutes * 60 * 1000);
  }
}

export { ErrorTracker, PerformanceMonitor };

Step 5: Integrate with Your App

Update lib/auth-context.tsx:

'use client';

import { createContext, useContext, useEffect, useState } from 'react';
import { TodoAPI } from './api';
import { analytics } from './analytics';
import { ErrorTracker } from './error-tracking';

export function AuthProvider({ children }: { children: React.ReactNode }) {
  // ... existing state ...

  const login = async (email: string, password: string) => {
    analytics.trackLoginAttempted('email');

    try {
      const response = await TodoAPI.login(email, password);
      setToken(response.token);
      setUser(response.user);
      
      // Set user context for error tracking
      ErrorTracker.setUser(response.user);
      
      localStorage.setItem('token', response.token);
      localStorage.setItem('user', JSON.stringify(response.user));

      analytics.trackLoginSuccessful('email', response.user.id);
    } catch (error) {
      const e = error as Error;
      
      // Track login failures for monitoring
      ErrorTracker.trackUserError('login', e.message, {
        email,
        timestamp: new Date().toISOString(),
      });
      
      analytics.trackLoginFailed('email', e.message);
      throw error;
    }
  };

  // ... rest of component
}

Update your API calls in components/todo-list.tsx:

const loadTodos = async () => {
  const startTime = Date.now();
  
  try {
    const data = await TodoAPI.getTodos();
    const loadTime = Date.now() - startTime;
    
    // Track slow API calls
    ErrorTracker.trackPerformanceIssue('load_todos', loadTime, 2000);
    
    setTodos(data);
    analytics.trackTodoListViewed({
      totalTodos: data.length,
      completedTodos: data.filter(t => t.completed).length,
    });
  } catch (error) {
    const e = error as Error;
    ErrorTracker.trackAPIError('/todos', 'GET', 500, e.message, {
      userId: user?.id,
      todosCount: todos.length,
    });
    analytics.trackAPIError('/todos', e.message);
  } finally {
    setIsLoading(false);
  }
};

Resource Usage Monitoring

Add Performance Monitoring to Your App

Update your API service lib/api.ts:

import { ErrorTracker, PerformanceMonitor } from './sentry';
import type { Todo, CreateTodoRequest, UpdateTodoRequest } from './types';

const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';

// Auth response types
interface AuthResponse {
  token: string;
  user: {
    id: string;
    email: string;
    name: string;
  };
  message: string;
}

export class TodoAPI {
  private static getAuthHeaders() {
    const token = localStorage.getItem('token');
    return {
      'Content-Type': 'application/json',
      ...(token && { Authorization: `Bearer ${token}` }),
    };
  }

  private static async request<T>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<T> {
    const url = `${API_URL}${endpoint}`;

    return PerformanceMonitor.measureAPICall(
      `${options.method || 'GET'} ${endpoint}`,
      async () => {
        const response = await fetch(url, {
          headers: this.getAuthHeaders(),
          ...options,
        });

        if (!response.ok) {
          const errorText = await response.text();

          ErrorTracker.trackAPIError(
            endpoint,
            options.method || 'GET',
            response.status,
            errorText
          );

          throw new Error(`HTTP ${response.status}: ${errorText}`);
        }

        return response.json();
      }
    );
  }

  // Auth methods - these don't use the private request method since they don't need auth headers
  static async register(
    email: string,
    password: string,
    name: string
  ): Promise<AuthResponse> {
    return PerformanceMonitor.measureAPICall(
      'POST /auth/register',
      async () => {
        const response = await fetch(`${API_URL}/auth/register`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ email, password, name }),
        });

        if (!response.ok) {
          const error = await response.json();
          const errorMessage = error.error || 'Registration failed';

          ErrorTracker.trackAPIError(
            '/auth/register',
            'POST',
            response.status,
            errorMessage
          );

          throw new Error(errorMessage);
        }

        return response.json();
      }
    );
  }

  static async login(email: string, password: string): Promise<AuthResponse> {
    return PerformanceMonitor.measureAPICall('POST /auth/login', async () => {
      const response = await fetch(`${API_URL}/auth/login`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      });

      if (!response.ok) {
        const error = await response.json();
        const errorMessage = error.error || 'Login failed';

        ErrorTracker.trackAPIError(
          '/auth/login',
          'POST',
          response.status,
          errorMessage
        );

        throw new Error(errorMessage);
      }

      return response.json();
    });
  }

  // Todo methods - use the private request method
  static async getTodos(): Promise<Todo[]> {
    return this.request<Todo[]>('/todos', {
      method: 'GET',
    });
  }

  static async createTodo(data: CreateTodoRequest): Promise<Todo> {
    return this.request<Todo>('/todos', {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  static async toggleTodo(id: string, data: UpdateTodoRequest): Promise<Todo> {
    return this.request<Todo>(`/todos/${id}`, {
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }

  static async deleteTodo(id: string): Promise<void> {
    return this.request<void>(`/todos/${id}`, {
      method: 'DELETE',
    });
  }
}

Initialize Performance Monitoring

Update lib/auth-context.tsx:

useEffect(() => {
  analytics.init();
  analytics.trackSessionStart();
  
  // Start performance monitoring
  PerformanceMonitor.startHealthMonitoring(5); // Check every 5 minutes
  
  // ... rest of initialization
}, []);

Key Monitoring Dashboards

Error Dashboard (Sentry)

Critical Metrics:

  • Error rate by endpoint
  • Most common error types
  • Errors by user segment
  • Recovery time after deployments

Performance Dashboard

Key Indicators:

  • API response times (95th percentile)
  • Memory usage trends
  • Slow query frequency
  • Client-side performance

Common Production Issues & Solutions

Issue 1: Memory Leaks

Symptoms:

// Memory usage keeps growing - check via DevTools
PerformanceMonitor.checkMemoryUsage(); // Shows increasing usage

Solution:

// Clean up event listeners and intervals
useEffect(() => {
  const interval = setInterval(() => {
    // Some periodic task
  }, 1000);

  return () => clearInterval(interval); // Always clean up!
}, []);

Issue 2: Slow API Calls

Symptoms:

// Sentry shows frequent "Slow Operation" warnings
ErrorTracker.trackPerformanceIssue('load_todos', 5000, 2000);

Solutions:

  • Add loading states for better UX
  • Implement request caching
  • Optimize database queries

Issue 3: Error Spikes After Deployments

Detection:

  • Sentry alerts show error rate increase
  • Performance monitoring shows degraded response times

Response:

  • Roll back deployment immediately
  • Check logs for new error patterns
  • Implement canary deployments

What You Learned

This week you added:

  • Professional error tracking with Sentry integration
  • Performance monitoring for API calls and memory usage
  • Resource usage tracking to prevent production issues
  • Health monitoring with automated reporting

Your TODO app now has production-grade observability. When things go wrong (and they will), you’ll know immediately and have the context to fix them quickly.