bowling-analysis-system
Version:
A comprehensive system for analyzing bowling techniques using video processing and metrics calculation
454 lines (382 loc) • 13.3 kB
JavaScript
/**
* 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;