next.js
f4b40dcd - refactor: remove ClientFileLogger, consolidate file logging into handleLog (#92329)

Commit
70 days ago
refactor: remove ClientFileLogger, consolidate file logging into handleLog (#92329) ## Background: three sources of file log entries Browser `console.*` calls in Next.js dev mode are forwarded to the server over a WebSocket. Before this PR, three independent mechanisms wrote to the `.next/logs/next-development.log` file: 1. **`ClientFileLogger`** (client → `client-file-logs` WS event → `handleClientFileLogs`) — a class in `forward-logs.ts` that intercepted every `console.*` call unconditionally, serialized args into message-only strings (no stack traces, no source mapping), and sent them to the server over a separate `client-file-logs` WebSocket event. The server's `handleClientFileLogs` wrote them directly to the file logger as `Browser` entries. 2. **`handleLog` direct file writes** (client → `browser-logs` WS event → `handleLog` → `fileLogger`) — the primary path. The browser's patched console methods serialize args into `ClientLogEntry` objects, batch them, and send them as `browser-logs` WebSocket events. On the server, `handleLog` in `receive-logs.ts` deserializes the args, source-maps stack traces, and forwards to the terminal. As a side effect, it also wrote directly to the file logger. 3. **`console-file.tsx` interception** (server-side `console.*` → `fileLogger.logServer`) — a server-side environment extension that patches the server process's own `console.*` methods. When `handleLog` calls `forwardConsole.error()` to print browser logs to the terminal, this interception captures that output and writes it as a `Server` entry. The `[browser]` text visible in these entries' messages is the terminal prefix leaking into the file log. ## Problems with the old system ### Duplicate entries On the client side, `createLogEntry` gated the `browser-logs` send behind `isTerminalLoggingEnabled`: ```javascript if (!isTerminalLoggingEnabled) { return // skip browser-logs WebSocket send } ``` But `clientFileLogger.log()` was called **before** this check, unconditionally. So `ClientFileLogger` always produced file log entries regardless of config. Meanwhile, `handleLog` was gated by `browserToTerminalConfig` in both hot reloaders. Because `experimental.browserDebugInfoInTerminal` defaults to `'warn'`, this gate was open by default — `handleLog` ran for errors and warnings even in code without explicit `logging.browserToTerminal` config, like this test. But `console.log` was filtered out at the `'warn'` level, so only `ClientFileLogger` wrote those. The net effect: errors and warnings were written to the file by **both** `ClientFileLogger` (message-only) and `handleLog` (with source-mapped stacks), producing duplicates. `console.log` was written only by `ClientFileLogger`. ### Mislabeled source The direct file writes inside `handleLog` had a bug: the `any-logged-error` and `formatted-error` code paths hardcoded `fileLogger.logBrowser()` instead of checking `isServerLog`. The `console` kind already checked `isServerLog` correctly. This inconsistency meant that when a server-decorated error (one with the `Symbol.for('NextjsError')` property set by `decorateServerError`) was logged via `console.error(serverError)` in the browser, the `sourceType` would be legitimately set to `'server'`, but the file log entry would still be labeled `Browser`. This bug didn't manifest in this test because the test calls `console.error('string')` with no Error objects — `forwardErrorLog` never finds an Error to call `getErrorSource` on, so `sourceType` remains `undefined` and `isServerLog` is `false`. The hardcoded `logBrowser` happened to produce the correct label by coincidence. ### The result With the default config, a single `console.error('string')` in the browser produced **three** file log entries: 1. `Browser ERROR` (message-only) — from `ClientFileLogger` 2. `Browser ERROR` (with stack) — from `handleLog`'s direct file write (hardcoded `logBrowser`) 3. `Server ERROR` (with stack + `[browser]` prefix) — from `console-file.tsx` intercepting `handleLog`'s `forwardConsole.error()` terminal output ## What this PR does **Removes `ClientFileLogger` and the `client-file-logs` channel entirely.** File logging now happens via `handleLog`'s direct writes and `console-file.tsx` interception. **Decouples the client-side send from terminal logging.** The old `createLogEntry` gated the `browser-logs` send on `isTerminalLoggingEnabled`. The new code gates on `shouldForwardLogs`: ```javascript const shouldForwardLogs = isTerminalLoggingEnabled || !!process.env.__NEXT_MCP_SERVER ``` This is the key mechanism that makes consolidation possible — `browser-logs` events now fire when either terminal logging or the MCP server (which controls file logging) is enabled, so `handleLog` always receives the entries it needs. **Decouples server-side file logging from terminal output.** `handleLog` is no longer gated by `browserToTerminalConfig` at the hot-reloader level — it always runs. Inside `handleLog`, terminal output and file logging are controlled independently: `shouldShowEntry(entry, config)` gates terminal output, `fileLogger.isEnabled()` gates file writes. **Fixes the `isServerLog` labeling bug.** All entry kinds (`console`, `any-logged-error`, `formatted-error`) now check `isServerLog` and call `logServer()`/`logBrowser()` accordingly. ## Snapshot diff explanation ### Old snapshot (7 entries) ``` 1. Browser LOG — "Complex circular object: ..." ← ClientFileLogger 2. Browser ERROR — "message" ← ClientFileLogger (message-only) 3. Browser WARN — "message" ← ClientFileLogger (message-only) 4. Server ERROR — "[browser] message + stack + loc" ← console-file.tsx (intercepting forwardConsole.error) 5. Browser ERROR — "message + stack + loc" ← handleLog any-logged-error (hardcoded logBrowser) 6. Server WARN — "[browser] message + loc" ← console-file.tsx (intercepting forwardConsole.warn) 7. Browser WARN — "message" ← handleLog console kind (logBrowser, isServerLog=false) ``` ### New snapshot (5 entries) ``` 1. Browser LOG — "Complex circular object: ..." ← handleLog console kind (logBrowser) 2. Browser ERROR — "message + stack + loc" ← handleLog any-logged-error (logBrowser) 3. Server ERROR — "[browser] message + stack + loc" ← console-file.tsx (intercepting forwardConsole.error) 4. Browser WARN — "message" ← handleLog console kind (logBrowser) 5. Server WARN — "[browser] message + loc" ← console-file.tsx (intercepting forwardConsole.warn) ``` ### What changed With `ClientFileLogger` removed, each `console.*` call now produces one `Browser` entry from `handleLog` instead of two (ClientFileLogger message-only + handleLog with stack). Per call: - **`console.log`**: old `#1` (ClientFileLogger) → new `#1` (handleLog). Previously only `ClientFileLogger` wrote this — `handleLog` skipped it because `log` level was below the `'warn'` terminal threshold. Now `handleLog` writes file logs for all entry kinds before the `shouldShow` check. - **`console.error`**: old `#2` (ClientFileLogger duplicate) removed. Old `#5` (handleLog) → new `#2`, now with cleaner formatting. - **`console.warn`**: old `#3` (ClientFileLogger duplicate) removed. Old `#7` (handleLog) → new `#4`. - **`console-file.tsx` entries unchanged**: old `#4`/`#6` → new `#3`/`#5`. These are artifacts of `console-file.tsx` intercepting the server process's terminal output, not from `handleLog` directly.
Author
Parents
Loading