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
- Go to sentry.io
- Sign up for free (5,000 errors/month)
- Create a new project: “TODO App Production”
- Choose “Next.js” as your platform
- 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.