|
| 1 | +#!/usr/bin/env node |
| 2 | +// |
| 3 | +// capture-logs.js - Capture Chrome DevTools Protocol logs from WASM tests |
| 4 | +// |
| 5 | + |
1 | 6 | const CDP = require('chrome-remote-interface'); |
2 | 7 |
|
3 | | -async function setupLogging(client, label) { |
4 | | - const {Log, Runtime, Network, Page} = client; |
| 8 | +const PORT = 9222; |
| 9 | +const VERBOSE = process.argv.includes('--verbose'); |
5 | 10 |
|
6 | | - await Log.enable(); |
7 | | - await Runtime.enable(); |
| 11 | +function log(msg) { |
| 12 | + console.log(`WASM:CDP: ${msg}`); |
| 13 | +} |
8 | 14 |
|
9 | | - // Try to enable Network and Page (may not be available on all targets) |
10 | | - try { await Network.enable(); } catch (e) { /* ignore */ } |
11 | | - try { await Page.enable(); } catch (e) { /* ignore */ } |
| 15 | +function logVerbose(msg) { |
| 16 | + if (VERBOSE) { |
| 17 | + console.log(`WASM:CDP: ${msg}`); |
| 18 | + } |
| 19 | +} |
12 | 20 |
|
13 | | - Log.entryAdded((params) => { |
14 | | - const {entry} = params; |
15 | | - console.log(`[${label}:LOG:${entry.level}] ${entry.text}`); |
16 | | - }); |
| 21 | +function outputLog(type, level, text) { |
| 22 | + console.log(`WASM:${type}:${level}: ${text}`); |
| 23 | +} |
| 24 | + |
| 25 | +async function replayBufferedMessages(Runtime) { |
| 26 | + try { |
| 27 | + const {result} = await Runtime.evaluate({ |
| 28 | + expression: `JSON.stringify(window.__cdp_console_buffer__ || [])`, |
| 29 | + returnByValue: true |
| 30 | + }); |
| 31 | + if (result.value) { |
| 32 | + const buffered = JSON.parse(result.value); |
| 33 | + if (buffered.length > 0) { |
| 34 | + log(`Replaying ${buffered.length} buffered message(s)`); |
| 35 | + for (const msg of buffered) { |
| 36 | + outputLog('CONSOLE', msg.type.toUpperCase(), msg.args.join(' ')); |
| 37 | + } |
| 38 | + // Clear buffer |
| 39 | + await Runtime.evaluate({ expression: `window.__cdp_console_buffer__ = [];` }); |
| 40 | + } |
| 41 | + } |
| 42 | + } catch (e) { |
| 43 | + // Ignore - page might not have the buffer |
| 44 | + } |
| 45 | +} |
17 | 46 |
|
| 47 | +async function setupRuntimeLogging(Runtime, label) { |
18 | 48 | Runtime.consoleAPICalled((params) => { |
19 | 49 | const args = params.args.map(arg => arg.value || arg.description || '').join(' '); |
20 | | - console.log(`[${label}:CONSOLE:${params.type}] ${args}`); |
| 50 | + outputLog('CONSOLE', params.type.toUpperCase(), args); |
21 | 51 | }); |
22 | 52 |
|
23 | | - // Catch exceptions |
24 | | - Runtime.exceptionThrown((params) => { |
25 | | - const details = params.exceptionDetails; |
26 | | - const text = details.exception?.description || details.text || JSON.stringify(details); |
27 | | - console.log(`[${label}:EXCEPTION] ${text}`); |
| 53 | + Runtime.exceptionThrown(({exceptionDetails}) => { |
| 54 | + const text = exceptionDetails.exception?.description |
| 55 | + || exceptionDetails.text |
| 56 | + || JSON.stringify(exceptionDetails); |
| 57 | + outputLog('EXCEPTION', 'ERROR', text); |
28 | 58 | }); |
| 59 | +} |
29 | 60 |
|
30 | | - // Network failures |
31 | | - Network.requestFailed && Network.requestFailed((params) => { |
32 | | - console.log(`[${label}:NET:FAILED] ${params.request?.url} - ${params.errorText}`); |
33 | | - }); |
| 61 | +async function attachToTarget(Target, targetId, targetType) { |
| 62 | + try { |
| 63 | + const client = await CDP({port: PORT, target: targetId}); |
| 64 | + const {Runtime, Log, Network} = client; |
34 | 65 |
|
35 | | - // Network responses (for 4xx/5xx) |
36 | | - Network.responseReceived && Network.responseReceived((params) => { |
37 | | - const {response} = params; |
38 | | - if (response.status >= 400) { |
39 | | - console.log(`[${label}:NET:${response.status}] ${response.url}`); |
40 | | - } |
41 | | - }); |
| 66 | + // Enable and set up listeners immediately |
| 67 | + await Runtime.enable(); |
| 68 | + setupRuntimeLogging(Runtime, targetType); |
42 | 69 |
|
43 | | - // Page crashes |
44 | | - Page.javascriptDialogOpening && Page.javascriptDialogOpening((params) => { |
45 | | - console.log(`[${label}:DIALOG] ${params.type}: ${params.message}`); |
46 | | - }); |
| 70 | + // Replay any buffered messages |
| 71 | + await replayBufferedMessages(Runtime); |
| 72 | + |
| 73 | + // Optional extras |
| 74 | + try { |
| 75 | + await Log.enable(); |
| 76 | + Log.entryAdded(({entry}) => { |
| 77 | + if (entry.level === 'verbose' && !VERBOSE) return; |
| 78 | + outputLog('LOG', entry.level.toUpperCase(), entry.text); |
| 79 | + }); |
| 80 | + } catch (e) {} |
47 | 81 |
|
48 | | - console.error(`CDP: Logging enabled for ${label}`); |
| 82 | + try { |
| 83 | + await Network.enable(); |
| 84 | + Network.responseReceived && Network.responseReceived(({response}) => { |
| 85 | + if (response.status >= 400 && !response.url.includes('favicon')) { |
| 86 | + outputLog('NET', String(response.status), response.url); |
| 87 | + } |
| 88 | + }); |
| 89 | + } catch (e) {} |
| 90 | + |
| 91 | + logVerbose(`Attached to ${targetType}`); |
| 92 | + } catch (err) { |
| 93 | + logVerbose(`Failed to attach to ${targetId}: ${err.message}`); |
| 94 | + } |
49 | 95 | } |
50 | 96 |
|
51 | 97 | async function captureLogs() { |
52 | | - // Retry connection forever - CI timeout will protect us |
53 | 98 | let browser; |
54 | 99 | let attempt = 0; |
| 100 | + |
| 101 | + // Retry connection forever |
55 | 102 | while (!browser) { |
56 | 103 | try { |
57 | | - browser = await CDP({port: 9222}); |
| 104 | + browser = await CDP({port: PORT}); |
58 | 105 | } catch (err) { |
59 | 106 | attempt++; |
60 | | - if (attempt % 10 === 0) { |
61 | | - console.error(`CDP: Waiting for Chrome (attempt ${attempt})...`); |
| 107 | + if (attempt % 30 === 0) { |
| 108 | + log(`Waiting for Chrome on port ${PORT} (attempt ${attempt})...`); |
62 | 109 | } |
63 | 110 | await new Promise(r => setTimeout(r, 1000)); |
64 | 111 | } |
65 | 112 | } |
66 | 113 |
|
67 | | - // Set up logging on browser-level connection too |
68 | | - await setupLogging(browser, 'browser'); |
| 114 | + log('Connected'); |
69 | 115 |
|
70 | | - const {Target} = browser; |
| 116 | + const {Target, Page, Runtime} = browser; |
71 | 117 |
|
72 | | - // Discover all targets |
| 118 | + // Set up target discovery FIRST - this is the critical path |
73 | 119 | await Target.setDiscoverTargets({discover: true}); |
74 | 120 |
|
75 | | - // Attach to each page target we find |
76 | | - const attachToPage = async (targetId, targetType) => { |
77 | | - try { |
78 | | - // Create a new CDP session for this target |
79 | | - const {sessionId} = await Target.attachToTarget({targetId, flatten: true}); |
80 | | - console.error(`CDP: Attached to ${targetType} ${targetId} (session: ${sessionId})`); |
81 | | - |
82 | | - // Connect to this specific target |
83 | | - const client = await CDP({port: 9222, target: targetId}); |
84 | | - await setupLogging(client, targetType); |
85 | | - } catch (err) { |
86 | | - console.error(`CDP: Failed to attach to ${targetId}: ${err.message}`); |
87 | | - } |
| 121 | + const attachedTargets = new Set(); |
| 122 | + const attachIfNeeded = async (targetId, targetType) => { |
| 123 | + if (attachedTargets.has(targetId)) return; |
| 124 | + attachedTargets.add(targetId); |
| 125 | + await attachToTarget(Target, targetId, targetType); |
88 | 126 | }; |
89 | 127 |
|
90 | 128 | // Listen for new targets |
91 | 129 | Target.targetCreated(async ({targetInfo}) => { |
92 | | - console.error(`CDP: New target: ${targetInfo.type} - ${targetInfo.url || targetInfo.targetId}`); |
93 | | - if (targetInfo.type === 'page' || targetInfo.type === 'worker' || targetInfo.type === 'iframe') { |
94 | | - await attachToPage(targetInfo.targetId, targetInfo.type); |
| 130 | + const {type, targetId} = targetInfo; |
| 131 | + if (type === 'page' || type === 'worker' || type === 'iframe') { |
| 132 | + await attachIfNeeded(targetId, type); |
95 | 133 | } |
96 | 134 | }); |
97 | 135 |
|
98 | | - // Attach to existing targets |
| 136 | + // Attach to existing targets immediately |
99 | 137 | const {targetInfos} = await Target.getTargets(); |
100 | | - for (const info of targetInfos) { |
101 | | - console.error(`CDP: Existing target: ${info.type} - ${info.url || info.targetId}`); |
102 | | - if (info.type === 'page' || info.type === 'worker' || info.type === 'iframe') { |
103 | | - await attachToPage(info.targetId, info.type); |
104 | | - } |
| 138 | + const pageTargets = targetInfos.filter(t => |
| 139 | + t.type === 'page' || t.type === 'worker' || t.type === 'iframe' |
| 140 | + ); |
| 141 | + |
| 142 | + // Attach in parallel for speed |
| 143 | + await Promise.all(pageTargets.map(t => attachIfNeeded(t.targetId, t.type))); |
| 144 | + |
| 145 | + // Install early interceptor for future pages |
| 146 | + try { |
| 147 | + await Page.enable(); |
| 148 | + await Page.addScriptToEvaluateOnNewDocument({ |
| 149 | + source: ` |
| 150 | + if (!window.__cdp_console_injected__) { |
| 151 | + window.__cdp_console_buffer__ = []; |
| 152 | + window.__cdp_console_injected__ = true; |
| 153 | + ['log', 'warn', 'error', 'info', 'debug'].forEach(m => { |
| 154 | + const orig = console[m].bind(console); |
| 155 | + console[m] = function(...args) { |
| 156 | + window.__cdp_console_buffer__.push({ |
| 157 | + type: m, |
| 158 | + args: args.map(a => { try { return typeof a === 'object' ? JSON.stringify(a) : String(a); } catch(e) { return String(a); } }) |
| 159 | + }); |
| 160 | + return orig(...args); |
| 161 | + }; |
| 162 | + }); |
| 163 | + } |
| 164 | + ` |
| 165 | + }); |
| 166 | + } catch (e) { |
| 167 | + logVerbose(`Early interceptor failed: ${e.message}`); |
105 | 168 | } |
106 | 169 |
|
107 | | - console.error('CDP: Setup complete, listening for all targets...'); |
| 170 | + // Also set up browser-level runtime (sometimes catches things) |
| 171 | + try { |
| 172 | + await Runtime.enable(); |
| 173 | + setupRuntimeLogging(Runtime, 'browser'); |
| 174 | + } catch (e) {} |
| 175 | + |
| 176 | + log('Listening'); |
| 177 | + |
| 178 | + // Periodically check for buffered messages on known targets |
| 179 | + setInterval(async () => { |
| 180 | + for (const targetId of attachedTargets) { |
| 181 | + try { |
| 182 | + const client = await CDP({port: PORT, target: targetId}); |
| 183 | + await replayBufferedMessages(client.Runtime); |
| 184 | + } catch (e) {} |
| 185 | + } |
| 186 | + }, 2000); |
108 | 187 | } |
109 | 188 |
|
110 | 189 | captureLogs().catch(err => { |
111 | | - console.error('CDP: Fatal error:', err); |
| 190 | + console.error(`WASM:CDP:ERROR: ${err.message}`); |
112 | 191 | process.exit(1); |
113 | 192 | }); |
0 commit comments