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.
- package/daemon/dist/index.js +61 -3
- package/daemon/dist/staticServing.js +2 -2
- package/daemon/dist/tabtape/api/follow.js +174 -0
- package/daemon/dist/tabtape/api/http.js +221 -0
- package/daemon/dist/tabtape/api/record.js +149 -0
- package/daemon/dist/tabtape/api/router.js +519 -0
- package/daemon/dist/tabtape/api/router.spec.js +604 -0
- package/daemon/dist/tabtape/api/static.js +70 -0
- package/daemon/dist/tabtape/api/ws.js +166 -0
- package/daemon/dist/tabtape/buffer/buffer.js +347 -0
- package/daemon/dist/tabtape/buffer/buffer.spec.js +137 -0
- package/daemon/dist/tabtape/config.js +108 -0
- package/daemon/dist/tabtape/e2e-server.js +96 -0
- package/daemon/dist/tabtape/errors.js +77 -0
- package/daemon/dist/tabtape/export/export.js +331 -0
- package/daemon/dist/tabtape/export/export.spec.js +262 -0
- package/daemon/dist/tabtape/har/gostd.js +395 -0
- package/daemon/dist/tabtape/har/gostd.spec.js +139 -0
- package/daemon/dist/tabtape/har/har.js +582 -0
- package/daemon/dist/tabtape/har/har.spec.js +325 -0
- package/daemon/dist/tabtape/index.js +134 -0
- package/daemon/dist/tabtape/index.spec.js +104 -0
- package/daemon/dist/tabtape/lock.js +67 -0
- package/daemon/dist/tabtape/log.js +32 -0
- package/daemon/dist/tabtape/mcp/definitions.js +362 -0
- package/daemon/dist/tabtape/mcp/gofmt.js +227 -0
- package/daemon/dist/tabtape/mcp/params.js +345 -0
- package/daemon/dist/tabtape/mcp/params.spec.js +172 -0
- package/daemon/dist/tabtape/mcp/recordLink.js +122 -0
- package/daemon/dist/tabtape/mcp/server.js +411 -0
- package/daemon/dist/tabtape/mcp/server.spec.js +722 -0
- package/daemon/dist/tabtape/mcp/tools.js +1085 -0
- package/daemon/dist/tabtape/query/format.js +195 -0
- package/daemon/dist/tabtape/query/network.js +133 -0
- package/daemon/dist/tabtape/query/perf.js +93 -0
- package/daemon/dist/tabtape/query/query.spec.js +233 -0
- package/daemon/dist/tabtape/query/reader.js +602 -0
- package/daemon/dist/tabtape/query/types.js +274 -0
- package/daemon/dist/tabtape/redact.js +37 -0
- package/daemon/dist/tabtape/repro/lang.js +95 -0
- package/daemon/dist/tabtape/repro/repro.js +422 -0
- package/daemon/dist/tabtape/repro/repro.spec.js +210 -0
- package/daemon/dist/tabtape/sink.js +24 -0
- package/daemon/dist/tabtape/store/record.js +99 -0
- package/daemon/dist/tabtape/store/segment.js +267 -0
- package/daemon/dist/tabtape/store/store.js +846 -0
- package/daemon/dist/tabtape/store/store.spec.js +485 -0
- package/daemon/dist/tabtape/testing/querytest.js +119 -0
- package/daemon/dist/tabtape/testing/tmp.js +33 -0
- package/package.json +3 -2
- package/tabtape/extension/dist/background.js +8 -0
- package/tabtape/extension/dist/background.js.map +7 -0
- package/tabtape/extension/dist/bridge.js +3 -0
- package/tabtape/extension/dist/bridge.js.map +7 -0
- package/tabtape/extension/dist/icons/128.png +0 -0
- package/tabtape/extension/dist/icons/16.png +0 -0
- package/tabtape/extension/dist/icons/32.png +0 -0
- package/tabtape/extension/dist/icons/48.png +0 -0
- package/tabtape/extension/dist/manifest.json +28 -0
- package/tabtape/extension/dist/popup.css +62 -0
- package/tabtape/extension/dist/popup.html +34 -0
- package/tabtape/extension/dist/popup.js +4 -0
- package/tabtape/extension/dist/popup.js.map +7 -0
- package/tabtape/extension/dist/recorder.js +133 -0
- package/tabtape/extension/dist/recorder.js.map +1 -0
- package/tabtape/extension/dist/state.js +2 -0
- package/tabtape/extension/dist/state.js.map +1 -0
- package/tabtape/playwright/mcp.mjs +58 -0
- package/tabtape/playwright/tabtape.d.mts +37 -0
- package/tabtape/playwright/tabtape.mjs +146 -0
- package/tabtape/ui/dist/app/assets/index-Canv3Ljk.css +1 -0
- package/tabtape/ui/dist/app/assets/index-iWNSSrAE.js +145 -0
- package/tabtape/ui/dist/app/index.html +14 -0
- package/tabtape/ui/dist/export/viewer.css +2 -0
- package/tabtape/ui/dist/export/viewer.js +143 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildHar = buildHar;
|
|
4
|
+
/**
|
|
5
|
+
* Converts a recorded session's network data to HAR 1.2, the format Chrome
|
|
6
|
+
* DevTools, Charles and Proxyman import.
|
|
7
|
+
*
|
|
8
|
+
* Requests captured in the page (fetch, XHR, beacons) carry the headers and
|
|
9
|
+
* bodies the page could see and two timings: waiting for the response
|
|
10
|
+
* headers, and reading the body. Assets seen through Resource Timing have a
|
|
11
|
+
* size and a duration only. Requests recorded in deep mode (through the
|
|
12
|
+
* Chrome DevTools Protocol) have the real headers, the full timing split
|
|
13
|
+
* (DNS, connect, TLS, ...), the protocol and the server address, and
|
|
14
|
+
* WebSocket connections carry their messages in _webSocketMessages, like
|
|
15
|
+
* Chrome's own HAR export. Unknown values are -1, as the format asks.
|
|
16
|
+
*
|
|
17
|
+
* Objects are built with the fields in HAR order and optional fields left
|
|
18
|
+
* out when empty, so JSON.stringify(har, null, 2) writes the same file the Go
|
|
19
|
+
* server did.
|
|
20
|
+
*/
|
|
21
|
+
const network_1 = require("../query/network");
|
|
22
|
+
const types_1 = require("../query/types");
|
|
23
|
+
const gostd_1 = require("./gostd");
|
|
24
|
+
const UNKNOWN = -1;
|
|
25
|
+
/** Converts the session's page views, network records and WebSocket messages. */
|
|
26
|
+
async function buildHar(v, opts) {
|
|
27
|
+
const pages = [];
|
|
28
|
+
const nets = [];
|
|
29
|
+
const wsLines = new Set();
|
|
30
|
+
for (const l of v.lines) {
|
|
31
|
+
if (l.ev && l.ev.name === 'page_view')
|
|
32
|
+
pages.push({ ts: l.ts, url: l.ev.url, title: l.ev.title });
|
|
33
|
+
else if (l.net && inRange(opts, v, l.ts))
|
|
34
|
+
nets.push(l);
|
|
35
|
+
else if (l.ws && l.ws.error === '' && inRange(opts, v, l.ts))
|
|
36
|
+
wsLines.add(l);
|
|
37
|
+
}
|
|
38
|
+
// Headers, bodies and message data are not in the index: read the
|
|
39
|
+
// records, in file order, and keep the ones in range.
|
|
40
|
+
const want = new Set(nets);
|
|
41
|
+
const full = new Map();
|
|
42
|
+
const messages = new Map();
|
|
43
|
+
await v.scan((l, raw) => {
|
|
44
|
+
if (want.has(l)) {
|
|
45
|
+
const data = recordData(raw);
|
|
46
|
+
if (data === UNREADABLE)
|
|
47
|
+
return; // the index decoded it; a changed file is skipped
|
|
48
|
+
full.set(l, (0, types_1.decodeNetwork)(data));
|
|
49
|
+
}
|
|
50
|
+
else if (wsLines.has(l)) {
|
|
51
|
+
const data = recordData(raw);
|
|
52
|
+
if (data === UNREADABLE)
|
|
53
|
+
return;
|
|
54
|
+
const f = (0, types_1.decodeWS)(data);
|
|
55
|
+
let list = messages.get(f.conn);
|
|
56
|
+
if (!list)
|
|
57
|
+
messages.set(f.conn, (list = []));
|
|
58
|
+
list.push({ ts: l.ts, f });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
const entries = [];
|
|
62
|
+
const used = new Set();
|
|
63
|
+
let deep = false;
|
|
64
|
+
let inPage = false;
|
|
65
|
+
for (const l of nets) {
|
|
66
|
+
const n = full.get(l);
|
|
67
|
+
if (!n)
|
|
68
|
+
continue; // deleted by the ring buffer meanwhile
|
|
69
|
+
deep = deep || n.source === 'cdp';
|
|
70
|
+
inPage = inPage || n.source !== 'cdp';
|
|
71
|
+
const i = pageFor(pages, l.ts, n.kind === 'navigation');
|
|
72
|
+
if (i >= 0)
|
|
73
|
+
used.add(i);
|
|
74
|
+
const ws = (0, types_1.isWebSocket)(n) ? wsMessages(messages.get(n.id)) : [];
|
|
75
|
+
entries.push(entry(opts, l.ts, n, pageURL(pages, l.ts), i >= 0 ? pageID(i) : '', ws));
|
|
76
|
+
}
|
|
77
|
+
// Stable, like Go's slices.SortStableFunc; the times are ASCII.
|
|
78
|
+
entries.sort((a, b) => (a.startedDateTime < b.startedDateTime ? -1 : a.startedDateTime > b.startedDateTime ? 1 : 0));
|
|
79
|
+
const harPages = [];
|
|
80
|
+
pages.forEach((p, i) => {
|
|
81
|
+
if (!used.has(i) && !inRange(opts, v, p.ts))
|
|
82
|
+
return;
|
|
83
|
+
harPages.push({
|
|
84
|
+
startedDateTime: isoTime(p.ts),
|
|
85
|
+
id: pageID(i),
|
|
86
|
+
title: p.title || p.url,
|
|
87
|
+
pageTimings: { onContentLoad: UNKNOWN, onLoad: UNKNOWN },
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
const b = browser(v.session.userAgent ?? '');
|
|
91
|
+
return {
|
|
92
|
+
log: {
|
|
93
|
+
version: '1.2',
|
|
94
|
+
creator: { name: 'tabtape', version: opts.version || 'dev' },
|
|
95
|
+
...(b ? { browser: b } : {}),
|
|
96
|
+
pages: harPages,
|
|
97
|
+
entries,
|
|
98
|
+
comment: comment(opts, deep, inPage),
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const UNREADABLE = Symbol('unreadable');
|
|
103
|
+
/** A raw record's "data" field; UNREADABLE when the line is not a record object (it changed on disk). */
|
|
104
|
+
function recordData(raw) {
|
|
105
|
+
let rec;
|
|
106
|
+
try {
|
|
107
|
+
rec = JSON.parse(raw.toString('utf8'));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return UNREADABLE;
|
|
111
|
+
}
|
|
112
|
+
if (!(0, types_1.isObj)(rec))
|
|
113
|
+
return UNREADABLE;
|
|
114
|
+
const data = rec.data;
|
|
115
|
+
return data === undefined || data === null || (0, types_1.isObj)(data) ? data : UNREADABLE;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Uses the replay's clamped offsets, like the UI: requests before the first
|
|
119
|
+
* snapshot count as 0.
|
|
120
|
+
*/
|
|
121
|
+
function inRange(o, v, ts) {
|
|
122
|
+
const off = v.offset(ts);
|
|
123
|
+
if (off < (o.from ?? 0))
|
|
124
|
+
return false;
|
|
125
|
+
const to = o.to ?? 0;
|
|
126
|
+
return to <= 0 || off <= to;
|
|
127
|
+
}
|
|
128
|
+
function pageID(i) {
|
|
129
|
+
return `page_${i + 1}`;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The page a request belongs to: the last page view at or before it, or for
|
|
133
|
+
* a document request the page it loads. -1 means none.
|
|
134
|
+
*/
|
|
135
|
+
function pageFor(pages, ts, document) {
|
|
136
|
+
// Binary search for i: pages[:i] start at or before ts.
|
|
137
|
+
let i = 0;
|
|
138
|
+
let j = pages.length;
|
|
139
|
+
while (i < j) {
|
|
140
|
+
const h = (i + j) >>> 1;
|
|
141
|
+
if (pages[h].ts <= ts)
|
|
142
|
+
i = h + 1;
|
|
143
|
+
else
|
|
144
|
+
j = h;
|
|
145
|
+
}
|
|
146
|
+
if (document && i < pages.length)
|
|
147
|
+
return i;
|
|
148
|
+
return i - 1;
|
|
149
|
+
}
|
|
150
|
+
function pageURL(pages, ts) {
|
|
151
|
+
const i = pageFor(pages, ts, false);
|
|
152
|
+
return i >= 0 ? pages[i].url : '';
|
|
153
|
+
}
|
|
154
|
+
function entry(o, ts, n, page, pageref, ws) {
|
|
155
|
+
const u = absolute(n.url, page);
|
|
156
|
+
const version = httpVersion(n.protocol);
|
|
157
|
+
const duration = n.duration !== undefined ? Math.max(n.duration, 0) : 0;
|
|
158
|
+
let timings = { blocked: UNKNOWN, dns: UNKNOWN, connect: UNKNOWN, send: 0, wait: 0, receive: 0, ssl: UNKNOWN };
|
|
159
|
+
let time;
|
|
160
|
+
if ((0, types_1.isWebSocket)(n)) {
|
|
161
|
+
// The connection's lifetime, so viewers draw it across the waterfall.
|
|
162
|
+
timings.receive = duration;
|
|
163
|
+
time = duration;
|
|
164
|
+
}
|
|
165
|
+
else if (n.timing) {
|
|
166
|
+
[timings, time] = deepTimings(n.timing);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
timings.wait = duration;
|
|
170
|
+
if (n.ttfb !== undefined && n.ttfb >= 0 && n.ttfb <= duration) {
|
|
171
|
+
timings.wait = n.ttfb;
|
|
172
|
+
timings.receive = duration - n.ttfb;
|
|
173
|
+
}
|
|
174
|
+
time = duration;
|
|
175
|
+
}
|
|
176
|
+
let status = n.status;
|
|
177
|
+
let error = '';
|
|
178
|
+
const notes = [];
|
|
179
|
+
if (n.error !== '' || n.corsErrorStatus !== undefined || n.blockedReason !== '') {
|
|
180
|
+
status = 0;
|
|
181
|
+
error = n.error || (0, network_1.failureReason)(n);
|
|
182
|
+
notes.push('Failed: ' + (0, network_1.failureReason)(n));
|
|
183
|
+
}
|
|
184
|
+
else if (n.pending && (0, types_1.isWebSocket)(n)) {
|
|
185
|
+
notes.push('Still open when the recording stopped');
|
|
186
|
+
}
|
|
187
|
+
else if (n.pending) {
|
|
188
|
+
notes.push('Still in flight when the page unloaded or the recording stopped');
|
|
189
|
+
}
|
|
190
|
+
if ((0, types_1.isAsset)(n) && n.source !== 'cdp')
|
|
191
|
+
notes.push('Seen through Resource Timing: no headers or body');
|
|
192
|
+
if (n.worker)
|
|
193
|
+
notes.push(`Requested by a ${n.worker.type.replace(/_/g, ' ')}: ${n.worker.url}`);
|
|
194
|
+
for (const c of n.blockedCookies ?? [])
|
|
195
|
+
notes.push(`Cookie ${c.name} not sent: ${(0, network_1.cookieReasons)(c.reasons)}`);
|
|
196
|
+
for (const c of n.blockedSetCookies ?? [])
|
|
197
|
+
notes.push(`Set-Cookie ${c.name} rejected: ${(0, network_1.cookieReasons)(c.reasons)}`);
|
|
198
|
+
const note = notes.join('. ');
|
|
199
|
+
const [reqHeaders, reqCookies] = headers(o, n.reqHeaders, 'cookie');
|
|
200
|
+
const [resHeaders, resCookies] = headers(o, n.resHeaders, 'set-cookie');
|
|
201
|
+
let redirectURL = '';
|
|
202
|
+
const loc = header(n.resHeaders, 'location');
|
|
203
|
+
if (loc !== '' && n.status >= 300 && n.status < 400)
|
|
204
|
+
redirectURL = absolute(loc, u);
|
|
205
|
+
let postData;
|
|
206
|
+
let bodySize = 0;
|
|
207
|
+
if (n.reqBody !== undefined || n.reqBodyNote !== '') {
|
|
208
|
+
const mimeType = header(n.reqHeaders, 'content-type') || 'application/octet-stream';
|
|
209
|
+
if (n.reqBody !== undefined) {
|
|
210
|
+
bodySize = Buffer.byteLength(n.reqBody, 'utf8');
|
|
211
|
+
let pdComment = '';
|
|
212
|
+
if (n.reqBodyTruncated) {
|
|
213
|
+
pdComment = truncated(n.reqBody);
|
|
214
|
+
bodySize = UNKNOWN;
|
|
215
|
+
}
|
|
216
|
+
postData = { mimeType, text: n.reqBody, ...(pdComment ? { comment: pdComment } : {}) };
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
postData = { mimeType, text: '', comment: n.reqBodyNote };
|
|
220
|
+
bodySize = UNKNOWN;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
let size = 0;
|
|
224
|
+
if (n.resSize !== undefined)
|
|
225
|
+
size = intOf(n.resSize);
|
|
226
|
+
else if (n.resBody !== undefined && !n.resBodyTruncated)
|
|
227
|
+
size = Buffer.byteLength(n.resBody, 'utf8');
|
|
228
|
+
let text = '';
|
|
229
|
+
let contentComment = '';
|
|
230
|
+
if (n.resBody !== undefined) {
|
|
231
|
+
text = n.resBody;
|
|
232
|
+
if (n.resBodyTruncated)
|
|
233
|
+
contentComment = truncated(n.resBody);
|
|
234
|
+
}
|
|
235
|
+
else if (n.resBodyNote !== '') {
|
|
236
|
+
contentComment = n.resBodyNote;
|
|
237
|
+
}
|
|
238
|
+
const content = {
|
|
239
|
+
size,
|
|
240
|
+
mimeType: n.mime || header(n.resHeaders, 'content-type') || 'x-unknown',
|
|
241
|
+
...(text ? { text } : {}),
|
|
242
|
+
...(contentComment ? { comment: contentComment } : {}),
|
|
243
|
+
};
|
|
244
|
+
const serverIP = n.remoteAddress !== '' ? (0, network_1.remoteIP)(n.remoteAddress) : '';
|
|
245
|
+
const fromCache = n.cache === 'memory' || n.cache === 'disk' ? n.cache : '';
|
|
246
|
+
const resourceType = resourceTypeOf(n);
|
|
247
|
+
return {
|
|
248
|
+
...(pageref ? { pageref } : {}),
|
|
249
|
+
startedDateTime: isoTime(ts),
|
|
250
|
+
time,
|
|
251
|
+
request: {
|
|
252
|
+
method: n.method.toUpperCase() || 'GET',
|
|
253
|
+
url: u,
|
|
254
|
+
httpVersion: version,
|
|
255
|
+
cookies: reqCookies,
|
|
256
|
+
headers: reqHeaders,
|
|
257
|
+
queryString: queryString(u),
|
|
258
|
+
...(postData ? { postData } : {}),
|
|
259
|
+
headersSize: UNKNOWN,
|
|
260
|
+
bodySize,
|
|
261
|
+
},
|
|
262
|
+
response: {
|
|
263
|
+
status,
|
|
264
|
+
statusText: n.statusText,
|
|
265
|
+
httpVersion: version,
|
|
266
|
+
cookies: resCookies,
|
|
267
|
+
headers: resHeaders,
|
|
268
|
+
content,
|
|
269
|
+
redirectURL,
|
|
270
|
+
headersSize: UNKNOWN,
|
|
271
|
+
bodySize: UNKNOWN,
|
|
272
|
+
...(n.transferSize !== undefined ? { _transferSize: intOf(n.transferSize) } : {}),
|
|
273
|
+
...(error ? { _error: error } : {}),
|
|
274
|
+
},
|
|
275
|
+
cache: {},
|
|
276
|
+
timings,
|
|
277
|
+
...(serverIP ? { serverIPAddress: serverIP } : {}),
|
|
278
|
+
...(note ? { comment: note } : {}),
|
|
279
|
+
...(resourceType ? { _resourceType: resourceType } : {}),
|
|
280
|
+
...(fromCache ? { _fromCache: fromCache } : {}),
|
|
281
|
+
...(ws.length > 0 ? { _webSocketMessages: ws } : {}),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function truncated(body) {
|
|
285
|
+
return `Truncated by tabtape after ${runeCount(body)} characters`;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Converts a deep mode timing split; HAR wants send, wait and receive
|
|
289
|
+
* non-negative and time to be the sum of the known phases (ssl is part of
|
|
290
|
+
* connect).
|
|
291
|
+
*/
|
|
292
|
+
function deepTimings(t) {
|
|
293
|
+
const known = (v) => (v === undefined || v < 0 ? UNKNOWN : v);
|
|
294
|
+
const nonNeg = (v) => Math.max(known(v), 0);
|
|
295
|
+
const tm = {
|
|
296
|
+
blocked: known(t.blocked),
|
|
297
|
+
dns: known(t.dns),
|
|
298
|
+
connect: known(t.connect),
|
|
299
|
+
send: nonNeg(t.send),
|
|
300
|
+
wait: nonNeg(t.wait),
|
|
301
|
+
receive: nonNeg(t.receive),
|
|
302
|
+
ssl: known(t.ssl),
|
|
303
|
+
};
|
|
304
|
+
let total = tm.send + tm.wait + tm.receive;
|
|
305
|
+
for (const v of [tm.blocked, tm.dns, tm.connect])
|
|
306
|
+
total += Math.max(v, 0);
|
|
307
|
+
return [tm, total];
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Lists a connection's recorded messages; binary data and data left out by
|
|
311
|
+
* the recorder's limits are empty.
|
|
312
|
+
*/
|
|
313
|
+
function wsMessages(ms) {
|
|
314
|
+
if (!ms || ms.length === 0)
|
|
315
|
+
return [];
|
|
316
|
+
ms.sort((a, b) => a.ts - b.ts); // stable
|
|
317
|
+
return ms.map((m) => ({ type: m.f.dir === 'send' ? 'send' : 'receive', time: m.ts / 1000, opcode: m.f.opcode, data: m.f.data ?? '' }));
|
|
318
|
+
}
|
|
319
|
+
/** Names a protocol like Chrome's HAR export does. */
|
|
320
|
+
function httpVersion(protocol) {
|
|
321
|
+
const p = protocol.toLowerCase();
|
|
322
|
+
if (p === '')
|
|
323
|
+
return '';
|
|
324
|
+
if (p === 'h2')
|
|
325
|
+
return 'HTTP/2';
|
|
326
|
+
if (p.startsWith('h3'))
|
|
327
|
+
return 'HTTP/3';
|
|
328
|
+
if (p.startsWith('http/'))
|
|
329
|
+
return p.toUpperCase();
|
|
330
|
+
return protocol;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Lists headers sorted by name, leaving out sensitive ones unless secrets is
|
|
334
|
+
* set, and parses the cookie header given.
|
|
335
|
+
*/
|
|
336
|
+
function headers(o, m, cookieHeader) {
|
|
337
|
+
const out = [];
|
|
338
|
+
const cookies = [];
|
|
339
|
+
if (!m)
|
|
340
|
+
return [out, cookies];
|
|
341
|
+
for (const name of sortedKeys(m)) {
|
|
342
|
+
if (!o.secrets && o.redact.has(name))
|
|
343
|
+
continue;
|
|
344
|
+
out.push({ name, value: m[name] });
|
|
345
|
+
if (name.toLowerCase() === cookieHeader) {
|
|
346
|
+
for (const c of parseCookies(m[name], cookieHeader === 'set-cookie'))
|
|
347
|
+
cookies.push(c);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return [out, cookies];
|
|
351
|
+
}
|
|
352
|
+
function parseCookies(v, set) {
|
|
353
|
+
if (set) {
|
|
354
|
+
// One cookie per line: "name=value; Path=/; HttpOnly".
|
|
355
|
+
const out = [];
|
|
356
|
+
for (const line of v.split('\n')) {
|
|
357
|
+
const c = (0, gostd_1.parseSetCookie)((0, gostd_1.trimSpace)(line));
|
|
358
|
+
if (c)
|
|
359
|
+
out.push(c);
|
|
360
|
+
}
|
|
361
|
+
return out;
|
|
362
|
+
}
|
|
363
|
+
return (0, gostd_1.parseCookie)(v) ?? [];
|
|
364
|
+
}
|
|
365
|
+
function header(m, name) {
|
|
366
|
+
if (!m)
|
|
367
|
+
return '';
|
|
368
|
+
for (const k of Object.keys(m)) {
|
|
369
|
+
if (k.toLowerCase() === name)
|
|
370
|
+
return m[k];
|
|
371
|
+
}
|
|
372
|
+
return '';
|
|
373
|
+
}
|
|
374
|
+
function queryString(raw) {
|
|
375
|
+
const out = [];
|
|
376
|
+
const u = (0, gostd_1.parseURL)(raw);
|
|
377
|
+
if (!u || u.rawQuery === '')
|
|
378
|
+
return out;
|
|
379
|
+
for (const part of u.rawQuery.split('&')) {
|
|
380
|
+
if (part === '')
|
|
381
|
+
continue;
|
|
382
|
+
const eq = part.indexOf('=');
|
|
383
|
+
const k = eq < 0 ? part : part.slice(0, eq);
|
|
384
|
+
const val = eq < 0 ? '' : part.slice(eq + 1);
|
|
385
|
+
const name = (0, gostd_1.queryUnescape)(k);
|
|
386
|
+
const value = (0, gostd_1.queryUnescape)(val);
|
|
387
|
+
out.push(name === null || value === null ? { name: k, value: val } : { name, value });
|
|
388
|
+
}
|
|
389
|
+
return out;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Resolves a URL against the page it was requested from. The resolved URL is
|
|
393
|
+
* the WHATWG serialization (Go wrote its own, which differs only in
|
|
394
|
+
* normalization details such as escaping spaces in the query).
|
|
395
|
+
*/
|
|
396
|
+
function absolute(raw, base) {
|
|
397
|
+
const u = (0, gostd_1.parseURL)(raw);
|
|
398
|
+
if (!u || u.scheme !== '')
|
|
399
|
+
return raw;
|
|
400
|
+
const b = (0, gostd_1.parseURL)(base);
|
|
401
|
+
if (!b || b.scheme === '')
|
|
402
|
+
return raw;
|
|
403
|
+
try {
|
|
404
|
+
return new URL(raw, base).href;
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
return raw;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/** Maps a record to Chrome's resource types. */
|
|
411
|
+
function resourceTypeOf(n) {
|
|
412
|
+
if (n.resourceType !== '')
|
|
413
|
+
return n.resourceType; // deep mode: Chrome's own
|
|
414
|
+
switch (n.kind) {
|
|
415
|
+
case 'websocket':
|
|
416
|
+
return 'websocket';
|
|
417
|
+
case 'fetch':
|
|
418
|
+
return 'fetch';
|
|
419
|
+
case 'xhr':
|
|
420
|
+
return 'xhr';
|
|
421
|
+
case 'beacon':
|
|
422
|
+
return 'ping';
|
|
423
|
+
case 'navigation':
|
|
424
|
+
return 'document';
|
|
425
|
+
}
|
|
426
|
+
const t = typeOfMime(n.mime);
|
|
427
|
+
if (t !== '')
|
|
428
|
+
return t;
|
|
429
|
+
switch ((0, gostd_1.pathExt)(urlPath(n.url)).toLowerCase()) {
|
|
430
|
+
case '.js':
|
|
431
|
+
case '.mjs':
|
|
432
|
+
return 'script';
|
|
433
|
+
case '.css':
|
|
434
|
+
return 'stylesheet';
|
|
435
|
+
case '.png':
|
|
436
|
+
case '.jpg':
|
|
437
|
+
case '.jpeg':
|
|
438
|
+
case '.gif':
|
|
439
|
+
case '.svg':
|
|
440
|
+
case '.webp':
|
|
441
|
+
case '.avif':
|
|
442
|
+
case '.ico':
|
|
443
|
+
case '.bmp':
|
|
444
|
+
return 'image';
|
|
445
|
+
case '.woff':
|
|
446
|
+
case '.woff2':
|
|
447
|
+
case '.ttf':
|
|
448
|
+
case '.otf':
|
|
449
|
+
case '.eot':
|
|
450
|
+
return 'font';
|
|
451
|
+
case '.mp4':
|
|
452
|
+
case '.webm':
|
|
453
|
+
case '.mp3':
|
|
454
|
+
case '.ogg':
|
|
455
|
+
case '.wav':
|
|
456
|
+
case '.m4a':
|
|
457
|
+
return 'media';
|
|
458
|
+
}
|
|
459
|
+
switch (n.initiator) {
|
|
460
|
+
case 'script':
|
|
461
|
+
return 'script';
|
|
462
|
+
case 'img':
|
|
463
|
+
case 'image':
|
|
464
|
+
return 'image';
|
|
465
|
+
case 'css':
|
|
466
|
+
return 'stylesheet';
|
|
467
|
+
case 'xmlhttprequest':
|
|
468
|
+
return 'xhr';
|
|
469
|
+
case 'fetch':
|
|
470
|
+
return 'fetch';
|
|
471
|
+
case 'beacon':
|
|
472
|
+
return 'ping';
|
|
473
|
+
case 'iframe':
|
|
474
|
+
case 'frame':
|
|
475
|
+
return 'document';
|
|
476
|
+
case 'video':
|
|
477
|
+
case 'audio':
|
|
478
|
+
return 'media';
|
|
479
|
+
}
|
|
480
|
+
return 'other';
|
|
481
|
+
}
|
|
482
|
+
function typeOfMime(m) {
|
|
483
|
+
const mt = (0, gostd_1.mediaType)(m);
|
|
484
|
+
if (mt === null)
|
|
485
|
+
return '';
|
|
486
|
+
if (mt === 'text/css')
|
|
487
|
+
return 'stylesheet';
|
|
488
|
+
if (mt.indexOf('javascript') >= 0)
|
|
489
|
+
return 'script';
|
|
490
|
+
if (mt.startsWith('image/'))
|
|
491
|
+
return 'image';
|
|
492
|
+
if (mt.startsWith('font/'))
|
|
493
|
+
return 'font';
|
|
494
|
+
if (mt.startsWith('video/') || mt.startsWith('audio/'))
|
|
495
|
+
return 'media';
|
|
496
|
+
if (mt === 'text/html')
|
|
497
|
+
return 'document';
|
|
498
|
+
return '';
|
|
499
|
+
}
|
|
500
|
+
function urlPath(raw) {
|
|
501
|
+
const u = (0, gostd_1.parseURL)(raw);
|
|
502
|
+
return u ? u.path : raw;
|
|
503
|
+
}
|
|
504
|
+
const BROWSERS = [
|
|
505
|
+
{ name: 'Microsoft Edge', re: /Edg\/([\d.]+)/ },
|
|
506
|
+
{ name: 'Chrome', re: /Chrome\/([\d.]+)/ },
|
|
507
|
+
{ name: 'Firefox', re: /Firefox\/([\d.]+)/ },
|
|
508
|
+
];
|
|
509
|
+
function browser(ua) {
|
|
510
|
+
for (const b of BROWSERS) {
|
|
511
|
+
const m = b.re.exec(ua);
|
|
512
|
+
if (m)
|
|
513
|
+
return { name: b.name, version: m[1] };
|
|
514
|
+
}
|
|
515
|
+
const safari = safariVersion(ua);
|
|
516
|
+
return safari !== null ? { name: 'Safari', version: safari } : null;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Go's `Version/([\d.]+).*Safari/` without backtracking: the first
|
|
520
|
+
* "Version/<digits>" followed by "Safari/" on the same line.
|
|
521
|
+
*/
|
|
522
|
+
function safariVersion(ua) {
|
|
523
|
+
const re = /Version\/([\d.]+)/g;
|
|
524
|
+
for (let m = re.exec(ua); m; m = re.exec(ua)) {
|
|
525
|
+
const end = m.index + m[0].length;
|
|
526
|
+
const s = ua.indexOf('Safari/', end);
|
|
527
|
+
if (s < 0)
|
|
528
|
+
return null;
|
|
529
|
+
const nl = ua.indexOf('\n', end);
|
|
530
|
+
if (nl < 0 || s < nl)
|
|
531
|
+
return m[1];
|
|
532
|
+
}
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
const PAGE_NOTE = 'Recorded in the page by tabtape: requests the page made itself (fetch, XHR, beacons) with the headers and bodies it could see, ' +
|
|
536
|
+
'and assets from Resource Timing. Timings are split into waiting and receiving only.';
|
|
537
|
+
const CDP_NOTE = 'Recorded by tabtape in deep mode, through the Chrome DevTools Protocol: every request of the tab with the headers Chrome sent and received, ' +
|
|
538
|
+
'the full timing split, and WebSocket messages (_webSocketMessages). Response bodies are kept for fetch and XHR only.';
|
|
539
|
+
const MIXED_NOTE = ' Deep mode ended during the recording: later requests were captured in the page, with the headers the page could see and ' +
|
|
540
|
+
'timings split into waiting and receiving only.';
|
|
541
|
+
function comment(o, deep, inPage) {
|
|
542
|
+
let s = PAGE_NOTE;
|
|
543
|
+
if (deep && inPage)
|
|
544
|
+
s = CDP_NOTE + MIXED_NOTE;
|
|
545
|
+
else if (deep)
|
|
546
|
+
s = CDP_NOTE;
|
|
547
|
+
const names = o.redact.list();
|
|
548
|
+
if (!o.secrets && names.length > 0)
|
|
549
|
+
s += ` Sensitive headers were left out: ${names.join(', ')}.`;
|
|
550
|
+
return s;
|
|
551
|
+
}
|
|
552
|
+
/** Date's range; timestamps beyond it (never recorded in practice) are clamped rather than throwing. */
|
|
553
|
+
const MAX_DATE_MS = 8.64e15;
|
|
554
|
+
function isoTime(ms) {
|
|
555
|
+
return new Date(Math.min(Math.max(ms, -MAX_DATE_MS), MAX_DATE_MS)).toISOString();
|
|
556
|
+
}
|
|
557
|
+
/** A non-negative size rounded half away from zero (Go's math.Round); 0 for NaN and negatives. */
|
|
558
|
+
function intOf(f) {
|
|
559
|
+
if (Number.isNaN(f) || f < 0)
|
|
560
|
+
return 0;
|
|
561
|
+
return Math.round(f);
|
|
562
|
+
}
|
|
563
|
+
/** Characters as Go counts runes: code points, a lone surrogate counting as one. */
|
|
564
|
+
function runeCount(s) {
|
|
565
|
+
let n = 0;
|
|
566
|
+
for (let i = 0; i < s.length; i++, n++) {
|
|
567
|
+
const c = s.charCodeAt(i);
|
|
568
|
+
if (c >= 0xd800 && c <= 0xdbff && i + 1 < s.length) {
|
|
569
|
+
const d = s.charCodeAt(i + 1);
|
|
570
|
+
if (d >= 0xdc00 && d <= 0xdfff)
|
|
571
|
+
i++;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return n;
|
|
575
|
+
}
|
|
576
|
+
/** Header names sorted case-insensitively; names equal but for case keep a fixed order. */
|
|
577
|
+
function sortedKeys(m) {
|
|
578
|
+
return Object.keys(m)
|
|
579
|
+
.map((k) => ({ k, lower: k.toLowerCase() }))
|
|
580
|
+
.sort((a, b) => (0, gostd_1.compareStrings)(a.lower, b.lower) || (0, gostd_1.compareStrings)(a.k, b.k))
|
|
581
|
+
.map((x) => x.k);
|
|
582
|
+
}
|