Error Handling and Observability for Next.js and Supabase Applications
Developer Guide

Error Handling and Observability for Next.js and Supabase Applications

Comprehensive guide to error handling, logging, monitoring, and observability for production Next.js and Supabase applications. Learn error boundaries, structured logging, performance monitoring, and debugging strategies.

2026-03-23
30 min read
Error Handling and Observability for Next.js and Supabase Applications

Error handling and observability are critical for maintaining reliable, high-performance applications in production. This comprehensive guide teaches you how to implement robust error handling, structured logging, and comprehensive monitoring for Next.js and Supabase applications.

Proper error handling prevents crashes and provides graceful degradation when things go wrong. Observability gives you visibility into your application's behavior, helping you identify issues before they impact users and debug problems quickly when they occur.

This guide covers everything from basic error boundaries to advanced distributed tracing, with production-tested patterns that will help you build resilient, maintainable applications.

Error Handling Fundamentals#

Structured Error Types#

Define consistent error types and handling patterns across your application.

// lib/errors/types.ts
export enum ErrorCode {
  // Authentication errors
  UNAUTHORIZED = 'UNAUTHORIZED',
  FORBIDDEN = 'FORBIDDEN',
  SESSION_EXPIRED = 'SESSION_EXPIRED',
  
  // Validation errors
  VALIDATION_ERROR = 'VALIDATION_ERROR',
  INVALID_INPUT = 'INVALID_INPUT',
  
  // Database errors
  DATABASE_ERROR = 'DATABASE_ERROR',
  RECORD_NOT_FOUND = 'RECORD_NOT_FOUND',
  CONSTRAINT_VIOLATION = 'CONSTRAINT_VIOLATION',
  
  // Network errors
  NETWORK_ERROR = 'NETWORK_ERROR',
  TIMEOUT_ERROR = 'TIMEOUT_ERROR',
  
  // Business logic errors
  BUSINESS_RULE_VIOLATION = 'BUSINESS_RULE_VIOLATION',
  INSUFFICIENT_PERMISSIONS = 'INSUFFICIENT_PERMISSIONS',
  
  // System errors
  INTERNAL_ERROR = 'INTERNAL_ERROR',
  SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE'
}

export class AppError extends Error {
  public readonly code: ErrorCode
  public readonly statusCode: number
  public readonly isOperational: boolean
  public readonly context?: Record<string, any>
  public readonly timestamp: string
  public readonly requestId?: string

  constructor(
    message: string,
    code: ErrorCode,
    statusCode = 500,
    isOperational = true,
    context?: Record<string, any>,
    requestId?: string
  ) {
    super(message)
    
    this.name = this.constructor.name
    this.code = code
    this.statusCode = statusCode
    this.isOperational = isOperational
    this.context = context
    this.timestamp = new Date().toISOString()
    this.requestId = requestId

    Error.captureStackTrace(this, this.constructor)
  }

  toJSON() {
    return {
      name: this.name,
      message: this.message,
      code: this.code,
      statusCode: this.statusCode,
      isOperational: this.isOperational,
      context: this.context,
      timestamp: this.timestamp,
      requestId: this.requestId,
      stack: this.stack
    }
  }
}

// Specific error classes
export class ValidationError extends AppError {
  constructor(message: string, context?: Record<string, any>, requestId?: string) {
    super(message, ErrorCode.VALIDATION_ERROR, 400, true, context, requestId)
  }
}

export class AuthenticationError extends AppError {
  constructor(message: string, context?: Record<string, any>, requestId?: string) {
    super(message, ErrorCode.UNAUTHORIZED, 401, true, context, requestId)
  }
}

export class DatabaseError extends AppError {
  constructor(message: string, context?: Record<string, any>, requestId?: string) {
    super(message, ErrorCode.DATABASE_ERROR, 500, true, context, requestId)
  }
}

export class BusinessRuleError extends AppError {
  constructor(message: string, context?: Record<string, any>, requestId?: string) {
    super(message, ErrorCode.BUSINESS_RULE_VIOLATION, 422, true, context, requestId)
  }
}

Supabase Error Handling#

Implement comprehensive error handling for Supabase operations.

// lib/errors/supabase-handler.ts
import { PostgrestError } from '@supabase/supabase-js'
import { AppError, ErrorCode, DatabaseError, AuthenticationError, ValidationError } from './types'

export class SupabaseErrorHandler {
  static handleError(error: any, context?: Record<string, any>, requestId?: string): AppError {
    // Handle Supabase-specific errors
    if (this.isPostgrestError(error)) {
      return this.handlePostgrestError(error, context, requestId)
    }

    // Handle authentication errors
    if (this.isAuthError(error)) {
      return this.handleAuthError(error, context, requestId)
    }

    // Handle network errors
    if (this.isNetworkError(error)) {
      return new AppError(
        'Network connection failed',
        ErrorCode.NETWORK_ERROR,
        503,
        true,
        { ...context, originalError: error.message },
        requestId
      )
    }

    // Handle unknown errors
    return new AppError(
      'An unexpected error occurred',
      ErrorCode.INTERNAL_ERROR,
      500,
      false,
      { ...context, originalError: error.message },
      requestId
    )
  }

  private static isPostgrestError(error: any): error is PostgrestError {
    return error && typeof error.code === 'string' && typeof error.message === 'string'
  }

  private static isAuthError(error: any): boolean {
    return error && (
      error.message?.includes('JWT') ||
      error.message?.includes('auth') ||
      error.message?.includes('session')
    )
  }

  private static isNetworkError(error: any): boolean {
    return error && (
      error.code === 'NETWORK_ERROR' ||
      error.message?.includes('fetch') ||
      error.message?.includes('network')
    )
  }

  private static handlePostgrestError(
    error: PostgrestError, 
    context?: Record<string, any>, 
    requestId?: string
  ): AppError {
    const errorContext = {
      ...context,
      postgrestCode: error.code,
      details: error.details,
      hint: error.hint
    }

    // Map PostgreSQL error codes to application errors
    switch (error.code) {
      case '23505': // unique_violation
        return new ValidationError(
          'A record with this information already exists',
          errorContext,
          requestId
        )
      
      case '23503': // foreign_key_violation
        return new ValidationError(
          'Referenced record does not exist',
          errorContext,
          requestId
        )
      
      case '23514': // check_violation
        return new ValidationError(
          'Data validation failed',
          errorContext,
          requestId
        )
      
      case '42501': // insufficient_privilege
        return new AuthenticationError(
          'Insufficient permissions to perform this operation',
          errorContext,
          requestId
        )
      
      case 'PGRST116': // No rows found
        return new AppError(
          'Record not found',
          ErrorCode.RECORD_NOT_FOUND,
          404,
          true,
          errorContext,
          requestId
        )
      
      default:
        return new DatabaseError(
          error.message || 'Database operation failed',
          errorContext,
          requestId
        )
    }
  }

  private static handleAuthError(
    error: any, 
    context?: Record<string, any>, 
    requestId?: string
  ): AppError {
    if (error.message?.includes('JWT expired')) {
      return new AppError(
        'Session expired. Please log in again.',
        ErrorCode.SESSION_EXPIRED,
        401,
        true,
        context,
        requestId
      )
    }

    return new AuthenticationError(
      error.message || 'Authentication failed',
      context,
      requestId
    )
  }
}

// Usage in data access layer
export async function safeSupabaseOperation<T>(
  operation: () => Promise<{ data: T | null, error: any }>,
  context?: Record<string, any>,
  requestId?: string
): Promise<T> {
  try {
    const { data, error } = await operation()
    
    if (error) {
      throw SupabaseErrorHandler.handleError(error, context, requestId)
    }
    
    if (data === null) {
      throw new AppError(
        'No data returned from operation',
        ErrorCode.RECORD_NOT_FOUND,
        404,
        true,
        context,
        requestId
      )
    }
    
    return data
  } catch (error) {
    if (error instanceof AppError) {
      throw error
    }
    
    throw SupabaseErrorHandler.handleError(error, context, requestId)
  }
}

Error Boundaries and Recovery#

Implement React error boundaries for graceful error handling in the UI.

// components/ErrorBoundary.tsx
'use client'

import React, { Component, ErrorInfo, ReactNode } from 'react'
import { AppError } from '@/lib/errors/types'
import { Logger } from '@/lib/logging/logger'

interface Props {
  children: ReactNode
  fallback?: ReactNode
  onError?: (error: Error, errorInfo: ErrorInfo) => void
}

interface State {
  hasError: boolean
  error?: Error
  errorId?: string
}

export class ErrorBoundary extends Component<Props, State> {
  private logger = new Logger('ErrorBoundary')

  constructor(props: Props) {
    super(props)
    this.state = { hasError: false }
  }

  static getDerivedStateFromError(error: Error): State {
    return {
      hasError: true,
      error,
      errorId: Math.random().toString(36).substring(2, 15)
    }
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    const errorId = this.state.errorId!
    
    // Log the error
    this.logger.error('React Error Boundary caught an error', {
      error: error.message,
      stack: error.stack,
      componentStack: errorInfo.componentStack,
      errorId,
      timestamp: new Date().toISOString()
    })

    // Call custom error handler
    this.props.onError?.(error, errorInfo)

    // Report to error monitoring service
    this.reportError(error, errorInfo, errorId)
  }

  private async reportError(error: Error, errorInfo: ErrorInfo, errorId: string) {
    try {
      await fetch('/api/errors/report', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          message: error.message,
          stack: error.stack,
          componentStack: errorInfo.componentStack,
          errorId,
          url: window.location.href,
          userAgent: navigator.userAgent,
          timestamp: new Date().toISOString()
        })
      })
    } catch (reportingError) {
      console.error('Failed to report error:', reportingError)
    }
  }

  render() {
    if (this.state.hasError) {
      if (this.props.fallback) {
        return this.props.fallback
      }

      return (
        <div className="error-boundary">
          <div className="error-content">
            <h2>Something went wrong</h2>
            <p>We apologize for the inconvenience. The error has been reported and we're working to fix it.</p>
            <details className="error-details">
              <summary>Error Details (ID: {this.state.errorId})</summary>
              <pre>{this.state.error?.message}</pre>
            </details>
            <button 
              onClick={() => window.location.reload()}
              className="retry-button"
            >
              Reload Page
            </button>
          </div>
        </div>
      )
    }

    return this.props.children
  }
}

// Global error boundary for the app
// app/global-error.tsx
'use client'

import { ErrorBoundary } from '@/components/ErrorBoundary'

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <html>
      <body>
        <div className="global-error">
          <h1>Application Error</h1>
          <p>A critical error occurred. Please try refreshing the page.</p>
          {error.digest && (
            <p className="error-digest">Error ID: {error.digest}</p>
          )}
          <button onClick={reset}>Try Again</button>
        </div>
      </body>
    </html>
  )
}

// Route-level error handling
// app/posts/error.tsx
'use client'

export default function PostsError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div className="posts-error">
      <h2>Failed to load posts</h2>
      <p>There was an error loading the posts. Please try again.</p>
      <button onClick={reset}>Retry</button>
    </div>
  )
}

Structured Logging System#

Logger Implementation#

Implement a comprehensive logging system with structured output and multiple transports.

// lib/logging/logger.ts
export enum LogLevel {
  ERROR = 0,
  WARN = 1,
  INFO = 2,
  DEBUG = 3
}

export interface LogEntry {
  level: LogLevel
  message: string
  timestamp: string
  context?: Record<string, any>
  requestId?: string
  userId?: string
  organizationId?: string
  source: string
  environment: string
}

export interface LogTransport {
  log(entry: LogEntry): Promise<void>
}

export class Logger {
  private transports: LogTransport[] = []
  private minLevel: LogLevel
  private source: string

  constructor(source: string, minLevel = LogLevel.INFO) {
    this.source = source
    this.minLevel = minLevel
    
    // Add default transports
    if (typeof window === 'undefined') {
      // Server-side transports
      this.transports.push(new ConsoleTransport())
      if (process.env.NODE_ENV === 'production') {
        this.transports.push(new FileTransport())
        this.transports.push(new DatabaseTransport())
      }
    } else {
      // Client-side transports
      this.transports.push(new ConsoleTransport())
      this.transports.push(new RemoteTransport())
    }
  }

  private async log(level: LogLevel, message: string, context?: Record<string, any>) {
    if (level > this.minLevel) return

    const entry: LogEntry = {
      level,
      message,
      timestamp: new Date().toISOString(),
      context,
      requestId: this.getRequestId(),
      userId: this.getUserId(),
      organizationId: this.getOrganizationId(),
      source: this.source,
      environment: process.env.NODE_ENV || 'development'
    }

    // Log to all transports
    await Promise.allSettled(
      this.transports.map(transport => transport.log(entry))
    )
  }

  error(message: string, context?: Record<string, any>) {
    return this.log(LogLevel.ERROR, message, context)
  }

  warn(message: string, context?: Record<string, any>) {
    return this.log(LogLevel.WARN, message, context)
  }

  info(message: string, context?: Record<string, any>) {
    return this.log(LogLevel.INFO, message, context)
  }

  debug(message: string, context?: Record<string, any>) {
    return this.log(LogLevel.DEBUG, message, context)
  }

  private getRequestId(): string | undefined {
    // Get from async local storage or headers
    return typeof window === 'undefined' 
      ? process.env.REQUEST_ID 
      : undefined
  }

  private getUserId(): string | undefined {
    // Get from session or context
    return typeof window !== 'undefined' 
      ? localStorage.getItem('userId') || undefined
      : undefined
  }

  private getOrganizationId(): string | undefined {
    // Get from session or context
    return typeof window !== 'undefined' 
      ? localStorage.getItem('organizationId') || undefined
      : undefined
  }
}

// Console transport
class ConsoleTransport implements LogTransport {
  async log(entry: LogEntry): Promise<void> {
    const levelNames = ['ERROR', 'WARN', 'INFO', 'DEBUG']
    const levelName = levelNames[entry.level]
    
    const logMessage = `[${entry.timestamp}] ${levelName} [${entry.source}] ${entry.message}`
    
    switch (entry.level) {
      case LogLevel.ERROR:
        console.error(logMessage, entry.context)
        break
      case LogLevel.WARN:
        console.warn(logMessage, entry.context)
        break
      case LogLevel.INFO:
        console.info(logMessage, entry.context)
        break
      case LogLevel.DEBUG:
        console.debug(logMessage, entry.context)
        break
    }
  }
}

// File transport (server-side)
class FileTransport implements LogTransport {
  async log(entry: LogEntry): Promise<void> {
    if (typeof window !== 'undefined') return

    const fs = await import('fs/promises')
    const path = await import('path')
    
    const logDir = path.join(process.cwd(), 'logs')
    const logFile = path.join(logDir, `app-${new Date().toISOString().split('T')[0]}.log`)
    
    try {
      await fs.mkdir(logDir, { recursive: true })
      await fs.appendFile(logFile, JSON.stringify(entry) + '\n')
    } catch (error) {
      console.error('Failed to write to log file:', error)
    }
  }
}

// Database transport (server-side)
class DatabaseTransport implements LogTransport {
  async log(entry: LogEntry): Promise<void> {
    if (typeof window !== 'undefined') return
    if (entry.level > LogLevel.WARN) return // Only log warnings and errors to DB

    try {
      const { createClient } = await import('@/lib/supabase/server')
      const supabase = createClient()
      
      await supabase.from('application_logs').insert({
        level: entry.level,
        message: entry.message,
        context: entry.context,
        request_id: entry.requestId,
        user_id: entry.userId,
        organization_id: entry.organizationId,
        source: entry.source,
        environment: entry.environment,
        timestamp: entry.timestamp
      })
    } catch (error) {
      console.error('Failed to write to database log:', error)
    }
  }
}

// Remote transport (client-side)
class RemoteTransport implements LogTransport {
  private buffer: LogEntry[] = []
  private flushInterval: NodeJS.Timeout

  constructor() {
    // Flush logs every 5 seconds
    this.flushInterval = setInterval(() => this.flush(), 5000)
    
    // Flush on page unload
    if (typeof window !== 'undefined') {
      window.addEventListener('beforeunload', () => this.flush())
    }
  }

  async log(entry: LogEntry): Promise<void> {
    this.buffer.push(entry)
    
    // Flush immediately for errors
    if (entry.level === LogLevel.ERROR) {
      await this.flush()
    }
  }

  private async flush(): Promise<void> {
    if (this.buffer.length === 0) return

    const logs = [...this.buffer]
    this.buffer = []

    try {
      await fetch('/api/logs', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ logs })
      })
    } catch (error) {
      console.error('Failed to send logs to server:', error)
      // Put logs back in buffer for retry
      this.buffer.unshift(...logs)
    }
  }
}

Request Correlation and Tracing#

Implement request correlation for tracking requests across services.

// lib/tracing/correlation.ts
import { AsyncLocalStorage } from 'async_hooks'

interface RequestContext {
  requestId: string
  userId?: string
  organizationId?: string
  startTime: number
  path: string
  method: string
}

export class CorrelationManager {
  private static storage = new AsyncLocalStorage<RequestContext>()

  static run<T>(context: RequestContext, callback: () => T): T {
    return this.storage.run(context, callback)
  }

  static getContext(): RequestContext | undefined {
    return this.storage.getStore()
  }

  static getRequestId(): string | undefined {
    return this.getContext()?.requestId
  }

  static getUserId(): string | undefined {
    return this.getContext()?.userId
  }

  static getOrganizationId(): string | undefined {
    return this.getContext()?.organizationId
  }
}

// Middleware for Next.js API routes
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
import { CorrelationManager } from '@/lib/tracing/correlation'

export function middleware(request: NextRequest) {
  const requestId = crypto.randomUUID()
  const startTime = Date.now()
  
  // Add request ID to headers
  const response = NextResponse.next()
  response.headers.set('X-Request-ID', requestId)
  
  // Set up correlation context
  const context = {
    requestId,
    startTime,
    path: request.nextUrl.pathname,
    method: request.method
  }
  
  return CorrelationManager.run(context, () => response)
}

export const config = {
  matcher: [
    '/api/:path*',
    '/((?!_next/static|_next/image|favicon.ico).*)'
  ]
}

This comprehensive error handling and observability guide provides the foundation for building resilient, maintainable applications. Proper error handling and monitoring are essential for production applications, helping you identify issues quickly and provide excellent user experiences even when things go wrong.

Performance Monitoring#

Core Web Vitals Tracking#

Monitor and optimize Core Web Vitals for better user experience and SEO.

// lib/monitoring/web-vitals.ts
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'

export interface WebVitalMetric {
  name: string
  value: number
  rating: 'good' | 'needs-improvement' | 'poor'
  delta: number
  id: string
  navigationType: string
}

export class WebVitalsMonitor {
  private metrics: WebVitalMetric[] = []
  private reportingEndpoint = '/api/analytics/web-vitals'

  constructor() {
    if (typeof window !== 'undefined') {
      this.initializeTracking()
    }
  }

  private initializeTracking() {
    // Track all Core Web Vitals
    getCLS(this.handleMetric.bind(this))
    getFID(this.handleMetric.bind(this))
    getFCP(this.handleMetric.bind(this))
    getLCP(this.handleMetric.bind(this))
    getTTFB(this.handleMetric.bind(this))
  }

  private handleMetric(metric: any) {
    const webVitalMetric: WebVitalMetric = {
      name: metric.name,
      value: metric.value,
      rating: this.getRating(metric.name, metric.value),
      delta: metric.delta,
      id: metric.id,
      navigationType: metric.navigationType || 'navigate'
    }

    this.metrics.push(webVitalMetric)
    this.reportMetric(webVitalMetric)
  }

  private getRating(name: string, value: number): 'good' | 'needs-improvement' | 'poor' {
    const thresholds = {
      CLS: { good: 0.1, poor: 0.25 },
      FID: { good: 100, poor: 300 },
      FCP: { good: 1800, poor: 3000 },
      LCP: { good: 2500, poor: 4000 },
      TTFB: { good: 800, poor: 1800 }
    }

    const threshold = thresholds[name as keyof typeof thresholds]
    if (!threshold) return 'good'

    if (value <= threshold.good) return 'good'
    if (value <= threshold.poor) return 'needs-improvement'
    return 'poor'
  }

  private async reportMetric(metric: WebVitalMetric) {
    try {
      await fetch(this.reportingEndpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          ...metric,
          url: window.location.href,
          userAgent: navigator.userAgent,
          timestamp: Date.now()
        })
      })
    } catch (error) {
      console.error('Failed to report web vital:', error)
    }
  }

  getMetrics(): WebVitalMetric[] {
    return [...this.metrics]
  }

  getMetricsByRating(rating: 'good' | 'needs-improvement' | 'poor'): WebVitalMetric[] {
    return this.metrics.filter(metric => metric.rating === rating)
  }
}

// Initialize global web vitals monitoring
export const webVitalsMonitor = new WebVitalsMonitor()

// React hook for component-level monitoring
export function useWebVitals() {
  return {
    metrics: webVitalsMonitor.getMetrics(),
    goodMetrics: webVitalsMonitor.getMetricsByRating('good'),
    poorMetrics: webVitalsMonitor.getMetricsByRating('poor')
  }
}

API Performance Monitoring#

Monitor API route performance and database query times.

// lib/monitoring/api-performance.ts
export class APIPerformanceMonitor {
  private static instance: APIPerformanceMonitor
  private metrics: Map<string, PerformanceMetric[]> = new Map()

  static getInstance(): APIPerformanceMonitor {
    if (!this.instance) {
      this.instance = new APIPerformanceMonitor()
    }
    return this.instance
  }

  async trackAPICall<T>(
    operation: string,
    fn: () => Promise<T>,
    context?: Record<string, any>
  ): Promise<T> {
    const startTime = Date.now()
    const requestId = context?.requestId || crypto.randomUUID()

    try {
      const result = await fn()
      const duration = Date.now() - startTime

      await this.recordMetric({
        operation,
        duration,
        status: 'success',
        requestId,
        timestamp: new Date().toISOString(),
        context
      })

      return result
    } catch (error) {
      const duration = Date.now() - startTime

      await this.recordMetric({
        operation,
        duration,
        status: 'error',
        error: error instanceof Error ? error.message : 'Unknown error',
        requestId,
        timestamp: new Date().toISOString(),
        context
      })

      throw error
    }
  }

  private async recordMetric(metric: PerformanceMetric) {
    // Store in memory
    const existing = this.metrics.get(metric.operation) || []
    existing.push(metric)
    this.metrics.set(metric.operation, existing.slice(-100)) // Keep last 100

    // Send to monitoring service
    await this.sendToMonitoring(metric)
  }

  private async sendToMonitoring(metric: PerformanceMetric) {
    try {
      if (typeof window === 'undefined') {
        // Server-side: store in database
        const { createClient } = await import('@/lib/supabase/server')
        const supabase = createClient()

        await supabase.from('api_metrics').insert({
          operation: metric.operation,
          duration: metric.duration,
          status: metric.status,
          error: metric.error,
          request_id: metric.requestId,
          context: metric.context,
          timestamp: metric.timestamp
        })
      } else {
        // Client-side: send to API
        await fetch('/api/monitoring/metrics', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(metric)
        })
      }
    } catch (error) {
      console.error('Failed to send metric to monitoring:', error)
    }
  }

  getMetrics(operation?: string): PerformanceMetric[] {
    if (operation) {
      return this.metrics.get(operation) || []
    }
    
    return Array.from(this.metrics.values()).flat()
  }

  getAverageResponseTime(operation: string, timeWindow = 300000): number {
    const cutoff = Date.now() - timeWindow
    const metrics = this.getMetrics(operation)
      .filter(m => new Date(m.timestamp).getTime() > cutoff)
      .filter(m => m.status === 'success')

    if (metrics.length === 0) return 0

    const total = metrics.reduce((sum, m) => sum + m.duration, 0)
    return total / metrics.length
  }

  getErrorRate(operation: string, timeWindow = 300000): number {
    const cutoff = Date.now() - timeWindow
    const metrics = this.getMetrics(operation)
      .filter(m => new Date(m.timestamp).getTime() > cutoff)

    if (metrics.length === 0) return 0

    const errors = metrics.filter(m => m.status === 'error').length
    return errors / metrics.length
  }
}

interface PerformanceMetric {
  operation: string
  duration: number
  status: 'success' | 'error'
  error?: string
  requestId: string
  timestamp: string
  context?: Record<string, any>
}

// Decorator for automatic API monitoring
export function MonitorPerformance(operation?: string) {
  return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
    const method = descriptor.value
    const operationName = operation || `${target.constructor.name}.${propertyName}`

    descriptor.value = async function (...args: any[]) {
      const monitor = APIPerformanceMonitor.getInstance()
      return monitor.trackAPICall(operationName, () => method.apply(this, args))
    }

    return descriptor
  }
}

// Usage in API routes
// app/api/posts/route.ts
import { APIPerformanceMonitor } from '@/lib/monitoring/api-performance'

export async function GET(request: Request) {
  const monitor = APIPerformanceMonitor.getInstance()
  
  return monitor.trackAPICall('posts.list', async () => {
    const supabase = createClient()
    
    const { data, error } = await supabase
      .from('posts')
      .select('*')
      .eq('status', 'published')
      .order('created_at', { ascending: false })
    
    if (error) throw error
    
    return Response.json(data)
  }, {
    requestId: request.headers.get('x-request-id') || undefined
  })
}

Database Query Monitoring#

Monitor Supabase query performance and identify slow queries.

// lib/monitoring/database-monitor.ts
export class DatabaseMonitor {
  private queryMetrics: Map<string, QueryMetric[]> = new Map()

  async trackQuery<T>(
    queryName: string,
    query: () => Promise<T>,
    context?: Record<string, any>
  ): Promise<T> {
    const startTime = performance.now()
    const requestId = context?.requestId || crypto.randomUUID()

    try {
      const result = await query()
      const duration = performance.now() - startTime

      await this.recordQueryMetric({
        queryName,
        duration,
        status: 'success',
        requestId,
        timestamp: new Date().toISOString(),
        context
      })

      // Log slow queries
      if (duration > 1000) { // Queries taking more than 1 second
        console.warn(`Slow query detected: ${queryName} took ${duration.toFixed(2)}ms`, {
          context,
          requestId
        })
      }

      return result
    } catch (error) {
      const duration = performance.now() - startTime

      await this.recordQueryMetric({
        queryName,
        duration,
        status: 'error',
        error: error instanceof Error ? error.message : 'Unknown error',
        requestId,
        timestamp: new Date().toISOString(),
        context
      })

      throw error
    }
  }

  private async recordQueryMetric(metric: QueryMetric) {
    // Store in memory for immediate access
    const existing = this.queryMetrics.get(metric.queryName) || []
    existing.push(metric)
    this.queryMetrics.set(metric.queryName, existing.slice(-50)) // Keep last 50

    // Store in database for long-term analysis
    if (typeof window === 'undefined') {
      try {
        const { createClient } = await import('@/lib/supabase/server')
        const supabase = createClient()

        await supabase.from('query_metrics').insert({
          query_name: metric.queryName,
          duration: metric.duration,
          status: metric.status,
          error: metric.error,
          request_id: metric.requestId,
          context: metric.context,
          timestamp: metric.timestamp
        })
      } catch (error) {
        console.error('Failed to store query metric:', error)
      }
    }
  }

  getSlowQueries(thresholdMs = 1000): QueryMetric[] {
    return Array.from(this.queryMetrics.values())
      .flat()
      .filter(metric => metric.duration > thresholdMs)
      .sort((a, b) => b.duration - a.duration)
  }

  getQueryStats(queryName: string): {
    averageDuration: number
    errorRate: number
    totalExecutions: number
  } {
    const metrics = this.queryMetrics.get(queryName) || []
    
    if (metrics.length === 0) {
      return { averageDuration: 0, errorRate: 0, totalExecutions: 0 }
    }

    const successfulQueries = metrics.filter(m => m.status === 'success')
    const averageDuration = successfulQueries.length > 0
      ? successfulQueries.reduce((sum, m) => sum + m.duration, 0) / successfulQueries.length
      : 0

    const errorRate = metrics.filter(m => m.status === 'error').length / metrics.length

    return {
      averageDuration,
      errorRate,
      totalExecutions: metrics.length
    }
  }
}

interface QueryMetric {
  queryName: string
  duration: number
  status: 'success' | 'error'
  error?: string
  requestId: string
  timestamp: string
  context?: Record<string, any>
}

// Enhanced Supabase client with monitoring
export function createMonitoredSupabaseClient() {
  const supabase = createClient()
  const monitor = new DatabaseMonitor()

  return {
    ...supabase,
    from: (table: string) => {
      const queryBuilder = supabase.from(table)
      
      // Wrap common methods with monitoring
      return {
        ...queryBuilder,
        select: (columns?: string) => {
          const builder = queryBuilder.select(columns)
          
          return {
            ...builder,
            execute: () => monitor.trackQuery(
              `${table}.select`,
              () => builder
            )
          }
        },
        insert: (values: any) => {
          const builder = queryBuilder.insert(values)
          
          return {
            ...builder,
            execute: () => monitor.trackQuery(
              `${table}.insert`,
              () => builder
            )
          }
        },
        update: (values: any) => {
          const builder = queryBuilder.update(values)
          
          return {
            ...builder,
            execute: () => monitor.trackQuery(
              `${table}.update`,
              () => builder
            )
          }
        },
        delete: () => {
          const builder = queryBuilder.delete()
          
          return {
            ...builder,
            execute: () => monitor.trackQuery(
              `${table}.delete`,
              () => builder
            )
          }
        }
      }
    }
  }
}

export const databaseMonitor = new DatabaseMonitor()

Alerting and Incident Response#

Alert Configuration#

Set up intelligent alerting for critical issues and performance degradation.

// lib/alerting/alert-manager.ts
export enum AlertSeverity {
  LOW = 'low',
  MEDIUM = 'medium',
  HIGH = 'high',
  CRITICAL = 'critical'
}

export interface AlertRule {
  id: string
  name: string
  condition: AlertCondition
  severity: AlertSeverity
  channels: AlertChannel[]
  cooldownMinutes: number
  enabled: boolean
}

export interface AlertCondition {
  metric: string
  operator: 'gt' | 'lt' | 'eq' | 'gte' | 'lte'
  threshold: number
  timeWindow: number // minutes
  evaluationInterval: number // minutes
}

export interface AlertChannel {
  type: 'email' | 'slack' | 'webhook'
  config: Record<string, any>
}

export class AlertManager {
  private rules: Map<string, AlertRule> = new Map()
  private lastAlerts: Map<string, number> = new Map()

  constructor() {
    this.loadDefaultRules()
    this.startEvaluationLoop()
  }

  private loadDefaultRules() {
    const defaultRules: AlertRule[] = [
      {
        id: 'high-error-rate',
        name: 'High Error Rate',
        condition: {
          metric: 'error_rate',
          operator: 'gt',
          threshold: 0.05, // 5%
          timeWindow: 5,
          evaluationInterval: 1
        },
        severity: AlertSeverity.HIGH,
        channels: [
          { type: 'email', config: { recipients: ['admin@example.com'] } },
          { type: 'slack', config: { webhook: process.env.SLACK_WEBHOOK_URL } }
        ],
        cooldownMinutes: 15,
        enabled: true
      },
      {
        id: 'slow-response-time',
        name: 'Slow Response Time',
        condition: {
          metric: 'avg_response_time',
          operator: 'gt',
          threshold: 2000, // 2 seconds
          timeWindow: 10,
          evaluationInterval: 2
        },
        severity: AlertSeverity.MEDIUM,
        channels: [
          { type: 'slack', config: { webhook: process.env.SLACK_WEBHOOK_URL } }
        ],
        cooldownMinutes: 30,
        enabled: true
      },
      {
        id: 'database-connection-errors',
        name: 'Database Connection Errors',
        condition: {
          metric: 'db_connection_errors',
          operator: 'gt',
          threshold: 10,
          timeWindow: 5,
          evaluationInterval: 1
        },
        severity: AlertSeverity.CRITICAL,
        channels: [
          { type: 'email', config: { recipients: ['admin@example.com'] } },
          { type: 'slack', config: { webhook: process.env.SLACK_WEBHOOK_URL } }
        ],
        cooldownMinutes: 5,
        enabled: true
      }
    ]

    defaultRules.forEach(rule => this.rules.set(rule.id, rule))
  }

  private startEvaluationLoop() {
    setInterval(() => {
      this.evaluateRules()
    }, 60000) // Evaluate every minute
  }

  private async evaluateRules() {
    for (const rule of this.rules.values()) {
      if (!rule.enabled) continue

      try {
        const shouldAlert = await this.evaluateRule(rule)
        
        if (shouldAlert && this.canSendAlert(rule.id, rule.cooldownMinutes)) {
          await this.sendAlert(rule)
          this.lastAlerts.set(rule.id, Date.now())
        }
      } catch (error) {
        console.error(`Failed to evaluate rule ${rule.id}:`, error)
      }
    }
  }

  private async evaluateRule(rule: AlertRule): Promise<boolean> {
    const metric = await this.getMetricValue(rule.condition)
    
    switch (rule.condition.operator) {
      case 'gt': return metric > rule.condition.threshold
      case 'lt': return metric < rule.condition.threshold
      case 'eq': return metric === rule.condition.threshold
      case 'gte': return metric >= rule.condition.threshold
      case 'lte': return metric <= rule.condition.threshold
      default: return false
    }
  }

  private async getMetricValue(condition: AlertCondition): Promise<number> {
    const endTime = Date.now()
    const startTime = endTime - (condition.timeWindow * 60 * 1000)

    // This would integrate with your metrics storage system
    switch (condition.metric) {
      case 'error_rate':
        return this.calculateErrorRate(startTime, endTime)
      case 'avg_response_time':
        return this.calculateAverageResponseTime(startTime, endTime)
      case 'db_connection_errors':
        return this.countDatabaseErrors(startTime, endTime)
      default:
        return 0
    }
  }

  private async calculateErrorRate(startTime: number, endTime: number): Promise<number> {
    // Query your metrics database
    const { createClient } = await import('@/lib/supabase/server')
    const supabase = createClient()

    const { data } = await supabase
      .rpc('calculate_error_rate', {
        start_time: new Date(startTime).toISOString(),
        end_time: new Date(endTime).toISOString()
      })

    return data?.error_rate || 0
  }

  private async calculateAverageResponseTime(startTime: number, endTime: number): Promise<number> {
    const { createClient } = await import('@/lib/supabase/server')
    const supabase = createClient()

    const { data } = await supabase
      .rpc('calculate_avg_response_time', {
        start_time: new Date(startTime).toISOString(),
        end_time: new Date(endTime).toISOString()
      })

    return data?.avg_response_time || 0
  }

  private async countDatabaseErrors(startTime: number, endTime: number): Promise<number> {
    const { createClient } = await import('@/lib/supabase/server')
    const supabase = createClient()

    const { count } = await supabase
      .from('application_logs')
      .select('*', { count: 'exact', head: true })
      .eq('level', 0) // ERROR level
      .ilike('message', '%database%')
      .gte('timestamp', new Date(startTime).toISOString())
      .lte('timestamp', new Date(endTime).toISOString())

    return count || 0
  }

  private canSendAlert(ruleId: string, cooldownMinutes: number): boolean {
    const lastAlert = this.lastAlerts.get(ruleId)
    if (!lastAlert) return true

    const cooldownMs = cooldownMinutes * 60 * 1000
    return Date.now() - lastAlert > cooldownMs
  }

  private async sendAlert(rule: AlertRule) {
    const alertData = {
      rule: rule.name,
      severity: rule.severity,
      timestamp: new Date().toISOString(),
      condition: rule.condition
    }

    for (const channel of rule.channels) {
      try {
        await this.sendToChannel(channel, alertData)
      } catch (error) {
        console.error(`Failed to send alert to ${channel.type}:`, error)
      }
    }
  }

  private async sendToChannel(channel: AlertChannel, alertData: any) {
    switch (channel.type) {
      case 'email':
        await this.sendEmailAlert(channel.config, alertData)
        break
      case 'slack':
        await this.sendSlackAlert(channel.config, alertData)
        break
      case 'webhook':
        await this.sendWebhookAlert(channel.config, alertData)
        break
    }
  }

  private async sendEmailAlert(config: any, alertData: any) {
    // Implement email sending logic
    console.log('Sending email alert:', alertData)
  }

  private async sendSlackAlert(config: any, alertData: any) {
    if (!config.webhook) return

    const message = {
      text: `🚨 Alert: ${alertData.rule}`,
      attachments: [
        {
          color: this.getSeverityColor(alertData.severity),
          fields: [
            { title: 'Severity', value: alertData.severity, short: true },
            { title: 'Time', value: alertData.timestamp, short: true },
            { title: 'Condition', value: JSON.stringify(alertData.condition), short: false }
          ]
        }
      ]
    }

    await fetch(config.webhook, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(message)
    })
  }

  private async sendWebhookAlert(config: any, alertData: any) {
    await fetch(config.url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(alertData)
    })
  }

  private getSeverityColor(severity: AlertSeverity): string {
    switch (severity) {
      case AlertSeverity.LOW: return 'good'
      case AlertSeverity.MEDIUM: return 'warning'
      case AlertSeverity.HIGH: return 'danger'
      case AlertSeverity.CRITICAL: return '#ff0000'
      default: return 'good'
    }
  }
}

export const alertManager = new AlertManager()

This comprehensive error handling and observability guide provides the foundation for building production-ready applications with robust monitoring, alerting, and debugging capabilities. These patterns will help you identify and resolve issues quickly, maintain high availability, and provide excellent user experiences even when problems occur.

Remember that observability is not just about collecting data—it's about having the right information at the right time to make informed decisions about your application's health and performance. Start with basic error handling and logging, then gradually add more sophisticated monitoring and alerting as your application grows.

Frequently Asked Questions

|

Have more questions? Contact us