lingjing/node_modules/next/dist/next-devtools/userspace/app/forward-logs.js
2026-02-23 09:51:43 +00:00

505 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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