UNPKG

a11yanalyze

Version:

A command-line tool for developers and QA engineers to test web pages and websites for WCAG 2.2 AA accessibility compliance

774 lines (697 loc) 20.8 kB
/** * Error Logging and Technical Issue Reporting System * Provides comprehensive error tracking, logging, and technical diagnostics */ import { writeFileSync, appendFileSync, existsSync, mkdirSync } from 'fs'; import { join, dirname } from 'path'; import { ScanResult, AccessibilityIssue } from '../types'; import { CrawlSession } from '../types/crawler'; /** * Error severity levels */ export type ErrorLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal'; /** * Error categories for classification */ export type ErrorCategory = | 'browser' // Browser/Playwright related errors | 'network' // Network connectivity issues | 'parsing' // HTML/CSS parsing errors | 'timeout' // Timeout-related issues | 'validation' // Input validation errors | 'configuration' // Configuration issues | 'scanning' // Accessibility scanning errors | 'crawling' // Site crawling errors | 'output' // Report generation errors | 'system' // System/OS level errors | 'unknown'; // Unclassified errors /** * Structured error entry */ export interface ErrorEntry { /** Unique error identifier */ id: string; /** Error severity level */ level: ErrorLevel; /** Error category */ category: ErrorCategory; /** Human-readable error message */ message: string; /** Technical details */ details?: string; /** Error stack trace */ stack?: string; /** Associated URL if applicable */ url?: string; /** Timestamp when error occurred */ timestamp: string; /** Source component that generated the error */ source: string; /** Additional context data */ context?: Record<string, any>; /** Recovery action taken */ recoveryAction?: string; /** Whether error was recovered from */ recovered: boolean; } /** * Technical issue report */ export interface TechnicalIssue { /** Issue identifier */ id: string; /** Issue type */ type: 'performance' | 'compatibility' | 'resource' | 'security' | 'accessibility' | 'other'; /** Issue severity */ severity: 'low' | 'medium' | 'high' | 'critical'; /** Issue title */ title: string; /** Detailed description */ description: string; /** Affected URLs */ affectedUrls: string[]; /** Reproduction steps */ reproductionSteps?: string[]; /** Suggested fixes */ suggestedFixes: string[]; /** Technical details */ technicalDetails: Record<string, any>; /** First occurrence */ firstOccurrence: string; /** Last occurrence */ lastOccurrence: string; /** Occurrence count */ occurrenceCount: number; } /** * System diagnostics information */ export interface SystemDiagnostics { /** System information */ system: { platform: string; arch: string; nodeVersion: string; memory: { total: number; used: number; percentage: number; }; cpu: { model: string; cores: number; usage?: number; }; }; /** Browser information */ browser: { name: string; version: string; executablePath?: string; userAgent: string; }; /** Network information */ network: { connectivity: 'online' | 'offline' | 'limited'; dnsResolution: boolean; responseTime?: number; }; /** Tool configuration */ configuration: { scanOptions: Record<string, any>; crawlOptions: Record<string, any>; outputOptions: Record<string, any>; }; } /** * Error logging configuration */ export interface ErrorLoggerConfig { /** Enable file logging */ enableFileLogging: boolean; /** Log file path */ logFilePath?: string; /** Minimum level to log */ minLevel: ErrorLevel; /** Maximum log file size (bytes) */ maxFileSize: number; /** Number of log files to retain */ maxFiles: number; /** Enable console logging */ enableConsoleLogging: boolean; /** Include stack traces */ includeStackTraces: boolean; /** Enable structured JSON logging */ structuredLogging: boolean; /** Log rotation enabled */ enableRotation: boolean; } /** * Error statistics for reporting */ export interface ErrorStatistics { /** Total errors by level */ byLevel: Record<ErrorLevel, number>; /** Total errors by category */ byCategory: Record<ErrorCategory, number>; /** Error trends over time */ timeline: Array<{ timestamp: string; level: ErrorLevel; category: ErrorCategory; count: number; }>; /** Most frequent errors */ frequentErrors: Array<{ message: string; count: number; level: ErrorLevel; category: ErrorCategory; }>; /** Recovery statistics */ recovery: { totalAttempts: number; successfulRecoveries: number; recoveryRate: number; }; } /** * Comprehensive error logging and technical issue reporting system */ export class ErrorLogger { private config: ErrorLoggerConfig; private errors: ErrorEntry[] = []; private technicalIssues: Map<string, TechnicalIssue> = new Map(); private sessionId: string; private startTime: Date; // Error level priorities for filtering private static readonly LEVEL_PRIORITIES: Record<ErrorLevel, number> = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4, }; constructor(config: Partial<ErrorLoggerConfig> = {}) { this.config = { enableFileLogging: true, logFilePath: './logs/a11yanalyze.log', minLevel: 'info', maxFileSize: 10 * 1024 * 1024, // 10MB maxFiles: 5, enableConsoleLogging: true, includeStackTraces: true, structuredLogging: true, enableRotation: true, ...config, }; this.sessionId = this.generateSessionId(); this.startTime = new Date(); this.initializeLogging(); } /** * Log an error entry */ logError( level: ErrorLevel, category: ErrorCategory, message: string, details?: { error?: Error; url?: string; source?: string; context?: Record<string, any>; recoveryAction?: string; recovered?: boolean; } ): string { const errorId = this.generateErrorId(); const timestamp = new Date().toISOString(); const errorEntry: ErrorEntry = { id: errorId, level, category, message, details: details?.error?.message || details?.context?.details, stack: this.config.includeStackTraces ? details?.error?.stack : undefined, url: details?.url, timestamp, source: details?.source || 'unknown', context: details?.context, recoveryAction: details?.recoveryAction, recovered: details?.recovered || false, }; // Check if this error meets minimum logging level if (this.shouldLogLevel(level)) { this.errors.push(errorEntry); // Write to file if enabled if (this.config.enableFileLogging) { this.writeToFile(errorEntry); } // Log to console if enabled if (this.config.enableConsoleLogging) { this.logToConsole(errorEntry); } // Check for technical issues this.analyzeForTechnicalIssues(errorEntry); } return errorId; } /** * Log debug information */ debug(message: string, source?: string, context?: Record<string, any>): string { return this.logError('debug', 'system', message, { source, context }); } /** * Log informational message */ info(message: string, source?: string, context?: Record<string, any>): string { return this.logError('info', 'system', message, { source, context }); } /** * Log warning */ warn(message: string, category: ErrorCategory = 'unknown', source?: string, context?: Record<string, any>): string { return this.logError('warn', category, message, { source, context }); } /** * Log error */ error(message: string, category: ErrorCategory = 'unknown', error?: Error, source?: string, context?: Record<string, any>): string { return this.logError('error', category, message, { error, source, context }); } /** * Log fatal error */ fatal(message: string, category: ErrorCategory = 'unknown', error?: Error, source?: string, context?: Record<string, any>): string { return this.logError('fatal', category, message, { error, source, context }); } /** * Log browser-specific errors */ logBrowserError(message: string, error?: Error, url?: string, recoveryAction?: string): string { return this.logError('error', 'browser', message, { error, url, source: 'browser-manager', recoveryAction, recovered: !!recoveryAction, }); } /** * Log network errors */ logNetworkError(message: string, url: string, error?: Error, responseCode?: number): string { return this.logError('error', 'network', message, { error, url, source: 'network', context: { responseCode }, }); } /** * Log scanning errors */ logScanningError(message: string, url: string, error?: Error, scanPhase?: string): string { return this.logError('error', 'scanning', message, { error, url, source: 'page-scanner', context: { scanPhase }, }); } /** * Log crawling errors */ logCrawlingError(message: string, url: string, error?: Error, depth?: number): string { return this.logError('error', 'crawling', message, { error, url, source: 'site-crawler', context: { depth }, }); } /** * Log timeout errors with recovery information */ logTimeoutError(message: string, url: string, timeout: number, recoveryAction?: string): string { return this.logError('warn', 'timeout', message, { url, source: 'timeout-handler', context: { timeout }, recoveryAction, recovered: !!recoveryAction, }); } /** * Report a technical issue */ reportTechnicalIssue( type: TechnicalIssue['type'], severity: TechnicalIssue['severity'], title: string, description: string, affectedUrls: string[], technicalDetails: Record<string, any>, suggestedFixes: string[] = [] ): string { const issueId = this.generateIssueId(); const timestamp = new Date().toISOString(); const existingIssue = Array.from(this.technicalIssues.values()) .find(issue => issue.title === title && issue.type === type); if (existingIssue) { // Update existing issue existingIssue.lastOccurrence = timestamp; existingIssue.occurrenceCount += 1; existingIssue.affectedUrls = [...new Set([...existingIssue.affectedUrls, ...affectedUrls])]; return existingIssue.id; } const technicalIssue: TechnicalIssue = { id: issueId, type, severity, title, description, affectedUrls, suggestedFixes, technicalDetails, firstOccurrence: timestamp, lastOccurrence: timestamp, occurrenceCount: 1, }; this.technicalIssues.set(issueId, technicalIssue); // Log the technical issue as an error this.logError('error', 'system', `Technical issue reported: ${title}`, { source: 'technical-issue-reporter', context: { issueId, type, severity }, }); return issueId; } /** * Collect system diagnostics */ async collectSystemDiagnostics(): Promise<SystemDiagnostics> { const os = await import('os'); const process = await import('process'); return { system: { platform: os.platform(), arch: os.arch(), nodeVersion: process.version, memory: { total: os.totalmem(), used: os.totalmem() - os.freemem(), percentage: ((os.totalmem() - os.freemem()) / os.totalmem()) * 100, }, cpu: { model: os.cpus()[0]?.model || 'unknown', cores: os.cpus().length, }, }, browser: { name: 'chromium', version: 'unknown', // Would be populated by browser manager userAgent: 'a11yanalyze/1.0.0', }, network: { connectivity: 'online', // Would be tested dynamically dnsResolution: true, // Would be tested dynamically }, configuration: { scanOptions: {}, crawlOptions: {}, outputOptions: {}, }, }; } /** * Generate error statistics */ getErrorStatistics(): ErrorStatistics { const byLevel: Record<ErrorLevel, number> = { debug: 0, info: 0, warn: 0, error: 0, fatal: 0 }; const byCategory: Record<ErrorCategory, number> = { browser: 0, network: 0, parsing: 0, timeout: 0, validation: 0, configuration: 0, scanning: 0, crawling: 0, output: 0, system: 0, unknown: 0 }; // Count errors by level and category this.errors.forEach(error => { byLevel[error.level]++; byCategory[error.category]++; }); // Generate timeline (simplified) const timeline = this.errors.map(error => ({ timestamp: error.timestamp, level: error.level, category: error.category, count: 1, })); // Find frequent errors const errorMessages = new Map<string, { count: number; level: ErrorLevel; category: ErrorCategory }>(); this.errors.forEach(error => { const key = error.message; const existing = errorMessages.get(key); if (existing) { existing.count++; } else { errorMessages.set(key, { count: 1, level: error.level, category: error.category }); } }); const frequentErrors = Array.from(errorMessages.entries()) .map(([message, data]) => ({ message, ...data })) .sort((a, b) => b.count - a.count) .slice(0, 10); // Calculate recovery statistics const recoveryAttempts = this.errors.filter(e => e.recoveryAction).length; const successfulRecoveries = this.errors.filter(e => e.recovered).length; return { byLevel, byCategory, timeline, frequentErrors, recovery: { totalAttempts: recoveryAttempts, successfulRecoveries, recoveryRate: recoveryAttempts > 0 ? successfulRecoveries / recoveryAttempts : 0, }, }; } /** * Get all errors for reporting */ getErrors(level?: ErrorLevel, category?: ErrorCategory): ErrorEntry[] { return this.errors.filter(error => { if (level && error.level !== level) return false; if (category && error.category !== category) return false; return true; }); } /** * Get all technical issues */ getTechnicalIssues(): TechnicalIssue[] { return Array.from(this.technicalIssues.values()); } /** * Export error log to file */ exportErrorLog(filePath: string, format: 'json' | 'csv' | 'txt' = 'json'): void { const data = { sessionId: this.sessionId, startTime: this.startTime.toISOString(), endTime: new Date().toISOString(), errors: this.errors, technicalIssues: Array.from(this.technicalIssues.values()), statistics: this.getErrorStatistics(), }; switch (format) { case 'json': writeFileSync(filePath, JSON.stringify(data, null, 2)); break; case 'csv': this.exportToCsv(filePath); break; case 'txt': this.exportToText(filePath); break; } } /** * Clear all logged errors */ clear(): void { this.errors = []; this.technicalIssues.clear(); this.sessionId = this.generateSessionId(); this.startTime = new Date(); } /** * Get session information */ getSessionInfo(): { id: string; startTime: Date; errorCount: number; issueCount: number } { return { id: this.sessionId, startTime: this.startTime, errorCount: this.errors.length, issueCount: this.technicalIssues.size, }; } /** * Private helper methods */ private initializeLogging(): void { if (this.config.enableFileLogging && this.config.logFilePath) { const logDir = dirname(this.config.logFilePath); if (!existsSync(logDir)) { mkdirSync(logDir, { recursive: true }); } } // Log session start this.info('Error logging session started', 'error-logger', { sessionId: this.sessionId, config: this.config, }); } private shouldLogLevel(level: ErrorLevel): boolean { return ErrorLogger.LEVEL_PRIORITIES[level] >= ErrorLogger.LEVEL_PRIORITIES[this.config.minLevel]; } private writeToFile(error: ErrorEntry): void { if (!this.config.logFilePath) return; try { const logLine = this.config.structuredLogging ? JSON.stringify(error) + '\n' : this.formatTextLogLine(error) + '\n'; appendFileSync(this.config.logFilePath, logLine); // Check for rotation if enabled if (this.config.enableRotation) { this.checkLogRotation(); } } catch (err) { // Avoid infinite recursion by not logging this error console.error('Failed to write to log file:', err); } } private logToConsole(error: ErrorEntry): void { const message = `[${error.level.toUpperCase()}] ${error.category}: ${error.message}`; switch (error.level) { case 'debug': console.debug(message); break; case 'info': console.info(message); break; case 'warn': console.warn(message); break; case 'error': case 'fatal': console.error(message); if (error.stack && this.config.includeStackTraces) { console.error(error.stack); } break; } } private analyzeForTechnicalIssues(error: ErrorEntry): void { // Analyze error patterns to automatically generate technical issues if (error.level === 'error' || error.level === 'fatal') { if (error.category === 'timeout') { this.reportTechnicalIssue( 'performance', 'medium', 'Timeout Issues Detected', `Multiple timeout errors detected during scanning: ${error.message}`, error.url ? [error.url] : [], { errorId: error.id, category: error.category }, ['Increase timeout values', 'Check network connectivity', 'Optimize page load performance'] ); } } } private formatTextLogLine(error: ErrorEntry): string { return `${error.timestamp} [${error.level.toUpperCase()}] ${error.category}:${error.source} - ${error.message}`; } private checkLogRotation(): void { // Simplified rotation logic - in production, would use proper log rotation if (!this.config.logFilePath) return; try { const stats = require('fs').statSync(this.config.logFilePath); if (stats.size > this.config.maxFileSize) { // Rotate log file const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const rotatedPath = this.config.logFilePath + '.' + timestamp; require('fs').renameSync(this.config.logFilePath, rotatedPath); } } catch (err) { // Ignore rotation errors } } private exportToCsv(filePath: string): void { const headers = ['timestamp', 'level', 'category', 'source', 'message', 'url', 'recovered']; const rows = this.errors.map(error => [ error.timestamp, error.level, error.category, error.source, error.message.replace(/,/g, ';'), // Escape commas error.url || '', error.recovered.toString(), ]); const csvContent = [headers, ...rows].map(row => row.join(',')).join('\n'); writeFileSync(filePath, csvContent); } private exportToText(filePath: string): void { const content = this.errors .map(error => this.formatTextLogLine(error)) .join('\n'); writeFileSync(filePath, content); } private generateSessionId(): string { return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } private generateErrorId(): string { return `error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } private generateIssueId(): string { return `issue_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Static utility methods */ /** * Create a default error logger */ static createDefault(): ErrorLogger { return new ErrorLogger(); } /** * Create a console-only error logger */ static createConsoleOnly(): ErrorLogger { return new ErrorLogger({ enableFileLogging: false, enableConsoleLogging: true, }); } /** * Create a file-only error logger */ static createFileOnly(logFilePath: string): ErrorLogger { return new ErrorLogger({ enableFileLogging: true, enableConsoleLogging: false, logFilePath, }); } /** * Create a debug-level error logger */ static createDebug(): ErrorLogger { return new ErrorLogger({ minLevel: 'debug', includeStackTraces: true, }); } }