/** * Client-Side Error Reporter * * Automatically captures and reports JavaScript errors during file uploads * to help diagnose issues during beta testing. */ class ErrorReporter { constructor() { this.uploadId = null; this.errorQueue = []; this.reportEndpoint = '/api/v1/captures/report_client_error'; // Setup global error handler this.setupGlobalErrorHandler(); } /** * Set current upload ID for error tracking */ setUploadId(uploadId) { this.uploadId = uploadId; console.log(`[ErrorReporter] Tracking upload: ${uploadId}`); } /** * Setup global error handlers */ setupGlobalErrorHandler() { // Catch unhandled JavaScript errors window.addEventListener('error', (event) => { this.reportError({ error_type: event.error?.name || 'Error', error_message: event.message, stack_trace: event.error?.stack, filename: event.filename, lineno: event.lineno, colno: event.colno, source: 'window.error' }); }); // Catch unhandled promise rejections window.addEventListener('unhandledrejection', (event) => { this.reportError({ error_type: 'UnhandledPromiseRejection', error_message: event.reason?.message || String(event.reason), stack_trace: event.reason?.stack, source: 'unhandledrejection' }); }); } /** * Report error to server */ async reportError(errorData) { const errorReport = { upload_id: this.uploadId || 'no_upload_id', timestamp: new Date().toISOString(), ...errorData, browser_info: this.getBrowserInfo(), page_url: window.location.href }; // Log to console console.error('[ErrorReporter] Reporting error:', errorReport); // Queue error (in case of network issues) this.errorQueue.push(errorReport); // Try to send immediately try { await this.sendErrorReport(errorReport); // Remove from queue if successful this.errorQueue = this.errorQueue.filter(e => e !== errorReport); } catch (e) { console.warn('[ErrorReporter] Failed to send error report:', e); // Will retry later } } /** * Send error report to server */ async sendErrorReport(errorReport) { const response = await fetch(this.reportEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(errorReport) }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return response.json(); } /** * Report parse error specifically */ reportParseError(filename, error, fileContentSample = '') { this.reportError({ error_type: 'SubFileParseError', error_message: error.message || String(error), stack_trace: error.stack, filename: filename, file_content_sample: fileContentSample.substring(0, 500), source: 'sub_parser' }); } /** * Report upload error */ reportUploadError(filename, error, stage = 'unknown') { this.reportError({ error_type: 'UploadError', error_message: error.message || String(error), stack_trace: error.stack, filename: filename, upload_stage: stage, source: 'upload_handler' }); } /** * Report validation error */ reportValidationError(field, value, reason) { this.reportError({ error_type: 'ValidationError', error_message: `${field} validation failed: ${reason}`, validation_field: field, validation_value: String(value), source: 'validation' }); } /** * Report network error */ reportNetworkError(url, status, statusText) { this.reportError({ error_type: 'NetworkError', error_message: `${status} ${statusText}`, request_url: url, http_status: status, source: 'network' }); } /** * Get browser information */ getBrowserInfo() { return { user_agent: navigator.userAgent, platform: navigator.platform, language: navigator.language, screen_width: screen.width, screen_height: screen.height, viewport_width: window.innerWidth, viewport_height: window.innerHeight, connection_type: navigator.connection?.effectiveType, device_memory: navigator.deviceMemory, hardware_concurrency: navigator.hardwareConcurrency, online: navigator.onLine }; } /** * Flush error queue (send all pending errors) */ async flushErrorQueue() { const pending = [...this.errorQueue]; this.errorQueue = []; for (const error of pending) { try { await this.sendErrorReport(error); } catch (e) { // If still fails, put back in queue this.errorQueue.push(error); } } return { sent: pending.length - this.errorQueue.length, failed: this.errorQueue.length }; } /** * Generate upload ID */ generateUploadId() { const timestamp = new Date().toISOString().replace(/[-:]/g, '').replace('T', '_').substring(0, 15); const random = Math.random().toString(36).substring(2, 10); return `upload_${timestamp}_${random}`; } } // Create global instance window.errorReporter = new ErrorReporter(); // Export for module use if (typeof module !== 'undefined' && module.exports) { module.exports = ErrorReporter; }