Setting the file. One moment. Query · Browser Trace · browserbase/skills · Skills Docs- Number
- 4.5
- Position
- 5 of 12
- Type
- JavaScript
- Size
- 9 KB
- Lines
- 235
scripts/query.mjs
JavaScript·235 lines·9 KB
14// pid + kind.
15// node scripts/query.mjs <run-id> hosts [pid|all] Top hosts by request count.
16// node scripts/query.mjs <run-id> host <hostname> [pid|all] Requests + responses for one host.
17// node scripts/query.mjs <run-id> timeline Compact navigation+lifecycle timeline.
18
19import fs from 'node:fs';
20import path from 'node:path';
21
22import { runDir, readJson, readJsonl, isTopNav } from './lib.mjs';
23
24const [runId, cmd, ...args] = process.argv.slice(2);
25if (!runId || !cmd) usage();
26
27const RD = runDir(runId);
28const cdpDir = path.join(RD, 'cdp');
29if (!fs.existsSync(cdpDir)) {
30 console.error(`no run dir at ${RD}`);
31 process.exit(1);
32}
33
34switch (cmd) {
35 case 'list': cmdList(); break;
36 case 'summary': cmdSummary(); break;
37 case 'page': cmdPage(args[0], args[1]); break;
38 case 'errors': cmdErrors(args[0]); break;
39 case 'hosts': cmdHosts(args[0]); break;
40 case 'host': cmdHost(args[0], args[1]); break;
41 case 'timeline': cmdTimeline(); break;
42 default:
43 console.error(`unknown command: ${cmd}`);
44 usage();
45}
46
47// ---------------------------------------------------------------------------
48
49function usage() {
50 console.error([
51 'usage: query.mjs <run-id> <command> [args...]',
52 '',
53 ' list page table',
54 ' summary full cdp/summary.json',
55 ' page <pid> per-page summary',
56 ' page <pid> <bucket> cat pages/<pid>/<bucket>.jsonl (e.g. network/failed, raw)',
57 ' errors [pid|all] unified errors with pid + kind',
58 ' hosts [pid|all] top hosts by request count',
59 ' host <hostname> [pid|all] all requests/responses for a hostname',
60 ' timeline nav + lifecycle markers',
61 ].join('\n'));
62 process.exit(2);
63}
64
65function pageDir(pid) {
66 return path.join(cdpDir, 'pages', String(pid).padStart(3, '0'));
67}
68
69function listPids(filter) {
70 if (filter && filter !== 'all') return [Number(filter)];
71 const root = path.join(cdpDir, 'pages');
72 if (!fs.existsSync(root)) return [];
73 return fs.readdirSync(root)
74 .filter(d => /^\d+$/.test(d))
75 .map(Number)
76 .sort((a, b) => a - b);
77}
78
79// Exact host match — uses `URL.host` (which includes the port when present)
80// so `cmdHosts` output is directly consumable as input to `cmdHost`. The
81// equality check still rejects impostors like `example.com.evil.tld` whose
82// `host` is the full malicious string, not the prefix.
83function hostMatches(url, host) {
84 try { return new URL(url).host === host; }
85 catch { return false; }
86}
87
88// ---------------------------------------------------------------------------
89
90function cmdList() {
91 const summary = readJson(path.join(cdpDir, 'summary.json'));
92 if (!summary) { console.error('no summary.json — run bisect-cdp.mjs first'); process.exit(1); }
93
94 // Pad columns: pid, eventCount, durationSeconds, url.
95 const rows = summary.pages.map(p => ([
96 String(p.pageId),
97 `${p.eventCount}evt`,
98 `${((p.durationMs ?? 0) / 1000).toFixed(2)}s`,
99 p.url,
100 ]));
101 const widths = rows[0]?.map((_, i) => Math.max(...rows.map(r => r[i].length))) ?? [];
102 for (const r of rows) {
103 console.log(r.map((c, i) => c.padEnd(widths[i])).join(' '));
104 }
105}
106
107function cmdSummary() {
108 const s = readJson(path.join(cdpDir, 'summary.json'));
109 if (!s) { console.error('no summary.json — run bisect-cdp.mjs first'); process.exit(1); }
110 console.log(JSON.stringify(s, null, 2));
111}
112
113function cmdPage(pidArg, bucketArg) {
114 if (pidArg === undefined) { console.error('page id required'); process.exit(2); }
115 const pdir = pageDir(pidArg);
116 if (!fs.existsSync(pdir)) { console.error(`no such page: ${pidArg}`); process.exit(1); }
117
118 if (!bucketArg) {
119 const s = readJson(path.join(pdir, 'summary.json'));
120 if (!s) { console.error(`no summary.json for page ${pidArg}`); process.exit(1); }
121 console.log(JSON.stringify(s, null, 2));
122 return;
123 }
124
125 if (bucketArg === 'raw') {
126 const raw = path.join(pdir, 'raw.jsonl');
127 if (!fs.existsSync(raw)) { console.error('(empty)'); return; }
128 process.stdout.write(fs.readFileSync(raw));
129 return;
130 }
131
132 const file = path.join(pdir, `${bucketArg}.jsonl`);
133 if (!fs.existsSync(file)) { console.error(`(empty: ${bucketArg} for page ${pidArg})`); return; }
134 process.stdout.write(fs.readFileSync(file));
135}
136
137function cmdErrors(filter) {
138 for (const pid of listPids(filter)) {
139 const pdir = pageDir(pid);
140
141 for (const ev of readJsonl(path.join(pdir, 'network/failed.jsonl'))) {
142 console.log(JSON.stringify({
143 pid, kind: 'network.failed',
144 rid: ev?.params?.requestId,
145 errorText: ev?.params?.errorText,
146 type: ev?.params?.type,
147 }));
148 }
149 for (const ev of readJsonl(path.join(pdir, 'console/exceptions.jsonl'))) {
150 console.log(JSON.stringify({
151 pid, kind: 'runtime.exception',
152 text: ev?.params?.exceptionDetails?.text,
153 message: ev?.params?.exceptionDetails?.exception?.description,
154 }));
155 }
156 for (const ev of readJsonl(path.join(pdir, 'console/logs.jsonl'))) {
157 if (ev?.params?.type !== 'error') continue;
158 const arg0 = ev?.params?.args?.[0];
159 console.log(JSON.stringify({
160 pid, kind: 'console.error',
161 msg: arg0?.value ?? arg0?.description ?? '',
162 }));
163 }
164 for (const ev of readJsonl(path.join(pdir, 'log/entries.jsonl'))) {
165 if (ev?.params?.entry?.level !== 'error') continue;
166 console.log(JSON.stringify({
167 pid, kind: 'log.error',
168 source: ev?.params?.entry?.source,
169 text: ev?.params?.entry?.text,
170 }));
171 }
172 }
173}
174
175function cmdHosts(filter) {
176 const counts = new Map();
177 for (const pid of listPids(filter)) {
178 for (const ev of readJsonl(path.join(pageDir(pid), 'network/requests.jsonl'))) {
179 const url = ev?.params?.request?.url;
180 if (!url) continue;
181 let host;
182 try { host = new URL(url).host; } catch { host = ''; }
183 counts.set(host, (counts.get(host) ?? 0) + 1);
184 }
185 }
186 const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]);
187 for (const [host, n] of sorted) {
188 console.log(`${String(n).padStart(4)} ${host}`);
189 }
190}
191
192function cmdHost(hostname, filter) {
193 if (!hostname) { console.error('hostname required'); process.exit(2); }
194 for (const pid of listPids(filter)) {
195 const pdir = pageDir(pid);
196 for (const ev of readJsonl(path.join(pdir, 'network/requests.jsonl'))) {
197 const url = ev?.params?.request?.url ?? '';
198 if (!hostMatches(url, hostname)) continue;
199 console.log(JSON.stringify({
200 pid, kind: 'request',
201 method: ev?.params?.request?.method,
202 url,
203 type: ev?.params?.type,
204 }));
205 }
206 for (const ev of readJsonl(path.join(pdir, 'network/responses.jsonl'))) {
207 const url = ev?.params?.response?.url ?? '';
208 if (!hostMatches(url, hostname)) continue;
209 console.log(JSON.stringify({
210 pid, kind: 'response',
211 status: ev?.params?.response?.status,
212 url,
213 }));
214 }
215 }
216}
217
218function cmdTimeline() {
219 // Read raw.ndjson directly so nav + lifecycle events come out in the order
220 // they actually fired. The bisected per-method buckets group by type and
221 // would otherwise print all NAVs before any lifecycle markers, even when
222 // navigations occurred between lifecycle phases.
223 const rawPath = path.join(cdpDir, 'raw.ndjson');
224 if (!fs.existsSync(rawPath)) {
225 console.error('no raw.ndjson — capture may not have started');
226 process.exit(1);
227 }
228 for (const ev of readJsonl(rawPath)) {
229 if (isTopNav(ev)) {
230 console.log(`[NAV ${ev?.params?.frame?.url ?? '?'}]`);
231 } else if (ev?.method === 'Page.lifecycleEvent') {
232 console.log(`[${ev?.params?.name ?? '?'}]`);
233 }
234 }
235}