All files / src/lib/errors error-analytics.ts

48.46% Statements 79/163
100% Branches 0/0
0% Functions 0/7
48.46% Lines 79/163

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 1641x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x           1x 1x 1x 1x 1x                                                           1x 1x 1x 1x 1x                             1x 1x 1x 1x 1x                                     1x 1x 1x 1x 1x             1x 1x 1x 1x 1x             1x 1x 1x 1x 1x 1x 1x 1x 1x 1x             1x 1x  
/**
 * Error Analytics - Client-side error logging with batching
 *
 * Features:
 * - Batched error logging for performance
 * - Automatic severity classification
 * - Queue management with max size
 * - Development mode console logging
 * - Graceful degradation on network failures
 */
 
import { ErrorType, classifyError } from './error-classifier';
 
export type ErrorSeverity = 'low' | 'medium' | 'high' | 'critical';
 
interface ErrorLogPayload {
  type: ErrorType;
  message: string;
  stack?: string;
  context?: string;
  url: string;
  userAgent: string;
  timestamp: string;
  digest?: string;
  severity: ErrorSeverity;
  metadata?: Record<string, unknown>;
}
 
interface LogOptions {
  context?: string;
  digest?: string;
  severity?: ErrorSeverity;
  metadata?: Record<string, unknown>;
}
 
class ErrorAnalytics {
  private queue: ErrorLogPayload[] = [];
  private flushInterval: number = 5000; // 5 seconds
  private maxQueueSize: number = 10;
  private intervalId: ReturnType<typeof setInterval> | null = null;
 
  constructor() {
    if (typeof window !== 'undefined') {
      this.intervalId = setInterval(() => this.flush(), this.flushInterval);
      window.addEventListener('beforeunload', () => this.flush());
    }
  }
 
  /**
   * Log an error with optional context and metadata
   */
  log(error: Error | unknown, options: LogOptions = {}): void {
    const classified = classifyError(error);

    const payload: ErrorLogPayload = {
      type: classified.type,
      message: classified.technicalMessage,
      stack: error instanceof Error ? error.stack : undefined,
      context: options.context,
      url: typeof window !== 'undefined' ? window.location.href : '',
      userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : '',
      timestamp: new Date().toISOString(),
      digest: options.digest,
      severity: options.severity || this.getSeverity(classified.type),
      metadata: options.metadata,
    };

    this.queue.push(payload);

    // Flush immediately for critical errors
    if (payload.severity === 'critical') {
      this.flush();
    } else if (this.queue.length >= this.maxQueueSize) {
      this.flush();
    }

    // Log to console in development
    if (process.env.NODE_ENV === 'development') {
      console.error('[Error Analytics]', payload);
    }
  }
 
  /**
   * Determine severity based on error type
   */
  private getSeverity(type: ErrorType): ErrorSeverity {
    switch (type) {
      case ErrorType.SERVER:
      case ErrorType.PAYMENT:
        return 'high';
      case ErrorType.AUTHENTICATION:
      case ErrorType.AUTHORIZATION:
        return 'medium';
      case ErrorType.VALIDATION:
      case ErrorType.NOT_FOUND:
        return 'low';
      default:
        return 'medium';
    }
  }
 
  /**
   * Flush queued errors to the server
   */
  private async flush(): Promise<void> {
    if (this.queue.length === 0) return;

    const errors = [...this.queue];
    this.queue = [];

    try {
      await fetch('/api/monitoring/errors', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ errors }),
      });
    } catch {
      // Put errors back in queue if failed (limited retry)
      if (errors.length < this.maxQueueSize * 2) {
        this.queue.unshift(...errors);
      }
    }
  }
 
  /**
   * Stop the analytics service
   */
  destroy(): void {
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
    this.flush();
  }
}
 
// Singleton instance
let analyticsInstance: ErrorAnalytics | null = null;
 
function getAnalytics(): ErrorAnalytics {
  if (!analyticsInstance) {
    analyticsInstance = new ErrorAnalytics();
  }
  return analyticsInstance;
}
 
/**
 * Log an error to the analytics service
 *
 * @example
 * ```tsx
 * logError(error, { context: 'products', digest: error.digest });
 * logError(error, { severity: 'critical', metadata: { orderId: '123' } });
 * ```
 */
export function logError(
  error: Error | unknown,
  options?: LogOptions
): void {
  getAnalytics().log(error, options);
}
 
export { ErrorAnalytics };