repoburg 1.3.255 → 1.3.257

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,422 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateRepro = generateRepro;
4
+ exports.code = code;
5
+ /**
6
+ * Turns a recorded session into markdown reproduction steps for a bug
7
+ * report. The replay UI, the HTTP API and the MCP server all use it, so they
8
+ * produce the same text.
9
+ */
10
+ const errors_1 = require("../errors");
11
+ const format_1 = require("../query/format");
12
+ const network_1 = require("../query/network");
13
+ const perf_1 = require("../query/perf");
14
+ const reader_1 = require("../query/reader");
15
+ const types_1 = require("../query/types");
16
+ const lang_1 = require("./lang");
17
+ const MERGE_CLICKS_WITHIN = 1000; // ms
18
+ const MAX_VALUE = 120; // characters of a typed value
19
+ const MAX_LABEL = 80;
20
+ const MAX_BODY = 200;
21
+ const MAX_CONSOLE = 200;
22
+ const MAX_PROPS = 120;
23
+ /** The shortest long animation frame worth a line. */
24
+ const FREEZE_MS = 500;
25
+ /** Builds the steps for v. Throws EmptyRangeError when to is not after from. */
26
+ async function generateRepro(v, opts) {
27
+ const optFrom = opts.from ?? 0;
28
+ const optTo = opts.to ?? 0;
29
+ const ranged = optFrom > 0 || optTo > 0;
30
+ const from = Math.max(optFrom, 0);
31
+ let to = v.duration();
32
+ if (optTo > 0)
33
+ to = Math.min(optTo, to);
34
+ if (ranged && to <= from && optTo > 0)
35
+ throw new errors_1.EmptyRangeError();
36
+ const t = (0, lang_1.langFor)(opts.lang);
37
+ const g = new Gen(t, (0, format_1.origin)(v.session.url ?? ''));
38
+ let current = ''; // page URL at the time of the line being read
39
+ let startURL = '';
40
+ let userID = '';
41
+ let failureAt = -1;
42
+ const finals = v.finalVitals();
43
+ for (const l of v.lines) {
44
+ g.flush(l.ts, current);
45
+ const off = v.offset(l.ts);
46
+ if (l.ev && l.ev.name === 'identify' && off <= to)
47
+ userID = l.ev.userId;
48
+ if (off < from) {
49
+ if (l.ev && l.ev.name === 'page_view')
50
+ current = l.ev.url;
51
+ continue;
52
+ }
53
+ if (off > to)
54
+ break;
55
+ if (startURL === '')
56
+ startURL = current;
57
+ if (l.ev) {
58
+ if (l.ev.name === 'page_view') {
59
+ if (startURL === '')
60
+ startURL = l.ev.url;
61
+ if (g.samePage(l.ev, current)) {
62
+ current = l.ev.url;
63
+ continue;
64
+ }
65
+ current = l.ev.url;
66
+ }
67
+ g.event(l, l.ev);
68
+ }
69
+ else if (l.net && (0, types_1.failed)(l.net) && !(0, types_1.isAsset)(l.net)) {
70
+ if (failureAt < 0)
71
+ failureAt = off;
72
+ let full;
73
+ try {
74
+ full = await v.fullNetwork(l);
75
+ }
76
+ catch {
77
+ full = l.net; // deleted meanwhile: report without the body
78
+ }
79
+ g.attach(l.ts, current, g.request(full));
80
+ }
81
+ else if (l.con && l.con.level === 'error' && l.con.source !== 'resource') {
82
+ if (failureAt < 0)
83
+ failureAt = off;
84
+ g.attach(l.ts, current, t.consoleErr(code((0, format_1.oneLine)((0, format_1.firstLine)(l.con.text), MAX_CONSOLE))));
85
+ }
86
+ else if (l.perf) {
87
+ g.perf(l, finals);
88
+ }
89
+ }
90
+ g.flush(-1, current);
91
+ if (startURL === '')
92
+ startURL = current;
93
+ if (startURL === '')
94
+ startURL = v.session.url ?? '';
95
+ if (g.site === '')
96
+ g.site = (0, format_1.origin)(startURL);
97
+ let linkAt = 0;
98
+ if (ranged)
99
+ linkAt = from;
100
+ else if (failureAt >= 0)
101
+ linkAt = failureAt;
102
+ const link = (0, format_1.replayLink)(opts.baseURL, v.session.id, linkAt);
103
+ let b = '';
104
+ const env = [(0, format_1.browserLabel)(v.session.userAgent ?? '')];
105
+ const [w, h] = await viewport(v);
106
+ if (w > 0)
107
+ env.push(`${w}×${h}`);
108
+ env.push((0, format_1.formatTime)(v.start + from, 'YYYY-MM-DD HH:mm', opts.timeZone));
109
+ const user = userID || v.session.userId || '';
110
+ if (user !== '')
111
+ env.push(t.user(code((0, format_1.oneLine)(user, MAX_LABEL))));
112
+ b += `**${t.env}:** ${env.filter((s) => s !== '').join(' · ')}\n`;
113
+ b += `**${t.startURL}:** ${startURL}\n\n`;
114
+ if (g.steps.length === 0)
115
+ b += `${t.noSteps}\n`;
116
+ g.steps.forEach((s, i) => {
117
+ b += `${i + 1}. ${g.render(s)}\n`;
118
+ for (const sub of s.subs)
119
+ b += ` - ${sub}\n`;
120
+ });
121
+ b += `\n**${t.expected}:** _${t.fillIn}_\n`;
122
+ b += `**${t.replay}:** ${link}\n`;
123
+ return { markdown: b, steps: g.steps.length, link };
124
+ }
125
+ class Gen {
126
+ constructor(t,
127
+ /** The recorded site's scheme://host; same-origin URLs are shown as paths. */
128
+ site) {
129
+ this.t = t;
130
+ this.site = site;
131
+ this.steps = [];
132
+ /** Perf notes waiting for their moment, by time. */
133
+ this.pending = [];
134
+ }
135
+ /**
136
+ * Notes poor Web Vitals (their final value) and long freezes. A freeze is
137
+ * placed by its end, so that it lands under the click that caused it; a
138
+ * metric by when it was reported, which is after the interaction it
139
+ * measures.
140
+ */
141
+ perf(l, finals) {
142
+ const p = l.perf;
143
+ if (!p)
144
+ return;
145
+ let n;
146
+ if (p.kind === 'vital' && p.rating === 'poor' && finals.has(l)) {
147
+ let target = '';
148
+ if (p.attribution && p.attribution.target !== '')
149
+ target = code((0, format_1.oneLine)(p.attribution.target, MAX_LABEL));
150
+ n = { at: l.ts, text: this.t.poorVital(p.name, (0, perf_1.vitalValue)(p.name, p.value), target) };
151
+ }
152
+ else if (p.kind === 'loaf' && p.duration >= FREEZE_MS) {
153
+ let script = '';
154
+ const s = (0, perf_1.longestScript)(p);
155
+ if (s !== '')
156
+ script = code((0, format_1.oneLine)(s, MAX_LABEL));
157
+ n = { at: l.ts + Math.trunc(p.duration), text: this.t.froze((0, format_1.fmtMs)(p.duration), script) };
158
+ }
159
+ else {
160
+ return;
161
+ }
162
+ // After every note due at the same moment: notes keep their order.
163
+ let i = 0;
164
+ while (i < this.pending.length && this.pending[i].at <= n.at)
165
+ i++;
166
+ this.pending.splice(i, 0, n);
167
+ }
168
+ /** Attaches the notes due at or before ts; ts < 0 flushes all. */
169
+ flush(ts, current) {
170
+ while (this.pending.length > 0 && (ts < 0 || this.pending[0].at <= ts)) {
171
+ const n = this.pending.shift();
172
+ this.attach(n.at, current, n.text);
173
+ }
174
+ }
175
+ /**
176
+ * Whether a page view only repeats the current page: the same URL, or a
177
+ * replaceState that changed nothing but the query string.
178
+ */
179
+ samePage(ev, current) {
180
+ if (ev.url === current)
181
+ return true;
182
+ return ev.kind === 'replace' && stripQuery(ev.url) === stripQuery(current);
183
+ }
184
+ event(l, ev) {
185
+ const last = this.last();
186
+ switch (ev.name) {
187
+ case 'page_view':
188
+ this.push({ kind: 'page', ts: l.ts, lastTS: 0, count: 0, key: '', ev, subs: [] });
189
+ break;
190
+ case 'click': {
191
+ if (isField(ev.target))
192
+ return; // focusing a field; the change that follows says what happened
193
+ const key = targetKey(ev.target);
194
+ if (last && last.kind === 'click' && last.key === key && l.ts - last.lastTS <= MERGE_CLICKS_WITHIN) {
195
+ last.count++;
196
+ last.lastTS = l.ts;
197
+ return;
198
+ }
199
+ this.push({ kind: 'click', ts: l.ts, lastTS: l.ts, count: 1, key, ev, subs: [] });
200
+ break;
201
+ }
202
+ case 'change': {
203
+ const key = targetKey(ev.target);
204
+ if (last && last.kind === 'change' && last.key === key) {
205
+ last.ev = ev; // keep the final value
206
+ return;
207
+ }
208
+ this.push({ kind: 'change', ts: l.ts, lastTS: 0, count: 0, key, ev, subs: [] });
209
+ break;
210
+ }
211
+ case 'submit':
212
+ this.push({ kind: 'submit', ts: l.ts, lastTS: 0, count: 0, key: '', ev, subs: [] });
213
+ break;
214
+ case 'custom': {
215
+ const key = ev.event + '\u0000' + (0, perf_1.propsText)(ev.props, MAX_PROPS);
216
+ if (last && last.kind === 'custom' && last.key === key) {
217
+ last.count++;
218
+ return;
219
+ }
220
+ this.push({ kind: 'custom', ts: l.ts, lastTS: 0, count: 1, key, ev, subs: [] });
221
+ break;
222
+ }
223
+ }
224
+ }
225
+ push(s) {
226
+ this.steps.push(s);
227
+ }
228
+ last() {
229
+ return this.steps.length === 0 ? undefined : this.steps[this.steps.length - 1];
230
+ }
231
+ /** Puts a failure under the step before it, opening one for the current page when nothing happened yet. */
232
+ attach(ts, current, text) {
233
+ if (this.steps.length === 0) {
234
+ const ev = { ...(0, types_1.decodeEvent)({}), name: 'page_view', kind: 'start', url: current };
235
+ this.push({ kind: 'page', ts, lastTS: 0, count: 0, key: '', ev, subs: [] });
236
+ }
237
+ this.last().subs.push(text);
238
+ }
239
+ render(s) {
240
+ const t = this.t;
241
+ const ev = s.ev;
242
+ switch (s.kind) {
243
+ case 'page': {
244
+ const p = code(this.display(ev.url));
245
+ switch (ev.kind) {
246
+ case 'push':
247
+ case 'replace':
248
+ case 'pop':
249
+ case 'hash':
250
+ return t.navigated(p);
251
+ }
252
+ return t.opened(p);
253
+ }
254
+ case 'click': {
255
+ let label = targetLabel(ev.target);
256
+ if ((0, format_1.targetText)(ev.target) === '' && ev.target && ev.target.href !== '') {
257
+ label = t.link(code(this.display(ev.target.href)));
258
+ }
259
+ return t.clicked(label, s.count);
260
+ }
261
+ case 'change': {
262
+ const field = fieldLabel(ev.target);
263
+ const typ = ev.target ? ev.target.type.toLowerCase() : '';
264
+ const value = ev.value ?? '';
265
+ if (ev.masked)
266
+ return t.masked(field);
267
+ if (typ === 'checkbox' || typ === 'radio')
268
+ return value === 'checked' ? t.checked(field) : t.unchecked(field);
269
+ if (typ === 'file')
270
+ return t.file(field, code((0, format_1.oneLine)(value, MAX_VALUE)));
271
+ if (ev.target && ev.target.tag === 'select')
272
+ return t.selected(field, code((0, format_1.oneLine)(value, MAX_VALUE)));
273
+ if (value === '')
274
+ return t.cleared(field);
275
+ return t.typed(field, code((0, format_1.oneLine)(value, MAX_VALUE)));
276
+ }
277
+ case 'submit':
278
+ return t.submitted(fieldLabel(ev.target));
279
+ case 'custom':
280
+ return t.tracked(code((0, format_1.oneLine)(ev.event, MAX_LABEL)), (0, perf_1.propsText)(ev.props, MAX_PROPS), s.count);
281
+ }
282
+ return '';
283
+ }
284
+ request(n) {
285
+ let path = (0, format_1.splitURL)(n.url).path;
286
+ if ((0, format_1.origin)(n.url) !== this.site)
287
+ path = n.url;
288
+ const req = code(n.method + ' ' + path);
289
+ const dur = n.duration !== undefined ? (0, format_1.fmtMs)(n.duration) : '';
290
+ const reason = (0, network_1.failureReason)(n);
291
+ if (reason !== '') {
292
+ // Deep mode knows why: "CORS: the response has no Access-Control-Allow-Origin header".
293
+ return this.t.request(req, this.t.failed, dur, code((0, format_1.oneLine)(reason, MAX_BODY)));
294
+ }
295
+ let body = '';
296
+ if (n.resBody !== undefined && goTrimSpace(n.resBody) !== '')
297
+ body = code((0, format_1.oneLine)(n.resBody, MAX_BODY));
298
+ return this.t.request(req, String(n.status), dur, body);
299
+ }
300
+ /** Shows same-origin URLs as a path. */
301
+ display(u) {
302
+ if (this.site !== '' && (0, format_1.origin)(u) === this.site)
303
+ return (0, format_1.splitURL)(u).path;
304
+ return u;
305
+ }
306
+ }
307
+ function targetLabel(t) {
308
+ const text = (0, format_1.targetText)(t);
309
+ if (text !== '')
310
+ return '"' + (0, format_1.oneLine)(text, MAX_LABEL) + '"';
311
+ return code((0, format_1.targetTag)(t));
312
+ }
313
+ function fieldLabel(t) {
314
+ const name = (0, format_1.fieldName)(t);
315
+ if (name !== '')
316
+ return '"' + (0, format_1.oneLine)(name, MAX_LABEL) + '"';
317
+ return code((0, format_1.targetTag)(t));
318
+ }
319
+ /**
320
+ * Identifies an element across events. The text is not part of it: a
321
+ * counter button's label changes with every click.
322
+ */
323
+ function targetKey(t) {
324
+ if (!t)
325
+ return '';
326
+ if (t.selector !== '')
327
+ return t.selector;
328
+ return t.tag + '\u0000' + t.text;
329
+ }
330
+ /** Whether clicking the element only focuses a form field. */
331
+ function isField(t) {
332
+ if (!t)
333
+ return false;
334
+ switch (t.tag) {
335
+ case 'textarea':
336
+ case 'select':
337
+ return true;
338
+ case 'input':
339
+ switch (t.type.toLowerCase()) {
340
+ case 'button':
341
+ case 'submit':
342
+ case 'reset':
343
+ case 'image':
344
+ case 'checkbox':
345
+ case 'radio':
346
+ case 'file':
347
+ case 'color':
348
+ case 'range':
349
+ return false;
350
+ }
351
+ return true;
352
+ }
353
+ return false;
354
+ }
355
+ /** Go's json.Unmarshal of a number into an int: only integers fit. */
356
+ function intField(v) {
357
+ if (v === undefined || v === null)
358
+ return 0;
359
+ return typeof v === 'number' && Number.isInteger(v) ? v : null;
360
+ }
361
+ /** The window size: from the first page view that has one, else from the first rrweb Meta event. */
362
+ async function viewport(v) {
363
+ for (const l of v.lines) {
364
+ if (l.ev && l.ev.name === 'page_view' && l.ev.viewport && l.ev.viewport.w > 0)
365
+ return [l.ev.viewport.w, l.ev.viewport.h];
366
+ }
367
+ for (const l of v.lines) {
368
+ if (l.rr !== reader_1.RR_META)
369
+ continue;
370
+ try {
371
+ const meta = await v.rrwebData(l);
372
+ if ((0, types_1.isObj)(meta)) {
373
+ const w = intField(meta.width);
374
+ const h = intField(meta.height);
375
+ if (w !== null && h !== null && w > 0)
376
+ return [w, h];
377
+ }
378
+ }
379
+ catch {
380
+ // unreadable: no viewport
381
+ }
382
+ break;
383
+ }
384
+ return [0, 0];
385
+ }
386
+ /** Wraps s in a markdown code span that survives backticks inside it. */
387
+ function code(s) {
388
+ let fence = '`';
389
+ while (s.includes(fence))
390
+ fence += '`';
391
+ if (s.startsWith('`') || s.endsWith('`'))
392
+ return fence + ' ' + s + ' ' + fence;
393
+ return fence + s + fence;
394
+ }
395
+ function stripQuery(u) {
396
+ const i = u.search(/[?#]/);
397
+ return i >= 0 ? u.slice(0, i) : u;
398
+ }
399
+ /** Go's unicode.IsSpace. */
400
+ function isGoSpace(c) {
401
+ return ((c >= 0x09 && c <= 0x0d) ||
402
+ c === 0x20 ||
403
+ c === 0x85 ||
404
+ c === 0xa0 ||
405
+ c === 0x1680 ||
406
+ (c >= 0x2000 && c <= 0x200a) ||
407
+ c === 0x2028 ||
408
+ c === 0x2029 ||
409
+ c === 0x202f ||
410
+ c === 0x205f ||
411
+ c === 0x3000);
412
+ }
413
+ /** Go's strings.TrimSpace: Unicode white space at both ends. */
414
+ function goTrimSpace(s) {
415
+ let i = 0;
416
+ let j = s.length;
417
+ while (i < j && isGoSpace(s.charCodeAt(i)))
418
+ i++;
419
+ while (j > i && isGoSpace(s.charCodeAt(j - 1)))
420
+ j--;
421
+ return s.slice(i, j);
422
+ }
@@ -0,0 +1,210 @@
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 errors_1 = require("../errors");
37
+ const reader_1 = require("../query/reader");
38
+ const qt = __importStar(require("../testing/querytest"));
39
+ const tmp_1 = require("../testing/tmp");
40
+ const repro_1 = require("./repro");
41
+ const t0 = qt.T0; // 2025-10-05 07:47:12 UTC
42
+ const site = 'http://localhost:7788';
43
+ const base = 'http://127.0.0.1:9998/tabtape';
44
+ const email = { tag: 'input', type: 'email', id: 'email', name: 'email', selector: 'input#email' };
45
+ const password = { tag: 'input', type: 'password', id: 'password', name: 'password', selector: 'input#password' };
46
+ const signIn = { tag: 'button', type: 'submit', id: 'login-btn', selector: 'button#login-btn', text: 'Sign in' };
47
+ const form = { tag: 'form', id: 'login-form', selector: 'form#login-form' };
48
+ const broken = { tag: 'button', id: 'broken', selector: 'button#broken', text: 'Broken API' };
49
+ const thrower = { tag: 'button', id: 'throw', selector: 'button#throw', text: 'Throw error' };
50
+ const counter = (n) => ({ tag: 'button', id: 'counter', selector: 'button#counter', text: `Clicked ${n} times` });
51
+ afterEach(async () => {
52
+ await qt.closeTestStores();
53
+ (0, tmp_1.cleanupTempDirs)();
54
+ });
55
+ async function demoView() {
56
+ const st = qt.testStore();
57
+ qt.start(st, 's1', t0, site + '/', 'Acme Shop');
58
+ const u = site + '/';
59
+ qt.append(st, 's1', qt.pageView(t0, 'start', u, 'Acme Shop'), qt.rrMeta(t0 + 10, u, 1440, 900), qt.rrFull(t0 + 10), qt.click(t0 + 1000, u, email), qt.change(t0 + 2000, u, email, 'ce'), qt.change(t0 + 2500, u, email, 'cel'), qt.change(t0 + 3000, u, email, 'celal@example.com'), qt.click(t0 + 3500, u, password), qt.change(t0 + 5000, u, password, '', true), qt.click(t0 + 5200, u, signIn), qt.submit(t0 + 5210, u, form), qt.click(t0 + 6000, u, broken), qt.net(t0 + 6100, {
60
+ kind: 'fetch',
61
+ method: 'GET',
62
+ url: site + '/api/broken',
63
+ status: 500,
64
+ duration: 12.3,
65
+ resBody: '{\n "error": "kaboom",\n "detail": "database connection refused"\n}',
66
+ }), qt.click(t0 + 7000, u, counter(0)), qt.click(t0 + 7100, u, counter(1)), qt.click(t0 + 7200, u, counter(2)), qt.click(t0 + 7300, u, counter(3)), qt.net(t0 + 8000, { kind: 'resource', method: 'GET', url: site + '/missing.png', status: 404 }), qt.consoleRec(t0 + 8000, { level: 'error', source: 'resource', text: 'Failed to load image missing.png' }), qt.click(t0 + 9000, u, thrower), qt.consoleRec(t0 + 9010, { level: 'error', source: 'uncaught', text: 'Boom from test page\n at index.html:88' }), qt.pageView(t0 + 10000, 'push', site + '/products/42', 'Product 42'), qt.pageView(t0 + 11000, 'replace', site + '/products/42?tab=2', 'Product 42'), qt.pageView(t0 + 12000, 'load', site + '/b.html', 'Page B'), qt.rrInc(t0 + 12500));
67
+ return new reader_1.Reader(st).view('s1');
68
+ }
69
+ async function generate(opts) {
70
+ return (0, repro_1.generateRepro)(await demoView(), { ...opts, baseURL: base, timeZone: 'UTC' });
71
+ }
72
+ describe('generateRepro', () => {
73
+ it('describes the whole session', async () => {
74
+ const res = await generate({});
75
+ const want = '**Environment:** Chrome 141 · macOS · 1440×900 · 2025-10-05 07:47\n' +
76
+ '**Start URL:** http://localhost:7788/\n\n' +
77
+ '1. Opened `/`\n' +
78
+ '2. Typed `celal@example.com` into "email"\n' +
79
+ '3. Filled "password" (masked)\n' +
80
+ '4. Clicked "Sign in"\n' +
81
+ '5. Submitted form "login-form"\n' +
82
+ '6. Clicked "Broken API"\n' +
83
+ ' - `GET /api/broken` → **500** in 12 ms: `{ "error": "kaboom", "detail": "database connection refused" }`\n' +
84
+ '7. Clicked "Clicked 0 times" ×4\n' +
85
+ '8. Clicked "Throw error"\n' +
86
+ ' - Console error: `Boom from test page`\n' +
87
+ '9. Navigated to `/products/42`\n' +
88
+ '10. Opened `/b.html`\n\n' +
89
+ '**Expected:** _fill in_\n' +
90
+ `**Replay:** ${base}/#/s/s1?t=6090\n`;
91
+ expect(res.markdown).toBe(want);
92
+ expect(res.steps).toBe(10);
93
+ expect(res.link).toBe(`${base}/#/s/s1?t=6090`);
94
+ });
95
+ it('describes a range in Turkish', async () => {
96
+ const res = await generate({ from: 5000, to: 9500, lang: 'tr' });
97
+ const want = '**Ortam:** Chrome 141 · macOS · 1440×900 · 2025-10-05 07:47\n' +
98
+ "**Başlangıç URL'si:** http://localhost:7788/\n\n" +
99
+ '1. "Sign in" tıklandı\n' +
100
+ '2. "login-form" formu gönderildi\n' +
101
+ '3. "Broken API" tıklandı\n' +
102
+ ' - `GET /api/broken` → **500**, 12 ms: `{ "error": "kaboom", "detail": "database connection refused" }`\n' +
103
+ '4. "Clicked 0 times" ×4 tıklandı\n' +
104
+ '5. "Throw error" tıklandı\n' +
105
+ ' - Console hatası: `Boom from test page`\n\n' +
106
+ '**Beklenen:** _doldurun_\n' +
107
+ `**Replay:** ${base}/#/s/s1?t=5000\n`;
108
+ expect(res.markdown).toBe(want);
109
+ });
110
+ it('rejects an empty range and reports a range without actions', async () => {
111
+ await expect((0, repro_1.generateRepro)(await demoView(), { from: 9000, to: 8000, baseURL: base })).rejects.toBeInstanceOf(errors_1.EmptyRangeError);
112
+ await expect((0, repro_1.generateRepro)(await demoView(), { from: 9000, to: 8000, baseURL: base })).rejects.toThrow('empty range: to must be after from');
113
+ const res = await generate({ from: 12100, to: 12400 });
114
+ expect(res.markdown).toContain('No user actions in this range.');
115
+ expect(res.steps).toBe(0);
116
+ });
117
+ it('fences values that contain backticks', async () => {
118
+ const st = qt.testStore();
119
+ qt.append(st, 's1', qt.rrMeta(t0, 'http://a/', 800, 600), qt.rrFull(t0), qt.change(t0 + 1, 'http://a/', { tag: 'input', name: 'q', selector: 'input' }, 'a`b'));
120
+ const res = await (0, repro_1.generateRepro)(await new reader_1.Reader(st).view('s1'), { baseURL: '' });
121
+ expect(res.markdown).toContain('Typed ``a`b`` into "q"');
122
+ // Without a page view the viewport comes from the rrweb Meta event; without a user agent there is no browser.
123
+ expect(res.markdown).toContain('**Environment:** 800×600 · ');
124
+ expect((0, repro_1.code)('`a')).toBe('`` `a ``');
125
+ });
126
+ it('names links without text by their target', async () => {
127
+ const st = qt.testStore();
128
+ qt.start(st, 's1', t0, site + '/', 'Acme');
129
+ qt.append(st, 's1', qt.rrMeta(t0, site + '/', 800, 600), qt.rrFull(t0), qt.click(t0 + 1, site + '/', { tag: 'a', selector: 'td > a', href: site + '/items/7/edit' }));
130
+ const v = await new reader_1.Reader(st).view('s1');
131
+ const cases = [
132
+ ['en', '1. Clicked link to `/items/7/edit`'],
133
+ ['tr', '1. `/items/7/edit` bağlantısı tıklandı'],
134
+ ];
135
+ for (const [lang, want] of cases) {
136
+ const res = await (0, repro_1.generateRepro)(v, { lang, baseURL: base });
137
+ expect(res.markdown).toContain(want);
138
+ }
139
+ });
140
+ it('reports tracked events, the user and performance', async () => {
141
+ const st = qt.testStore();
142
+ const u = site + '/slow.html';
143
+ const slow = { tag: 'button', id: 'slow', selector: 'button#slow', text: 'Slow button' };
144
+ qt.start(st, 's2', t0, u, 'Slow');
145
+ qt.append(st, 's2', qt.pageView(t0, 'start', u, 'Slow'), qt.rrMeta(t0 + 10, u, 1440, 900), qt.rrFull(t0 + 10), qt.vital(t0 + 300, 'LCP', 4200, 'poor'), qt.vital(t0 + 400, 'LCP', 2000, 'good'), // supersedes the poor value
146
+ qt.identify(t0 + 500, u, 'u_demo'), qt.track(t0 + 1000, u, 'demo_clicked', { n: 1 }), qt.track(t0 + 1100, u, 'demo_clicked', { n: 1 }), qt.loaf(t0 + 1990, 612, { url: site + '/slow.js', fn: 'handleClick', duration: 600 }), qt.click(t0 + 2000, u, slow), qt.loaf(t0 + 2100, 120), // too short to mention
147
+ qt.vital(t0 + 2700, 'INP', 240, 'needs-improvement'), qt.vital(t0 + 2800, 'INP', 640, 'poor', { target: 'button#slow' }), qt.rrInc(t0 + 3000));
148
+ const v = await new reader_1.Reader(st).view('s2');
149
+ const gen = async (lang) => (await (0, repro_1.generateRepro)(v, { lang, baseURL: base, timeZone: 'UTC' })).markdown;
150
+ const want = '**Environment:** Chrome 141 · macOS · 1440×900 · 2025-10-05 07:47 · user `u_demo`\n' +
151
+ '**Start URL:** http://localhost:7788/slow.html\n\n' +
152
+ '1. Opened `/slow.html`\n' +
153
+ '2. Event `demo_clicked` (n: 1) ×2\n' +
154
+ '3. Clicked "Slow button"\n' +
155
+ ' - Page froze for 612 ms (`slow.js handleClick`)\n' +
156
+ ' - Poor INP: 640 ms on `button#slow`\n\n' +
157
+ '**Expected:** _fill in_\n' +
158
+ `**Replay:** ${base}/#/s/s2?t=0\n`;
159
+ expect(await gen('en')).toBe(want);
160
+ const got = await gen('tr');
161
+ for (const s of [
162
+ 'kullanıcı `u_demo`',
163
+ '2. `demo_clicked` olayı (n: 1) ×2',
164
+ ' - Sayfa 612 ms dondu (`slow.js handleClick`)',
165
+ ' - Kötü INP: 640 ms, `button#slow`',
166
+ ]) {
167
+ expect(got).toContain(s);
168
+ }
169
+ });
170
+ it('explains deep mode failures', async () => {
171
+ const st = qt.testStore();
172
+ const u = site + '/cors.html';
173
+ const btn = { tag: 'button', id: 'cross', selector: 'button#cross', text: 'Cross-origin request' };
174
+ qt.start(st, 's3', t0, u, 'CORS');
175
+ qt.append(st, 's3', qt.pageView(t0, 'start', u, 'CORS'), qt.rrMeta(t0 + 10, u, 1440, 900), qt.rrFull(t0 + 10), qt.click(t0 + 1000, u, btn), qt.net(t0 + 1010, {
176
+ kind: 'fetch',
177
+ method: 'GET',
178
+ url: 'http://127.0.0.1:7789/api/cors',
179
+ source: 'cdp',
180
+ error: 'net::ERR_FAILED',
181
+ corsErrorStatus: { corsError: 'MissingAllowOriginHeader', failedParameter: '' },
182
+ duration: 4,
183
+ }), qt.rrInc(t0 + 2000));
184
+ const res = await (0, repro_1.generateRepro)(await new reader_1.Reader(st).view('s3'), { baseURL: base, timeZone: 'UTC' });
185
+ expect(res.markdown).toContain('`CORS: the response has no Access-Control-Allow-Origin header`');
186
+ // A cross-origin request keeps its full URL.
187
+ expect(res.markdown).toContain('`GET http://127.0.0.1:7789/api/cors` → **failed** in 4 ms');
188
+ });
189
+ it('opens a step for failures before any action and handles form controls', async () => {
190
+ const st = qt.testStore();
191
+ const u = site + '/form.html';
192
+ qt.start(st, 's4', t0, u, 'Form');
193
+ qt.append(st, 's4', qt.rrMeta(t0, u, 800, 600), qt.rrFull(t0), qt.pageView(t0 + 10, 'load', u, 'Form'), // before the range: only sets the current page
194
+ qt.consoleRec(t0 + 100, { level: 'error', source: 'console', text: 'early failure' }), qt.change(t0 + 200, u, { tag: 'input', type: 'checkbox', name: 'terms', selector: 'input#terms' }, 'checked'), qt.change(t0 + 300, u, { tag: 'select', name: 'plan', selector: 'select#plan' }, 'pro'), qt.change(t0 + 400, u, { tag: 'input', type: 'text', name: 'nick', selector: 'input#nick' }, ''), qt.change(t0 + 500, u, { tag: 'input', type: 'radio', name: 'size', selector: 'input#size' }, ''), qt.change(t0 + 600, u, { tag: 'input', type: 'file', name: 'avatar', selector: 'input#avatar' }, 'me.png'), qt.rrInc(t0 + 1000));
195
+ const res = await (0, repro_1.generateRepro)(await new reader_1.Reader(st).view('s4'), { from: 50, baseURL: base, timeZone: 'UTC' });
196
+ expect(res.markdown).toContain('**Start URL:** http://localhost:7788/form.html\n\n' +
197
+ '1. Opened `/form.html`\n' +
198
+ ' - Console error: `early failure`\n' +
199
+ '2. Checked "terms"\n' +
200
+ '3. Selected `pro` in "plan"\n' +
201
+ '4. Cleared "nick"\n' +
202
+ '5. Unchecked "size"\n' +
203
+ '6. Chose file `me.png` in "avatar"\n');
204
+ // A range opens the replay at its start.
205
+ expect(res.link).toBe(`${base}/#/s/s4?t=50`);
206
+ // Without a range, the first failure does.
207
+ const whole = await (0, repro_1.generateRepro)(await new reader_1.Reader(st).view('s4'), { baseURL: base, timeZone: 'UTC' });
208
+ expect(whole.link).toBe(`${base}/#/s/s4?t=100`);
209
+ });
210
+ });
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.collect = collect;
4
+ exports.counter = counter;
5
+ /** Collects everything written; for tests and small outputs. */
6
+ function collect() {
7
+ const parts = [];
8
+ return {
9
+ sink: (chunk) => {
10
+ parts.push(Buffer.from(chunk));
11
+ },
12
+ bytes: () => Buffer.concat(parts),
13
+ };
14
+ }
15
+ /** Counts bytes and drops them (export size estimates). */
16
+ function counter() {
17
+ let n = 0;
18
+ return {
19
+ sink: (chunk) => {
20
+ n += chunk.length;
21
+ },
22
+ count: () => n,
23
+ };
24
+ }