File size: 18,270 Bytes
e7427b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
// --- File: lib/errorHandler.ts ---
// --- Tech Used ---
// - TypeScript: For strong typing and better code organization.
// - localStorage API: For persisting a small number of recent error logs across sessions.
// - Navigator Storage API (`navigator.storage.estimate()`): For checking client-side storage quotas.

// --- Interface for Error Log Entries ---
interface ErrorLog {
  id: string; // Unique identifier for the error log.
  timestamp: Date; // When the error occurred.
  module: string; // The module/component where the error originated.
  errorType: 'calculation' | 'storage' | 'validation' | 'network' | 'system' | 'unknown'; // Categorization of the error.
  message: string; // The error message.
  stack?: string; // The JavaScript stack trace, if available.
  userAction?: string; // Context: What the user was doing.
  recoveryAction?: string; // Suggested action for the user or developer.
  extraData?: object; // Additional contextual data (e.g., componentStack from ErrorBoundary).
}

// --- ErrorHandler Class (Singleton Pattern) ---
// Function: Provides a centralized system for logging, categorizing, persisting,
// and managing client-side JavaScript errors.
class ErrorHandler {
  private errorLogs: ErrorLog[] = []; // In-memory store for error logs.
  private readonly maxMemoryLogs = 1000; // Max logs to keep in memory.
  private readonly maxPersistedLogs = 100; // Max logs to persist in localStorage.
  private readonly localStorageKey = 'prithvi_error_logs_v1'; // Key for localStorage.

  constructor() {
    // Attempt to load persisted logs when the handler is instantiated.
    this.loadPersistedLogs();
  }

  // --- Core Logging Method ---
  // Logs an error with context, categorizes it, and handles persistence.
  public logError(
    error: Error | unknown, // Can accept unknown and cast, or ensure Error instance before calling
    module: string,
    userAction?: string,
    extraData?: object
  ): string {
    const actualError = error instanceof Error ? error : new Error(String(error || 'Unknown error occurred'));
    const errorId = this.generateErrorId();

    const errorLogEntry: ErrorLog = {
      id: errorId,
      timestamp: new Date(),
      module,
      errorType: this.categorizeError(actualError),
      message: actualError.message,
      stack: actualError.stack,
      userAction,
      recoveryAction: this.getRecoveryAction(actualError, module),
      extraData,
    };

    this.errorLogs.push(errorLogEntry);

    // Trim in-memory logs if exceeding max.
    if (this.errorLogs.length > this.maxMemoryLogs) {
      this.errorLogs = this.errorLogs.slice(-this.maxMemoryLogs);
    }

    this.persistErrorLogs(); // Persist to localStorage.

    // Log to console for immediate developer visibility.
    console.error(
      `[${module}] Error ID: ${errorId} (Action: ${userAction || 'N/A'}, Type: ${errorLogEntry.errorType})`,
      actualError,
      extraData ? { extraData } : ''
    );
    
    // --- Placeholder for External Error Tracking ---
    // Example: Integrate with Sentry or a similar service here.
    // if (process.env.NODE_ENV === 'production' || IS_EXTERNAL_LOGGING_ENABLED) {
    //   this.sendToExternalService(actualError, errorLogEntry);
    // }

    return errorId;
  }

  // --- Private Helper Methods ---

  // Categorizes error based on its message content or type.
  private categorizeError(error: Error): ErrorLog['errorType'] {
    const message = error.message.toLowerCase();
    const errorName = error.name?.toLowerCase();

    // Prioritize specific error types if available (e.g., custom error classes)
    if ((error as any).errorType) return (error as any).errorType; // If error has a pre-defined type

    if (errorName === 'validationerror' || message.includes('validation') || message.includes('required') || message.includes('invalid input')) {
      return 'validation';
    }
    if (message.includes('calculation') || message.includes('math') || message.includes('nan') || errorName === 'rangeerror') {
      return 'calculation';
    }
    if (message.includes('storage') || message.includes('indexeddb') || message.includes('localstorage') || message.includes('quota') || message.includes('failed to execute \'setItem\' on \'Storage\'')) {
      return 'storage';
    }
    if (message.includes('network') || message.includes('fetch') || message.includes('offline') || message.includes('timeout') || errorName === 'networkerror') {
      return 'network';
    }
    if (errorName === 'typeerror' || errorName === 'referenceerror' || errorName === 'syntaxerror') {
        return 'system';
    }
    
    return 'unknown'; // Default if no specific category matches
  }

  // Suggests a recovery action based on the categorized error type.
  private getRecoveryAction(error: Error, module: string): string {
    const errorType = this.categorizeError(error);
    switch (errorType) {
      case 'calculation': return `Please check the input values for '${module}' and ensure they are valid numbers.`;
      case 'storage': return 'Try clearing browser cache/storage or ensure sufficient disk space. Refreshing might help.';
      case 'validation': return `Please review the form inputs for '${module}' and correct any highlighted errors.`;
      case 'network': return 'Please check your internet connection. Some features may work offline.';
      case 'system': return 'A system error occurred. Please try refreshing the page. If it persists, note the Error ID.';
      default: return 'An unexpected error occurred. Please try again or refresh the page. Note the Error ID if it persists.';
    }
  }

  // Generates a reasonably unique error ID.
  private generateErrorId(): string {
    return `ERR_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 7)}`;
  }

  // Persists the most recent logs to localStorage.
  private persistErrorLogs(): void {
    try {
      const logsToPersist = this.errorLogs.slice(-this.maxPersistedLogs);
      localStorage.setItem(this.localStorageKey, JSON.stringify(logsToPersist));
    } catch (e) {
      // This can happen if localStorage is full or disabled (e.g., private browsing).
      console.warn('ErrorHandler: Could not persist error logs to localStorage.', e);
    }
  }

  // Loads logs from localStorage on initialization.
  private loadPersistedLogs(): void {
    try {
      const storedLogs = localStorage.getItem(this.localStorageKey);
      if (storedLogs) {
        const parsedLogs: ErrorLog[] = JSON.parse(storedLogs);
        // Basic validation of loaded logs
        if (Array.isArray(parsedLogs) && parsedLogs.every(log => typeof log.id === 'string')) {
          this.errorLogs = [...parsedLogs, ...this.errorLogs] // Prepend older logs, then newer in-memory logs
                           .filter((log, index, self) => index === self.findIndex(l => l.id === log.id)) // Deduplicate
                           .sort((a,b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()) // Ensure chronological
                           .slice(-this.maxMemoryLogs); // Keep within memory limits
        }
      }
    } catch (e) {
      console.warn('ErrorHandler: Could not load persisted error logs from localStorage.', e);
    }
  }

  // --- Public Utility Methods ---

  public getRecentErrors(count = 10): ErrorLog[] {
    return this.errorLogs.slice(-Math.min(count, this.errorLogs.length));
  }

  public getErrorsByModule(module: string): ErrorLog[] {
    return this.errorLogs.filter(log => log.module === module);
  }

  public clearLogs(clearPersisted: boolean = true): void {
    this.errorLogs = [];
    if (clearPersisted) {
      localStorage.removeItem(this.localStorageKey);
    }
    console.info("ErrorHandler: Logs cleared.");
  }

  // Wraps an asynchronous operation, automatically catching and logging errors.
  public async handleAsync<T>(
    operation: () => Promise<T>,
    module: string,
    userAction?: string
  ): Promise<{ success: boolean; data?: T; errorId?: string }> {
    try {
      const data = await operation();
      return { success: true, data };
    } catch (err) {
      const error = err instanceof Error ? err : new Error(String(err || "Async operation failed"));
      const errorId = this.logError(error, module, userAction);
      return { success: false, errorId };
    }
  }

  // Wraps a synchronous operation, automatically catching and logging errors.
  public handleSync<T>(
    operation: () => T,
    module: string,
    userAction?: string
  ): { success: boolean; data?: T; errorId?: string } {
    try {
      const data = operation();
      return { success: true, data };
    } catch (err) {
      const error = err instanceof Error ? err : new Error(String(err || "Sync operation failed"));
      const errorId = this.logError(error, module, userAction);
      return { success: false, errorId };
    }
  }

  // --- Domain-Specific Utilities (Examples) ---

  // Validates a set of inputs against a list of required field names.
  // Throws a custom error if validation fails.
  public validateCalculationInputs(inputs: Record<string, any>, requiredFields: string[]): void {
    for (const field of requiredFields) {
      const value = inputs[field];
      if (value === undefined || value === null || String(value).trim() === '') {
        const err = new Error(`Validation failed: Required field '${field}' is missing or empty.`);
        err.name = 'ValidationError'; // For better categorization
        throw err;
      }
      // Enhanced number validation
      if (typeof value !== 'number' && typeof value !== 'string') {
          const err = new Error(`Validation failed: Field '${field}' must be a number or a string convertible to a number.`);
          err.name = 'ValidationError';
          throw err;
      }
      if (typeof value === 'string') {
          const numValue = Number(value);
          if (isNaN(numValue) || !isFinite(numValue)) {
              const err = new Error(`Validation failed: Field '${field}' ('${value}') is not a valid number.`);
              err.name = 'ValidationError';
              throw err;
          }
          inputs[field] = numValue; // Coerce to number if valid string representation
      } else if (typeof value === 'number' && (isNaN(value) || !isFinite(value))) {
          const err = new Error(`Validation failed: Field '${field}' is not a finite number.`);
          err.name = 'ValidationError';
          throw err;
      }
    }
  }

  // Checks available client-side storage quota.
  public async checkStorageQuota(): Promise<{ availableGB: number; usedGB: number; quotaGB: number; lowStorage: boolean }> {
    if (navigator.storage && navigator.storage.estimate) {
      try {
        const estimate = await navigator.storage.estimate();
        const quota = estimate.quota || 0;
        const usage = estimate.usage || 0;
        const toGB = (bytes: number) => parseFloat((bytes / (1024 ** 3)).toFixed(3));
        const availableBytes = quota - usage;
        return {
          availableGB: toGB(availableBytes),
          usedGB: toGB(usage),
          quotaGB: toGB(quota),
          lowStorage: availableBytes < (50 * 1024 * 1024) // Threshold: e.g., less than 50MB available
        };
      } catch (error) {
        this.logError(error as Error, 'ErrorHandler', 'checkStorageQuota_estimate_failed');
      }
    }
    console.warn("StorageManager API not available for quota estimation.");
    return { availableGB: 0, usedGB: 0, quotaGB: 0, lowStorage: false }; // Fallback if API not available
  }

  // Wraps a storage operation, checking quota and providing a fallback.
  public async handleStorageOperation<T>(
    operation: () => Promise<T>,
    module: string, // Added module for context
    userAction?: string, // Added userAction for context
    fallback?: () => T | Promise<T>
  ): Promise<T> {
    try {
      const storageInfo = await this.checkStorageQuota();
      if (storageInfo.lowStorage && storageInfo.quotaGB > 0) { // Only throw if quota API worked & indicates low storage
        const err = new Error('Insufficient storage space available for operation.');
        (err as any).errorType = 'storage'; // Help categorization
        throw err;
      }
      return await operation();
    } catch (err) {
      const error = err instanceof Error ? err : new Error(String(err || "Storage operation failed"));
      this.logError(error, module, userAction || 'storage_operation');
      if (fallback) {
        console.warn(`ErrorHandler: Storage operation for '${module}' failed, using fallback.`);
        return await Promise.resolve(fallback()); // Ensure fallback can be async
      }
      throw error; // Re-throw if no fallback
    }
  }

  // Generates a user-friendly error report string.
  public generateErrorReport(): string {
    const recentErrors = this.getRecentErrors(20);
    const errorsByModuleSummary = recentErrors.reduce((acc, error) => {
      acc[error.module] = (acc[error.module] || 0) + 1;
      return acc;
    }, {} as Record<string, number>);

    return `
--- Prithvi Guardian AI - Error Report ---
Generated: ${new Date().toISOString()}

Total Logs in Memory: ${this.errorLogs.length} (Max: ${this.maxMemoryLogs})
Persisted Logs (Last Session): Count may vary, up to ${this.maxPersistedLogs}

--- Summary of Last 20 Errors ---
Errors by Module:
${Object.entries(errorsByModuleSummary).length > 0
  ? Object.entries(errorsByModuleSummary).map(([module, count]) => `  - ${module}: ${count} error(s)`).join('\n')
  : '  No errors in the last 20 logs.'
}

--- Details of Last 20 Errors ---
${recentErrors.length > 0
  ? recentErrors.map(error => `
  Timestamp: ${error.timestamp.toISOString()}
  Module:    ${error.module}
  Error ID:  ${error.id}
  Type:      ${error.errorType}
  Message:   ${error.message}
  ${error.userAction ? `Action:    ${error.userAction}` : ''}
  Recovery:  ${error.recoveryAction}
  ${error.extraData ? `Extra:     ${JSON.stringify(error.extraData)}` : ''}
  ---`).join('\n')
  : 'No recent errors to display.'
}
    `.trim().replace(/^\s*\n/gm, ''); // Trim and remove empty leading lines from template literal
  }

  // Placeholder for sending logs to an external service
  // private sendToExternalService(error: Error, errorLogEntry: ErrorLog): void {
  //   // Example:
  //   // if (typeof Sentry !== 'undefined') { // Check if Sentry SDK is loaded
  //   //   Sentry.withScope(scope => {
  //   //     scope.setTag("module", errorLogEntry.module);
  //   //     scope.setTag("errorType", errorLogEntry.errorType);
  //   //     if (errorLogEntry.userAction) scope.setTag("userAction", errorLogEntry.userAction);
  //   //     scope.setExtra("errorId", errorLogEntry.id);
  //   //     scope.setExtra("recoveryAction", errorLogEntry.recoveryAction);
  //   //     if (errorLogEntry.extraData) scope.setContext("custom_extra_data", errorLogEntry.extraData);
  //   //     Sentry.captureException(error);
  //   //   });
  //   // }
  //   console.log("Placeholder: Would send to external service:", errorLogEntry);
  // }
}

// --- Singleton Instance ---
// Create and export a single instance of ErrorHandler for the entire application.
export const errorHandler = new ErrorHandler();

// --- Global Error Listeners ---
// These catch errors that are not handled by try/catch blocks or React Error Boundaries.

// Catches synchronous errors and unhandled errors from scripts.
window.addEventListener('error', (event: ErrorEvent) => {
  // event.error is the actual Error object, event.message is just the message string.
  const errorToLog = event.error || new Error(event.message || 'Unknown global error');
  errorHandler.logError(
    errorToLog,
    'global_sync_error_handler', // Module context
    'uncaught_exception', // User action/context
    { // Extra data
      filename: event.filename,
      lineno: event.lineno,
      colno: event.colno,
    }
  );
});

// Catches unhandled promise rejections.
window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {
  // event.reason can be anything, not necessarily an Error object.
  const reason = event.reason;
  const errorToLog = reason instanceof Error
    ? reason
    : new Error(typeof reason === 'string' ? reason : 'Unhandled promise rejection with non-error reason');

  errorHandler.logError(
    errorToLog,
    'global_promise_rejection_handler', // Module context
    'unhandled_rejection', // User action/context
    { originalReason: event.reason } // Store original reason if it wasn't an Error
  );
});

// --- Exported Utility Object for Convenience ---
// Provides a namespaced way to access common error handling wrappers.
export const withErrorHandling = {
  async: <T>(
    operation: () => Promise<T>,
    module: string,
    userAction?: string
  ): Promise<{ success: boolean; data?: T; errorId?: string }> =>
    errorHandler.handleAsync(operation, module, userAction),

  sync: <T>(
    operation: () => T,
    module: string,
    userAction?: string
  ): { success: boolean; data?: T; errorId?: string } =>
    errorHandler.handleSync(operation, module, userAction),

  validate: (
    inputs: Record<string, any>,
    requiredFields: string[],
    module: string // Added module for context in validation errors
  ): void => {
      try {
          errorHandler.validateCalculationInputs(inputs, requiredFields);
      } catch (error) {
          // Log the validation error specifically through the central logger
          errorHandler.logError(error as Error, module, 'input_validation_failed');
          throw error; // Re-throw to be caught by component or handleAsync/Sync
      }
  },

  storage: <T>(
    operation: () => Promise<T>,
    module: string, // Added module for context
    userAction?: string, // Added userAction for context
    fallback?: () => T | Promise<T>
  ): Promise<T> =>
    errorHandler.handleStorageOperation(operation, module, userAction, fallback),
};

// --- Optional: Example of how you might use a custom error type ---
// export class CalculationError extends Error {
//   public readonly errorType = 'calculation';
//   constructor(message: string, public readonly calculationDetails?: object) {
//     super(message);
//     this.name = 'CalculationError';
//   }
// }