PrithviGuardian / client /src /lib /errorHandler.ts
Varad Bakshi
Fresh upload
e7427b5
Raw
History Blame Contribute Delete
18.3 kB
// --- 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';
// }
// }