Setting the file. One moment. Bisect Cdp · Browser Trace · browserbase/skills · Skills DocsScript
scripts/bisect-cdp.mjs
JavaScript·228 lines·8 KB
import
fs
from
'node:fs'
;
16import path from 'node:path';
17
18import {
19 runDir, ensureDir, readJson, readJsonl, writeJson, writeJsonl,
20 BUCKETS, isTopNav,
21} from './lib.mjs';
22
23const [runId] = process.argv.slice(2);
24if (!runId) {
25 console.error('usage: bisect-cdp.mjs <run-id>');
26 process.exit(2);
27}
28
29const RD = runDir(runId);
30const cdpDir = path.join(RD, 'cdp');
31const rawPath = path.join(cdpDir, 'raw.ndjson');
32if (!fs.existsSync(rawPath)) {
33 console.error(`raw.ndjson not found at ${rawPath}`);
34 process.exit(1);
35}
36
37const events = readJsonl(rawPath);
38const manifest = readJson(path.join(RD, 'manifest.json'), {});
39
40// CDP exposes two clocks under .params.timestamp depending on the domain:
41// Network/Page → MonotonicTime, seconds since browser start (small)
42// Console.messageAdded etc. → TimeSinceEpoch in ms (large, > 1e9)
43// Anchor only on monotonic so wall-clock conversion stays consistent.
44const isMonotonic = ts => ts != null && ts < 1e9;
45const anchorCdp = events
46 .map(e => e?.params?.timestamp)
47 .find(isMonotonic) ?? null;
48
49const startedMs = manifest.started_at ? new Date(manifest.started_at).getTime() : null;
50const stoppedMs = manifest.stopped_at ? new Date(manifest.stopped_at).getTime() : null;
51
52function toMs(ts) {
53 if (ts == null || anchorCdp == null || startedMs == null) return null;
54 return Math.floor((ts - anchorCdp) * 1000 + startedMs);
55}
56
57// Walk events in order. Each top-level Page.frameNavigated bumps the page
58// counter. Events emitted before the first navigation are clamped to pid 0
59// so they fold into the first concrete page (their requests really are part
60// of loading that first page).
61let pid = -1;
62for (const ev of events) {
63 if (isTopNav(ev)) pid += 1;
64 ev._pid = pid < 0 ? 0 : pid;
65}
66
67// ---- session-wide buckets (always written, including empty files) ----
68ensureDir(cdpDir);
69for (const [bucket, predicate] of BUCKETS) {
70 const matched = events
71 .filter(e => predicate(e.method ?? ''))
72 .map(stripPid);
73 writeJsonl(path.join(cdpDir, `${bucket}.jsonl`), matched);
74}
75
76// ---- per-page slices ----
77const pagesRoot = path.join(cdpDir, 'pages');
78if (fs.existsSync(pagesRoot)) fs.rmSync(pagesRoot, { recursive: true, force: true });
79ensureDir(pagesRoot);
80
81// Group events by pid. If the run had zero events we still create page 0
82// so the run dir has a predictable shape.
83const pageMap = new Map();
84for (const ev of events) {
85 if (!pageMap.has(ev._pid)) pageMap.set(ev._pid, []);
86 pageMap.get(ev._pid).push(ev);
87}
88if (pageMap.size === 0) pageMap.set(0, []);
89
90const pageSummaries = [];
91for (const [thisPid, pageEvents] of [...pageMap.entries()].sort((a, b) => a[0] - b[0])) {
92 const padded = String(thisPid).padStart(3, '0');
93 const pdir = path.join(pagesRoot, padded);
94 ensureDir(pdir);
95
96 // URL: first top-level frameNavigated in this page; "(initial)" if none.
97 const navEv = pageEvents.find(isTopNav);
98 const url = navEv?.params?.frame?.url ?? '(initial)';
99 fs.writeFileSync(path.join(pdir, 'url.txt'), url + '\n');
100
101 // raw.jsonl for this page, _pid stripped.
102 writeJsonl(path.join(pdir, 'raw.jsonl'), pageEvents.map(stripPid));
103
104 // Per-bucket slices, only writing files that have content.
105 for (const [bucket, predicate] of BUCKETS) {
106 const matched = pageEvents
107 .filter(e => predicate(e.method ?? ''))
108 .map(stripPid);
109 writeJsonl(path.join(pdir, `${bucket}.jsonl`), matched, { skipEmpty: true });
110 }
111
112 const summary = computePageSummary(thisPid, url, pageEvents);
113 writeJson(path.join(pdir, 'summary.json'), summary);
114 pageSummaries.push(summary);
115}
116
117// ---- top-level summary.json ----
118const sessionId = manifest?.browserbase?.session_id || manifest.run_id || runId;
119const summary = {
120 sessionId,
121 duration: {
122 startMs: startedMs,
123 endMs: stoppedMs,
124 totalMs: (startedMs != null && stoppedMs != null) ? stoppedMs - startedMs : null,
125 },
126 totalEvents: events.length,
127 pages: pageSummaries,
128};
129writeJson(path.join(cdpDir, 'summary.json'), summary);
130
131// Compact stdout view (full file is on disk).
132console.log(JSON.stringify({
133 sessionId,
134 duration: summary.duration,
135 totalEvents: summary.totalEvents,
136 pages: pageSummaries.map(p => ({
137 pageId: p.pageId,
138 url: p.url,
139 durationMs: p.durationMs,
140 eventCount: p.eventCount,
141 })),
142}, null, 2));
143
144// ---------------------------------------------------------------------------
145
146function stripPid(ev) {
147 const { _pid, ...rest } = ev;
148 return rest;
149}
150
151function computePageSummary(pid, url, pageEvents) {
152 const ts = pageEvents
153 .map(e => e?.params?.timestamp)
154 .filter(isMonotonic);
155 const start = ts[0] ?? null;
156 const end = ts[ts.length - 1] ?? null;
157 const startMs = toMs(start);
158 const endMs = toMs(end);
159
160 // Per-CDP-domain rollup with optional errors/warnings keys.
161 const counts = new Map(); // domain -> count
162 const errors = new Map(); // domain -> errors
163 const warnings = new Map(); // domain -> warnings
164 const netTypes = new Map(); // resourceType -> count
165 let netRequests = 0;
166 let netFailed = 0;
167
168 const inc = (m, k, by = 1) => m.set(k, (m.get(k) ?? 0) + by);
169
170 // Classify each event into a logical "domain" bucket. Most CDP events go in
171 // the bucket named for their CDP domain (Network, Page, Runtime, …), but
172 // `Runtime.consoleAPICalled` is conceptually console activity, not runtime
173 // internals — without this remap, the Console bucket's `errors`/`warnings`
174 // counts would never line up with any entry in the counts map and would
175 // silently disappear from the per-page summary.
176 const domainFor = (method) =>
177 method === 'Runtime.consoleAPICalled' ? 'Console' : method.split('.')[0];
178
179 for (const ev of pageEvents) {
180 const method = ev.method;
181 if (!method) continue;
182 inc(counts, domainFor(method));
183
184 if (method === 'Network.loadingFailed') {
185 inc(errors, 'Network');
186 netFailed += 1;
187 } else if (method === 'Network.requestWillBeSent') {
188 netRequests += 1;
189 inc(netTypes, ev?.params?.type ?? 'Other');
190 } else if (method === 'Runtime.exceptionThrown') {
191 inc(errors, 'Runtime');
192 } else if (method === 'Runtime.consoleAPICalled') {
193 const t = ev?.params?.type;
194 if (t === 'error') inc(errors, 'Console');
195 else if (t === 'warning' || t === 'warn') inc(warnings, 'Console');
196 } else if (method === 'Log.entryAdded') {
197 const level = ev?.params?.entry?.level;
198 if (level === 'error') inc(errors, 'Log');
199 else if (level === 'warning') inc(warnings, 'Log');
200 }
201 }
202
203 const domains = {};
204 for (const [d, c] of [...counts.entries()].sort()) {
205 const block = { count: c };
206 if (errors.get(d)) block.errors = errors.get(d);
207 if (warnings.get(d)) block.warnings = warnings.get(d);
208 domains[d] = block;
209 }
210
211 const out = {
212 pageId: pid,
213 url,
214 startMs,
215 endMs,
216 durationMs: (startMs != null && endMs != null) ? endMs - startMs : null,
217 eventCount: pageEvents.length,
218 domains,
219 };
220
221 if (netRequests > 0 || netFailed > 0) {
222 const byType = {};
223 for (const [t, c] of [...netTypes.entries()].sort()) byType[t] = c;
224 out.network = { requests: netRequests, failed: netFailed, byType };
225 }
226
227 return out;
228}