repoburg 1.3.255 → 1.3.256

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/daemon/dist/index.js +61 -3
  2. package/daemon/dist/staticServing.js +2 -2
  3. package/daemon/dist/tabtape/api/follow.js +174 -0
  4. package/daemon/dist/tabtape/api/http.js +221 -0
  5. package/daemon/dist/tabtape/api/record.js +149 -0
  6. package/daemon/dist/tabtape/api/router.js +519 -0
  7. package/daemon/dist/tabtape/api/router.spec.js +604 -0
  8. package/daemon/dist/tabtape/api/static.js +70 -0
  9. package/daemon/dist/tabtape/api/ws.js +166 -0
  10. package/daemon/dist/tabtape/buffer/buffer.js +347 -0
  11. package/daemon/dist/tabtape/buffer/buffer.spec.js +137 -0
  12. package/daemon/dist/tabtape/config.js +108 -0
  13. package/daemon/dist/tabtape/e2e-server.js +96 -0
  14. package/daemon/dist/tabtape/errors.js +77 -0
  15. package/daemon/dist/tabtape/export/export.js +331 -0
  16. package/daemon/dist/tabtape/export/export.spec.js +262 -0
  17. package/daemon/dist/tabtape/har/gostd.js +395 -0
  18. package/daemon/dist/tabtape/har/gostd.spec.js +139 -0
  19. package/daemon/dist/tabtape/har/har.js +582 -0
  20. package/daemon/dist/tabtape/har/har.spec.js +325 -0
  21. package/daemon/dist/tabtape/index.js +134 -0
  22. package/daemon/dist/tabtape/index.spec.js +104 -0
  23. package/daemon/dist/tabtape/lock.js +67 -0
  24. package/daemon/dist/tabtape/log.js +32 -0
  25. package/daemon/dist/tabtape/mcp/definitions.js +362 -0
  26. package/daemon/dist/tabtape/mcp/gofmt.js +227 -0
  27. package/daemon/dist/tabtape/mcp/params.js +345 -0
  28. package/daemon/dist/tabtape/mcp/params.spec.js +172 -0
  29. package/daemon/dist/tabtape/mcp/recordLink.js +122 -0
  30. package/daemon/dist/tabtape/mcp/server.js +411 -0
  31. package/daemon/dist/tabtape/mcp/server.spec.js +722 -0
  32. package/daemon/dist/tabtape/mcp/tools.js +1085 -0
  33. package/daemon/dist/tabtape/query/format.js +195 -0
  34. package/daemon/dist/tabtape/query/network.js +133 -0
  35. package/daemon/dist/tabtape/query/perf.js +93 -0
  36. package/daemon/dist/tabtape/query/query.spec.js +233 -0
  37. package/daemon/dist/tabtape/query/reader.js +602 -0
  38. package/daemon/dist/tabtape/query/types.js +274 -0
  39. package/daemon/dist/tabtape/redact.js +37 -0
  40. package/daemon/dist/tabtape/repro/lang.js +95 -0
  41. package/daemon/dist/tabtape/repro/repro.js +422 -0
  42. package/daemon/dist/tabtape/repro/repro.spec.js +210 -0
  43. package/daemon/dist/tabtape/sink.js +24 -0
  44. package/daemon/dist/tabtape/store/record.js +99 -0
  45. package/daemon/dist/tabtape/store/segment.js +267 -0
  46. package/daemon/dist/tabtape/store/store.js +846 -0
  47. package/daemon/dist/tabtape/store/store.spec.js +485 -0
  48. package/daemon/dist/tabtape/testing/querytest.js +119 -0
  49. package/daemon/dist/tabtape/testing/tmp.js +33 -0
  50. package/package.json +3 -2
  51. package/tabtape/extension/dist/background.js +8 -0
  52. package/tabtape/extension/dist/background.js.map +7 -0
  53. package/tabtape/extension/dist/bridge.js +3 -0
  54. package/tabtape/extension/dist/bridge.js.map +7 -0
  55. package/tabtape/extension/dist/icons/128.png +0 -0
  56. package/tabtape/extension/dist/icons/16.png +0 -0
  57. package/tabtape/extension/dist/icons/32.png +0 -0
  58. package/tabtape/extension/dist/icons/48.png +0 -0
  59. package/tabtape/extension/dist/manifest.json +28 -0
  60. package/tabtape/extension/dist/popup.css +62 -0
  61. package/tabtape/extension/dist/popup.html +34 -0
  62. package/tabtape/extension/dist/popup.js +4 -0
  63. package/tabtape/extension/dist/popup.js.map +7 -0
  64. package/tabtape/extension/dist/recorder.js +133 -0
  65. package/tabtape/extension/dist/recorder.js.map +1 -0
  66. package/tabtape/extension/dist/state.js +2 -0
  67. package/tabtape/extension/dist/state.js.map +1 -0
  68. package/tabtape/playwright/mcp.mjs +58 -0
  69. package/tabtape/playwright/tabtape.d.mts +37 -0
  70. package/tabtape/playwright/tabtape.mjs +146 -0
  71. package/tabtape/ui/dist/app/assets/index-Canv3Ljk.css +1 -0
  72. package/tabtape/ui/dist/app/assets/index-iWNSSrAE.js +145 -0
  73. package/tabtape/ui/dist/app/index.html +14 -0
  74. package/tabtape/ui/dist/export/viewer.css +2 -0
  75. package/tabtape/ui/dist/export/viewer.js +143 -0
@@ -0,0 +1,1085 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Tools = exports.BODY_PAGE = exports.MAX_TEXT = void 0;
4
+ exports.clampLimit = clampLimit;
5
+ exports.parseCursor = parseCursor;
6
+ exports.window = window;
7
+ exports.kindMatcher = kindMatcher;
8
+ exports.levelMatcher = levelMatcher;
9
+ /**
10
+ * The MCP tools: read-only questions about recorded sessions. Each returns a
11
+ * text for the model and the same data as structured content. Texts are
12
+ * capped at MAX_TEXT bytes; long lists continue through cursors.
13
+ */
14
+ const errors_1 = require("../errors");
15
+ const format_1 = require("../query/format");
16
+ const network_1 = require("../query/network");
17
+ const perf_1 = require("../query/perf");
18
+ const types_1 = require("../query/types");
19
+ const repro_1 = require("../repro/repro");
20
+ const gofmt_1 = require("./gofmt");
21
+ const params_1 = require("./params");
22
+ const recordLink_1 = require("./recordLink");
23
+ /** Caps the text of one tool result (MCP-R3). Bytes, so it is also a cap on characters. */
24
+ exports.MAX_TEXT = 30000;
25
+ const LIST_BODY_CLIP = 2000;
26
+ exports.BODY_PAGE = 20000;
27
+ const STACK_LINES = 6;
28
+ /** How many WebSocket messages get_request returns at once. */
29
+ const FRAME_PAGE = 100;
30
+ const FRAME_CLIP = 2000;
31
+ /** How many store actions get_context_at lists (ST-R15). */
32
+ const STATE_ACTIONS = 10;
33
+ /** Long animation frames from this long on are freezes worth a summary line. */
34
+ const FREEZE_MS = 500;
35
+ /** The tools over one store. */
36
+ class Tools {
37
+ constructor(st, rd, opts) {
38
+ this.st = st;
39
+ this.rd = rd;
40
+ this.opts = opts;
41
+ }
42
+ // ---------- shared helpers ----------
43
+ /**
44
+ * Finds a session by id; "latest" (or empty) picks the recording in
45
+ * progress, else the most recently started one.
46
+ */
47
+ resolve(input) {
48
+ const id = (0, gofmt_1.goTrimSpace)(input);
49
+ if (id === '' || (0, gofmt_1.goToLower)(id) === 'latest') {
50
+ const sessions = this.st.list(); // newest first
51
+ if (sessions.length === 0)
52
+ throw new Error('there are no recordings yet: start one from the tabtape extension');
53
+ for (const sess of sessions) {
54
+ if (this.live(sess))
55
+ return sess;
56
+ }
57
+ return sessions[0];
58
+ }
59
+ try {
60
+ return this.st.get(id);
61
+ }
62
+ catch (err) {
63
+ if (!(err instanceof errors_1.NotFoundError))
64
+ throw err;
65
+ const ids = this.st
66
+ .list()
67
+ .slice(0, 5)
68
+ .map((s) => s.id);
69
+ if (ids.length === 0)
70
+ throw new Error(`session ${(0, gofmt_1.goQuote)(id)} not found, and there are no recordings yet`);
71
+ throw new Error(`session ${(0, gofmt_1.goQuote)(id)} not found. Recent sessions: ${ids.join(', ')} (or use "latest")`);
72
+ }
73
+ }
74
+ /** Mirrors the replay UI: no end yet and a write in the last 30 seconds. */
75
+ live(sess) {
76
+ const lastWrite = sess.lastWriteAt ?? 0;
77
+ return !sess.endedAt && lastWrite > 0 && this.opts.now() - lastWrite < 30000;
78
+ }
79
+ link(id, offset) {
80
+ return (0, format_1.replayLink)(this.opts.baseURL, id, offset);
81
+ }
82
+ headers(m) {
83
+ return this.opts.showSecrets ? m : this.opts.redact.apply(m);
84
+ }
85
+ /** A session's title for resource lists, e.g. "Oct 5 07:47 · Acme Shop". */
86
+ resourceTitle(sess) {
87
+ const title = sess.title || sess.url || sess.id;
88
+ return `${(0, gofmt_1.formatStamp)(sess.startedAt, this.opts.timeZone)} · ${title}`;
89
+ }
90
+ // ---------- list_sessions ----------
91
+ async listSessions(input) {
92
+ const since = (0, params_1.parseSince)(input.since, this.opts.now());
93
+ const limit = clampLimit(input.limit, 20, 200);
94
+ const q = (0, gofmt_1.goToLower)((0, gofmt_1.goTrimSpace)(input.query));
95
+ const user = (0, gofmt_1.goToLower)((0, gofmt_1.goTrimSpace)(input.user));
96
+ const out = { sessions: null, matched: 0 };
97
+ const sessions = [];
98
+ const b = new gofmt_1.TextBuf();
99
+ for (const sess of this.st.list()) {
100
+ const haystack = `${sess.title ?? ''} ${sess.url ?? ''} ${sess.lastUrl ?? ''} ${sess.id} ${sess.userId ?? ''}`;
101
+ if (q !== '' && !(0, gofmt_1.goToLower)(haystack).includes(q))
102
+ continue;
103
+ if (user !== '' && !(0, gofmt_1.goToLower)(sess.userId ?? '').includes(user))
104
+ continue;
105
+ if (since !== null && sess.startedAt < since)
106
+ continue;
107
+ if (input.has_errors && sess.counts.errors === 0 && sess.counts.netErrors === 0)
108
+ continue;
109
+ out.matched++;
110
+ if (sessions.length >= limit)
111
+ continue;
112
+ const so = this.sessionOut(sess);
113
+ const entry = sessionText(so);
114
+ if (b.len + (0, gofmt_1.byteLen)(entry) > exports.MAX_TEXT - 200)
115
+ continue;
116
+ b.write(entry);
117
+ sessions.push(so);
118
+ }
119
+ if (sessions.length > 0)
120
+ out.sessions = sessions;
121
+ let head = `${out.matched} recording(s), newest first`;
122
+ if (sessions.length < out.matched)
123
+ head += ` (showing ${sessions.length}; narrow with query, since or limit)`;
124
+ if (out.matched === 0)
125
+ head = 'No recordings match. Start one from the tabtape extension (⌥⇧R).';
126
+ return { text: head + ':\n' + b.toString(), out };
127
+ }
128
+ sessionOut(sess) {
129
+ const end = sess.endedAt || sess.lastTs || sess.startedAt || 0;
130
+ return {
131
+ id: sess.id,
132
+ startedAt: (0, gofmt_1.formatRFC3339)(sess.startedAt, this.opts.timeZone),
133
+ durationMs: Math.max(end - sess.startedAt, 0),
134
+ title: sess.title || undefined,
135
+ url: sess.url || undefined,
136
+ browser: (0, format_1.browserLabel)(sess.userAgent ?? '') || undefined,
137
+ pages: sess.counts.pages,
138
+ clicks: sess.counts.clicks,
139
+ errors: sess.counts.errors,
140
+ failedRequests: sess.counts.netErrors,
141
+ trimmed: sess.truncated || undefined,
142
+ live: this.live(sess) || undefined,
143
+ source: sess.source || undefined,
144
+ user: sess.userId || undefined,
145
+ perf: sess.perf || undefined,
146
+ replay: this.link(sess.id, 0),
147
+ };
148
+ }
149
+ // ---------- get_session_summary ----------
150
+ async getSessionSummary(input) {
151
+ const sess = this.resolve(input.session_id);
152
+ return this.summary(sess.id, input.cursor);
153
+ }
154
+ /** Renders a session's story. It also backs the session resources. */
155
+ async summary(id, cursor) {
156
+ const v = await this.rd.view(id);
157
+ const start = parseCursor(cursor);
158
+ const so = this.sessionOut(v.session);
159
+ so.durationMs = Math.max(so.durationMs, v.duration());
160
+ const out = { session: so, items: null, total: 0, next_cursor: undefined };
161
+ const b = new gofmt_1.TextBuf();
162
+ b.write(`# Session ${so.id}\n${(0, gofmt_1.goQuote)(so.title ?? '')} · ${so.url ?? ''}\n`);
163
+ b.write(`Started ${(0, gofmt_1.rfc3339DateTime)(so.startedAt)} · duration ${(0, format_1.clockShort)(so.durationMs)} · ${so.browser || 'unknown browser'}\n`);
164
+ const c = v.session.counts;
165
+ b.write(`${c.pages} pages · ${c.clicks} clicks · ${c.errors} errors · ${c.netErrors} failed requests · ${c.network} network requests · ${c.console} console messages\n`);
166
+ if (so.user)
167
+ b.write(`User: ${so.user} (from tabtape.identify)\n`);
168
+ const notes = dataNotes(v);
169
+ if (notes !== '')
170
+ b.write(notes);
171
+ if (so.live)
172
+ b.write('Live: the recording is still being written.\n');
173
+ if (so.trimmed)
174
+ b.write('Trimmed: the ring buffer deleted the beginning of this recording.\n');
175
+ if (!v.replayable)
176
+ b.write('No replayable snapshot is left; links still open the event lists.\n');
177
+ b.write(`Replay: ${so.replay}\n\n`);
178
+ const items = [];
179
+ const finals = v.finalVitals();
180
+ for (const l of v.lines) {
181
+ const s = story(l, finals);
182
+ if (!s)
183
+ continue;
184
+ const off = v.offset(l.ts);
185
+ items.push({ id: l.id, at: (0, format_1.clock)(off), offsetMs: off, kind: s.kind, text: s.text, replay: this.link(id, off) });
186
+ }
187
+ out.total = items.length;
188
+ if (items.length === 0) {
189
+ b.write('No user actions, errors or failed requests were recorded.\n');
190
+ return { text: b.toString(), out };
191
+ }
192
+ b.write(`Timeline (${items.length} entries; ids work with get_request and get_context_at):\n`);
193
+ const shown = [];
194
+ for (let i = start; i < items.length; i++) {
195
+ const line = itemLine(items[i]);
196
+ if (b.len + (0, gofmt_1.byteLen)(line) > exports.MAX_TEXT - 300) {
197
+ out.next_cursor = String(i);
198
+ b.write(`… ${items.length - i} more entries. Call again with cursor ${(0, gofmt_1.goQuote)(out.next_cursor)}, or narrow down with get_context_at.\n`);
199
+ break;
200
+ }
201
+ b.write(line);
202
+ shown.push(items[i]);
203
+ }
204
+ if (shown.length > 0)
205
+ out.items = shown;
206
+ return { text: b.toString(), out };
207
+ }
208
+ // ---------- get_network ----------
209
+ async getNetwork(input) {
210
+ const sess = this.resolve(input.session_id);
211
+ const v = await this.rd.view(sess.id);
212
+ const statusOK = (0, params_1.statusMatcher)(input.status);
213
+ const [from, to] = window(input.from, input.to);
214
+ const start = parseCursor(input.cursor);
215
+ const kindOK = kindMatcher(input.kind);
216
+ const limit = clampLimit(input.limit, 50, 500);
217
+ const urlPart = (0, gofmt_1.goToLower)(input.url_contains);
218
+ const method = (0, gofmt_1.goToLower)(input.method);
219
+ const matches = [];
220
+ for (const l of v.lines) {
221
+ const n = l.net;
222
+ if (!n)
223
+ continue;
224
+ const off = v.offset(l.ts);
225
+ if (off < from || (to >= 0 && off > to) || !statusOK(n.status, n.error !== '') || !kindOK(n.kind))
226
+ continue;
227
+ if (input.method !== '' && (0, gofmt_1.goToLower)(n.method) !== method)
228
+ continue;
229
+ if (urlPart !== '' && !(0, gofmt_1.goToLower)(n.url).includes(urlPart))
230
+ continue;
231
+ matches.push(l);
232
+ }
233
+ const out = { requests: null, matched: matches.length, next_cursor: undefined };
234
+ const requests = [];
235
+ const b = new gofmt_1.TextBuf();
236
+ b.write(`${matches.length} request(s) in session ${sess.id}`);
237
+ if (start > 0)
238
+ b.write(`, from #${start + 1}`);
239
+ b.write(':\n');
240
+ for (let i = start; i < matches.length; i++) {
241
+ if (requests.length === limit) {
242
+ out.next_cursor = String(i);
243
+ break;
244
+ }
245
+ const l = matches[i];
246
+ const n = l.net;
247
+ const rs = this.requestSummary(v, l);
248
+ let entry;
249
+ if ((0, types_1.isWebSocket)(n)) {
250
+ entry = `${rs.at} WS ${outcome(n)} ${durText(rs.durationMs)} ${(0, network_1.framesText)(n.frames)} ${rs.url} [${rs.id}] ${rs.replay}\n`;
251
+ }
252
+ else {
253
+ entry = `${rs.at} ${rs.method} ${outcome(n)} ${durText(rs.durationMs)} ${sizeText(rs.size ?? 0)} ${rs.url} [${rs.id}] ${rs.replay}\n`;
254
+ }
255
+ if (input.include_bodies) {
256
+ let full = null;
257
+ try {
258
+ full = await v.fullNetwork(l);
259
+ }
260
+ catch {
261
+ full = null;
262
+ }
263
+ if (full) {
264
+ rs.reqBody = bodyClip(full.reqBody, LIST_BODY_CLIP) || undefined;
265
+ rs.resBody = bodyClip(full.resBody, LIST_BODY_CLIP) || undefined;
266
+ entry += bodyText('request body', rs.reqBody ?? '', full.reqBodyNote);
267
+ entry += bodyText('response body', rs.resBody ?? '', full.resBodyNote);
268
+ }
269
+ }
270
+ if (b.len + (0, gofmt_1.byteLen)(entry) > exports.MAX_TEXT - 300) {
271
+ out.next_cursor = String(i);
272
+ break;
273
+ }
274
+ b.write(entry);
275
+ requests.push(rs);
276
+ }
277
+ if (requests.length > 0)
278
+ out.requests = requests;
279
+ if (out.next_cursor !== undefined) {
280
+ b.write(`… more requests. Call again with cursor ${(0, gofmt_1.goQuote)(out.next_cursor)}, or narrow down with status, url_contains, kind or from/to.\n`);
281
+ }
282
+ if (matches.length === 0)
283
+ b.write('No requests match.\n');
284
+ return { text: b.toString(), out };
285
+ }
286
+ requestSummary(v, l) {
287
+ const off = v.offset(l.ts);
288
+ const n = l.net;
289
+ return {
290
+ id: l.id,
291
+ at: (0, format_1.clock)(off),
292
+ offsetMs: off,
293
+ kind: n.kind,
294
+ method: n.method,
295
+ url: n.url,
296
+ status: n.status || undefined,
297
+ error: n.error || undefined,
298
+ durationMs: n.duration,
299
+ size: (0, types_1.sizeOf)(n) || undefined,
300
+ frames: framesOut(n.frames),
301
+ reqBody: undefined,
302
+ resBody: undefined,
303
+ replay: this.link(v.session.id, off),
304
+ };
305
+ }
306
+ // ---------- get_request ----------
307
+ async getRequest(input) {
308
+ const sess = this.resolve(input.session_id);
309
+ const v = await this.rd.view(sess.id);
310
+ const l = findRequest(v, (0, gofmt_1.goTrimSpace)(input.request_id));
311
+ if (!l) {
312
+ throw new Error(`request ${(0, gofmt_1.goQuote)(input.request_id)} not found in session ${sess.id}; ids come from get_network or get_session_summary`);
313
+ }
314
+ const n = await v.fullNetwork(l);
315
+ const out = {
316
+ ...this.requestSummary(v, l),
317
+ statusText: n.statusText || undefined,
318
+ ttfbMs: n.ttfb,
319
+ mime: n.mime || undefined,
320
+ initiator: n.initiator || undefined,
321
+ reqHeaders: nonEmptyMap(this.headers(n.reqHeaders)),
322
+ resHeaders: nonEmptyMap(this.headers(n.resHeaders)),
323
+ reqBodyNote: n.reqBodyNote || undefined,
324
+ resBodyNote: n.resBodyNote || undefined,
325
+ reqBodyLength: undefined,
326
+ resBodyLength: undefined,
327
+ next_body_offset: undefined,
328
+ recorded: undefined,
329
+ protocol: undefined,
330
+ remoteAddress: undefined,
331
+ cache: undefined,
332
+ worker: undefined,
333
+ timing: undefined,
334
+ failureReason: undefined,
335
+ blockedCookies: undefined,
336
+ blockedSetCookies: undefined,
337
+ messages: undefined,
338
+ messagesTotal: undefined,
339
+ next_frames_offset: undefined,
340
+ };
341
+ if (n.source === 'cdp') {
342
+ out.recorded = (0, types_1.recorded)(n);
343
+ out.protocol = n.protocol || undefined;
344
+ out.remoteAddress = n.remoteAddress || undefined;
345
+ out.cache = n.cache || undefined;
346
+ out.worker = n.worker ? { type: n.worker.type, url: n.worker.url } : undefined;
347
+ out.timing = timingOut(n.timing);
348
+ out.blockedCookies = cookiesOut(n.blockedCookies);
349
+ out.blockedSetCookies = cookiesOut(n.blockedSetCookies);
350
+ if (n.corsErrorStatus !== undefined || n.blockedReason !== '')
351
+ out.failureReason = (0, network_1.failureReason)(n) || undefined;
352
+ }
353
+ const offset = Math.max(input.body_offset, 0);
354
+ const req = page(n.reqBody, offset);
355
+ const res = page(n.resBody, offset);
356
+ out.reqBody = req.text || undefined;
357
+ out.reqBodyLength = req.length || undefined;
358
+ out.resBody = res.text || undefined;
359
+ out.resBodyLength = res.length || undefined;
360
+ if (req.more || res.more)
361
+ out.next_body_offset = offset + exports.BODY_PAGE;
362
+ const b = new gofmt_1.TextBuf();
363
+ b.write(`${n.method} ${n.url}\n`);
364
+ const parts = [outcome(n)];
365
+ if (n.statusText !== '' && n.error === '')
366
+ parts[0] += ' ' + n.statusText;
367
+ parts.push(n.kind);
368
+ if (n.duration !== undefined) {
369
+ let t = (0, format_1.fmtMs)(n.duration);
370
+ if (n.ttfb !== undefined)
371
+ t += ' (headers after ' + (0, format_1.fmtMs)(n.ttfb) + ')';
372
+ parts.push(t);
373
+ }
374
+ if ((0, types_1.sizeOf)(n) > 0)
375
+ parts.push((0, format_1.fmtBytes)((0, types_1.sizeOf)(n)));
376
+ parts.push(...nonEmpty(n.mime, n.initiator));
377
+ if (n.pending)
378
+ parts.push('still pending when the page unloaded or recording stopped');
379
+ b.write(`${parts.join(' · ')}\nAt ${out.at} · id ${l.id} · ${out.replay}\n`);
380
+ writeDeep(b, n, out);
381
+ writeHeaders(b, 'Request headers', out.reqHeaders);
382
+ writeFullBody(b, 'Request body', n.reqBody, out.reqBody ?? '', out.reqBodyLength ?? 0, offset, n.reqBodyNote, n.reqBodyTruncated);
383
+ writeHeaders(b, 'Response headers', out.resHeaders);
384
+ writeFullBody(b, 'Response body', n.resBody, out.resBody ?? '', out.resBodyLength ?? 0, offset, n.resBodyNote, n.resBodyTruncated);
385
+ if (out.next_body_offset !== undefined)
386
+ b.write(`\n… the body continues. Call again with body_offset ${out.next_body_offset}.\n`);
387
+ if ((0, types_1.isWebSocket)(n))
388
+ await this.writeMessages(b, v, n, Math.max(input.frames_offset, 0), out);
389
+ return { text: b.toString(), out };
390
+ }
391
+ /** Lists one page of a WebSocket connection's messages. */
392
+ async writeMessages(b, v, n, offset, out) {
393
+ const frames = v.lines.filter((fl) => fl.ws !== undefined && fl.ws.conn === n.id);
394
+ out.messagesTotal = frames.length || undefined;
395
+ if (frames.length === 0) {
396
+ b.write('\nNo messages were recorded.\n');
397
+ return;
398
+ }
399
+ if (offset >= frames.length) {
400
+ b.write(`\nNo messages from #${offset + 1}: the connection has ${frames.length}.\n`);
401
+ return;
402
+ }
403
+ let end = Math.min(offset + FRAME_PAGE, frames.length);
404
+ b.write(`\nMessages ${offset + 1}–${end} of ${frames.length} (↑ sent, ↓ received):\n`);
405
+ const messages = [];
406
+ for (let i = offset; i < end; i++) {
407
+ const fl = frames[i];
408
+ let f;
409
+ try {
410
+ f = await v.fullWS(fl);
411
+ }
412
+ catch (err) {
413
+ if (err instanceof errors_1.GoneError)
414
+ continue;
415
+ throw err;
416
+ }
417
+ const off = v.offset(fl.ts);
418
+ const fo = {
419
+ id: fl.id,
420
+ at: (0, format_1.clock)(off),
421
+ offsetMs: off,
422
+ dir: f.dir,
423
+ opcode: f.opcode,
424
+ size: f.size,
425
+ data: f.data !== undefined ? (0, format_1.clip)(f.data, FRAME_CLIP) || undefined : undefined,
426
+ truncated: f.truncated || undefined,
427
+ dataOmitted: f.dataOmitted || undefined,
428
+ dropped: f.dropped || undefined,
429
+ error: f.error || undefined,
430
+ };
431
+ const line = frameLine(fo);
432
+ if (b.len + (0, gofmt_1.byteLen)(line) > exports.MAX_TEXT - 300) {
433
+ end = i;
434
+ break;
435
+ }
436
+ b.write(line);
437
+ messages.push(fo);
438
+ }
439
+ if (messages.length > 0)
440
+ out.messages = messages;
441
+ if (end < frames.length) {
442
+ out.next_frames_offset = end || undefined;
443
+ b.write(`… ${frames.length - end} more messages. Call again with frames_offset ${end}.\n`);
444
+ }
445
+ }
446
+ // ---------- get_console ----------
447
+ async getConsole(input) {
448
+ const sess = this.resolve(input.session_id);
449
+ const v = await this.rd.view(sess.id);
450
+ const [from, to] = window(input.from, input.to);
451
+ const start = parseCursor(input.cursor);
452
+ const levelOK = levelMatcher(input.level);
453
+ const limit = clampLimit(input.limit, 100, 1000);
454
+ const textPart = (0, gofmt_1.goToLower)(input.text_contains);
455
+ const matches = [];
456
+ for (const l of v.lines) {
457
+ const c = l.con;
458
+ if (!c || !levelOK(c.level))
459
+ continue;
460
+ const off = v.offset(l.ts);
461
+ if (off < from || (to >= 0 && off > to))
462
+ continue;
463
+ if (textPart !== '' && !(0, gofmt_1.goToLower)(c.text).includes(textPart))
464
+ continue;
465
+ matches.push(l);
466
+ }
467
+ const out = { messages: null, matched: matches.length, next_cursor: undefined };
468
+ const messages = [];
469
+ const b = new gofmt_1.TextBuf();
470
+ b.write(`${matches.length} console message(s) in session ${sess.id}:\n`);
471
+ for (let i = start; i < matches.length; i++) {
472
+ if (messages.length === limit) {
473
+ out.next_cursor = String(i);
474
+ break;
475
+ }
476
+ const l = matches[i];
477
+ const c = l.con;
478
+ const off = v.offset(l.ts);
479
+ const loc = location(c);
480
+ const item = {
481
+ id: l.id,
482
+ at: (0, format_1.clock)(off),
483
+ offsetMs: off,
484
+ level: c.level,
485
+ source: c.source,
486
+ text: (0, format_1.clip)(c.text, 2000),
487
+ stack: undefined,
488
+ location: (loc.startsWith(' at ') ? loc.slice(4) : loc) || undefined,
489
+ replay: this.link(sess.id, off),
490
+ };
491
+ if (c.level === 'error' || c.level === 'warn')
492
+ item.stack = shortStack(c.stack) || undefined;
493
+ let entry = `${item.at} ${(0, gofmt_1.padRight)(c.level, 5)} ${(0, gofmt_1.padRight)(c.source, 18)} ${(0, format_1.oneLine)(item.text, 600)}${loc} [${item.id}] ${item.replay}\n`;
494
+ if (item.stack) {
495
+ for (const line of item.stack.split('\n'))
496
+ entry += ' ' + line + '\n';
497
+ }
498
+ if (b.len + (0, gofmt_1.byteLen)(entry) > exports.MAX_TEXT - 300) {
499
+ out.next_cursor = String(i);
500
+ break;
501
+ }
502
+ b.write(entry);
503
+ messages.push(item);
504
+ }
505
+ if (messages.length > 0)
506
+ out.messages = messages;
507
+ if (out.next_cursor !== undefined) {
508
+ b.write(`… more messages. Call again with cursor ${(0, gofmt_1.goQuote)(out.next_cursor)}, or narrow down with level, text_contains or from/to.\n`);
509
+ }
510
+ if (matches.length === 0)
511
+ b.write('No console messages match.\n');
512
+ return { text: b.toString(), out };
513
+ }
514
+ // ---------- get_context_at ----------
515
+ async getContextAt(input) {
516
+ const sess = this.resolve(input.session_id);
517
+ const v = await this.rd.view(sess.id);
518
+ const ref = (0, gofmt_1.goTrimSpace)(input.at);
519
+ let at = -1;
520
+ const anchor = v.find(ref) ?? findRequest(v, ref);
521
+ if (anchor) {
522
+ at = v.offset(anchor.ts);
523
+ }
524
+ else {
525
+ try {
526
+ at = (0, params_1.parseOffset)(input.at);
527
+ }
528
+ catch {
529
+ at = -1;
530
+ }
531
+ if (at < 0)
532
+ throw new Error(`cannot find ${(0, gofmt_1.goQuote)(input.at)}: pass a time like 01:23.4 or an id from another tool`);
533
+ }
534
+ const before = (0, params_1.parseSpan)(input.before, 10000);
535
+ const after = (0, params_1.parseSpan)(input.after, 2000);
536
+ const out = { at: (0, format_1.clock)(at), offsetMs: at, items: null, stateActions: undefined };
537
+ const items = [];
538
+ const b = new gofmt_1.TextBuf();
539
+ b.write(`Session ${sess.id} around ${out.at} (from ${(0, format_1.clock)(Math.max(at - before, 0))} to ${(0, format_1.clock)(at + after)}):\n`);
540
+ let marked = false;
541
+ const finals = v.finalVitals();
542
+ for (const l of v.lines) {
543
+ const off = v.offset(l.ts);
544
+ if (off < at - before || off > at + after)
545
+ continue;
546
+ const c = contextLine(l, finals);
547
+ if (!c)
548
+ continue;
549
+ if (!marked && (l === anchor || (anchor === undefined && off > at))) {
550
+ if (anchor === undefined)
551
+ b.write(`${out.at} ▶ requested moment\n`);
552
+ marked = true;
553
+ }
554
+ const it = { id: l.id, at: (0, format_1.clock)(off), offsetMs: off, kind: c.kind, text: c.text, replay: this.link(sess.id, off) };
555
+ let line = itemLine(it);
556
+ if (l === anchor)
557
+ line = '▶ ' + line;
558
+ if (b.len + (0, gofmt_1.byteLen)(line) > exports.MAX_TEXT - 300) {
559
+ b.write('… cut: narrow the window with before/after.\n');
560
+ break;
561
+ }
562
+ b.write(line);
563
+ items.push(it);
564
+ }
565
+ if (!marked && anchor === undefined)
566
+ b.write(`${out.at} ▶ requested moment\n`);
567
+ if (items.length > 0)
568
+ out.items = items;
569
+ else
570
+ b.write('Nothing was recorded in this window apart from screen changes.\n');
571
+ const actions = await this.stateActions(v, at);
572
+ if (actions.length > 0) {
573
+ out.stateActions = actions;
574
+ b.write(`\nLast ${actions.length} store action(s) up to ${out.at}:\n`);
575
+ for (const a of actions) {
576
+ b.write(`${a.at} ${a.store} ${a.action} [${a.id}]\n`);
577
+ for (const c of a.changes ?? [])
578
+ b.write(' ' + c + '\n');
579
+ }
580
+ }
581
+ return { text: b.toString(), out };
582
+ }
583
+ /** The last store actions at or before an offset with the paths they changed (ST-R15). */
584
+ async stateActions(v, at) {
585
+ let lines = v.lines.filter((l) => l.state !== undefined && l.state.kind === 'action' && v.offset(l.ts) <= at);
586
+ lines = lines.slice(Math.max(lines.length - STATE_ACTIONS, 0));
587
+ const out = [];
588
+ for (const l of lines) {
589
+ const s = l.state;
590
+ const off = v.offset(l.ts);
591
+ let changes;
592
+ if (s.burst)
593
+ changes = ['(type only: over 50 actions per second)'];
594
+ else if (s.full)
595
+ changes = ['(the whole state was written: the change was too big to list)'];
596
+ else
597
+ changes = await changeTexts(v, l);
598
+ out.push({
599
+ id: l.id,
600
+ at: (0, format_1.clock)(off),
601
+ offsetMs: off,
602
+ store: s.store,
603
+ action: s.action,
604
+ changes: changes.length > 0 ? changes : undefined,
605
+ replay: this.link(v.session.id, off),
606
+ });
607
+ }
608
+ return out;
609
+ }
610
+ // ---------- get_repro_steps ----------
611
+ async getReproSteps(input) {
612
+ const sess = this.resolve(input.session_id);
613
+ const v = await this.rd.view(sess.id);
614
+ const [from, to] = window(input.from, input.to);
615
+ const res = await (0, repro_1.generateRepro)(v, {
616
+ from,
617
+ to: Math.max(to, 0),
618
+ lang: input.lang,
619
+ baseURL: this.opts.baseURL,
620
+ timeZone: this.opts.timeZone,
621
+ });
622
+ return { text: res.markdown, out: { markdown: res.markdown, steps: res.steps, replay: res.link } };
623
+ }
624
+ // ---------- get_record_link ----------
625
+ async getRecordLink(input) {
626
+ return (0, recordLink_1.getRecordLink)({ baseURL: this.opts.baseURL, recordToken: this.opts.recordToken }, input);
627
+ }
628
+ }
629
+ exports.Tools = Tools;
630
+ // ---------- texts ----------
631
+ function sessionText(so) {
632
+ const flags = [`${so.pages} pages, ${so.clicks} clicks, ${so.errors} errors, ${so.failedRequests} failed requests`];
633
+ if (so.live)
634
+ flags.push('live');
635
+ if (so.trimmed)
636
+ flags.push('trimmed (beginning deleted by the ring buffer)');
637
+ switch (so.source) {
638
+ case 'buffer':
639
+ flags.push('saved from buffer mode');
640
+ break;
641
+ case 'automation':
642
+ flags.push('started by automation (record link or Playwright)');
643
+ break;
644
+ }
645
+ if (so.user)
646
+ flags.push('user ' + so.user);
647
+ if (so.perf === 'poor')
648
+ flags.push('poor Web Vitals');
649
+ const started = (0, gofmt_1.rfc3339DateTime)(so.startedAt);
650
+ return `- ${so.id} · ${started} · ${(0, format_1.clockShort)(so.durationMs)} · ${(0, gofmt_1.goQuote)(so.title ?? '')} · ${so.url ?? ''}\n ${flags.join(' · ')}\n ${so.replay}\n`;
651
+ }
652
+ /** Mentions deep mode network data and store actions, which other tools return. */
653
+ function dataNotes(v) {
654
+ let deep = false;
655
+ let sockets = 0;
656
+ let actions = 0;
657
+ const stores = [];
658
+ for (const l of v.lines) {
659
+ if (l.net && l.net.source === 'cdp') {
660
+ deep = true;
661
+ if ((0, types_1.isWebSocket)(l.net))
662
+ sockets++;
663
+ }
664
+ else if (l.state) {
665
+ if (l.state.kind === 'action')
666
+ actions++;
667
+ if (!stores.includes(l.state.store))
668
+ stores.push(l.state.store);
669
+ }
670
+ }
671
+ let b = '';
672
+ if (deep) {
673
+ b += `Network recorded in deep mode: real headers and cookies, timing split, failure reasons; ${sockets} WebSocket connection(s) (get_network kind=websocket, get_request pages the messages).\n`;
674
+ }
675
+ if (stores.length > 0) {
676
+ b += `State: ${actions} action(s) in store(s) ${stores.join(', ')} (get_context_at lists the last actions before a moment).\n`;
677
+ }
678
+ return b;
679
+ }
680
+ function itemLine(it) {
681
+ return `${it.at} ${(0, gofmt_1.padRight)(it.kind, 9)} ${it.text} [${it.id}] ${it.replay}\n`;
682
+ }
683
+ /**
684
+ * Describes the lines the replay UI shows in its Events tab. finals holds the
685
+ * final report of each Web Vitals metric (see View.finalVitals).
686
+ */
687
+ function story(l, finals) {
688
+ if (l.ev) {
689
+ const ev = l.ev;
690
+ switch (ev.name) {
691
+ case 'page_view': {
692
+ const { host, path } = (0, format_1.splitURL)(ev.url);
693
+ return { kind: 'page', text: `Page viewed ${path} — ${nonEmpty(ev.title, host).join(' · ')}` };
694
+ }
695
+ case 'click':
696
+ return { kind: 'click', text: `Clicked ${label(ev.target)} (${selector(ev.target)})` };
697
+ case 'rage_click':
698
+ return { kind: 'rage', text: `Rage click ×${ev.count} on ${label(ev.target)} (${selector(ev.target)})` };
699
+ case 'change': {
700
+ let value = '(empty)';
701
+ if (ev.masked)
702
+ value = '•••••••• (password, not recorded)';
703
+ else if (ev.value !== undefined && ev.value !== '')
704
+ value = (0, gofmt_1.goQuote)((0, format_1.oneLine)(ev.value, 200));
705
+ return { kind: 'change', text: `Changed ${field(ev.target)} → ${value}` };
706
+ }
707
+ case 'submit':
708
+ return { kind: 'submit', text: (0, gofmt_1.goTrimSpace)(`Submitted form ${field(ev.target)} ${ev.method} ${ev.action}`) };
709
+ case 'marker':
710
+ return { kind: 'marker', text: 'Marker: ' + ev.label };
711
+ case 'custom': {
712
+ let text = 'Event ' + ev.event;
713
+ const props = (0, perf_1.propsText)(ev.props, 200);
714
+ if (props !== '')
715
+ text += ' (' + props + ')';
716
+ return { kind: 'custom', text };
717
+ }
718
+ case 'identify': {
719
+ let text = 'Identified user ' + ev.userId;
720
+ const traits = (0, perf_1.propsText)(ev.traits, 200);
721
+ if (traits !== '')
722
+ text += ' (' + traits + ')';
723
+ return { kind: 'identify', text };
724
+ }
725
+ }
726
+ return null;
727
+ }
728
+ if (l.perf)
729
+ return perfLine(l, finals, 'poor', FREEZE_MS);
730
+ if (l.con && l.con.level === 'error') {
731
+ return { kind: 'error', text: consoleLabel(l.con) + ': ' + (0, format_1.oneLine)((0, format_1.firstLine)(l.con.text), 300) + location(l.con) };
732
+ }
733
+ if (l.net && (0, types_1.failed)(l.net))
734
+ return { kind: 'net-error', text: `${l.net.method} ${l.net.url} → ${outcome(l.net)}` };
735
+ return null;
736
+ }
737
+ /**
738
+ * Describes every record get_context_at shows: the story plus successful API
739
+ * requests and all console levels. Assets are left out unless they failed.
740
+ */
741
+ function contextLine(l, finals) {
742
+ const s = story(l, finals);
743
+ if (s)
744
+ return s;
745
+ if (l.perf)
746
+ return perfLine(l, finals, '', 200);
747
+ if (l.net && !(0, types_1.isAsset)(l.net)) {
748
+ return { kind: 'request', text: `${l.net.method} ${l.net.url} → ${outcome(l.net)} ${durText(l.net.duration)}` };
749
+ }
750
+ if (l.con)
751
+ return { kind: l.con.level, text: (0, format_1.oneLine)((0, format_1.firstLine)(l.con.text), 300) + location(l.con) };
752
+ return null;
753
+ }
754
+ /** A final Web Vitals report with the given rating (any when empty) or a long animation frame of at least minFreeze ms. */
755
+ function perfLine(l, finals, rating, minFreeze) {
756
+ const p = l.perf;
757
+ if (!p)
758
+ return null;
759
+ if (p.kind === 'vital' && finals.has(l) && (rating === '' || p.rating === rating))
760
+ return { kind: 'vital', text: (0, perf_1.vitalText)(p) };
761
+ if (p.kind === 'loaf' && p.duration >= minFreeze) {
762
+ let text = 'Page froze for ' + (0, format_1.fmtMs)(p.duration);
763
+ const s = (0, perf_1.longestScript)(p);
764
+ if (s !== '')
765
+ text += ' (longest script: ' + s + ')';
766
+ return { kind: 'freeze', text };
767
+ }
768
+ return null;
769
+ }
770
+ /** Adds what deep mode recorded about a request. */
771
+ function writeDeep(b, n, out) {
772
+ if (n.source !== 'cdp')
773
+ return;
774
+ const facts = ['Recorded in ' + (0, types_1.recorded)(n), ...nonEmpty(n.protocol, n.remoteAddress)];
775
+ if (n.cache !== '')
776
+ facts.push('from the ' + n.cache + ' cache');
777
+ else if (n.fromServiceWorker)
778
+ facts.push((0, gofmt_1.goTrimSpace)('answered by a service worker ' + n.swSource));
779
+ b.write(facts.join(' · ') + '\n');
780
+ if (out.failureReason)
781
+ b.write(`Why it failed: ${out.failureReason}\n`);
782
+ const t = (0, network_1.timingText)(n.timing);
783
+ if (t !== '')
784
+ b.write(`Timing: ${t}\n`);
785
+ if (n.worker)
786
+ b.write(`Requested by a ${n.worker.type.split('_').join(' ')}: ${n.worker.url}\n`);
787
+ for (const c of n.blockedCookies ?? [])
788
+ b.write(`Cookie not sent: ${c.name} — ${(0, network_1.cookieReasons)(c.reasons)}\n`);
789
+ for (const c of n.blockedSetCookies ?? [])
790
+ b.write(`Set-Cookie rejected: ${c.name} — ${(0, network_1.cookieReasons)(c.reasons)}\n`);
791
+ if ((0, types_1.isWebSocket)(n)) {
792
+ const closed = n.closedAt > 0 ? 'closed after ' + durText(n.duration) : 'still open when the recording stopped';
793
+ b.write(`WebSocket ${closed} · ${(0, network_1.framesText)(n.frames)}\n`);
794
+ }
795
+ }
796
+ function frameLine(f) {
797
+ let b = '';
798
+ if (f.dropped)
799
+ b += ` … ${f.dropped} message(s) not recorded (over 200 per second)\n`;
800
+ const arrow = f.dir === 'send' ? '↑' : '↓';
801
+ let what;
802
+ if (f.error)
803
+ what = 'error: ' + f.error;
804
+ else if (f.dataOmitted)
805
+ what = '(data not recorded: over 1 MB per second)';
806
+ else if (f.opcode === 2)
807
+ what = 'binary';
808
+ else if (f.opcode === 8)
809
+ what = 'close';
810
+ else if (f.opcode === 9)
811
+ what = 'ping';
812
+ else if (f.opcode === 10)
813
+ what = 'pong';
814
+ else {
815
+ what = (0, format_1.oneLine)(f.data ?? '', FRAME_CLIP);
816
+ if (f.truncated)
817
+ what += ' … (cut at 64 KB by the recorder)';
818
+ }
819
+ b += ` ${f.at} ${arrow} ${sizeText(f.size)} ${what} [${f.id}]\n`;
820
+ return b;
821
+ }
822
+ /** A request by line id, else by the recorder's own request id. */
823
+ function findRequest(v, id) {
824
+ const l = v.find(id);
825
+ if (l && l.net)
826
+ return l;
827
+ return v.lines.find((x) => x.net !== undefined && x.net.id === id);
828
+ }
829
+ /** An action's changes as "path = value" lines. */
830
+ async function changeTexts(v, l) {
831
+ const most = 8;
832
+ let changes;
833
+ try {
834
+ changes = (await v.fullState(l)).changes ?? [];
835
+ }
836
+ catch {
837
+ return l.state.paths.slice(); // deleted meanwhile: paths only
838
+ }
839
+ const out = [];
840
+ for (let i = 0; i < changes.length; i++) {
841
+ const c = changes[i];
842
+ if (i === most) {
843
+ out.push(`… ${changes.length - most} more changed paths`);
844
+ break;
845
+ }
846
+ const path = c.path || '(whole state)';
847
+ if (c.deleted) {
848
+ out.push(path + ' deleted');
849
+ continue;
850
+ }
851
+ // The store keeps records as compact JSON, so this is the recorded text.
852
+ const value = c.value === undefined ? '' : JSON.stringify(c.value);
853
+ out.push(path + ' = ' + (0, format_1.clip)(value, 120));
854
+ }
855
+ if (out.length === 0)
856
+ out.push('(no state change)');
857
+ return out;
858
+ }
859
+ // ---------- helpers ----------
860
+ function clampLimit(n, def, most) {
861
+ return n <= 0 ? def : Math.min(n, most);
862
+ }
863
+ function parseCursor(c) {
864
+ if (c === '')
865
+ return 0;
866
+ const n = (0, params_1.atoi)(c);
867
+ if (n === null || n < 0)
868
+ throw new Error(`invalid cursor ${(0, gofmt_1.goQuote)(c)}: pass next_cursor from the previous result`);
869
+ return n;
870
+ }
871
+ /** Parses from/to; a missing from is 0, a missing to is -1 (the end). */
872
+ function window(fromS, toS) {
873
+ const from = (0, params_1.parseOffset)(fromS);
874
+ const to = (0, params_1.parseOffset)(toS);
875
+ return [Math.max(from, 0), to];
876
+ }
877
+ function kindMatcher(kind) {
878
+ const k = (0, gofmt_1.goToLower)((0, gofmt_1.goTrimSpace)(kind));
879
+ switch (k) {
880
+ case '':
881
+ return () => true;
882
+ case 'api':
883
+ return (x) => x === 'fetch' || x === 'xhr' || x === 'beacon';
884
+ case 'document':
885
+ case 'navigation':
886
+ case 'doc':
887
+ return (x) => x === 'navigation';
888
+ case 'resource':
889
+ case 'asset':
890
+ case 'assets':
891
+ return (x) => x === 'resource';
892
+ case 'fetch':
893
+ case 'xhr':
894
+ case 'beacon':
895
+ case 'websocket':
896
+ return (x) => x === k;
897
+ case 'ws':
898
+ case 'websockets':
899
+ return (x) => x === 'websocket';
900
+ }
901
+ throw new Error(`unknown kind ${(0, gofmt_1.goQuote)(kind)}; use fetch, xhr, beacon, api, document, resource or websocket`);
902
+ }
903
+ function levelMatcher(level) {
904
+ const l = (0, gofmt_1.goToLower)((0, gofmt_1.goTrimSpace)(level));
905
+ switch (l) {
906
+ case '':
907
+ return () => true;
908
+ case 'warn+':
909
+ case 'warning+':
910
+ case 'warnings':
911
+ return (x) => x === 'warn' || x === 'error';
912
+ case 'warning':
913
+ return (x) => x === 'warn';
914
+ case 'error':
915
+ case 'warn':
916
+ case 'info':
917
+ case 'log':
918
+ case 'debug':
919
+ case 'trace':
920
+ return (x) => x === l;
921
+ }
922
+ throw new Error(`unknown level ${(0, gofmt_1.goQuote)(level)}; use error, warn, warn+, info, log, debug or trace`);
923
+ }
924
+ function outcome(n) {
925
+ if (n.error !== '' || n.corsErrorStatus !== undefined || n.blockedReason !== '')
926
+ return 'failed (' + (0, network_1.failureReason)(n) + ')';
927
+ if (n.pending && (0, types_1.isWebSocket)(n))
928
+ return 'open';
929
+ if (n.pending)
930
+ return 'pending';
931
+ if (n.status > 0)
932
+ return String(n.status);
933
+ if (n.kind === 'beacon')
934
+ return 'sent';
935
+ return '—';
936
+ }
937
+ function durText(d) {
938
+ return d === undefined ? '—' : (0, format_1.fmtMs)(d);
939
+ }
940
+ function sizeText(n) {
941
+ return n <= 0 ? '—' : (0, format_1.fmtBytes)(n);
942
+ }
943
+ function bodyClip(body, n) {
944
+ return body === undefined ? '' : (0, format_1.clip)(body, n);
945
+ }
946
+ function bodyText(name, body, note) {
947
+ if (body !== '')
948
+ return ` ${name}: ${body.split('\n').join('\n ')}\n`;
949
+ if (note !== '')
950
+ return ` ${name}: (${note})\n`;
951
+ return '';
952
+ }
953
+ /** One page of a body, counted in code points (Go's runes). */
954
+ function page(body, offset) {
955
+ if (body === undefined)
956
+ return { text: '', length: 0, more: false };
957
+ const length = (0, gofmt_1.runeCount)(body);
958
+ if (offset >= length)
959
+ return { text: '', length, more: false };
960
+ const end = Math.min(offset + exports.BODY_PAGE, length);
961
+ return { text: runeSlice(body, offset, end), length, more: end < length };
962
+ }
963
+ /** s from rune start to rune end. */
964
+ function runeSlice(s, start, end) {
965
+ let i = 0;
966
+ let r = 0;
967
+ let from = -1;
968
+ while (i < s.length) {
969
+ if (r === start)
970
+ from = i;
971
+ if (r === end)
972
+ break;
973
+ const c = s.charCodeAt(i);
974
+ const d = s.charCodeAt(i + 1);
975
+ i += c >= 0xd800 && c <= 0xdbff && d >= 0xdc00 && d <= 0xdfff ? 2 : 1;
976
+ r++;
977
+ }
978
+ if (from < 0)
979
+ from = i;
980
+ return s.slice(from, i);
981
+ }
982
+ function writeHeaders(b, title, h) {
983
+ if (!h)
984
+ return;
985
+ const keys = Object.keys(h).sort();
986
+ if (keys.length === 0)
987
+ return;
988
+ b.write(`\n${title}:\n`);
989
+ for (const k of keys)
990
+ b.write(` ${k}: ${h[k]}\n`);
991
+ }
992
+ function writeFullBody(b, title, body, pageText, length, offset, note, truncated) {
993
+ if (body === undefined) {
994
+ if (note !== '')
995
+ b.write(`\n${title}: (${note})\n`);
996
+ return;
997
+ }
998
+ if (length === 0) {
999
+ b.write(`\n${title}: (empty)\n`);
1000
+ return;
1001
+ }
1002
+ const extra = truncated ? ', cut by the recorder at 1 MB' : '';
1003
+ const n = (0, gofmt_1.runeCount)(pageText);
1004
+ if (offset > 0 || n < length)
1005
+ b.write(`\n${title} (characters ${offset}–${offset + n} of ${length}${extra}):\n${pageText}\n`);
1006
+ else
1007
+ b.write(`\n${title} (${length} characters${extra}):\n${pageText}\n`);
1008
+ }
1009
+ function shortStack(stack) {
1010
+ if (stack === '')
1011
+ return '';
1012
+ let lines = (0, gofmt_1.goTrimSpace)(stack).split('\n');
1013
+ if (lines.length > STACK_LINES)
1014
+ lines = [...lines.slice(0, STACK_LINES), `… ${lines.length - STACK_LINES} more frames`];
1015
+ return lines.map((l) => (0, format_1.clip)((0, gofmt_1.goTrimSpace)(l), 300)).join('\n');
1016
+ }
1017
+ function label(t) {
1018
+ const text = (0, format_1.targetText)(t);
1019
+ if (text !== '')
1020
+ return (0, gofmt_1.goQuote)((0, format_1.oneLine)(text, 100));
1021
+ return '<' + (0, format_1.targetTag)(t) + '>';
1022
+ }
1023
+ function field(t) {
1024
+ const name = (0, format_1.fieldName)(t);
1025
+ if (name !== '')
1026
+ return (0, gofmt_1.goQuote)(name);
1027
+ return '<' + (0, format_1.targetTag)(t) + '>';
1028
+ }
1029
+ function selector(t) {
1030
+ return !t || t.selector === '' ? 'no selector' : t.selector;
1031
+ }
1032
+ function consoleLabel(c) {
1033
+ switch (c.source) {
1034
+ case 'uncaught':
1035
+ return 'Uncaught error';
1036
+ case 'unhandledrejection':
1037
+ return 'Unhandled rejection';
1038
+ case 'resource':
1039
+ return 'Resource failed to load';
1040
+ }
1041
+ return 'console.error';
1042
+ }
1043
+ function location(c) {
1044
+ if (c.url === '')
1045
+ return '';
1046
+ const { path } = (0, format_1.splitURL)(c.url);
1047
+ return c.line > 0 ? ` at ${path}:${c.line}` : ' at ' + path;
1048
+ }
1049
+ function nonEmpty(...ss) {
1050
+ return ss.filter((s) => s !== '');
1051
+ }
1052
+ /** A map for JSON output: keys sorted like Go's encoder, nothing when empty (omitempty). */
1053
+ function nonEmptyMap(m) {
1054
+ if (!m)
1055
+ return undefined;
1056
+ const keys = Object.keys(m).sort();
1057
+ if (keys.length === 0)
1058
+ return undefined;
1059
+ const out = {};
1060
+ for (const k of keys)
1061
+ out[k] = m[k];
1062
+ return out;
1063
+ }
1064
+ function framesOut(f) {
1065
+ if (!f)
1066
+ return undefined;
1067
+ return {
1068
+ sent: f.sent,
1069
+ received: f.received,
1070
+ sentBytes: f.sentBytes,
1071
+ receivedBytes: f.receivedBytes,
1072
+ dropped: f.dropped || undefined,
1073
+ dataOmitted: f.dataOmitted || undefined,
1074
+ };
1075
+ }
1076
+ function timingOut(t) {
1077
+ if (!t)
1078
+ return undefined;
1079
+ return { blocked: t.blocked, dns: t.dns, connect: t.connect, ssl: t.ssl, send: t.send, wait: t.wait, receive: t.receive };
1080
+ }
1081
+ function cookiesOut(list) {
1082
+ if (!list || list.length === 0)
1083
+ return undefined;
1084
+ return list.map((c) => ({ name: c.name, reasons: c.reasons, domain: c.domain || undefined, path: c.path || undefined }));
1085
+ }