// Copyright (c) 2026 Gordon Bolton. MIT License. const logQueue = []; const MAX_QUEUE_SIZE = 20; const FLUSH_INTERVAL_MS = 2000; // Original console functions const originalLog = console.log; const originalWarn = console.warn; const originalError = console.error; let isSending = false; let isInitialized = false; let flushInterval = null; function formatArgs(args) { return args.map(arg => { if (arg === null) return 'null'; if (arg === undefined) return 'undefined'; if (arg instanceof Error) { return `${arg.name}: ${arg.message}\n${arg.stack || ''}`; } if (typeof arg === 'object') { try { return JSON.stringify(arg); } catch { return String(arg); } } return String(arg); }).join(' '); } function enqueue(level, args) { const formattedMsg = formatArgs(args); // Still write to the developer console! if (level === 'info') originalLog.apply(console, args); else if (level === 'warn') originalWarn.apply(console, args); else if (level === 'error') originalError.apply(console, args); // Guard against infinite loop during logs dispatching if (isSending) return; logQueue.push({ timestamp: new Date().toISOString(), level, message: formattedMsg }); // Flush immediately if queue is full if (logQueue.length >= MAX_QUEUE_SIZE) { flushQueue(); } } async function flushQueue() { if (logQueue.length === 0 || isSending) return; isSending = true; const batch = [...logQueue]; logQueue.length = 0; try { const response = await fetch('/api/debug/client-logs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(batch), // keepalive allows request to survive page unload keepalive: true }); if (!response.ok) { originalError.call(console, '[clientLogCapture] Ingestion server returned error status:', response.status); } } catch (err) { originalError.call(console, '[clientLogCapture] Ingestion post request failed:', err.message); } finally { isSending = false; } } // Perform a fast/unblocked payload flush using sendBeacon on page unload function flushOnUnload() { if (logQueue.length === 0) return; const batch = [...logQueue]; logQueue.length = 0; try { const blob = new Blob([JSON.stringify(batch)], { type: 'application/json' }); navigator.sendBeacon('/api/debug/client-logs', blob); } catch (err) { // sendBeacon failure, fallback to synchronous fetch with keepalive if available try { fetch('/api/debug/client-logs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(batch), keepalive: true }); } catch { // Ignore } } } export async function initClientLogCapture() { if (isInitialized) return; try { // 1. Check if the server toggle for logging is active const response = await fetch('/api/debug/status'); if (!response.ok) return; const data = await response.json(); if (data && data.enabled === true) { // 2. Override global console methods console.log = (...args) => enqueue('info', args); console.warn = (...args) => enqueue('warn', args); console.error = (...args) => enqueue('error', args); // 3. Set interval for batch updates flushInterval = setInterval(flushQueue, FLUSH_INTERVAL_MS); // 4. Setup beforeunload listener for clean flushing window.addEventListener('beforeunload', flushOnUnload); isInitialized = true; console.log('[clientLogCapture] Browser console logging interceptor initialized successfully.'); } } catch (err) { originalError.call(console, '[clientLogCapture] Check failed to start interceptor:', err.message); } }