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