libretto 0.5.1 → 0.5.2
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/README.md +7 -3
- package/dist/cli/commands/init.js +4 -21
- package/dist/cli/core/ai-config.js +12 -2
- package/dist/cli/core/browser.js +75 -8
- package/dist/cli/core/session-telemetry.js +429 -172
- package/dist/cli/core/telemetry.js +10 -2
- package/dist/shared/condense-dom/condense-dom.js +11 -56
- package/dist/shared/dom-semantics.d.ts +8 -0
- package/dist/shared/dom-semantics.js +69 -0
- package/dist/shared/run/browser.js +40 -1
- package/dist/shared/visualization/ghost-cursor.js +17 -4
- package/package.json +1 -1
- package/skills/libretto/SKILL.md +52 -38
- package/skills/libretto/references/action-logs.md +101 -0
- package/skills/libretto/references/auth-profiles.md +1 -2
- package/skills/libretto/references/code-generation-rules.md +4 -2
- package/skills/libretto/references/pages-and-page-targeting.md +1 -1
- package/src/cli/commands/init.ts +5 -24
- package/src/cli/core/ai-config.ts +12 -1
- package/src/cli/core/browser.ts +82 -8
- package/src/cli/core/session-telemetry.ts +431 -190
- package/src/cli/core/telemetry.ts +23 -1
- package/src/shared/condense-dom/condense-dom.ts +12 -64
- package/src/shared/dom-semantics.ts +68 -0
- package/src/shared/run/browser.ts +53 -0
- package/src/shared/visualization/ghost-cursor.ts +22 -4
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
filterSemanticClasses,
|
|
3
|
+
INTERACTIVE_ROLE_NAMES,
|
|
4
|
+
INTERACTIVE_TAG_NAMES,
|
|
5
|
+
isObfuscatedClass,
|
|
6
|
+
TEST_ATTRIBUTE_NAMES,
|
|
7
|
+
TRUSTED_ATTRIBUTE_NAMES
|
|
8
|
+
} from "../../shared/dom-semantics.js";
|
|
1
9
|
async function installSessionTelemetry(options) {
|
|
2
10
|
const STATIC_EXT_RE = /\.(css|js|png|jpg|jpeg|gif|woff|woff2|ttf|ico|svg)(\?|$)/i;
|
|
3
11
|
const { context, initialPage, logAction, logNetwork } = options;
|
|
@@ -152,6 +160,426 @@ async function installSessionTelemetry(options) {
|
|
|
152
160
|
}
|
|
153
161
|
return locator;
|
|
154
162
|
};
|
|
163
|
+
const createUserDomTrackingInitScript = () => {
|
|
164
|
+
const selectorAttributeNames = [
|
|
165
|
+
"id",
|
|
166
|
+
...TEST_ATTRIBUTE_NAMES,
|
|
167
|
+
"name",
|
|
168
|
+
"for",
|
|
169
|
+
"role",
|
|
170
|
+
"aria-label",
|
|
171
|
+
"title",
|
|
172
|
+
"placeholder",
|
|
173
|
+
"alt",
|
|
174
|
+
"href",
|
|
175
|
+
"type"
|
|
176
|
+
];
|
|
177
|
+
return `
|
|
178
|
+
(() => {
|
|
179
|
+
if (window.__btDomListenersInstalled) return;
|
|
180
|
+
window.__btDomListenersInstalled = true;
|
|
181
|
+
|
|
182
|
+
const TEST_ATTRS = new Set(${JSON.stringify([...TEST_ATTRIBUTE_NAMES])});
|
|
183
|
+
const TRUSTED_ATTRS = new Set(${JSON.stringify([...TRUSTED_ATTRIBUTE_NAMES])});
|
|
184
|
+
const INTERACTIVE_TAGS = new Set(${JSON.stringify([...INTERACTIVE_TAG_NAMES])});
|
|
185
|
+
const INTERACTIVE_ROLES = new Set(${JSON.stringify([...INTERACTIVE_ROLE_NAMES])});
|
|
186
|
+
const SELECTOR_ATTRS = ${JSON.stringify(selectorAttributeNames)};
|
|
187
|
+
|
|
188
|
+
${filterSemanticClasses.toString()}
|
|
189
|
+
${isObfuscatedClass.toString()}
|
|
190
|
+
|
|
191
|
+
const normalizeWhitespace = (value) =>
|
|
192
|
+
String(value || "").replace(/\\s+/g, " ").trim();
|
|
193
|
+
|
|
194
|
+
const clipText = (value, limit = 120) => {
|
|
195
|
+
const normalized = normalizeWhitespace(value);
|
|
196
|
+
if (!normalized) return "";
|
|
197
|
+
return normalized.length > limit
|
|
198
|
+
? \`\${normalized.slice(0, limit - 1)}\u2026\`
|
|
199
|
+
: normalized;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const cssEscape = (value) => {
|
|
203
|
+
if (globalThis.CSS && typeof globalThis.CSS.escape === "function") {
|
|
204
|
+
return globalThis.CSS.escape(String(value));
|
|
205
|
+
}
|
|
206
|
+
return String(value).replace(/[^a-zA-Z0-9_-]/g, (char) => {
|
|
207
|
+
const hex = char.charCodeAt(0).toString(16);
|
|
208
|
+
return \`\\\\\${hex} \`;
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const quoteAttrValue = (value) =>
|
|
213
|
+
String(value).replace(/\\\\/g, "\\\\\\\\").replace(/"/g, '\\\\"');
|
|
214
|
+
|
|
215
|
+
const isElementNode = (value) => value instanceof Element;
|
|
216
|
+
|
|
217
|
+
const isInteractiveElement = (el) => {
|
|
218
|
+
if (!el || !el.tagName) return false;
|
|
219
|
+
const tagName = el.tagName.toLowerCase();
|
|
220
|
+
if (INTERACTIVE_TAGS.has(tagName)) return true;
|
|
221
|
+
if (el.hasAttribute("tabindex") || el.hasAttribute("contenteditable")) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
const role = normalizeWhitespace(el.getAttribute("role")).toLowerCase();
|
|
225
|
+
return Boolean(role) && INTERACTIVE_ROLES.has(role);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const getFilteredClassSelector = (el) => {
|
|
229
|
+
const className =
|
|
230
|
+
typeof el.className === "string"
|
|
231
|
+
? filterSemanticClasses(el.className)
|
|
232
|
+
: "";
|
|
233
|
+
const classes = className
|
|
234
|
+
.split(/\\s+/)
|
|
235
|
+
.filter(Boolean)
|
|
236
|
+
.slice(0, 2);
|
|
237
|
+
if (classes.length === 0) return "";
|
|
238
|
+
return classes.map((cls) => \`.\${cssEscape(cls)}\`).join("");
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const getMeaningfulAttrValue = (el, attrName) => {
|
|
242
|
+
if (!el.hasAttribute(attrName)) return "";
|
|
243
|
+
const value = clipText(el.getAttribute(attrName) || "", 80);
|
|
244
|
+
if (!value) return "";
|
|
245
|
+
if (attrName === "href" && value.startsWith("javascript:")) return "";
|
|
246
|
+
return value;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const buildSelectorForElement = (el) => {
|
|
250
|
+
if (!isElementNode(el)) return "";
|
|
251
|
+
const tagName = el.tagName.toLowerCase();
|
|
252
|
+
const id = clipText(el.id || "", 80);
|
|
253
|
+
if (id) {
|
|
254
|
+
return \`\${tagName}#\${cssEscape(id)}\`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (const attrName of SELECTOR_ATTRS) {
|
|
258
|
+
if (attrName === "id") continue;
|
|
259
|
+
const value = getMeaningfulAttrValue(el, attrName);
|
|
260
|
+
if (!value) continue;
|
|
261
|
+
if (attrName.startsWith("data-")) {
|
|
262
|
+
return \`\${tagName}[\${attrName}="\${quoteAttrValue(value)}"]\`;
|
|
263
|
+
}
|
|
264
|
+
return \`\${tagName}[\${attrName}="\${quoteAttrValue(value)}"]\`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const classSelector = getFilteredClassSelector(el);
|
|
268
|
+
if (classSelector) {
|
|
269
|
+
return \`\${tagName}\${classSelector}\`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return tagName;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const getElementSummary = (el) => {
|
|
276
|
+
if (!isElementNode(el)) return "";
|
|
277
|
+
const selector = buildSelectorForElement(el);
|
|
278
|
+
const role = clipText(el.getAttribute("role") || "", 40);
|
|
279
|
+
const text = getElementText(el);
|
|
280
|
+
const suffix = [];
|
|
281
|
+
if (role) suffix.push(\`role=\${role}\`);
|
|
282
|
+
if (text) suffix.push(\`text="\${text}"\`);
|
|
283
|
+
return suffix.length > 0 ? \`\${selector} [\${suffix.join(", ")}]\` : selector;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const getElementText = (el) => {
|
|
287
|
+
if (!isElementNode(el)) return "";
|
|
288
|
+
const tagName = el.tagName.toLowerCase();
|
|
289
|
+
if (tagName === "input") {
|
|
290
|
+
const inputType = normalizeWhitespace(el.getAttribute("type")).toLowerCase();
|
|
291
|
+
if (inputType === "password") return "";
|
|
292
|
+
const value = clipText(el.value || el.getAttribute("value") || "", 80);
|
|
293
|
+
if (value) return value;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const attrCandidates = [
|
|
297
|
+
el.getAttribute("aria-label"),
|
|
298
|
+
el.getAttribute("title"),
|
|
299
|
+
el.getAttribute("placeholder"),
|
|
300
|
+
el.getAttribute("alt"),
|
|
301
|
+
el.textContent,
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
for (const candidate of attrCandidates) {
|
|
305
|
+
const text = clipText(candidate || "", 120);
|
|
306
|
+
if (text.length >= 2) return text;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return "";
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const isMeaningfulAncestor = (el) => {
|
|
313
|
+
if (!isElementNode(el)) return false;
|
|
314
|
+
const tagName = el.tagName.toLowerCase();
|
|
315
|
+
if (isInteractiveElement(el)) return true;
|
|
316
|
+
if (
|
|
317
|
+
[
|
|
318
|
+
"tr",
|
|
319
|
+
"li",
|
|
320
|
+
"td",
|
|
321
|
+
"th",
|
|
322
|
+
"label",
|
|
323
|
+
"section",
|
|
324
|
+
"article",
|
|
325
|
+
"dialog",
|
|
326
|
+
"fieldset",
|
|
327
|
+
"summary",
|
|
328
|
+
].includes(tagName)
|
|
329
|
+
) {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
if (el.id) return true;
|
|
333
|
+
for (const attrName of TEST_ATTRS) {
|
|
334
|
+
if (el.hasAttribute(attrName)) return true;
|
|
335
|
+
}
|
|
336
|
+
const role = normalizeWhitespace(el.getAttribute("role")).toLowerCase();
|
|
337
|
+
return Boolean(role);
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const hasStrongSelectorSignal = (el, selector) => {
|
|
341
|
+
if (!isElementNode(el)) return false;
|
|
342
|
+
if (!selector) return false;
|
|
343
|
+
const tagName = el.tagName.toLowerCase();
|
|
344
|
+
if (el.id) return true;
|
|
345
|
+
if (isInteractiveElement(el)) return true;
|
|
346
|
+
if (getFilteredClassSelector(el)) return true;
|
|
347
|
+
for (const attrName of SELECTOR_ATTRS) {
|
|
348
|
+
if (attrName === "id") continue;
|
|
349
|
+
if (getMeaningfulAttrValue(el, attrName)) return true;
|
|
350
|
+
}
|
|
351
|
+
return selector !== tagName;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const getAncestorSelectors = (target) => {
|
|
355
|
+
const selectors = [];
|
|
356
|
+
let current = isElementNode(target) ? target : null;
|
|
357
|
+
let depth = 0;
|
|
358
|
+
while (current && depth < 7) {
|
|
359
|
+
const selector = buildSelectorForElement(current);
|
|
360
|
+
if (
|
|
361
|
+
selector &&
|
|
362
|
+
!selectors.includes(selector) &&
|
|
363
|
+
(
|
|
364
|
+
(depth === 0 && hasStrongSelectorSignal(current, selector)) ||
|
|
365
|
+
isMeaningfulAncestor(current)
|
|
366
|
+
)
|
|
367
|
+
) {
|
|
368
|
+
selectors.push(selector);
|
|
369
|
+
}
|
|
370
|
+
current = current.parentElement;
|
|
371
|
+
depth += 1;
|
|
372
|
+
}
|
|
373
|
+
if (selectors.length === 0 && isElementNode(target)) {
|
|
374
|
+
selectors.push(buildSelectorForElement(target));
|
|
375
|
+
}
|
|
376
|
+
return selectors;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const getNearbyText = (target) => {
|
|
380
|
+
let current = isElementNode(target) ? target : null;
|
|
381
|
+
let depth = 0;
|
|
382
|
+
while (current && depth < 6) {
|
|
383
|
+
const text = getElementText(current);
|
|
384
|
+
if (text.length >= 2) return text;
|
|
385
|
+
current = current.parentElement;
|
|
386
|
+
depth += 1;
|
|
387
|
+
}
|
|
388
|
+
return "";
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const getComposedPathSummary = (event) => {
|
|
392
|
+
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
|
|
393
|
+
return path
|
|
394
|
+
.filter((entry) => isElementNode(entry))
|
|
395
|
+
.slice(0, 6)
|
|
396
|
+
.map((entry) => getElementSummary(entry));
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const getCoordinates = (event) => {
|
|
400
|
+
if (!(event instanceof MouseEvent)) return undefined;
|
|
401
|
+
return {
|
|
402
|
+
x: Math.round(event.clientX),
|
|
403
|
+
y: Math.round(event.clientY),
|
|
404
|
+
};
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const buildActionPayload = (event, action, extra = {}) => {
|
|
408
|
+
const target = isElementNode(event.target) ? event.target : null;
|
|
409
|
+
const ancestorSelectors = getAncestorSelectors(target);
|
|
410
|
+
const bestSemanticSelector = ancestorSelectors[0] || "";
|
|
411
|
+
const targetSelector = target ? buildSelectorForElement(target) : "";
|
|
412
|
+
return {
|
|
413
|
+
action,
|
|
414
|
+
bestSemanticSelector: bestSemanticSelector || undefined,
|
|
415
|
+
targetSelector: targetSelector || undefined,
|
|
416
|
+
ancestorSelectors: ancestorSelectors.length > 0 ? ancestorSelectors : undefined,
|
|
417
|
+
nearbyText: getNearbyText(target) || undefined,
|
|
418
|
+
composedPath: getComposedPathSummary(event),
|
|
419
|
+
coordinates: getCoordinates(event),
|
|
420
|
+
success: true,
|
|
421
|
+
...extra,
|
|
422
|
+
};
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
let clickTimer = null;
|
|
426
|
+
let pendingClick = null;
|
|
427
|
+
|
|
428
|
+
document.addEventListener(
|
|
429
|
+
"click",
|
|
430
|
+
(event) => {
|
|
431
|
+
if (window.__btApiActionInProgress) return;
|
|
432
|
+
const target = isElementNode(event.target) ? event.target : null;
|
|
433
|
+
if (target?.type === "checkbox") {
|
|
434
|
+
window.__btActionLog(
|
|
435
|
+
JSON.stringify(
|
|
436
|
+
buildActionPayload(event, target.checked ? "check" : "uncheck"),
|
|
437
|
+
),
|
|
438
|
+
);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
pendingClick = buildActionPayload(event, "click");
|
|
443
|
+
if (clickTimer) clearTimeout(clickTimer);
|
|
444
|
+
clickTimer = setTimeout(() => {
|
|
445
|
+
if (pendingClick) {
|
|
446
|
+
window.__btActionLog(JSON.stringify(pendingClick));
|
|
447
|
+
}
|
|
448
|
+
pendingClick = null;
|
|
449
|
+
clickTimer = null;
|
|
450
|
+
}, 200);
|
|
451
|
+
},
|
|
452
|
+
true,
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
document.addEventListener(
|
|
456
|
+
"dblclick",
|
|
457
|
+
(event) => {
|
|
458
|
+
if (window.__btApiActionInProgress) return;
|
|
459
|
+
if (clickTimer) {
|
|
460
|
+
clearTimeout(clickTimer);
|
|
461
|
+
clickTimer = null;
|
|
462
|
+
pendingClick = null;
|
|
463
|
+
}
|
|
464
|
+
window.__btActionLog(
|
|
465
|
+
JSON.stringify(buildActionPayload(event, "dblclick")),
|
|
466
|
+
);
|
|
467
|
+
},
|
|
468
|
+
true,
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
const inputTimers = new WeakMap();
|
|
472
|
+
document.addEventListener(
|
|
473
|
+
"input",
|
|
474
|
+
(event) => {
|
|
475
|
+
if (window.__btApiActionInProgress) return;
|
|
476
|
+
const target = isElementNode(event.target) ? event.target : null;
|
|
477
|
+
if (!target) return;
|
|
478
|
+
|
|
479
|
+
if (target.tagName === "SELECT") {
|
|
480
|
+
window.__btActionLog(
|
|
481
|
+
JSON.stringify(
|
|
482
|
+
buildActionPayload(event, "selectOption", {
|
|
483
|
+
value: target.value,
|
|
484
|
+
}),
|
|
485
|
+
),
|
|
486
|
+
);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const existing = inputTimers.get(target);
|
|
491
|
+
if (existing) clearTimeout(existing);
|
|
492
|
+
inputTimers.set(
|
|
493
|
+
target,
|
|
494
|
+
setTimeout(() => {
|
|
495
|
+
inputTimers.delete(target);
|
|
496
|
+
window.__btActionLog(
|
|
497
|
+
JSON.stringify(
|
|
498
|
+
buildActionPayload(event, "fill", {
|
|
499
|
+
value: String(target.value || "").slice(0, 100),
|
|
500
|
+
}),
|
|
501
|
+
),
|
|
502
|
+
);
|
|
503
|
+
}, 500),
|
|
504
|
+
);
|
|
505
|
+
},
|
|
506
|
+
true,
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
const specialKeys = new Set([
|
|
510
|
+
"Enter",
|
|
511
|
+
"Escape",
|
|
512
|
+
"Tab",
|
|
513
|
+
"Backspace",
|
|
514
|
+
"Delete",
|
|
515
|
+
"ArrowUp",
|
|
516
|
+
"ArrowDown",
|
|
517
|
+
"ArrowLeft",
|
|
518
|
+
"ArrowRight",
|
|
519
|
+
"Home",
|
|
520
|
+
"End",
|
|
521
|
+
"PageUp",
|
|
522
|
+
"PageDown",
|
|
523
|
+
"F1",
|
|
524
|
+
"F2",
|
|
525
|
+
"F3",
|
|
526
|
+
"F4",
|
|
527
|
+
"F5",
|
|
528
|
+
"F6",
|
|
529
|
+
"F7",
|
|
530
|
+
"F8",
|
|
531
|
+
"F9",
|
|
532
|
+
"F10",
|
|
533
|
+
"F11",
|
|
534
|
+
"F12",
|
|
535
|
+
]);
|
|
536
|
+
|
|
537
|
+
document.addEventListener(
|
|
538
|
+
"keydown",
|
|
539
|
+
(event) => {
|
|
540
|
+
if (window.__btApiActionInProgress) return;
|
|
541
|
+
const isShortcut = event.ctrlKey || event.metaKey || event.altKey;
|
|
542
|
+
if (!isShortcut && !specialKeys.has(event.key)) return;
|
|
543
|
+
const keyDesc =
|
|
544
|
+
(event.ctrlKey ? "Ctrl+" : "") +
|
|
545
|
+
(event.metaKey ? "Meta+" : "") +
|
|
546
|
+
(event.altKey ? "Alt+" : "") +
|
|
547
|
+
(event.shiftKey ? "Shift+" : "") +
|
|
548
|
+
event.key;
|
|
549
|
+
window.__btActionLog(
|
|
550
|
+
JSON.stringify(
|
|
551
|
+
buildActionPayload(event, "press", {
|
|
552
|
+
value: keyDesc,
|
|
553
|
+
}),
|
|
554
|
+
),
|
|
555
|
+
);
|
|
556
|
+
},
|
|
557
|
+
true,
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
let scrollTimer = null;
|
|
561
|
+
document.addEventListener(
|
|
562
|
+
"scroll",
|
|
563
|
+
() => {
|
|
564
|
+
if (window.__btApiActionInProgress) return;
|
|
565
|
+
if (scrollTimer) clearTimeout(scrollTimer);
|
|
566
|
+
scrollTimer = setTimeout(() => {
|
|
567
|
+
scrollTimer = null;
|
|
568
|
+
window.__btActionLog(
|
|
569
|
+
JSON.stringify({
|
|
570
|
+
action: "scroll",
|
|
571
|
+
bestSemanticSelector: "document",
|
|
572
|
+
success: true,
|
|
573
|
+
value: \`y=\${window.scrollY}\`,
|
|
574
|
+
}),
|
|
575
|
+
);
|
|
576
|
+
}, 300);
|
|
577
|
+
},
|
|
578
|
+
true,
|
|
579
|
+
);
|
|
580
|
+
})();
|
|
581
|
+
`;
|
|
582
|
+
};
|
|
155
583
|
const installUserDomTracking = async (page, pageId) => {
|
|
156
584
|
if (exposedPages.has(page)) return;
|
|
157
585
|
exposedPages.add(page);
|
|
@@ -163,178 +591,7 @@ async function installSessionTelemetry(options) {
|
|
|
163
591
|
...parsed
|
|
164
592
|
});
|
|
165
593
|
});
|
|
166
|
-
await page.addInitScript(()
|
|
167
|
-
if (window.__btDomListenersInstalled) return;
|
|
168
|
-
window.__btDomListenersInstalled = true;
|
|
169
|
-
const identify = (el) => {
|
|
170
|
-
if (!el || !el.tagName) return "";
|
|
171
|
-
const testId = el.getAttribute("data-testid");
|
|
172
|
-
if (testId) return `[data-testid="${testId}"]`;
|
|
173
|
-
const role = el.getAttribute("role") || "";
|
|
174
|
-
const id = el.id;
|
|
175
|
-
if (role && id) return `${role}#${id}`;
|
|
176
|
-
const label = el.getAttribute("aria-label") || (el.textContent || "").trim().slice(0, 30) || "";
|
|
177
|
-
if (role && label) return `${role} "${label}"`;
|
|
178
|
-
const tag = el.tagName.toLowerCase();
|
|
179
|
-
const cls = el.className && typeof el.className === "string" ? "." + el.className.trim().split(/\s+/).slice(0, 2).join(".") : "";
|
|
180
|
-
return `${tag}${cls}`;
|
|
181
|
-
};
|
|
182
|
-
let clickTimer = null;
|
|
183
|
-
let pendingClick = null;
|
|
184
|
-
document.addEventListener(
|
|
185
|
-
"click",
|
|
186
|
-
(event) => {
|
|
187
|
-
if (window.__btApiActionInProgress) return;
|
|
188
|
-
const target = event.target;
|
|
189
|
-
const selector = identify(target);
|
|
190
|
-
if (target?.type === "checkbox") {
|
|
191
|
-
window.__btActionLog(
|
|
192
|
-
JSON.stringify({
|
|
193
|
-
action: target.checked ? "check" : "uncheck",
|
|
194
|
-
selector,
|
|
195
|
-
success: true
|
|
196
|
-
})
|
|
197
|
-
);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
pendingClick = { selector };
|
|
201
|
-
if (clickTimer) clearTimeout(clickTimer);
|
|
202
|
-
clickTimer = setTimeout(() => {
|
|
203
|
-
if (pendingClick) {
|
|
204
|
-
window.__btActionLog(
|
|
205
|
-
JSON.stringify({
|
|
206
|
-
action: "click",
|
|
207
|
-
selector: pendingClick.selector,
|
|
208
|
-
success: true
|
|
209
|
-
})
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
pendingClick = null;
|
|
213
|
-
clickTimer = null;
|
|
214
|
-
}, 200);
|
|
215
|
-
},
|
|
216
|
-
true
|
|
217
|
-
);
|
|
218
|
-
document.addEventListener(
|
|
219
|
-
"dblclick",
|
|
220
|
-
(event) => {
|
|
221
|
-
if (window.__btApiActionInProgress) return;
|
|
222
|
-
if (clickTimer) {
|
|
223
|
-
clearTimeout(clickTimer);
|
|
224
|
-
clickTimer = null;
|
|
225
|
-
pendingClick = null;
|
|
226
|
-
}
|
|
227
|
-
const selector = identify(event.target);
|
|
228
|
-
window.__btActionLog(
|
|
229
|
-
JSON.stringify({ action: "dblclick", selector, success: true })
|
|
230
|
-
);
|
|
231
|
-
},
|
|
232
|
-
true
|
|
233
|
-
);
|
|
234
|
-
const inputTimers = /* @__PURE__ */ new WeakMap();
|
|
235
|
-
document.addEventListener(
|
|
236
|
-
"input",
|
|
237
|
-
(event) => {
|
|
238
|
-
if (window.__btApiActionInProgress) return;
|
|
239
|
-
const target = event.target;
|
|
240
|
-
const selector = identify(target);
|
|
241
|
-
if (target.tagName === "SELECT") {
|
|
242
|
-
window.__btActionLog(
|
|
243
|
-
JSON.stringify({
|
|
244
|
-
action: "selectOption",
|
|
245
|
-
selector,
|
|
246
|
-
value: target.value,
|
|
247
|
-
success: true
|
|
248
|
-
})
|
|
249
|
-
);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
const existing = inputTimers.get(target);
|
|
253
|
-
if (existing) clearTimeout(existing);
|
|
254
|
-
inputTimers.set(
|
|
255
|
-
target,
|
|
256
|
-
setTimeout(() => {
|
|
257
|
-
inputTimers.delete(target);
|
|
258
|
-
window.__btActionLog(
|
|
259
|
-
JSON.stringify({
|
|
260
|
-
action: "fill",
|
|
261
|
-
selector,
|
|
262
|
-
value: (target.value || "").slice(0, 100),
|
|
263
|
-
success: true
|
|
264
|
-
})
|
|
265
|
-
);
|
|
266
|
-
}, 500)
|
|
267
|
-
);
|
|
268
|
-
},
|
|
269
|
-
true
|
|
270
|
-
);
|
|
271
|
-
const specialKeys = /* @__PURE__ */ new Set([
|
|
272
|
-
"Enter",
|
|
273
|
-
"Escape",
|
|
274
|
-
"Tab",
|
|
275
|
-
"Backspace",
|
|
276
|
-
"Delete",
|
|
277
|
-
"ArrowUp",
|
|
278
|
-
"ArrowDown",
|
|
279
|
-
"ArrowLeft",
|
|
280
|
-
"ArrowRight",
|
|
281
|
-
"Home",
|
|
282
|
-
"End",
|
|
283
|
-
"PageUp",
|
|
284
|
-
"PageDown",
|
|
285
|
-
"F1",
|
|
286
|
-
"F2",
|
|
287
|
-
"F3",
|
|
288
|
-
"F4",
|
|
289
|
-
"F5",
|
|
290
|
-
"F6",
|
|
291
|
-
"F7",
|
|
292
|
-
"F8",
|
|
293
|
-
"F9",
|
|
294
|
-
"F10",
|
|
295
|
-
"F11",
|
|
296
|
-
"F12"
|
|
297
|
-
]);
|
|
298
|
-
document.addEventListener(
|
|
299
|
-
"keydown",
|
|
300
|
-
(event) => {
|
|
301
|
-
if (window.__btApiActionInProgress) return;
|
|
302
|
-
const isShortcut = event.ctrlKey || event.metaKey || event.altKey;
|
|
303
|
-
if (!isShortcut && !specialKeys.has(event.key)) return;
|
|
304
|
-
const selector = identify(event.target);
|
|
305
|
-
const keyDesc = (event.ctrlKey ? "Ctrl+" : "") + (event.metaKey ? "Meta+" : "") + (event.altKey ? "Alt+" : "") + (event.shiftKey ? "Shift+" : "") + event.key;
|
|
306
|
-
window.__btActionLog(
|
|
307
|
-
JSON.stringify({
|
|
308
|
-
action: "press",
|
|
309
|
-
selector,
|
|
310
|
-
value: keyDesc,
|
|
311
|
-
success: true
|
|
312
|
-
})
|
|
313
|
-
);
|
|
314
|
-
},
|
|
315
|
-
true
|
|
316
|
-
);
|
|
317
|
-
let scrollTimer = null;
|
|
318
|
-
document.addEventListener(
|
|
319
|
-
"scroll",
|
|
320
|
-
() => {
|
|
321
|
-
if (window.__btApiActionInProgress) return;
|
|
322
|
-
if (scrollTimer) clearTimeout(scrollTimer);
|
|
323
|
-
scrollTimer = setTimeout(() => {
|
|
324
|
-
scrollTimer = null;
|
|
325
|
-
window.__btActionLog(
|
|
326
|
-
JSON.stringify({
|
|
327
|
-
action: "scroll",
|
|
328
|
-
selector: "document",
|
|
329
|
-
value: `y=${window.scrollY}`,
|
|
330
|
-
success: true
|
|
331
|
-
})
|
|
332
|
-
);
|
|
333
|
-
}, 300);
|
|
334
|
-
},
|
|
335
|
-
true
|
|
336
|
-
);
|
|
337
|
-
});
|
|
594
|
+
await page.addInitScript({ content: createUserDomTrackingInitScript() });
|
|
338
595
|
};
|
|
339
596
|
const wrapPageActions = (page, pageId) => {
|
|
340
597
|
if (wrappedPages.has(page)) return;
|
|
@@ -87,7 +87,7 @@ function readActionLog(session, opts = {}) {
|
|
|
87
87
|
if (opts.filter) {
|
|
88
88
|
const re = new RegExp(opts.filter, "i");
|
|
89
89
|
entries = entries.filter(
|
|
90
|
-
(e) => re.test(e.action) || re.test(e.selector || "") || re.test(e.value || "") || re.test(e.url || "")
|
|
90
|
+
(e) => re.test(e.action) || re.test(e.selector || "") || re.test(e.bestSemanticSelector || "") || re.test(e.targetSelector || "") || re.test((e.ancestorSelectors || []).join(" ")) || re.test(e.nearbyText || "") || re.test((e.composedPath || []).join(" ")) || re.test(e.value || "") || re.test(e.url || "")
|
|
91
91
|
);
|
|
92
92
|
}
|
|
93
93
|
if (opts.pageId) {
|
|
@@ -102,8 +102,16 @@ function readActionLog(session, opts = {}) {
|
|
|
102
102
|
function formatActionEntry(e) {
|
|
103
103
|
const time = e.ts.replace(/.*T/, "").replace(/\.\d+Z$/, "");
|
|
104
104
|
const src = e.source.toUpperCase().padEnd(5);
|
|
105
|
+
const displaySelector = e.bestSemanticSelector || e.selector;
|
|
105
106
|
const parts = [`[${time}]`, `[${src}]`, e.action];
|
|
106
|
-
if (
|
|
107
|
+
if (displaySelector) parts.push(displaySelector);
|
|
108
|
+
if (e.targetSelector && e.targetSelector !== displaySelector) {
|
|
109
|
+
parts.push(`target=${e.targetSelector}`);
|
|
110
|
+
}
|
|
111
|
+
if (e.nearbyText) parts.push(`text="${e.nearbyText}"`);
|
|
112
|
+
if (e.coordinates) {
|
|
113
|
+
parts.push(`@(${e.coordinates.x},${e.coordinates.y})`);
|
|
114
|
+
}
|
|
107
115
|
if (e.value) parts.push(`"${e.value}"`);
|
|
108
116
|
if (e.url) parts.push(e.url);
|
|
109
117
|
if (e.duration != null) parts.push(`${e.duration}ms`);
|