/** * JavaScript Error Logger for Darexa Web Frontend * Logs all JavaScript errors and image load failures to the backend */ class WebErrorLogger { constructor() { this.logQueue = []; this.maxQueueSize = 20; this.flushInterval = 30000; // Flush every 30 seconds this.startAutoFlush(); this.initErrorHandlers(); } /** * Log an error to the queue */ log(category, message, details = {}) { const logEntry = { timestamp: new Date().toISOString(), category, message, url: window.location.href, userAgent: navigator.userAgent, details, }; console.error(`[${category}]`, message, details); this.logQueue.push(logEntry); // Flush if queue is getting full if (this.logQueue.length >= this.maxQueueSize) { this.flush(); } } /** * Log image load error */ logImageError(url, error) { this.log('IMAGE_DISPLAY', `Image failed to load: ${url}`, { url, error: error?.message || String(error), errorType: error?.name || 'Unknown', }); } /** * Log network error */ logNetworkError(endpoint, statusCode, error) { this.log('NETWORK', `API request failed: ${endpoint}`, { endpoint, statusCode, error: error?.message || String(error), }); } /** * Log JavaScript error */ logJSError(error, context = '') { this.log('JS_ERROR', `JavaScript error: ${error.message}`, { message: error.message, stack: error.stack, context, }); } /** * Initialize global error handlers */ initErrorHandlers() { // Global error handler window.addEventListener('error', (event) => { this.logJSError(event.error, 'Global error handler'); }); // Unhandled promise rejection handler window.addEventListener('unhandledrejection', (event) => { this.log('PROMISE_REJECTION', `Unhandled promise rejection`, { reason: event.reason?.message || String(event.reason), }); }); // Network error interception const originalFetch = window.fetch; window.fetch = function (...args) { return originalFetch .apply(this, args) .then((response) => { if (!response.ok) { const [resource] = args; const endpoint = typeof resource === 'string' ? resource : resource.url; errorLogger.logNetworkError(endpoint, response.status, null); } return response; }) .catch((error) => { const [resource] = args; const endpoint = typeof resource === 'string' ? resource : resource.url; errorLogger.logNetworkError(endpoint, null, error); throw error; }); }; } /** * Start auto-flushing logs */ startAutoFlush() { setInterval(() => { if (this.logQueue.length > 0) { this.flush(); } }, this.flushInterval); } /** * Flush logs to backend */ async flush() { if (this.logQueue.length === 0) { return; } const logsToFlush = [...this.logQueue]; this.logQueue = []; try { const response = await fetch('/api/web/logs', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': this.getCsrfToken(), }, body: JSON.stringify({ logs: logsToFlush, }), }); if (!response.ok) { console.warn(`Failed to flush logs: ${response.status}`); // Re-queue logs on failure this.logQueue = logsToFlush.concat(this.logQueue); } } catch (error) { console.warn('Error flushing logs:', error); // Re-queue logs on error this.logQueue = logsToFlush.concat(this.logQueue); } } /** * Get CSRF token from meta tag */ getCsrfToken() { return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''; } /** * Manual flush */ flushNow() { this.flush(); } } // Initialize error logger when DOM is ready const errorLogger = new WebErrorLogger(); // Expose globally for use in image error handlers window.errorLogger = errorLogger; // Handle image load errors on dynamically loaded content document.addEventListener('error', function (event) { if (event.target instanceof HTMLImageElement) { errorLogger.logImageError(event.target.src, new Error('Image load failed')); event.target.style.display = 'none'; // Show fallback const fallback = document.createElement('div'); fallback.className = 'w-full h-40 bg-surface-container-high flex items-center justify-center text-primary rounded-xl'; fallback.innerHTML = 'image_not_supported'; event.target.parentNode.insertBefore(fallback, event.target.nextSibling); } }, true);