// --- 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( operation: () => Promise, 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( 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, 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( operation: () => Promise, module: string, // Added module for context userAction?: string, // Added userAction for context fallback?: () => T | Promise ): Promise { 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); 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: ( operation: () => Promise, module: string, userAction?: string ): Promise<{ success: boolean; data?: T; errorId?: string }> => errorHandler.handleAsync(operation, module, userAction), sync: ( operation: () => T, module: string, userAction?: string ): { success: boolean; data?: T; errorId?: string } => errorHandler.handleSync(operation, module, userAction), validate: ( inputs: Record, 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: ( operation: () => Promise, module: string, // Added module for context userAction?: string, // Added userAction for context fallback?: () => T | Promise ): Promise => 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'; // } // }