repoburg 1.3.254 → 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 (78) hide show
  1. package/agent-pane/frontend/dist/assets/{index-hYkepxdY.js → index-BzDC6DcX.js} +86 -86
  2. package/agent-pane/frontend/dist/index.html +1 -1
  3. package/daemon/dist/index.js +61 -3
  4. package/daemon/dist/staticServing.js +2 -2
  5. package/daemon/dist/tabtape/api/follow.js +174 -0
  6. package/daemon/dist/tabtape/api/http.js +221 -0
  7. package/daemon/dist/tabtape/api/record.js +149 -0
  8. package/daemon/dist/tabtape/api/router.js +519 -0
  9. package/daemon/dist/tabtape/api/router.spec.js +604 -0
  10. package/daemon/dist/tabtape/api/static.js +70 -0
  11. package/daemon/dist/tabtape/api/ws.js +166 -0
  12. package/daemon/dist/tabtape/buffer/buffer.js +347 -0
  13. package/daemon/dist/tabtape/buffer/buffer.spec.js +137 -0
  14. package/daemon/dist/tabtape/config.js +108 -0
  15. package/daemon/dist/tabtape/e2e-server.js +96 -0
  16. package/daemon/dist/tabtape/errors.js +77 -0
  17. package/daemon/dist/tabtape/export/export.js +331 -0
  18. package/daemon/dist/tabtape/export/export.spec.js +262 -0
  19. package/daemon/dist/tabtape/har/gostd.js +395 -0
  20. package/daemon/dist/tabtape/har/gostd.spec.js +139 -0
  21. package/daemon/dist/tabtape/har/har.js +582 -0
  22. package/daemon/dist/tabtape/har/har.spec.js +325 -0
  23. package/daemon/dist/tabtape/index.js +134 -0
  24. package/daemon/dist/tabtape/index.spec.js +104 -0
  25. package/daemon/dist/tabtape/lock.js +67 -0
  26. package/daemon/dist/tabtape/log.js +32 -0
  27. package/daemon/dist/tabtape/mcp/definitions.js +362 -0
  28. package/daemon/dist/tabtape/mcp/gofmt.js +227 -0
  29. package/daemon/dist/tabtape/mcp/params.js +345 -0
  30. package/daemon/dist/tabtape/mcp/params.spec.js +172 -0
  31. package/daemon/dist/tabtape/mcp/recordLink.js +122 -0
  32. package/daemon/dist/tabtape/mcp/server.js +411 -0
  33. package/daemon/dist/tabtape/mcp/server.spec.js +722 -0
  34. package/daemon/dist/tabtape/mcp/tools.js +1085 -0
  35. package/daemon/dist/tabtape/query/format.js +195 -0
  36. package/daemon/dist/tabtape/query/network.js +133 -0
  37. package/daemon/dist/tabtape/query/perf.js +93 -0
  38. package/daemon/dist/tabtape/query/query.spec.js +233 -0
  39. package/daemon/dist/tabtape/query/reader.js +602 -0
  40. package/daemon/dist/tabtape/query/types.js +274 -0
  41. package/daemon/dist/tabtape/redact.js +37 -0
  42. package/daemon/dist/tabtape/repro/lang.js +95 -0
  43. package/daemon/dist/tabtape/repro/repro.js +422 -0
  44. package/daemon/dist/tabtape/repro/repro.spec.js +210 -0
  45. package/daemon/dist/tabtape/sink.js +24 -0
  46. package/daemon/dist/tabtape/store/record.js +99 -0
  47. package/daemon/dist/tabtape/store/segment.js +267 -0
  48. package/daemon/dist/tabtape/store/store.js +846 -0
  49. package/daemon/dist/tabtape/store/store.spec.js +485 -0
  50. package/daemon/dist/tabtape/testing/querytest.js +119 -0
  51. package/daemon/dist/tabtape/testing/tmp.js +33 -0
  52. package/package.json +3 -2
  53. package/tabtape/extension/dist/background.js +8 -0
  54. package/tabtape/extension/dist/background.js.map +7 -0
  55. package/tabtape/extension/dist/bridge.js +3 -0
  56. package/tabtape/extension/dist/bridge.js.map +7 -0
  57. package/tabtape/extension/dist/icons/128.png +0 -0
  58. package/tabtape/extension/dist/icons/16.png +0 -0
  59. package/tabtape/extension/dist/icons/32.png +0 -0
  60. package/tabtape/extension/dist/icons/48.png +0 -0
  61. package/tabtape/extension/dist/manifest.json +28 -0
  62. package/tabtape/extension/dist/popup.css +62 -0
  63. package/tabtape/extension/dist/popup.html +34 -0
  64. package/tabtape/extension/dist/popup.js +4 -0
  65. package/tabtape/extension/dist/popup.js.map +7 -0
  66. package/tabtape/extension/dist/recorder.js +133 -0
  67. package/tabtape/extension/dist/recorder.js.map +1 -0
  68. package/tabtape/extension/dist/state.js +2 -0
  69. package/tabtape/extension/dist/state.js.map +1 -0
  70. package/tabtape/playwright/mcp.mjs +58 -0
  71. package/tabtape/playwright/tabtape.d.mts +37 -0
  72. package/tabtape/playwright/tabtape.mjs +146 -0
  73. package/tabtape/ui/dist/app/assets/index-Canv3Ljk.css +1 -0
  74. package/tabtape/ui/dist/app/assets/index-iWNSSrAE.js +145 -0
  75. package/tabtape/ui/dist/app/index.html +14 -0
  76. package/tabtape/ui/dist/export/viewer.css +2 -0
  77. package/tabtape/ui/dist/export/viewer.js +143 -0
  78. package/visual-editor-proxy/dist/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.clock = clock;
4
+ exports.clockShort = clockShort;
5
+ exports.replayLink = replayLink;
6
+ exports.browserLabel = browserLabel;
7
+ exports.splitURL = splitURL;
8
+ exports.origin = origin;
9
+ exports.targetText = targetText;
10
+ exports.targetTag = targetTag;
11
+ exports.fieldName = fieldName;
12
+ exports.oneLine = oneLine;
13
+ exports.clip = clip;
14
+ exports.firstLine = firstLine;
15
+ exports.fmtMs = fmtMs;
16
+ exports.fmtBytes = fmtBytes;
17
+ exports.formatTime = formatTime;
18
+ const pad = (n, w = 2) => String(n).padStart(w, '0');
19
+ /** An offset as mm:ss.mmm (h:mm:ss.mmm past an hour). */
20
+ function clock(ms) {
21
+ const t = Math.max(Math.trunc(ms), 0);
22
+ const s = Math.floor(t / 1000);
23
+ const milli = t % 1000;
24
+ const h = Math.floor(s / 3600);
25
+ const m = Math.floor((s % 3600) / 60);
26
+ if (h > 0)
27
+ return `${h}:${pad(m)}:${pad(s % 60)}.${pad(milli, 3)}`;
28
+ return `${pad(m)}:${pad(s % 60)}.${pad(milli, 3)}`;
29
+ }
30
+ /** An offset as mm:ss (h:mm:ss past an hour). */
31
+ function clockShort(ms) {
32
+ const s = Math.floor(Math.max(Math.trunc(ms), 0) / 1000);
33
+ const h = Math.floor(s / 3600);
34
+ const m = Math.floor((s % 3600) / 60);
35
+ if (h > 0)
36
+ return `${h}:${pad(m)}:${pad(s % 60)}`;
37
+ return `${pad(m)}:${pad(s % 60)}`;
38
+ }
39
+ /** Opens the replay UI at an offset. */
40
+ function replayLink(base, id, offset) {
41
+ return `${base.replace(/\/+$/, '')}/#/s/${id}?t=${Math.max(Math.trunc(offset), 0)}`;
42
+ }
43
+ const RE_EDGE = /Edg\/(\d+)/;
44
+ const RE_OPERA = /OPR\/(\d+)/;
45
+ const RE_CHROME = /Chrome\/(\d+)/;
46
+ /** Turns a user agent into "Chrome 141 · macOS", like the UI. */
47
+ function browserLabel(ua) {
48
+ if (!ua)
49
+ return '';
50
+ let browser = 'Browser';
51
+ let m;
52
+ if ((m = RE_EDGE.exec(ua)))
53
+ browser = `Edge ${m[1]}`;
54
+ else if ((m = RE_OPERA.exec(ua)))
55
+ browser = `Opera ${m[1]}`;
56
+ else if ((m = RE_CHROME.exec(ua)))
57
+ browser = `Chrome ${m[1]}`;
58
+ if (ua.includes('Mac OS X'))
59
+ return `${browser} · macOS`;
60
+ if (ua.includes('Windows'))
61
+ return `${browser} · Windows`;
62
+ if (ua.includes('Linux'))
63
+ return `${browser} · Linux`;
64
+ return browser;
65
+ }
66
+ function parseURL(raw) {
67
+ try {
68
+ return new URL(raw);
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ }
74
+ /** The host and the path with query and fragment; no host means the raw string as the path. */
75
+ function splitURL(raw) {
76
+ const u = parseURL(raw);
77
+ if (!u || !u.host)
78
+ return { host: '', path: raw };
79
+ return { host: u.host, path: u.pathname + u.search + u.hash || '/' };
80
+ }
81
+ /** scheme://host of a URL, or "" if it has none. */
82
+ function origin(raw) {
83
+ const u = parseURL(raw);
84
+ if (!u || !u.host)
85
+ return '';
86
+ return `${u.protocol.replace(/:$/, '')}://${u.host}`;
87
+ }
88
+ /** The visible label of an element, if it has one. */
89
+ function targetText(t) {
90
+ if (!t)
91
+ return '';
92
+ return t.text || t.ariaLabel || t.title || t.alt || t.placeholder || '';
93
+ }
94
+ /** A short tag#id description of an element. */
95
+ function targetTag(t) {
96
+ if (!t)
97
+ return 'element';
98
+ return t.id ? `${t.tag}#${t.id}` : t.tag;
99
+ }
100
+ /** Names a form field: its name, id, label or placeholder. */
101
+ function fieldName(t) {
102
+ if (!t)
103
+ return '';
104
+ return t.name || t.id || t.ariaLabel || t.placeholder || t.testId || '';
105
+ }
106
+ /** Collapses whitespace and clips to n characters. */
107
+ function oneLine(s, n) {
108
+ return clip(s.replace(/[\t\n\f\r ]+/g, ' ').trim(), n);
109
+ }
110
+ /** Shortens s to at most n characters (code points), ending in "…" when cut. */
111
+ function clip(s, n) {
112
+ if (s.length <= n)
113
+ return s; // UTF-16 length >= code points
114
+ const keep = Math.max(n - 1, 0);
115
+ let count = 0;
116
+ let cut = 0;
117
+ let i = 0;
118
+ for (const ch of s) {
119
+ count++;
120
+ i += ch.length;
121
+ if (count === keep)
122
+ cut = i;
123
+ if (count > n)
124
+ return s.slice(0, cut) + '…';
125
+ }
126
+ return s;
127
+ }
128
+ /** The first line of s. */
129
+ function firstLine(s) {
130
+ const i = s.indexOf('\n');
131
+ return i < 0 ? s : s.slice(0, i);
132
+ }
133
+ /** A duration in milliseconds like the UI: "412 ms", "1.24 s". */
134
+ function fmtMs(ms) {
135
+ if (ms < 1000)
136
+ return `${ms.toFixed(0)} ms`;
137
+ if (ms < 10000)
138
+ return `${(ms / 1000).toFixed(2)} s`;
139
+ return `${(ms / 1000).toFixed(1)} s`;
140
+ }
141
+ /** A size like the UI: "812 B", "1.2 KB", "3.4 MB". */
142
+ function fmtBytes(n) {
143
+ if (n < 1024)
144
+ return `${n.toFixed(0)} B`;
145
+ if (n < 1024 * 1024)
146
+ return `${(n / 1024).toFixed(1)} KB`;
147
+ if (n < 1024 * 1024 * 1024)
148
+ return `${(n / 1024 / 1024).toFixed(1)} MB`;
149
+ return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
150
+ }
151
+ const formatters = new Map();
152
+ /** Building a formatter costs far more than using one: keep one per time zone. */
153
+ function formatterFor(timeZone) {
154
+ const key = timeZone ?? '';
155
+ let f = formatters.get(key);
156
+ if (!f) {
157
+ f = new Intl.DateTimeFormat('en-US', {
158
+ timeZone,
159
+ year: 'numeric',
160
+ month: '2-digit',
161
+ day: '2-digit',
162
+ hour: '2-digit',
163
+ minute: '2-digit',
164
+ hourCycle: 'h23',
165
+ });
166
+ formatters.set(key, f);
167
+ }
168
+ return f;
169
+ }
170
+ /** Formats epoch ms in a time zone (default: the machine's) with a pattern of YYYY, MM, DD, HH, mm, MMM, D. */
171
+ function formatTime(ms, pattern, timeZone) {
172
+ const parts = formatterFor(timeZone).formatToParts(new Date(ms));
173
+ const get = (type) => parts.find((p) => p.type === type)?.value ?? '';
174
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
175
+ const month = Number(get('month'));
176
+ return pattern.replace(/YYYY|MMM|MM|DD|HH|mm|D/g, (tok) => {
177
+ switch (tok) {
178
+ case 'YYYY':
179
+ return get('year');
180
+ case 'MMM':
181
+ return months[month - 1] ?? '';
182
+ case 'MM':
183
+ return get('month');
184
+ case 'DD':
185
+ return get('day');
186
+ case 'D':
187
+ return String(Number(get('day')));
188
+ case 'HH':
189
+ return get('hour');
190
+ case 'mm':
191
+ return get('minute');
192
+ }
193
+ return tok;
194
+ });
195
+ }
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.failureReason = failureReason;
4
+ exports.cookieReasons = cookieReasons;
5
+ exports.timingText = timingText;
6
+ exports.framesText = framesText;
7
+ exports.remoteIP = remoteIP;
8
+ /**
9
+ * Network failure texts. The sentences match the replay UI
10
+ * (tabtape/ui/src/lib/network.ts), so a failure reads the same in the Network
11
+ * tab, MCP results and reproduction steps.
12
+ */
13
+ const format_1 = require("./format");
14
+ const CORS_ERRORS = {
15
+ MissingAllowOriginHeader: 'CORS: the response has no Access-Control-Allow-Origin header',
16
+ AllowOriginMismatch: "CORS: Access-Control-Allow-Origin does not match this page's origin",
17
+ MultipleAllowOriginValues: 'CORS: the response has more than one Access-Control-Allow-Origin value',
18
+ InvalidAllowOriginValue: 'CORS: Access-Control-Allow-Origin has an invalid value',
19
+ WildcardOriginNotAllowed: 'CORS: Access-Control-Allow-Origin is * but the request sends credentials',
20
+ InvalidAllowCredentials: 'CORS: Access-Control-Allow-Credentials must be true for a request with credentials',
21
+ PreflightInvalidStatus: 'CORS: the preflight request did not succeed (HTTP status not 2xx)',
22
+ PreflightMissingAllowOriginHeader: 'CORS: the preflight response has no Access-Control-Allow-Origin header',
23
+ PreflightAllowOriginMismatch: "CORS: the preflight's Access-Control-Allow-Origin does not match this page's origin",
24
+ MethodDisallowedByPreflightResponse: 'CORS: the preflight does not allow this method',
25
+ HeaderDisallowedByPreflightResponse: 'CORS: the preflight does not allow this header',
26
+ PreflightDisallowedRedirect: 'CORS: the preflight was redirected, which is not allowed',
27
+ DisallowedByMode: 'CORS: the request mode does not allow cross-origin responses',
28
+ CorsDisabledScheme: 'CORS: cross-origin requests are not supported for this URL scheme',
29
+ };
30
+ const BLOCKED_REASONS = {
31
+ 'mixed-content': 'Blocked: mixed content (an http:// request from an https:// page)',
32
+ csp: "Blocked by the page's Content Security Policy",
33
+ inspector: 'Blocked by DevTools request blocking',
34
+ 'subresource-filter': "Blocked by Chrome's ad filter",
35
+ 'content-type': "Blocked: the response's Content-Type is not allowed here",
36
+ origin: "Blocked: the request's origin is not allowed",
37
+ };
38
+ const own = (m, k) => Object.prototype.hasOwnProperty.call(m, k) ? m[k] : undefined;
39
+ /**
40
+ * Why a request failed, in one sentence: the CORS check or the reason Chrome
41
+ * blocked it (deep mode), else the error text. Empty for requests that did
42
+ * not fail at the transport level.
43
+ */
44
+ function failureReason(n) {
45
+ const c = n.corsErrorStatus;
46
+ if (c && c.corsError) {
47
+ let s = own(CORS_ERRORS, c.corsError) ?? `CORS error: ${c.corsError}`;
48
+ if (c.failedParameter)
49
+ s += ` (${c.failedParameter})`;
50
+ return s;
51
+ }
52
+ const r = n.blockedReason;
53
+ if (r) {
54
+ const s = own(BLOCKED_REASONS, r);
55
+ if (s)
56
+ return s;
57
+ if (r.startsWith('corp-'))
58
+ return "Blocked by the response's Cross-Origin-Resource-Policy header";
59
+ if (r.startsWith('coep-'))
60
+ return 'Blocked by Cross-Origin-Embedder-Policy';
61
+ return `Blocked by Chrome (${r})`;
62
+ }
63
+ return n.error;
64
+ }
65
+ const COOKIE_REASONS = {
66
+ SameSiteNoneInsecure: 'SameSite=None requires Secure',
67
+ SecureOnly: 'Secure cookie on an insecure connection',
68
+ SameSiteLax: 'SameSite=Lax: not sent on cross-site requests',
69
+ SameSiteStrict: 'SameSite=Strict: not sent on cross-site requests',
70
+ SameSiteUnspecifiedTreatedAsLax: 'no SameSite attribute (treated as Lax)',
71
+ DomainMismatch: 'domain does not match',
72
+ NotOnPath: 'path does not match',
73
+ UserPreferences: "blocked by the user's cookie settings",
74
+ ThirdPartyPhaseout: 'third-party cookies are blocked',
75
+ ThirdPartyBlockedInFirstPartySet: 'third-party cookies are blocked',
76
+ SyntaxError: 'invalid Set-Cookie syntax',
77
+ InvalidDomain: 'invalid Domain attribute',
78
+ InvalidPrefix: 'invalid __Secure-/__Host- prefix',
79
+ OverwriteSecure: 'would overwrite a Secure cookie',
80
+ SchemeNotSupported: 'URL scheme does not support cookies',
81
+ };
82
+ /** Chrome's reasons for blocking a cookie, e.g. "SameSite=None requires Secure [SameSiteNoneInsecure]". */
83
+ function cookieReasons(codes) {
84
+ return codes
85
+ .map((c) => {
86
+ const s = own(COOKIE_REASONS, c);
87
+ return s ? `${s} [${c}]` : c;
88
+ })
89
+ .join('; ');
90
+ }
91
+ /** A deep mode request's timing phases, e.g. "DNS 12 ms · connect 30 ms (TLS 18 ms) · wait 120 ms · download 4 ms". */
92
+ function timingText(t) {
93
+ if (!t)
94
+ return '';
95
+ const parts = [];
96
+ const add = (name, v) => {
97
+ if (v !== undefined && v > 0)
98
+ parts.push(`${name} ${(0, format_1.fmtMs)(v)}`);
99
+ };
100
+ add('queued', t.blocked);
101
+ add('DNS', t.dns);
102
+ if (t.connect !== undefined && t.connect > 0) {
103
+ let s = `connect ${(0, format_1.fmtMs)(t.connect)}`;
104
+ if (t.ssl !== undefined && t.ssl > 0)
105
+ s += ` (TLS ${(0, format_1.fmtMs)(t.ssl)})`;
106
+ parts.push(s);
107
+ }
108
+ add('send', t.send);
109
+ add('wait', t.wait);
110
+ add('download', t.receive);
111
+ return parts.join(' · ');
112
+ }
113
+ /** Summarizes a WebSocket connection, e.g. "3↑ 3↓ messages, 1.2 KB". */
114
+ function framesText(f) {
115
+ if (!f)
116
+ return '';
117
+ let s = `${f.sent}↑ ${f.received}↓ messages, ${(0, format_1.fmtBytes)(f.sentBytes + f.receivedBytes)}`;
118
+ if (f.dropped > 0)
119
+ s += `, ${f.dropped} not recorded (over 200/s)`;
120
+ if (f.dataOmitted > 0)
121
+ s += `, ${f.dataOmitted} without data (over 1 MB/s)`;
122
+ return s;
123
+ }
124
+ /** The address part of a remote address ("ip:port", "[v6]:port"). */
125
+ function remoteIP(addr) {
126
+ const v6 = /^\[([^\]]*)\]:\d*$/.exec(addr);
127
+ if (v6)
128
+ return v6[1];
129
+ const i = addr.indexOf(':');
130
+ if (i >= 0 && i === addr.lastIndexOf(':'))
131
+ return addr.slice(0, i);
132
+ return addr.replace(/^[[\]]+|[[\]]+$/g, '');
133
+ }
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.vitalValue = vitalValue;
4
+ exports.vitalText = vitalText;
5
+ exports.baseName = baseName;
6
+ exports.longestScript = longestScript;
7
+ exports.sortedJSON = sortedJSON;
8
+ exports.propsText = propsText;
9
+ /** Web Vitals and long animation frame texts. */
10
+ const format_1 = require("./format");
11
+ /** A metric's value; CLS has no unit. */
12
+ function vitalValue(name, value) {
13
+ return name === 'CLS' ? value.toFixed(2) : (0, format_1.fmtMs)(value);
14
+ }
15
+ /** Describes a report, e.g. "INP 612 ms (poor) on button#slow". */
16
+ function vitalText(p) {
17
+ let s = `${p.name} ${vitalValue(p.name, p.value)} (${p.rating})`;
18
+ if (p.attribution && p.attribution.target)
19
+ s += ` on ${p.attribution.target}`;
20
+ return s;
21
+ }
22
+ /** The path of a URL; relative URLs are taken as they are, without query or fragment. */
23
+ function urlPath(raw) {
24
+ try {
25
+ const u = new URL(raw);
26
+ try {
27
+ return decodeURIComponent(u.pathname);
28
+ }
29
+ catch {
30
+ return u.pathname;
31
+ }
32
+ }
33
+ catch {
34
+ return raw.split(/[?#]/, 1)[0];
35
+ }
36
+ }
37
+ /** Like Go's path.Base. */
38
+ function baseName(p) {
39
+ const trimmed = p.replace(/\/+$/, '');
40
+ if (trimmed === '')
41
+ return '/';
42
+ return trimmed.slice(trimmed.lastIndexOf('/') + 1);
43
+ }
44
+ /** Names the longest script of a long animation frame, e.g. "slow.js handleClick"; empty when the frame has none. */
45
+ function longestScript(p) {
46
+ if (!p.scripts || p.scripts.length === 0)
47
+ return '';
48
+ const s = p.scripts[0];
49
+ let name = s.url;
50
+ const path = s.url ? urlPath(s.url) : '';
51
+ if (path !== '' && path !== '/')
52
+ name = baseName(path);
53
+ const parts = [name, s.fn].filter(Boolean);
54
+ if (parts.length > 0)
55
+ return parts.join(' ');
56
+ return s.invoker;
57
+ }
58
+ /** JSON with object keys sorted, like Go's encoding of maps. */
59
+ function sortedJSON(v) {
60
+ return JSON.stringify(v, (_k, x) => {
61
+ if (x && typeof x === 'object' && !Array.isArray(x)) {
62
+ const o = x;
63
+ return Object.keys(o)
64
+ .sort()
65
+ .reduce((acc, k) => {
66
+ acc[k] = o[k];
67
+ return acc;
68
+ }, {});
69
+ }
70
+ return x;
71
+ });
72
+ }
73
+ function propValue(v) {
74
+ if (typeof v === 'string')
75
+ return (0, format_1.oneLine)(v, 40);
76
+ if (v === null || v === undefined)
77
+ return 'null';
78
+ if (typeof v === 'number' || typeof v === 'boolean')
79
+ return String(v);
80
+ try {
81
+ return (0, format_1.clip)(sortedJSON(v) ?? '?', 40);
82
+ }
83
+ catch {
84
+ return '?';
85
+ }
86
+ }
87
+ /** Event properties as "key: value" pairs, sorted by key and clipped to n characters. */
88
+ function propsText(props, n) {
89
+ if (!props)
90
+ return '';
91
+ const keys = Object.keys(props).sort();
92
+ return (0, format_1.clip)(keys.map((k) => `${k}: ${propValue(props[k])}`).join(', '), n);
93
+ }
@@ -0,0 +1,233 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const log_1 = require("../log");
37
+ const store_1 = require("../store/store");
38
+ const qt = __importStar(require("../testing/querytest"));
39
+ const tmp_1 = require("../testing/tmp");
40
+ const format_1 = require("./format");
41
+ const network_1 = require("./network");
42
+ const perf_1 = require("./perf");
43
+ const reader_1 = require("./reader");
44
+ const types_1 = require("./types");
45
+ const t0 = qt.T0;
46
+ afterEach(async () => {
47
+ await qt.closeTestStores();
48
+ (0, tmp_1.cleanupTempDirs)();
49
+ });
50
+ describe('Reader', () => {
51
+ it('orders lines and finds the replay start', async () => {
52
+ const st = qt.testStore();
53
+ qt.start(st, 's1', t0, 'http://app.test/', 'App');
54
+ // A page view that arrives before the first snapshot, and a network line
55
+ // that arrives out of order (the background posts it later).
56
+ qt.append(st, 's1', qt.pageView(t0 + 5, 'start', 'http://app.test/', 'App'), qt.rrMeta(t0 + 10, 'http://app.test/', 1280, 800), qt.rrFull(t0 + 10), qt.rrInc(t0 + 500), qt.click(t0 + 900, 'http://app.test/', { tag: 'button', selector: 'button#go', text: 'Go' }));
57
+ qt.append(st, 's1', qt.net(t0 + 700, { kind: 'fetch', method: 'GET', url: 'http://app.test/api', status: 500, resBody: '{"error":"x"}' }), qt.consoleRec(t0 + 950, { level: 'error', source: 'uncaught', text: 'Boom' }));
58
+ const v = await new reader_1.Reader(st).view('s1');
59
+ expect([v.replayable, v.start - t0, v.end - t0]).toEqual([true, 10, 500]);
60
+ expect(v.lines.map((l) => l.type).join(',')).toBe('event,rrweb,rrweb,rrweb,network,event,console');
61
+ expect(v.offset(t0 + 5)).toBe(0);
62
+ const n = v.lines[4];
63
+ expect(n.net?.status).toBe(500);
64
+ expect(n.net?.resBody).toBeUndefined(); // bodies are not indexed
65
+ expect((await v.fullNetwork(n)).resBody).toBe('{"error":"x"}');
66
+ expect([v.lines[1].rr, v.lines[2].rr]).toEqual([reader_1.RR_META, reader_1.RR_FULL_SNAPSHOT]);
67
+ expect(n.id.startsWith('n')).toBe(true);
68
+ expect(v.find(n.id)).toBe(n);
69
+ });
70
+ it('is incremental and keeps ids stable', async () => {
71
+ const st = qt.testStore();
72
+ const r = new reader_1.Reader(st);
73
+ qt.append(st, 's1', qt.rrMeta(t0, 'http://a/', 800, 600), qt.rrFull(t0), qt.consoleRec(t0 + 1, { level: 'log', text: 'one' }));
74
+ const v1 = await r.view('s1');
75
+ const id = v1.lines[2].id;
76
+ qt.append(st, 's1', qt.consoleRec(t0 + 2, { level: 'log', text: 'two' }));
77
+ const v2 = await r.view('s1');
78
+ expect(v2.lines.length).toBe(4);
79
+ expect(v2.lines[2].id).toBe(id);
80
+ expect(v2.lines[3].con?.text).toBe('two');
81
+ });
82
+ it('drops evicted segments', async () => {
83
+ const st = store_1.Store.open({ dir: (0, tmp_1.tempDir)(), maxBytes: 4096, segmentBytes: 1024, logger: log_1.silentLogger });
84
+ try {
85
+ const r = new reader_1.Reader(st);
86
+ const big = 'x'.repeat(300);
87
+ for (let i = 0; i < 40; i++) {
88
+ qt.append(st, 's1', qt.consoleRec(t0 + i, { level: 'log', text: big }));
89
+ await r.view('s1');
90
+ }
91
+ const v = await r.view('s1');
92
+ expect(v.session.truncated).toBe(true);
93
+ expect(v.lines.length).toBeGreaterThan(0);
94
+ expect(v.lines.length).toBeLessThan(40);
95
+ for (const l of v.lines)
96
+ await v.readLine(l);
97
+ }
98
+ finally {
99
+ await st.close();
100
+ }
101
+ });
102
+ it('reads compressed segments the same', async () => {
103
+ const st = qt.testStore({ maxBytes: 64 << 20, segmentBytes: 1 << 10, compress: 'gzip' });
104
+ const u = 'http://app.test/';
105
+ qt.start(st, 's1', t0, u, 'App');
106
+ qt.append(st, 's1', qt.rrMeta(t0, u, 1280, 800), qt.rrFull(t0));
107
+ for (let i = 0; i < 20; i++) {
108
+ qt.append(st, 's1', qt.net(t0 + i * 100, { kind: 'fetch', method: 'GET', url: u + 'api', status: 200, resBody: 'y'.repeat(200) + String.fromCharCode(97 + i) }));
109
+ }
110
+ const rd = new reader_1.Reader(st);
111
+ const before = await rd.view('s1'); // indexed while the segments are plain
112
+ st.end('s1', 0);
113
+ await (0, tmp_1.eventually)('segments to be compressed', () => st.snapshot('s1').segments.every((s) => s.path.endsWith('.gz')));
114
+ const after = await rd.view('s1');
115
+ expect(after.lines.length).toBe(before.lines.length);
116
+ // The old view still holds the plain paths; both read every body.
117
+ for (const v of [before, after]) {
118
+ let n = 0;
119
+ for (const l of v.lines) {
120
+ if (!l.net)
121
+ continue;
122
+ const full = await v.fullNetwork(l);
123
+ expect(full.resBody?.endsWith(String.fromCharCode(97 + n))).toBe(true);
124
+ n++;
125
+ }
126
+ let scanned = 0;
127
+ await v.scan(() => {
128
+ scanned++;
129
+ });
130
+ expect([n, scanned]).toEqual([20, v.lines.length]);
131
+ }
132
+ });
133
+ it('indexes WebSocket and state records', async () => {
134
+ const st = qt.testStore();
135
+ qt.append(st, 's1', qt.rrMeta(t0, 'http://app.test/', 1280, 800), qt.rrFull(t0), qt.ws(t0 + 100, 'ws-1', 'send', '{"type":"hello"}'), qt.stateRec(t0 + 200, { store: 'cart', kind: 'action', action: 'cart/add', changes: [{ path: '/cart/items/0', value: { id: 1 }, deleted: false }] }), qt.stateRec(t0 + 300, { store: 'cart', kind: 'snapshot', reason: 'interval', state: { cart: { items: [1] } } }));
136
+ const v = await new reader_1.Reader(st).view('s1');
137
+ const [w, action, snap] = [v.lines[2], v.lines[3], v.lines[4]];
138
+ expect(w.ws?.conn).toBe('ws-1');
139
+ expect(w.ws?.data).toBeUndefined(); // data is not indexed
140
+ expect(w.id[0]).toBe('w');
141
+ expect((await v.fullWS(w)).data).toBe('{"type":"hello"}');
142
+ expect(action.state).toMatchObject({ store: 'cart', action: 'cart/add', changes: 1, paths: ['/cart/items/0'], full: false });
143
+ expect(action.id[0]).toBe('s');
144
+ expect(snap.state).toMatchObject({ full: true, reason: 'interval' });
145
+ const full = await v.fullState(action);
146
+ expect(JSON.stringify(full.changes?.[0].value)).toBe('{"id":1}');
147
+ });
148
+ it('skips the bulk of rrweb lines but still reads them', async () => {
149
+ const st = qt.testStore();
150
+ const big = { ts: t0 + 1, type: 'rrweb', data: { type: 3, data: { source: 0, texts: [{ id: 1, value: 'z'.repeat(1 << 20) }] } } };
151
+ qt.append(st, 's1', qt.rrMeta(t0, 'http://a/', 800, 600), qt.rrFull(t0), big, qt.consoleRec(t0 + 2, { level: 'log', text: 'after' }));
152
+ const v = await new reader_1.Reader(st).view('s1');
153
+ expect(v.lines.map((l) => [l.type, l.rr])).toEqual([
154
+ ['rrweb', 4],
155
+ ['rrweb', 2],
156
+ ['rrweb', 3],
157
+ ['console', -1],
158
+ ]);
159
+ expect((await v.readLine(v.lines[2])).length).toBeGreaterThan(1 << 20);
160
+ expect(v.lines[3].con?.text).toBe('after');
161
+ let sizes = 0;
162
+ await v.scan((_l, raw) => {
163
+ sizes += raw.length;
164
+ });
165
+ expect(sizes).toBe(v.lines.reduce((s, l) => s + l.len, 0));
166
+ });
167
+ });
168
+ describe('format', () => {
169
+ it('formats clocks', () => {
170
+ const cases = [
171
+ [0, '00:00.000'],
172
+ [83400, '01:23.400'],
173
+ [3723004, '1:02:03.004'],
174
+ [-5, '00:00.000'],
175
+ ];
176
+ for (const [ms, want] of cases)
177
+ expect((0, format_1.clock)(ms)).toBe(want);
178
+ });
179
+ it('clips by characters', () => {
180
+ expect((0, format_1.clip)('abc', 3)).toBe('abc');
181
+ expect((0, format_1.clip)('abcd', 3)).toBe('ab…');
182
+ expect((0, format_1.clip)('😀😀😀😀', 3)).toBe('😀😀…');
183
+ expect((0, format_1.oneLine)(' a \n\t b ', 10)).toBe('a b');
184
+ });
185
+ it('formats durations and splits URLs', () => {
186
+ expect([(0, format_1.fmtMs)(412), (0, format_1.fmtMs)(1240), (0, format_1.fmtMs)(12340)]).toEqual(['412 ms', '1.24 s', '12.3 s']);
187
+ expect((0, format_1.splitURL)('https://a.test/x?y=1#z')).toEqual({ host: 'a.test', path: '/x?y=1#z' });
188
+ expect((0, format_1.splitURL)('/relative')).toEqual({ host: '', path: '/relative' });
189
+ });
190
+ });
191
+ describe('network texts', () => {
192
+ const n = (p) => ({ ...(0, types_1.decodeNetwork)({}), ...p });
193
+ const cases = [
194
+ [
195
+ 'cors',
196
+ n({ error: 'net::ERR_FAILED', corsErrorStatus: { corsError: 'MissingAllowOriginHeader', failedParameter: '' } }),
197
+ 'CORS: the response has no Access-Control-Allow-Origin header',
198
+ ],
199
+ [
200
+ 'cors with parameter',
201
+ n({ corsErrorStatus: { corsError: 'AllowOriginMismatch', failedParameter: 'http://other.test' } }),
202
+ "CORS: Access-Control-Allow-Origin does not match this page's origin (http://other.test)",
203
+ ],
204
+ [
205
+ 'unknown cors error',
206
+ n({ corsErrorStatus: { corsError: 'LocalNetworkAccessPermissionDenied', failedParameter: '' } }),
207
+ 'CORS error: LocalNetworkAccessPermissionDenied',
208
+ ],
209
+ ['mixed content', n({ error: 'net::ERR_BLOCKED_BY_CLIENT', blockedReason: 'mixed-content' }), 'Blocked: mixed content (an http:// request from an https:// page)'],
210
+ ['corp', n({ blockedReason: 'corp-not-same-site' }), "Blocked by the response's Cross-Origin-Resource-Policy header"],
211
+ ['other block', n({ blockedReason: 'sri-message-signature-mismatch' }), 'Blocked by Chrome (sri-message-signature-mismatch)'],
212
+ ['plain error', n({ error: 'net::ERR_CONNECTION_REFUSED' }), 'net::ERR_CONNECTION_REFUSED'],
213
+ ['http error', n({ status: 500 }), ''],
214
+ ];
215
+ for (const [name, input, want] of cases) {
216
+ it(`explains failures: ${name}`, () => expect((0, network_1.failureReason)(input)).toBe(want));
217
+ }
218
+ it('describes cookies, timings and addresses', () => {
219
+ expect((0, network_1.cookieReasons)(['SameSiteNoneInsecure', 'NewReason'])).toBe('SameSite=None requires Secure [SameSiteNoneInsecure]; NewReason');
220
+ expect((0, network_1.timingText)({ blocked: 1.5, dns: 12, connect: 30, ssl: 18, send: -1, wait: 120, receive: 4 })).toBe('queued 2 ms · DNS 12 ms · connect 30 ms (TLS 18 ms) · wait 120 ms · download 4 ms');
221
+ expect((0, network_1.remoteIP)('127.0.0.1:7788')).toBe('127.0.0.1');
222
+ expect((0, network_1.remoteIP)('[::1]:443')).toBe('::1');
223
+ expect((0, network_1.remoteIP)('10.0.0.1')).toBe('10.0.0.1');
224
+ });
225
+ });
226
+ describe('perf texts', () => {
227
+ it('names the longest script and renders props', () => {
228
+ const p = { kind: 'loaf', name: '', value: 0, rating: '', id: '', url: '', duration: 600, blockingDuration: 550 };
229
+ expect((0, perf_1.longestScript)({ ...p, scripts: [{ url: 'http://a.test/js/slow.js?v=1', fn: 'handleClick', invoker: '', invokerType: '', duration: 1 }] })).toBe('slow.js handleClick');
230
+ expect((0, perf_1.longestScript)({ ...p, scripts: [{ url: '', fn: '', invoker: 'BUTTON#go.onclick', invokerType: '', duration: 1 }] })).toBe('BUTTON#go.onclick');
231
+ expect((0, perf_1.propsText)({ plan: 'pro', items: 3, nested: { b: 1, a: 2 }, none: null }, 120)).toBe('items: 3, nested: {"a":2,"b":1}, none: null, plan: pro');
232
+ });
233
+ });