UNPKG

@ideal-photography/shared

Version:

Shared MongoDB and utility logic for Ideal Photography PWAs: users, products, services, bookings, orders/cart, galleries, reviews, notifications, campaigns, settings, audit logs, minimart items/orders, and push notification subscriptions.

574 lines (493 loc) 18.2 kB
/** * Error Tracking System * Cart System Refactor - Phase 5 Deployment * * Comprehensive error tracking, logging, and reporting */ const fs = require('fs'); const path = require('path'); class ErrorTracker { constructor(options = {}) { this.options = { logLevel: options.logLevel || 'error', logToFile: options.logToFile !== false, logToConsole: options.logToConsole !== false, maxLogFiles: options.maxLogFiles || 10, maxLogSize: options.maxLogSize || 10 * 1024 * 1024, // 10MB enableStackTrace: options.enableStackTrace !== false, enableSourceMap: options.enableSourceMap !== false, environment: options.environment || process.env.NODE_ENV || 'development', ...options }; this.logDir = path.join(__dirname, '..', '..', 'logs', 'errors'); this.metricsDir = path.join(__dirname, '..', '..', 'monitoring', 'errors'); this.errorCounts = new Map(); this.errorHistory = []; this.alertThresholds = { errorRate: 0.05, // 5% error rate criticalErrors: 10, // 10 critical errors per hour memoryLeaks: 100 * 1024 * 1024 // 100MB memory increase }; this.initializeDirectories(); this.setupGlobalHandlers(); } /** * Initialize required directories */ initializeDirectories() { const dirs = [this.logDir, this.metricsDir]; dirs.forEach(dir => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } }); } /** * Setup global error handlers */ setupGlobalHandlers() { // Uncaught exceptions process.on('uncaughtException', (error) => { this.logError(error, { type: 'uncaughtException', severity: 'critical', fatal: true }); // Give time for logging before exit setTimeout(() => { process.exit(1); }, 1000); }); // Unhandled promise rejections process.on('unhandledRejection', (reason, promise) => { this.logError(reason, { type: 'unhandledRejection', severity: 'critical', promise: promise.toString() }); }); // Warning events process.on('warning', (warning) => { this.logError(warning, { type: 'warning', severity: 'warning' }); }); } /** * Log an error with context * @param {Error|string} error - Error object or message * @param {Object} context - Additional context information */ logError(error, context = {}) { const errorEntry = this.createErrorEntry(error, context); // Update error counts this.updateErrorCounts(errorEntry); // Add to history this.errorHistory.push(errorEntry); // Keep history manageable if (this.errorHistory.length > 1000) { this.errorHistory = this.errorHistory.slice(-500); } // Log to console if (this.options.logToConsole) { this.logToConsole(errorEntry); } // Log to file if (this.options.logToFile) { this.logToFile(errorEntry); } // Check for alerts this.checkAlerts(errorEntry); // Send to external services if configured this.sendToExternalServices(errorEntry); return errorEntry; } /** * Create structured error entry * @param {Error|string} error - Error object or message * @param {Object} context - Additional context * @returns {Object} - Structured error entry */ createErrorEntry(error, context = {}) { const timestamp = new Date().toISOString(); const isErrorObject = error instanceof Error; const entry = { id: this.generateErrorId(), timestamp, environment: this.options.environment, level: context.severity || 'error', message: isErrorObject ? error.message : String(error), type: context.type || 'application', // Error details error: { name: isErrorObject ? error.name : 'Error', message: isErrorObject ? error.message : String(error), stack: isErrorObject && this.options.enableStackTrace ? error.stack : null, code: isErrorObject ? error.code : null }, // System context system: { nodeVersion: process.version, platform: process.platform, arch: process.arch, pid: process.pid, uptime: process.uptime(), memory: process.memoryUsage(), cpu: process.cpuUsage() }, // Request context (if available) request: context.request ? { method: context.request.method, url: context.request.url, headers: this.sanitizeHeaders(context.request.headers), userAgent: context.request.get ? context.request.get('User-Agent') : null, ip: context.request.ip, userId: context.request.user ? context.request.user.id : null } : null, // Additional context context: { ...context, request: undefined // Remove to avoid duplication }, // Fingerprint for grouping similar errors fingerprint: this.generateFingerprint(error, context) }; return entry; } /** * Generate unique error ID * @returns {string} - Unique error ID */ generateErrorId() { return `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Generate fingerprint for error grouping * @param {Error|string} error - Error object or message * @param {Object} context - Error context * @returns {string} - Error fingerprint */ generateFingerprint(error, context) { const isErrorObject = error instanceof Error; const message = isErrorObject ? error.message : String(error); const stack = isErrorObject ? error.stack : ''; const type = context.type || 'application'; // Create a hash-like fingerprint const fingerprint = `${type}:${message}:${stack.split('\n')[1] || ''}`; return Buffer.from(fingerprint).toString('base64').substr(0, 16); } /** * Update error counts for metrics * @param {Object} errorEntry - Error entry */ updateErrorCounts(errorEntry) { const key = `${errorEntry.level}:${errorEntry.fingerprint}`; const count = this.errorCounts.get(key) || 0; this.errorCounts.set(key, count + 1); } /** * Log error to console with formatting * @param {Object} errorEntry - Error entry */ logToConsole(errorEntry) { const colors = { critical: '\x1b[41m\x1b[37m', // Red background, white text error: '\x1b[31m', // Red text warning: '\x1b[33m', // Yellow text info: '\x1b[36m', // Cyan text debug: '\x1b[90m', // Gray text reset: '\x1b[0m' // Reset }; const color = colors[errorEntry.level] || colors.error; const reset = colors.reset; console.log(`${color}[${errorEntry.level.toUpperCase()}] ${errorEntry.timestamp}${reset}`); console.log(`${color}Message: ${errorEntry.message}${reset}`); console.log(`${color}Type: ${errorEntry.type}${reset}`); console.log(`${color}ID: ${errorEntry.id}${reset}`); if (errorEntry.error.stack && this.options.logLevel === 'debug') { console.log(`${color}Stack:${reset}`); console.log(errorEntry.error.stack); } if (errorEntry.request) { console.log(`${color}Request: ${errorEntry.request.method} ${errorEntry.request.url}${reset}`); } console.log('---'); } /** * Log error to file * @param {Object} errorEntry - Error entry */ logToFile(errorEntry) { const logFile = path.join(this.logDir, `errors_${this.getDateString()}.log`); const logLine = JSON.stringify(errorEntry) + '\n'; try { // Check file size and rotate if necessary if (fs.existsSync(logFile)) { const stats = fs.statSync(logFile); if (stats.size > this.options.maxLogSize) { this.rotateLogFile(logFile); } } fs.appendFileSync(logFile, logLine); } catch (writeError) { console.error('Failed to write error log:', writeError); } } /** * Rotate log file when it gets too large * @param {string} logFile - Path to log file */ rotateLogFile(logFile) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const rotatedFile = logFile.replace('.log', `_${timestamp}.log`); try { fs.renameSync(logFile, rotatedFile); // Clean up old log files this.cleanupOldLogFiles(); } catch (rotateError) { console.error('Failed to rotate log file:', rotateError); } } /** * Clean up old log files */ cleanupOldLogFiles() { try { const files = fs.readdirSync(this.logDir) .filter(file => file.startsWith('errors_') && file.endsWith('.log')) .map(file => ({ name: file, path: path.join(this.logDir, file), mtime: fs.statSync(path.join(this.logDir, file)).mtime })) .sort((a, b) => b.mtime - a.mtime); // Keep only the most recent files if (files.length > this.options.maxLogFiles) { const filesToDelete = files.slice(this.options.maxLogFiles); filesToDelete.forEach(file => { fs.unlinkSync(file.path); }); } } catch (cleanupError) { console.error('Failed to cleanup old log files:', cleanupError); } } /** * Check for alert conditions * @param {Object} errorEntry - Error entry */ checkAlerts(errorEntry) { // Critical error alert if (errorEntry.level === 'critical') { this.sendAlert({ type: 'critical_error', message: `Critical error occurred: ${errorEntry.message}`, errorId: errorEntry.id, severity: 'high' }); } // Error rate alert const recentErrors = this.getRecentErrors(60 * 60 * 1000); // Last hour const errorRate = recentErrors.length / 100; // Assuming 100 requests per hour baseline if (errorRate > this.alertThresholds.errorRate) { this.sendAlert({ type: 'high_error_rate', message: `High error rate detected: ${(errorRate * 100).toFixed(2)}%`, errorRate, severity: 'medium' }); } } /** * Send alert notification * @param {Object} alert - Alert information */ sendAlert(alert) { console.warn(`🚨 ALERT: ${alert.message}`); // Save alert to file const alertFile = path.join(this.metricsDir, `alerts_${this.getDateString()}.json`); const alertEntry = { ...alert, timestamp: new Date().toISOString(), id: this.generateErrorId() }; try { let alerts = []; if (fs.existsSync(alertFile)) { alerts = JSON.parse(fs.readFileSync(alertFile, 'utf8')); } alerts.push(alertEntry); fs.writeFileSync(alertFile, JSON.stringify(alerts, null, 2)); } catch (error) { console.error('Failed to save alert:', error); } } /** * Send error to external services * @param {Object} errorEntry - Error entry */ sendToExternalServices(errorEntry) { // Placeholder for external service integration // e.g., Sentry, LogRocket, Bugsnag, etc. if (process.env.SENTRY_DSN) { // Send to Sentry this.sendToSentry(errorEntry); } if (process.env.SLACK_ERROR_WEBHOOK) { // Send to Slack this.sendToSlack(errorEntry); } } /** * Send error to Sentry (placeholder) * @param {Object} errorEntry - Error entry */ sendToSentry(errorEntry) { // Implementation would depend on Sentry SDK console.log('Would send to Sentry:', errorEntry.id); } /** * Send error to Slack (placeholder) * @param {Object} errorEntry - Error entry */ sendToSlack(errorEntry) { // Implementation would use Slack webhook console.log('Would send to Slack:', errorEntry.id); } /** * Get recent errors within time window * @param {number} timeWindow - Time window in milliseconds * @returns {Array} - Recent errors */ getRecentErrors(timeWindow) { const cutoff = Date.now() - timeWindow; return this.errorHistory.filter(error => new Date(error.timestamp).getTime() > cutoff ); } /** * Get error statistics * @returns {Object} - Error statistics */ getStatistics() { const now = Date.now(); const lastHour = this.getRecentErrors(60 * 60 * 1000); const lastDay = this.getRecentErrors(24 * 60 * 60 * 1000); const levelCounts = {}; lastDay.forEach(error => { levelCounts[error.level] = (levelCounts[error.level] || 0) + 1; }); return { total: this.errorHistory.length, lastHour: lastHour.length, lastDay: lastDay.length, levelCounts, topErrors: this.getTopErrors(10), errorRate: lastHour.length / 60, // Errors per minute uniqueErrors: new Set(this.errorHistory.map(e => e.fingerprint)).size }; } /** * Get top errors by frequency * @param {number} limit - Number of top errors to return * @returns {Array} - Top errors */ getTopErrors(limit = 10) { const errorCounts = new Map(); this.errorHistory.forEach(error => { const key = error.fingerprint; const count = errorCounts.get(key) || 0; errorCounts.set(key, count + 1); }); return Array.from(errorCounts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, limit) .map(([fingerprint, count]) => { const example = this.errorHistory.find(e => e.fingerprint === fingerprint); return { fingerprint, count, message: example.message, level: example.level, lastSeen: example.timestamp }; }); } /** * Sanitize headers for logging * @param {Object} headers - Request headers * @returns {Object} - Sanitized headers */ sanitizeHeaders(headers = {}) { const sanitized = { ...headers }; const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key', 'x-auth-token']; sensitiveHeaders.forEach(header => { if (sanitized[header]) { sanitized[header] = '[REDACTED]'; } }); return sanitized; } /** * Get date string for file naming * @returns {string} - Date string (YYYY-MM-DD) */ getDateString() { return new Date().toISOString().split('T')[0]; } /** * Export error data * @param {Object} options - Export options * @returns {Object} - Exported data */ export(options = {}) { const { timeWindow, level, limit } = options; let errors = this.errorHistory; if (timeWindow) { errors = this.getRecentErrors(timeWindow); } if (level) { errors = errors.filter(error => error.level === level); } if (limit) { errors = errors.slice(-limit); } return { errors, statistics: this.getStatistics(), exportedAt: new Date().toISOString(), options }; } } // Create singleton instance const errorTracker = new ErrorTracker(); // Express middleware for error tracking const errorTrackingMiddleware = (err, req, res, next) => { errorTracker.logError(err, { type: 'http_request', severity: 'error', request: req }); next(err); }; // Async error wrapper const asyncErrorHandler = (fn) => { return (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(error => { errorTracker.logError(error, { type: 'async_handler', severity: 'error', request: req }); next(error); }); }; }; module.exports = { ErrorTracker, errorTracker, errorTrackingMiddleware, asyncErrorHandler };