/** * @file console-forwarder.js * @description Intercepts browser console logs and forwards them to the server. * Handles circular references, batches requests, and prevents infinite loops. */ (function () { // CONFIGURATION const LOG_ENDPOINT = '/api/logs/browser'; const BATCH_SIZE = 50; const FLUSH_INTERVAL_MS = 2000; const MAX_RETRIES = 3; // STATE let logQueue = []; let flushTimer = null; let isSending = false; let retryCount = 0; // CIRCULAR REFERENCE SAFE SERIALIZER const safeStringify = (obj) => { const seen = new WeakSet(); return JSON.stringify(obj, (key, value) => { if (typeof value === 'object' && value !== null) { if (seen.has(value)) { return '[Circular]'; } seen.add(value); } // Handle Error objects explicitly as they don't stringify well by default if (value instanceof Error) { return { message: value.message, stack: value.stack, name: value.name }; } return value; }); }; // LOG QUEUE MANAGER const flushQueue = async () => { if (logQueue.length === 0 || isSending) return; const batch = [...logQueue]; logQueue = []; // Clear queue immediately to prevent duplicates isSending = true; try { const response = await fetch(LOG_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ logs: batch }), // critical: prevent this fetch from triggering global error handlers // if it fails, to avoid infinite loops }); if (!response.ok) { throw new Error(`Server responded with ${response.status}`); } retryCount = 0; } catch (err) { // DO NOT log errors here to prevent infinite loops // If network fails, put specific logs back or drop them to prevent memory leaks if (retryCount < MAX_RETRIES) { retryCount++; // Re-queue logs at the front logQueue = [...batch, ...logQueue]; } } finally { isSending = false; } }; const scheduleFlush = () => { if (flushTimer) return; flushTimer = setTimeout(() => { flushTimer = null; flushQueue(); }, FLUSH_INTERVAL_MS); }; const pushLog = (level, args) => { // timestamp const timestamp = new Date().toISOString(); // safe serialization of arguments const serializedArgs = args.map(arg => { try { if (typeof arg === 'string') return arg; return JSON.parse(safeStringify(arg)); } catch (e) { return '[Unserializable]'; } }); logQueue.push({ level, timestamp, message: serializedArgs.join(' '), // Flatten for simple viewing data: serializedArgs // Keep structured for deep inspection if backend supports it }); if (logQueue.length >= BATCH_SIZE) { if (flushTimer) clearTimeout(flushTimer); flushTimer = null; flushQueue(); } else { scheduleFlush(); } }; // INTERCEPTORS const originalConsole = { log: console.log, info: console.info, warn: console.warn, error: console.error, debug: console.debug, }; const methods = ['log', 'info', 'warn', 'error', 'debug']; methods.forEach((method) => { console[method] = (...args) => { // 1. Call original method so developer tools still work originalConsole[method].apply(console, args); // 2. Push to queue try { pushLog(method, args); } catch (e) { // Silent fail - do not use console.error here! } }; }); // GLOBAL ERROR HANDLERS window.addEventListener('error', (event) => { pushLog('error', [`Uncaught Exception: ${event.message}`, event.filename, event.lineno, event.colno, event.error]); }); window.addEventListener('unhandledrejection', (event) => { pushLog('error', ['Unhandled Promise Rejection:', event.reason]); }); // PAGE UNLOAD FLUSH // Use sendBeacon for reliable "last gasp" logging window.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden' && logQueue.length > 0) { const batch = safeStringify({ logs: logQueue }); // sendBeacon is more reliable on unload than fetch if (navigator.sendBeacon) { const blob = new Blob([batch], { type: 'application/json' }); navigator.sendBeacon(LOG_ENDPOINT, blob); logQueue = []; } } }); console.info('[Console Forwarder] Initialized. Monitoring active.'); })();