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
text/typescript
/**
* 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,
});
}
}