fix(desktop): remove deprecated console-message arguments

Use Electron 40's event-object console-message contract, retain one-argument listener arity, and report malformed signature drift once so renderer logging cannot fail silently.

Co-authored-by: CupaJ12 <108900676+CupaJ12@users.noreply.github.com>
(cherry picked from commit 378701b2b7af1e0a3accfaec7b8daa46c30832d6)
This commit is contained in:
Andrex Ibiza, MBA
2026-08-25 06:03:56 -05:00
committed by brooklyn!
parent ac80df1410
commit 1498ae0233
2 changed files with 103 additions and 47 deletions

View File

@@ -2,51 +2,85 @@ import { describe, expect, it, vi } from 'vitest'
import { attachRendererConsoleCapture, formatRendererBoundaryReport, formatRendererConsoleLine } from './renderer-log'
type ConsoleMessageEvent = {
level?: unknown
message?: unknown
sourceId?: unknown
lineNumber?: unknown
}
type ConsoleMessageHandler = (event: ConsoleMessageEvent) => void
function createWindowHarness() {
let handler: ConsoleMessageHandler | undefined
const win = {
webContents: {
on: (_event: 'console-message', listener: ConsoleMessageHandler) => {
handler = listener
}
}
}
return { win, getHandler: () => handler }
}
describe('formatRendererConsoleLine', () => {
it('formats the canonical Electron 36+ details shape at error level', () => {
it('formats the canonical Electron console-message event at error level', () => {
const line = formatRendererConsoleLine('hud', {
level: 3,
level: 'error',
message: 'Minified React error #310',
sourceUrl: 'file:///app/index.js',
sourceId: 'file:///app/index.js',
lineNumber: 13
})
expect(line).toBe('[renderer console:hud] Minified React error #310 (file:///app/index.js:13)')
})
it('formats the deprecated positional shape at error level', () => {
const line = formatRendererConsoleLine('main', 3, 'boom', 7, 'file:///app/vendor.js')
expect(line).toBe('[renderer console:main] boom (file:///app/vendor.js:7)')
it('drops a canonical non-error string level', () => {
expect(
formatRendererConsoleLine('main', { level: 'info', message: 'x', sourceId: 's', lineNumber: 1 })
).toBeNull()
})
it('drops non-error levels in both shapes', () => {
expect(formatRendererConsoleLine('main', { level: 1, message: 'x', sourceUrl: 's', lineNumber: 1 })).toBeNull()
expect(formatRendererConsoleLine('main', 2, 'warn', 1, 's')).toBeNull()
it('drops malformed event objects', () => {
expect(formatRendererConsoleLine('main', {})).toBeNull()
})
})
describe('attachRendererConsoleCapture', () => {
it('logs error-level messages and skips the rest', () => {
it('registers a single-argument listener, logs errors, and skips the rest', () => {
const log = vi.fn()
let handler: ((...args: unknown[]) => void) | undefined
const capture = createWindowHarness()
const win = {
webContents: {
on: (_event: string, listener: (...args: unknown[]) => void) => {
handler = listener
}
}
}
attachRendererConsoleCapture(capture.win, 'quick-entry', log)
attachRendererConsoleCapture(win, 'quick-entry', log)
const handler = capture.getHandler()
expect(handler).toHaveLength(1)
handler?.({}, { level: 3, message: 'crash', sourceUrl: 'src', lineNumber: 2 })
handler?.({}, { level: 0, message: 'debug', sourceUrl: 'src', lineNumber: 3 })
handler?.({ level: 'error', message: 'crash', sourceId: 'src', lineNumber: 2 })
handler?.({ level: 'debug', message: 'debug', sourceId: 'src', lineNumber: 3 })
expect(log).toHaveBeenCalledTimes(1)
expect(log).toHaveBeenCalledWith('[renderer console:quick-entry] crash (src:2)')
})
it('reports signature drift once across renderer windows', () => {
const log = vi.fn()
const firstCapture = createWindowHarness()
const secondCapture = createWindowHarness()
attachRendererConsoleCapture(firstCapture.win, 'main', log)
attachRendererConsoleCapture(secondCapture.win, 'hud', log)
firstCapture.getHandler()?.({})
secondCapture.getHandler()?.({})
expect(log).toHaveBeenCalledTimes(1)
expect(log).toHaveBeenCalledWith(
'[renderer console] Electron console-message signature drift detected; renderer errors may not be captured'
)
})
})
describe('formatRendererBoundaryReport', () => {

View File

@@ -15,53 +15,75 @@
* tokens or PII we never want on disk.
*/
type ConsoleMessageLevel = 'info' | 'warning' | 'error' | 'debug'
interface ConsoleMessageEventLike {
level?: unknown
message?: unknown
sourceId?: unknown
lineNumber?: unknown
}
interface ConsoleMessageDetails {
level: number
level: ConsoleMessageLevel
message: string
sourceUrl: string
sourceId: string
lineNumber: number
}
interface WebContentsLike {
on(event: 'console-message', listener: (...args: unknown[]) => void): unknown
on(event: 'console-message', listener: (event: ConsoleMessageEventLike) => void): unknown
}
interface WindowLike {
webContents: WebContentsLike
}
/** Normalize Electron's two `console-message` signatures into one line, or
* null for non-error levels. Canonical (Electron 36+): `(event, details)`;
* deprecated positional: `(event, level, message, line, sourceId)`.
* `level` is numeric 0..3, where 3 === error. */
export function formatRendererConsoleLine(
label: string,
detailsOrLevel: unknown,
message?: unknown,
line?: unknown,
sourceId?: unknown
): string | null {
const details =
detailsOrLevel && typeof detailsOrLevel === 'object' ? (detailsOrLevel as ConsoleMessageDetails) : null
let didReportConsoleMessageSignatureDrift = false
const level = details ? details.level : detailsOrLevel
function isConsoleMessageDetails(value: unknown): value is ConsoleMessageDetails {
if (typeof value !== 'object' || value === null) {
return false
}
if (level !== 3) {
const event = value as ConsoleMessageEventLike
const isKnownLevel =
event.level === 'info' || event.level === 'warning' || event.level === 'error' || event.level === 'debug'
return (
isKnownLevel &&
typeof event.message === 'string' &&
typeof event.sourceId === 'string' &&
typeof event.lineNumber === 'number'
)
}
/** Format Electron's canonical console-message event object into one line, or
* null for non-error or malformed events. Hermes's pinned Electron 40.x line
* puts severity and source metadata on the event object itself; accepting one
* listener argument also avoids Electron's deprecated positional
* `(event, level, message, line, sourceId)` path. */
export function formatRendererConsoleLine(label: string, details: ConsoleMessageEventLike): string | null {
if (!isConsoleMessageDetails(details) || details.level !== 'error') {
return null
}
const text = details ? details.message : message
const src = details ? details.sourceUrl : sourceId
const lineNo = details ? details.lineNumber : line
return `[renderer console:${label}] ${String(text)} (${String(src)}:${String(lineNo)})`
return `[renderer console:${label}] ${details.message} (${details.sourceId}:${String(details.lineNumber)})`
}
/** Attach the error-level console hook to a renderer window. `log` is the
* desktop.log sink (rememberLog in main.ts). */
export function attachRendererConsoleCapture(win: WindowLike, label: string, log: (line: string) => void): void {
win.webContents.on('console-message', (_event, detailsOrLevel, message, line, sourceId) => {
const formatted = formatRendererConsoleLine(label, detailsOrLevel, message, line, sourceId)
win.webContents.on('console-message', (event) => {
if (!isConsoleMessageDetails(event)) {
if (!didReportConsoleMessageSignatureDrift) {
didReportConsoleMessageSignatureDrift = true
log('[renderer console] Electron console-message signature drift detected; renderer errors may not be captured')
}
return
}
const formatted = formatRendererConsoleLine(label, event)
if (formatted !== null) {
log(formatted)