Spaces:
Configuration error
Configuration error
| // --- 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'; | |
| // } | |
| // } |