UNPKG

next

Version:

The React Framework

505 lines (503 loc) 16.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); 0 && (module.exports = { forwardErrorLog: null, forwardUnhandledError: null, initializeDebugLogForwarding: null, logQueue: null, logUnhandledRejection: null }); function _export(target, all) { for(var name in all)Object.defineProperty(target, name, { enumerable: true, get: all[name] }); } _export(exports, { forwardErrorLog: function() { return forwardErrorLog; }, forwardUnhandledError: function() { return forwardUnhandledError; }, initializeDebugLogForwarding: function() { return initializeDebugLogForwarding; }, logQueue: function() { return logQueue; }, logUnhandledRejection: function() { return logUnhandledRejection; } }); const _stitchederror = require("./errors/stitched-error"); const _errorsource = require("../../../shared/lib/error-source"); const _terminalloggingconfig = require("./terminal-logging-config"); const _forwardlogsshared = require("../../shared/forward-logs-shared"); const _forwardlogsutils = require("./forward-logs-utils"); // Client-side file logger for browser logs class ClientFileLogger { formatTimestamp() { const now = new Date(); const hours = now.getHours().toString().padStart(2, '0'); const minutes = now.getMinutes().toString().padStart(2, '0'); const seconds = now.getSeconds().toString().padStart(2, '0'); const milliseconds = now.getMilliseconds().toString().padStart(3, '0'); return `${hours}:${minutes}:${seconds}.${milliseconds}`; } log(level, args) { if (isReactServerReplayedLog(args)) { return; } // Format the args into a message string const message = args.map((arg)=>{ if (typeof arg === 'string') return arg; if (typeof arg === 'number' || typeof arg === 'boolean') return String(arg); if (arg === null) return 'null'; if (arg === undefined) return 'undefined'; // Handle DOM nodes - only log the tag name to avoid React proxied elements if (arg instanceof Element) { return `<${arg.tagName.toLowerCase()}>`; } return (0, _forwardlogsutils.safeStringifyWithDepth)(arg); }).join(' '); const logEntry = { timestamp: this.formatTimestamp(), level: level.toUpperCase(), message }; this.logEntries.push(logEntry); // Schedule flush when new log is added scheduleLogFlush(); } getLogs() { return [ ...this.logEntries ]; } clear() { this.logEntries = []; } constructor(){ this.logEntries = []; } } const clientFileLogger = new ClientFileLogger(); // Set up flush-based sending of client file logs let logFlushTimeout = null; let heartbeatInterval = null; const scheduleLogFlush = ()=>{ if (logFlushTimeout) { clearTimeout(logFlushTimeout); } logFlushTimeout = setTimeout(()=>{ sendClientFileLogs(); logFlushTimeout = null; }, 100) // Send after 100ms (much faster with debouncing) ; }; const cancelLogFlush = ()=>{ if (logFlushTimeout) { clearTimeout(logFlushTimeout); logFlushTimeout = null; } }; const startHeartbeat = ()=>{ if (heartbeatInterval) return; heartbeatInterval = setInterval(()=>{ if (logQueue.socket && logQueue.socket.readyState === WebSocket.OPEN) { try { // Send a ping to keep the connection alive logQueue.socket.send(JSON.stringify({ event: 'ping' })); } catch (error) { // Connection might be closed, stop heartbeat stopHeartbeat(); } } else { stopHeartbeat(); } }, 5000) // Send ping every 5 seconds ; }; const stopHeartbeat = ()=>{ if (heartbeatInterval) { clearInterval(heartbeatInterval); heartbeatInterval = null; } }; const isTerminalLoggingEnabled = (0, _terminalloggingconfig.getIsTerminalLoggingEnabled)(); const methods = [ 'log', 'info', 'warn', 'debug', 'table', 'assert', 'dir', 'dirxml', 'group', 'groupCollapsed', 'groupEnd', 'trace' ]; const afterThisFrame = (cb)=>{ let timeout; const rafId = requestAnimationFrame(()=>{ timeout = setTimeout(()=>{ cb(); }); }); return ()=>{ cancelAnimationFrame(rafId); clearTimeout(timeout); }; }; let isPatched = false; const serializeEntries = (entries)=>entries.map((clientEntry)=>{ switch(clientEntry.kind){ case 'any-logged-error': case 'console': { return { ...clientEntry, args: clientEntry.args.map(stringifyUserArg) }; } case 'formatted-error': { return clientEntry; } default: { return null; } } }); // Function to send client file logs to server const sendClientFileLogs = ()=>{ if (!logQueue.socket || logQueue.socket.readyState !== WebSocket.OPEN) { return; } const logs = clientFileLogger.getLogs(); if (logs.length === 0) { return; } try { const payload = JSON.stringify({ event: 'client-file-logs', logs: logs }); logQueue.socket.send(payload); } catch (error) { console.error(error); } finally{ // Clear logs regardless of send success to prevent memory leaks clientFileLogger.clear(); } }; const logQueue = { entries: [], flushScheduled: false, cancelFlush: null, socket: null, sourceType: undefined, router: null, scheduleLogSend: (entry)=>{ logQueue.entries.push(entry); if (logQueue.flushScheduled) { return; } // safe to deref and use in setTimeout closure since we cancel on new socket const socket = logQueue.socket; if (!socket) { return; } // we probably dont need this logQueue.flushScheduled = true; // non blocking log flush, runs at most once per frame logQueue.cancelFlush = afterThisFrame(()=>{ logQueue.flushScheduled = false; // just incase try { const payload = JSON.stringify({ event: 'browser-logs', entries: serializeEntries(logQueue.entries), router: logQueue.router, // needed for source mapping, we just assign the sourceType from the last error for the whole batch sourceType: logQueue.sourceType }); socket.send(payload); logQueue.entries = []; logQueue.sourceType = undefined; // Also send client file logs sendClientFileLogs(); } catch { // error (make sure u don't infinite loop) /* noop */ } }); }, onSocketReady: (socket)=>{ // When MCP or terminal logging is enabled, we enable the socket connection, // otherwise it will not proceed. if (!isTerminalLoggingEnabled && !process.env.__NEXT_MCP_SERVER) { return; } if (socket.readyState !== WebSocket.OPEN) { // invariant return; } // incase an existing timeout was going to run with a stale socket logQueue.cancelFlush?.(); logQueue.socket = socket; // Add socket event listeners to track connection state socket.addEventListener('close', ()=>{ cancelLogFlush(); stopHeartbeat(); }); // Only send terminal logs if enabled if (isTerminalLoggingEnabled) { try { const payload = JSON.stringify({ event: 'browser-logs', entries: serializeEntries(logQueue.entries), router: logQueue.router, sourceType: logQueue.sourceType }); socket.send(payload); logQueue.entries = []; logQueue.sourceType = undefined; } catch { /** noop just incase */ } } // Always send client file logs when socket is ready sendClientFileLogs(); // Start heartbeat to keep connection alive startHeartbeat(); } }; const stringifyUserArg = (arg)=>{ if (arg.kind !== 'arg') { return arg; } return { ...arg, data: (0, _forwardlogsutils.logStringify)(arg.data) }; }; const createErrorArg = (error)=>{ const stack = stackWithOwners(error); return { kind: 'formatted-error-arg', prefix: error.message ? `${error.name}: ${error.message}` : `${error.name}`, stack }; }; const createLogEntry = (level, args)=>{ // Always log to client file logger with args (formatting done inside log method) clientFileLogger.log(level, args); // Only forward to terminal if enabled if (!isTerminalLoggingEnabled) { return; } // do not abstract this, it implicitly relies on which functions call it. forcing the inlined implementation makes you think about callers // error capture stack trace maybe const stack = stackWithOwners(new Error()); const stackLines = stack?.split('\n'); const cleanStack = stackLines?.slice(3).join('\n') // this is probably ignored anyways ; const entry = { kind: 'console', consoleMethodStack: cleanStack ?? null, method: level, args: args.map((arg)=>{ if (arg instanceof Error) { return createErrorArg(arg); } return { kind: 'arg', data: (0, _forwardlogsutils.preLogSerializationClone)(arg) }; }) }; logQueue.scheduleLogSend(entry); }; const forwardErrorLog = (args)=>{ // Always log to client file logger with args (formatting done inside log method) clientFileLogger.log('error', args); // Only forward to terminal if enabled if (!isTerminalLoggingEnabled) { return; } const errorObjects = args.filter((arg)=>arg instanceof Error); const first = errorObjects.at(0); if (first) { const source = (0, _errorsource.getErrorSource)(first); if (source) { logQueue.sourceType = source; } } /** * browser shows stack regardless of type of data passed to console.error, so we should do the same * * do not abstract this, it implicitly relies on which functions call it. forcing the inlined implementation makes you think about callers */ const stack = stackWithOwners(new Error()); const stackLines = stack?.split('\n'); const cleanStack = stackLines?.slice(3).join('\n'); const entry = { kind: 'any-logged-error', method: 'error', consoleErrorStack: cleanStack ?? '', args: args.map((arg)=>{ if (arg instanceof Error) { return createErrorArg(arg); } return { kind: 'arg', data: (0, _forwardlogsutils.preLogSerializationClone)(arg) }; }) }; logQueue.scheduleLogSend(entry); }; const createUncaughtErrorEntry = (errorName, errorMessage, fullStack)=>{ const entry = { kind: 'formatted-error', prefix: `Uncaught ${errorName}: ${errorMessage}`, stack: fullStack, method: 'error' }; logQueue.scheduleLogSend(entry); }; const stackWithOwners = (error)=>{ let ownerStack = ''; (0, _stitchederror.setOwnerStackIfAvailable)(error); ownerStack = (0, _stitchederror.getOwnerStack)(error) || ''; const stack = (error.stack || '') + ownerStack; return stack; }; function logUnhandledRejection(reason) { // Always log to client file logger const message = reason instanceof Error ? `${reason.name}: ${reason.message}` : JSON.stringify(reason); clientFileLogger.log('error', [ `unhandledRejection: ${message}` ]); // Only forward to terminal if enabled if (!isTerminalLoggingEnabled) { return; } if (reason instanceof Error) { createUnhandledRejectionErrorEntry(reason, stackWithOwners(reason)); return; } createUnhandledRejectionNonErrorEntry(reason); } const createUnhandledRejectionErrorEntry = (error, fullStack)=>{ const source = (0, _errorsource.getErrorSource)(error); if (source) { logQueue.sourceType = source; } const entry = { kind: 'formatted-error', prefix: `⨯ unhandledRejection: ${error.name}: ${error.message}`, stack: fullStack, method: 'error' }; logQueue.scheduleLogSend(entry); }; const createUnhandledRejectionNonErrorEntry = (reason)=>{ const entry = { kind: 'any-logged-error', // we can't access the stack since the event is dispatched async and creating an inline error would be meaningless consoleErrorStack: '', method: 'error', args: [ { kind: 'arg', data: `⨯ unhandledRejection:`, isRejectionMessage: true }, { kind: 'arg', data: (0, _forwardlogsutils.preLogSerializationClone)(reason) } ] }; logQueue.scheduleLogSend(entry); }; const isHMR = (args)=>{ const firstArg = args[0]; if (typeof firstArg !== 'string') { return false; } if (firstArg.startsWith('[Fast Refresh]')) { return true; } if (firstArg.startsWith('[HMR]')) { return true; } return false; }; /** * Matches the format of logs arguments React replayed from the RSC. */ const isReactServerReplayedLog = (args)=>{ if (args.length < 3) { return false; } const [format, styles, label] = args; if (typeof format !== 'string' || typeof styles !== 'string' || typeof label !== 'string') { return false; } return format.startsWith('%c%s%c') && styles.includes('background:'); }; function forwardUnhandledError(error) { // Always log to client file logger clientFileLogger.log('error', [ `uncaughtError: ${error.name}: ${error.message}` ]); // Only forward to terminal if enabled if (!isTerminalLoggingEnabled) { return; } createUncaughtErrorEntry(error.name, error.message, stackWithOwners(error)); } const initializeDebugLogForwarding = (router)=>{ // probably don't need this if (isPatched) { return; } // TODO(rob): why does this break rendering on server, important to know incase the same bug appears in browser if (typeof window === 'undefined') { return; } // better to be safe than sorry try { methods.forEach((method)=>(0, _forwardlogsshared.patchConsoleMethod)(method, (_, ...args)=>{ if (isHMR(args)) { return; } if (isReactServerReplayedLog(args)) { return; } createLogEntry(method, args); })); } catch {} logQueue.router = router; isPatched = true; // Cleanup on page unload window.addEventListener('beforeunload', ()=>{ cancelLogFlush(); stopHeartbeat(); // Send any remaining logs before page unloads sendClientFileLogs(); }); }; if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') { Object.defineProperty(exports.default, '__esModule', { value: true }); Object.assign(exports.default, exports); module.exports = exports.default; } //# sourceMappingURL=forward-logs.js.map