UNPKG

bowling-analysis-system

Version:

A comprehensive system for analyzing bowling techniques using video processing and metrics calculation

454 lines (382 loc) 13.3 kB
/** * Centralized error handling system for the video processing application * @class ErrorHandler */ class ErrorHandler { /** * Creates an instance of ErrorHandler * @param {Object} options - Error handler options * @param {Object} options.logger - Logger instance * @param {Object} [options.config] - Error configuration * @param {Boolean} [options.config.collectStackTraces=true] - Whether to collect stack traces * @param {Boolean} [options.config.normalizeErrors=true] - Whether to normalize errors * @param {Boolean} [options.config.addContextInfo=true] - Whether to add context to error objects * @param {Boolean} [options.config.logAllErrors=true] - Whether to log all errors * @param {Array<String>} [options.config.criticalErrorTypes=[]] - Error types considered critical * @param {Function} [options.errorCallback] - Callback for handling critical errors */ constructor(options) { if (!options || !options.logger) { throw new Error('Logger is required for ErrorHandler'); } this.logger = options.logger; this.config = options.config || {}; this.errorCallback = options.errorCallback; // Set config defaults this.collectStackTraces = this.config.collectStackTraces !== false; this.normalizeErrors = this.config.normalizeErrors !== false; this.addContextInfo = this.config.addContextInfo !== false; this.logAllErrors = this.config.logAllErrors !== false; // Error types considered critical this.criticalErrorTypes = this.config.criticalErrorTypes || [ 'SystemError', 'OutOfMemoryError', 'DataCorruptionError' ]; // Error history this.errorHistory = []; this.maxHistorySize = this.config.maxHistorySize || 100; // Error type counts this.errorTypeCounts = new Map(); this.logger.info('ErrorHandler initialized'); } /** * Handle an error that occurs in the application * @param {String} source - Source of the error (component/function) * @param {Error} error - Error object * @param {Object} [context={}] - Context information * @returns {Promise<Object>} Processed error object */ async handleError(source, error, context = {}) { // Process the error const processedError = this.processError(source, error, context); // Add to error history this._addToErrorHistory(processedError); // Log the error if configured to do so if (this.logAllErrors) { this._logError(processedError); } // Call error callback for critical errors if (processedError.critical && typeof this.errorCallback === 'function') { try { await this.errorCallback(processedError); } catch (callbackError) { this.logger.error('Error in error callback:', callbackError); } } return processedError; } /** * Process and normalize an error * @param {String} source - Source of the error * @param {Error} error - Error object * @param {Object} [context={}] - Context information * @returns {Object} Processed error object */ processError(source, error, context = {}) { // Create base error object const processedError = { id: `error-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, timestamp: new Date().toISOString(), source: source, message: error.message || 'Unknown error', name: error.name || 'Error', code: error.code, type: this._determineErrorType(error), critical: this._isCriticalError(error), context: {} }; // Add stack trace if configured if (this.collectStackTraces) { processedError.stack = error.stack; } // Add context information if configured if (this.addContextInfo && context) { // Avoid circular references and overly complex objects const sanitizedContext = this._sanitizeContext(context); processedError.context = sanitizedContext; } // Update error type counts this._updateErrorTypeCounts(processedError.type); return processedError; } /** * Create a new error with the specified parameters * @param {String} message - Error message * @param {Object} [options={}] - Error options * @param {String} [options.name] - Error name * @param {String} [options.type] - Error type * @param {String} [options.code] - Error code * @param {Boolean} [options.critical] - Whether the error is critical * @param {Error} [options.cause] - Original error that caused this one * @returns {Error} - Created error */ createError(message, options = {}) { const error = new Error(message); if (options.name) { error.name = options.name; } if (options.type) { error.type = options.type; } if (options.code) { error.code = options.code; } if (options.critical !== undefined) { error.critical = options.critical; } if (options.cause) { error.cause = options.cause; } return error; } /** * Wrap a function with error handling * @param {Function} fn - Function to wrap * @param {Object} [options={}] - Wrapper options * @param {String} [options.source] - Source identifier * @param {Object} [options.context] - Additional context * @returns {Function} Wrapped function */ wrapWithErrorHandling(fn, options = {}) { const self = this; const source = options.source || fn.name || 'anonymous'; const context = options.context || {}; return async function errorHandlingWrapper(...args) { try { return await fn.apply(this, args); } catch (error) { await self.handleError(source, error, { ...context, arguments: args.map(arg => typeof arg === 'object' ? (arg === null ? null : Object.keys(arg)) : typeof arg ) }); throw error; } }; } /** * Get error history * @param {Object} [options={}] - Filter options * @param {String} [options.source] - Filter by source * @param {String} [options.type] - Filter by error type * @param {Boolean} [options.critical] - Filter by critical flag * @param {Number} [options.limit] - Maximum number of errors to return * @returns {Array<Object>} Error history */ getErrorHistory(options = {}) { let history = [...this.errorHistory]; // Apply filters if (options.source) { history = history.filter(err => err.source === options.source); } if (options.type) { history = history.filter(err => err.type === options.type); } if (options.critical !== undefined) { history = history.filter(err => err.critical === options.critical); } // Apply limit if (options.limit && options.limit > 0) { history = history.slice(0, options.limit); } return history; } /** * Get error type counts * @returns {Object} Error type counts */ getErrorTypeCounts() { const result = {}; for (const [type, count] of this.errorTypeCounts.entries()) { result[type] = count; } return result; } /** * Clear error history */ clearErrorHistory() { this.errorHistory = []; this.errorTypeCounts.clear(); this.logger.info('Error history cleared'); } /** * Determine error type from error object * @param {Error} error - Error object * @returns {String} Error type * @private */ _determineErrorType(error) { // Check if error already has a type if (error.type) { return error.type; } // Map standard error names to types const typeMap = { 'ReferenceError': 'ReferenceError', 'SyntaxError': 'SyntaxError', 'TypeError': 'TypeError', 'RangeError': 'RangeError', 'URIError': 'URIError', 'EvalError': 'EvalError', 'SystemError': 'SystemError' }; if (error.name && typeMap[error.name]) { return typeMap[error.name]; } // Categorize by error code if available if (error.code) { if (error.code.startsWith('ENOT') || error.code.startsWith('EAI_') || error.code === 'ENOTFOUND') { return 'NetworkError'; } if (error.code.startsWith('ENOENT') || error.code === 'EEXIST') { return 'FileSystemError'; } if (error.code === 'ETIMEDOUT' || error.code === 'ESOCKETTIMEDOUT' || error.code === 'ECONNRESET') { return 'TimeoutError'; } } // Look for patterns in the error message const message = error.message || ''; if (message.includes('timeout') || message.includes('timed out')) { return 'TimeoutError'; } if (message.includes('permission') || message.includes('access') || message.includes('denied')) { return 'AccessError'; } if (message.includes('memory') || message.includes('allocation')) { return 'MemoryError'; } if (message.includes('unexpected end') || message.includes('invalid') || message.includes('malformed')) { return 'ValidationError'; } // Default to UnknownError return 'UnknownError'; } /** * Determine if an error is critical * @param {Error} error - Error object * @returns {Boolean} Whether the error is critical * @private */ _isCriticalError(error) { // Check if error is explicitly marked as critical if (error.critical === true) { return true; } // Check error type against critical types const errorType = this._determineErrorType(error); if (this.criticalErrorTypes.includes(errorType)) { return true; } // Check for critical error codes const criticalCodes = ['EPERM', 'EACCES', 'ENOMEM', 'ENOSPC']; if (error.code && criticalCodes.includes(error.code)) { return true; } return false; } /** * Add error to history * @param {Object} error - Processed error object * @private */ _addToErrorHistory(error) { this.errorHistory.unshift(error); // Limit history size if (this.errorHistory.length > this.maxHistorySize) { this.errorHistory = this.errorHistory.slice(0, this.maxHistorySize); } } /** * Update error type counts * @param {String} type - Error type * @private */ _updateErrorTypeCounts(type) { const count = this.errorTypeCounts.get(type) || 0; this.errorTypeCounts.set(type, count + 1); } /** * Log an error * @param {Object} error - Processed error object * @private */ _logError(error) { const logObject = { errorId: error.id, source: error.source, type: error.type, code: error.code, critical: error.critical }; if (error.context) { logObject.context = error.context; } if (error.critical) { this.logger.error(`CRITICAL ERROR: ${error.message}`, logObject); } else { this.logger.error(`ERROR: ${error.message}`, logObject); } if (error.stack && this.collectStackTraces) { this.logger.debug('Error stack trace:', error.stack); } } /** * Sanitize context object to avoid circular references * @param {Object} context - Context object * @returns {Object} Sanitized context * @private */ _sanitizeContext(context) { const sanitized = {}; const seen = new WeakMap(); const sanitizeValue = (value, depth = 0) => { if (depth > 3) return '[Nested Object]'; if (value === null || value === undefined) { return value; } if (typeof value !== 'object') { if (typeof value === 'function') { return '[Function]'; } return value; } if (value instanceof Date) { return value.toISOString(); } if (value instanceof Error) { return { name: value.name, message: value.message, code: value.code }; } if (seen.has(value)) { return '[Circular]'; } seen.set(value, true); if (Array.isArray(value)) { return value.map(v => sanitizeValue(v, depth + 1)); } const result = {}; for (const key of Object.keys(value)) { if (Object.prototype.hasOwnProperty.call(value, key)) { result[key] = sanitizeValue(value[key], depth + 1); } } return result; }; for (const key of Object.keys(context)) { if (Object.prototype.hasOwnProperty.call(context, key)) { sanitized[key] = sanitizeValue(context[key]); } } return sanitized; } } module.exports = ErrorHandler;