libretto 0.5.0 → 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 +109 -35
- package/dist/cli/cli.js +22 -97
- package/dist/cli/commands/browser.js +86 -59
- package/dist/cli/commands/execution.js +199 -86
- package/dist/cli/commands/init.js +34 -29
- package/dist/cli/commands/logs.js +4 -5
- package/dist/cli/commands/shared.js +30 -29
- package/dist/cli/commands/snapshot.js +26 -39
- package/dist/cli/core/ai-config.js +21 -4
- package/dist/cli/core/api-snapshot-analyzer.js +15 -5
- package/dist/cli/core/browser.js +207 -37
- package/dist/cli/core/context.js +4 -1
- package/dist/cli/core/session-telemetry.js +434 -174
- package/dist/cli/core/session.js +21 -8
- package/dist/cli/core/snapshot-analyzer.js +14 -31
- package/dist/cli/core/snapshot-api-config.js +2 -6
- package/dist/cli/core/telemetry.js +20 -4
- package/dist/cli/framework/simple-cli.js +45 -25
- package/dist/cli/router.js +14 -21
- package/dist/cli/workers/run-integration-runtime.js +24 -5
- package/dist/cli/workers/run-integration-worker-protocol.js +3 -1
- package/dist/cli/workers/run-integration-worker.js +1 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.js +7 -10
- package/dist/runtime/download/download.js +5 -1
- package/dist/runtime/extract/extract.js +11 -2
- package/dist/runtime/network/network.js +8 -1
- package/dist/runtime/recovery/agent.js +6 -2
- package/dist/runtime/recovery/errors.js +3 -1
- package/dist/runtime/recovery/recovery.js +3 -1
- package/dist/shared/condense-dom/condense-dom.js +17 -69
- package/dist/shared/config/config.d.ts +1 -9
- package/dist/shared/config/config.js +0 -18
- package/dist/shared/config/index.d.ts +2 -1
- package/dist/shared/config/index.js +0 -10
- package/dist/shared/debug/pause.js +9 -3
- package/dist/shared/dom-semantics.d.ts +8 -0
- package/dist/shared/dom-semantics.js +69 -0
- package/dist/shared/instrumentation/instrument.js +101 -5
- package/dist/shared/llm/ai-sdk-adapter.js +3 -1
- package/dist/shared/llm/client.js +3 -1
- package/dist/shared/logger/index.js +4 -1
- package/dist/shared/run/api.js +3 -1
- package/dist/shared/run/browser.js +47 -3
- package/dist/shared/state/session-state.d.ts +2 -1
- package/dist/shared/state/session-state.js +5 -2
- package/dist/shared/visualization/ghost-cursor.js +36 -14
- package/dist/shared/visualization/highlight.js +9 -6
- package/dist/shared/workflow/workflow.d.ts +4 -5
- package/dist/shared/workflow/workflow.js +3 -5
- package/package.json +6 -2
- package/scripts/check-skills-sync.mjs +25 -0
- package/scripts/compare-eval-summary.mjs +47 -0
- package/scripts/postinstall.mjs +15 -15
- package/scripts/prepare-release.sh +97 -0
- package/scripts/skills-libretto.mjs +103 -0
- package/scripts/summarize-evals.mjs +135 -0
- package/scripts/sync-skills.mjs +12 -0
- package/skills/libretto/SKILL.md +132 -54
- 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 +210 -0
- package/skills/libretto/references/configuration-file-reference.md +53 -0
- package/skills/libretto/references/pages-and-page-targeting.md +1 -1
- package/skills/libretto/references/site-security-review.md +143 -0
- package/src/cli/cli.ts +23 -110
- package/src/cli/commands/browser.ts +94 -70
- package/src/cli/commands/execution.ts +233 -102
- package/src/cli/commands/init.ts +37 -33
- package/src/cli/commands/logs.ts +7 -7
- package/src/cli/commands/shared.ts +36 -37
- package/src/cli/commands/snapshot.ts +44 -59
- package/src/cli/core/ai-config.ts +24 -4
- package/src/cli/core/api-snapshot-analyzer.ts +17 -6
- package/src/cli/core/browser.ts +260 -49
- package/src/cli/core/context.ts +7 -2
- package/src/cli/core/session-telemetry.ts +449 -197
- package/src/cli/core/session.ts +21 -7
- package/src/cli/core/snapshot-analyzer.ts +26 -46
- package/src/cli/core/snapshot-api-config.ts +170 -175
- package/src/cli/core/telemetry.ts +39 -4
- package/src/cli/framework/simple-cli.ts +144 -77
- package/src/cli/router.ts +13 -21
- package/src/cli/workers/run-integration-runtime.ts +36 -9
- package/src/cli/workers/run-integration-worker-protocol.ts +2 -0
- package/src/cli/workers/run-integration-worker.ts +1 -4
- package/src/index.ts +73 -66
- package/src/runtime/download/download.ts +62 -58
- package/src/runtime/download/index.ts +5 -5
- package/src/runtime/extract/extract.ts +71 -61
- package/src/runtime/network/index.ts +3 -3
- package/src/runtime/network/network.ts +99 -93
- package/src/runtime/recovery/agent.ts +217 -212
- package/src/runtime/recovery/errors.ts +107 -104
- package/src/runtime/recovery/index.ts +3 -3
- package/src/runtime/recovery/recovery.ts +38 -35
- package/src/shared/condense-dom/condense-dom.ts +27 -82
- package/src/shared/config/config.ts +0 -19
- package/src/shared/config/index.ts +0 -5
- package/src/shared/debug/pause.ts +57 -51
- package/src/shared/dom-semantics.ts +68 -0
- package/src/shared/instrumentation/errors.ts +64 -62
- package/src/shared/instrumentation/index.ts +5 -5
- package/src/shared/instrumentation/instrument.ts +339 -209
- package/src/shared/llm/ai-sdk-adapter.ts +58 -55
- package/src/shared/llm/client.ts +181 -174
- package/src/shared/llm/types.ts +39 -39
- package/src/shared/logger/index.ts +11 -4
- package/src/shared/logger/logger.ts +312 -306
- package/src/shared/logger/sinks.ts +118 -114
- package/src/shared/paths/paths.ts +50 -49
- package/src/shared/paths/repo-root.ts +17 -17
- package/src/shared/run/api.ts +5 -1
- package/src/shared/run/browser.ts +65 -3
- package/src/shared/state/index.ts +9 -9
- package/src/shared/state/session-state.ts +46 -43
- package/src/shared/visualization/ghost-cursor.ts +180 -149
- package/src/shared/visualization/highlight.ts +89 -86
- package/src/shared/visualization/index.ts +13 -13
- package/src/shared/workflow/workflow.ts +19 -25
- package/skills/libretto/references/reverse-engineering-network-requests.md +0 -39
- package/skills/libretto/references/user-action-log.md +0 -31
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { BrowserContext, Page } from "playwright";
|
|
2
|
+
import {
|
|
3
|
+
filterSemanticClasses,
|
|
4
|
+
INTERACTIVE_ROLE_NAMES,
|
|
5
|
+
INTERACTIVE_TAG_NAMES,
|
|
6
|
+
isObfuscatedClass,
|
|
7
|
+
TEST_ATTRIBUTE_NAMES,
|
|
8
|
+
TRUSTED_ATTRIBUTE_NAMES,
|
|
9
|
+
} from "../../shared/dom-semantics.js";
|
|
2
10
|
|
|
3
11
|
type TelemetryEntry = Record<string, unknown>;
|
|
4
12
|
|
|
@@ -13,7 +21,8 @@ type InstallSessionTelemetryOptions = {
|
|
|
13
21
|
export async function installSessionTelemetry(
|
|
14
22
|
options: InstallSessionTelemetryOptions,
|
|
15
23
|
): Promise<void> {
|
|
16
|
-
const STATIC_EXT_RE =
|
|
24
|
+
const STATIC_EXT_RE =
|
|
25
|
+
/\.(css|js|png|jpg|jpeg|gif|woff|woff2|ttf|ico|svg)(\?|$)/i;
|
|
17
26
|
const { context, initialPage, logAction, logNetwork } = options;
|
|
18
27
|
const includeUserDomActions = options.includeUserDomActions ?? false;
|
|
19
28
|
const pageIdCache = new WeakMap<Page, string>();
|
|
@@ -25,10 +34,12 @@ export async function installSessionTelemetry(
|
|
|
25
34
|
const cdpSession = await context.newCDPSession(page);
|
|
26
35
|
try {
|
|
27
36
|
const targetInfo = await cdpSession.send("Target.getTargetInfo");
|
|
28
|
-
const targetId = (targetInfo as { targetInfo?: { targetId?: unknown } })
|
|
29
|
-
?.targetId;
|
|
37
|
+
const targetId = (targetInfo as { targetInfo?: { targetId?: unknown } })
|
|
38
|
+
?.targetInfo?.targetId;
|
|
30
39
|
if (typeof targetId !== "string" || targetId.length === 0) {
|
|
31
|
-
throw new Error(
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Could not resolve target id for page at URL "${page.url()}".`,
|
|
42
|
+
);
|
|
32
43
|
}
|
|
33
44
|
pageIdCache.set(page, targetId);
|
|
34
45
|
return targetId;
|
|
@@ -51,7 +62,10 @@ export async function installSessionTelemetry(
|
|
|
51
62
|
});
|
|
52
63
|
};
|
|
53
64
|
|
|
54
|
-
const markApiActionInProgress = async (
|
|
65
|
+
const markApiActionInProgress = async (
|
|
66
|
+
page: Page,
|
|
67
|
+
inProgress: boolean,
|
|
68
|
+
): Promise<void> => {
|
|
55
69
|
await page.evaluate((flag) => {
|
|
56
70
|
(window as any).__btApiActionInProgress = flag;
|
|
57
71
|
}, inProgress);
|
|
@@ -180,209 +194,445 @@ export async function installSessionTelemetry(
|
|
|
180
194
|
return locator;
|
|
181
195
|
};
|
|
182
196
|
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
197
|
+
const createUserDomTrackingInitScript = (): string => {
|
|
198
|
+
const selectorAttributeNames = [
|
|
199
|
+
"id",
|
|
200
|
+
...TEST_ATTRIBUTE_NAMES,
|
|
201
|
+
"name",
|
|
202
|
+
"for",
|
|
203
|
+
"role",
|
|
204
|
+
"aria-label",
|
|
205
|
+
"title",
|
|
206
|
+
"placeholder",
|
|
207
|
+
"alt",
|
|
208
|
+
"href",
|
|
209
|
+
"type",
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
return `
|
|
213
|
+
(() => {
|
|
214
|
+
if (window.__btDomListenersInstalled) return;
|
|
215
|
+
window.__btDomListenersInstalled = true;
|
|
216
|
+
|
|
217
|
+
const TEST_ATTRS = new Set(${JSON.stringify([...TEST_ATTRIBUTE_NAMES])});
|
|
218
|
+
const TRUSTED_ATTRS = new Set(${JSON.stringify([...TRUSTED_ATTRIBUTE_NAMES])});
|
|
219
|
+
const INTERACTIVE_TAGS = new Set(${JSON.stringify([...INTERACTIVE_TAG_NAMES])});
|
|
220
|
+
const INTERACTIVE_ROLES = new Set(${JSON.stringify([...INTERACTIVE_ROLE_NAMES])});
|
|
221
|
+
const SELECTOR_ATTRS = ${JSON.stringify(selectorAttributeNames)};
|
|
222
|
+
|
|
223
|
+
${filterSemanticClasses.toString()}
|
|
224
|
+
${isObfuscatedClass.toString()}
|
|
225
|
+
|
|
226
|
+
const normalizeWhitespace = (value) =>
|
|
227
|
+
String(value || "").replace(/\\s+/g, " ").trim();
|
|
228
|
+
|
|
229
|
+
const clipText = (value, limit = 120) => {
|
|
230
|
+
const normalized = normalizeWhitespace(value);
|
|
231
|
+
if (!normalized) return "";
|
|
232
|
+
return normalized.length > limit
|
|
233
|
+
? \`\${normalized.slice(0, limit - 1)}…\`
|
|
234
|
+
: normalized;
|
|
235
|
+
};
|
|
186
236
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
237
|
+
const cssEscape = (value) => {
|
|
238
|
+
if (globalThis.CSS && typeof globalThis.CSS.escape === "function") {
|
|
239
|
+
return globalThis.CSS.escape(String(value));
|
|
240
|
+
}
|
|
241
|
+
return String(value).replace(/[^a-zA-Z0-9_-]/g, (char) => {
|
|
242
|
+
const hex = char.charCodeAt(0).toString(16);
|
|
243
|
+
return \`\\\\\${hex} \`;
|
|
194
244
|
});
|
|
245
|
+
};
|
|
195
246
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
(window as any).__btDomListenersInstalled = true;
|
|
199
|
-
|
|
200
|
-
const identify = (el: any): string => {
|
|
201
|
-
if (!el || !el.tagName) return "";
|
|
202
|
-
const testId = el.getAttribute("data-testid");
|
|
203
|
-
if (testId) return `[data-testid="${testId}"]`;
|
|
204
|
-
const role = el.getAttribute("role") || "";
|
|
205
|
-
const id = el.id;
|
|
206
|
-
if (role && id) return `${role}#${id}`;
|
|
207
|
-
const label =
|
|
208
|
-
el.getAttribute("aria-label") ||
|
|
209
|
-
(el.textContent || "").trim().slice(0, 30) ||
|
|
210
|
-
"";
|
|
211
|
-
if (role && label) return `${role} "${label}"`;
|
|
212
|
-
const tag = el.tagName.toLowerCase();
|
|
213
|
-
const cls =
|
|
214
|
-
el.className && typeof el.className === "string"
|
|
215
|
-
? "." + el.className.trim().split(/\s+/).slice(0, 2).join(".")
|
|
216
|
-
: "";
|
|
217
|
-
return `${tag}${cls}`;
|
|
218
|
-
};
|
|
247
|
+
const quoteAttrValue = (value) =>
|
|
248
|
+
String(value).replace(/\\\\/g, "\\\\\\\\").replace(/"/g, '\\\\"');
|
|
219
249
|
|
|
220
|
-
|
|
221
|
-
let pendingClick: { selector: string } | null = null;
|
|
222
|
-
|
|
223
|
-
document.addEventListener(
|
|
224
|
-
"click",
|
|
225
|
-
(event) => {
|
|
226
|
-
if ((window as any).__btApiActionInProgress) return;
|
|
227
|
-
const target = event.target as any;
|
|
228
|
-
const selector = identify(target);
|
|
229
|
-
if (target?.type === "checkbox") {
|
|
230
|
-
(window as any).__btActionLog(
|
|
231
|
-
JSON.stringify({
|
|
232
|
-
action: target.checked ? "check" : "uncheck",
|
|
233
|
-
selector,
|
|
234
|
-
success: true,
|
|
235
|
-
}),
|
|
236
|
-
);
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
pendingClick = { selector };
|
|
240
|
-
if (clickTimer) clearTimeout(clickTimer);
|
|
241
|
-
clickTimer = setTimeout(() => {
|
|
242
|
-
if (pendingClick) {
|
|
243
|
-
(window as any).__btActionLog(
|
|
244
|
-
JSON.stringify({
|
|
245
|
-
action: "click",
|
|
246
|
-
selector: pendingClick.selector,
|
|
247
|
-
success: true,
|
|
248
|
-
}),
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
pendingClick = null;
|
|
252
|
-
clickTimer = null;
|
|
253
|
-
}, 200);
|
|
254
|
-
},
|
|
255
|
-
true,
|
|
256
|
-
);
|
|
250
|
+
const isElementNode = (value) => value instanceof Element;
|
|
257
251
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
252
|
+
const isInteractiveElement = (el) => {
|
|
253
|
+
if (!el || !el.tagName) return false;
|
|
254
|
+
const tagName = el.tagName.toLowerCase();
|
|
255
|
+
if (INTERACTIVE_TAGS.has(tagName)) return true;
|
|
256
|
+
if (el.hasAttribute("tabindex") || el.hasAttribute("contenteditable")) {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
const role = normalizeWhitespace(el.getAttribute("role")).toLowerCase();
|
|
260
|
+
return Boolean(role) && INTERACTIVE_ROLES.has(role);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const getFilteredClassSelector = (el) => {
|
|
264
|
+
const className =
|
|
265
|
+
typeof el.className === "string"
|
|
266
|
+
? filterSemanticClasses(el.className)
|
|
267
|
+
: "";
|
|
268
|
+
const classes = className
|
|
269
|
+
.split(/\\s+/)
|
|
270
|
+
.filter(Boolean)
|
|
271
|
+
.slice(0, 2);
|
|
272
|
+
if (classes.length === 0) return "";
|
|
273
|
+
return classes.map((cls) => \`.\${cssEscape(cls)}\`).join("");
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const getMeaningfulAttrValue = (el, attrName) => {
|
|
277
|
+
if (!el.hasAttribute(attrName)) return "";
|
|
278
|
+
const value = clipText(el.getAttribute(attrName) || "", 80);
|
|
279
|
+
if (!value) return "";
|
|
280
|
+
if (attrName === "href" && value.startsWith("javascript:")) return "";
|
|
281
|
+
return value;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const buildSelectorForElement = (el) => {
|
|
285
|
+
if (!isElementNode(el)) return "";
|
|
286
|
+
const tagName = el.tagName.toLowerCase();
|
|
287
|
+
const id = clipText(el.id || "", 80);
|
|
288
|
+
if (id) {
|
|
289
|
+
return \`\${tagName}#\${cssEscape(id)}\`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
for (const attrName of SELECTOR_ATTRS) {
|
|
293
|
+
if (attrName === "id") continue;
|
|
294
|
+
const value = getMeaningfulAttrValue(el, attrName);
|
|
295
|
+
if (!value) continue;
|
|
296
|
+
if (attrName.startsWith("data-")) {
|
|
297
|
+
return \`\${tagName}[\${attrName}="\${quoteAttrValue(value)}"]\`;
|
|
298
|
+
}
|
|
299
|
+
return \`\${tagName}[\${attrName}="\${quoteAttrValue(value)}"]\`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const classSelector = getFilteredClassSelector(el);
|
|
303
|
+
if (classSelector) {
|
|
304
|
+
return \`\${tagName}\${classSelector}\`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return tagName;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const getElementSummary = (el) => {
|
|
311
|
+
if (!isElementNode(el)) return "";
|
|
312
|
+
const selector = buildSelectorForElement(el);
|
|
313
|
+
const role = clipText(el.getAttribute("role") || "", 40);
|
|
314
|
+
const text = getElementText(el);
|
|
315
|
+
const suffix = [];
|
|
316
|
+
if (role) suffix.push(\`role=\${role}\`);
|
|
317
|
+
if (text) suffix.push(\`text="\${text}"\`);
|
|
318
|
+
return suffix.length > 0 ? \`\${selector} [\${suffix.join(", ")}]\` : selector;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const getElementText = (el) => {
|
|
322
|
+
if (!isElementNode(el)) return "";
|
|
323
|
+
const tagName = el.tagName.toLowerCase();
|
|
324
|
+
if (tagName === "input") {
|
|
325
|
+
const inputType = normalizeWhitespace(el.getAttribute("type")).toLowerCase();
|
|
326
|
+
if (inputType === "password") return "";
|
|
327
|
+
const value = clipText(el.value || el.getAttribute("value") || "", 80);
|
|
328
|
+
if (value) return value;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const attrCandidates = [
|
|
332
|
+
el.getAttribute("aria-label"),
|
|
333
|
+
el.getAttribute("title"),
|
|
334
|
+
el.getAttribute("placeholder"),
|
|
335
|
+
el.getAttribute("alt"),
|
|
336
|
+
el.textContent,
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
for (const candidate of attrCandidates) {
|
|
340
|
+
const text = clipText(candidate || "", 120);
|
|
341
|
+
if (text.length >= 2) return text;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return "";
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const isMeaningfulAncestor = (el) => {
|
|
348
|
+
if (!isElementNode(el)) return false;
|
|
349
|
+
const tagName = el.tagName.toLowerCase();
|
|
350
|
+
if (isInteractiveElement(el)) return true;
|
|
351
|
+
if (
|
|
352
|
+
[
|
|
353
|
+
"tr",
|
|
354
|
+
"li",
|
|
355
|
+
"td",
|
|
356
|
+
"th",
|
|
357
|
+
"label",
|
|
358
|
+
"section",
|
|
359
|
+
"article",
|
|
360
|
+
"dialog",
|
|
361
|
+
"fieldset",
|
|
362
|
+
"summary",
|
|
363
|
+
].includes(tagName)
|
|
364
|
+
) {
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
if (el.id) return true;
|
|
368
|
+
for (const attrName of TEST_ATTRS) {
|
|
369
|
+
if (el.hasAttribute(attrName)) return true;
|
|
370
|
+
}
|
|
371
|
+
const role = normalizeWhitespace(el.getAttribute("role")).toLowerCase();
|
|
372
|
+
return Boolean(role);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const hasStrongSelectorSignal = (el, selector) => {
|
|
376
|
+
if (!isElementNode(el)) return false;
|
|
377
|
+
if (!selector) return false;
|
|
378
|
+
const tagName = el.tagName.toLowerCase();
|
|
379
|
+
if (el.id) return true;
|
|
380
|
+
if (isInteractiveElement(el)) return true;
|
|
381
|
+
if (getFilteredClassSelector(el)) return true;
|
|
382
|
+
for (const attrName of SELECTOR_ATTRS) {
|
|
383
|
+
if (attrName === "id") continue;
|
|
384
|
+
if (getMeaningfulAttrValue(el, attrName)) return true;
|
|
385
|
+
}
|
|
386
|
+
return selector !== tagName;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const getAncestorSelectors = (target) => {
|
|
390
|
+
const selectors = [];
|
|
391
|
+
let current = isElementNode(target) ? target : null;
|
|
392
|
+
let depth = 0;
|
|
393
|
+
while (current && depth < 7) {
|
|
394
|
+
const selector = buildSelectorForElement(current);
|
|
395
|
+
if (
|
|
396
|
+
selector &&
|
|
397
|
+
!selectors.includes(selector) &&
|
|
398
|
+
(
|
|
399
|
+
(depth === 0 && hasStrongSelectorSignal(current, selector)) ||
|
|
400
|
+
isMeaningfulAncestor(current)
|
|
401
|
+
)
|
|
402
|
+
) {
|
|
403
|
+
selectors.push(selector);
|
|
404
|
+
}
|
|
405
|
+
current = current.parentElement;
|
|
406
|
+
depth += 1;
|
|
407
|
+
}
|
|
408
|
+
if (selectors.length === 0 && isElementNode(target)) {
|
|
409
|
+
selectors.push(buildSelectorForElement(target));
|
|
410
|
+
}
|
|
411
|
+
return selectors;
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const getNearbyText = (target) => {
|
|
415
|
+
let current = isElementNode(target) ? target : null;
|
|
416
|
+
let depth = 0;
|
|
417
|
+
while (current && depth < 6) {
|
|
418
|
+
const text = getElementText(current);
|
|
419
|
+
if (text.length >= 2) return text;
|
|
420
|
+
current = current.parentElement;
|
|
421
|
+
depth += 1;
|
|
422
|
+
}
|
|
423
|
+
return "";
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const getComposedPathSummary = (event) => {
|
|
427
|
+
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
|
|
428
|
+
return path
|
|
429
|
+
.filter((entry) => isElementNode(entry))
|
|
430
|
+
.slice(0, 6)
|
|
431
|
+
.map((entry) => getElementSummary(entry));
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const getCoordinates = (event) => {
|
|
435
|
+
if (!(event instanceof MouseEvent)) return undefined;
|
|
436
|
+
return {
|
|
437
|
+
x: Math.round(event.clientX),
|
|
438
|
+
y: Math.round(event.clientY),
|
|
439
|
+
};
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const buildActionPayload = (event, action, extra = {}) => {
|
|
443
|
+
const target = isElementNode(event.target) ? event.target : null;
|
|
444
|
+
const ancestorSelectors = getAncestorSelectors(target);
|
|
445
|
+
const bestSemanticSelector = ancestorSelectors[0] || "";
|
|
446
|
+
const targetSelector = target ? buildSelectorForElement(target) : "";
|
|
447
|
+
return {
|
|
448
|
+
action,
|
|
449
|
+
bestSemanticSelector: bestSemanticSelector || undefined,
|
|
450
|
+
targetSelector: targetSelector || undefined,
|
|
451
|
+
ancestorSelectors: ancestorSelectors.length > 0 ? ancestorSelectors : undefined,
|
|
452
|
+
nearbyText: getNearbyText(target) || undefined,
|
|
453
|
+
composedPath: getComposedPathSummary(event),
|
|
454
|
+
coordinates: getCoordinates(event),
|
|
455
|
+
success: true,
|
|
456
|
+
...extra,
|
|
457
|
+
};
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
let clickTimer = null;
|
|
461
|
+
let pendingClick = null;
|
|
462
|
+
|
|
463
|
+
document.addEventListener(
|
|
464
|
+
"click",
|
|
465
|
+
(event) => {
|
|
466
|
+
if (window.__btApiActionInProgress) return;
|
|
467
|
+
const target = isElementNode(event.target) ? event.target : null;
|
|
468
|
+
if (target?.type === "checkbox") {
|
|
469
|
+
window.__btActionLog(
|
|
470
|
+
JSON.stringify(
|
|
471
|
+
buildActionPayload(event, target.checked ? "check" : "uncheck"),
|
|
472
|
+
),
|
|
473
|
+
);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
pendingClick = buildActionPayload(event, "click");
|
|
478
|
+
if (clickTimer) clearTimeout(clickTimer);
|
|
479
|
+
clickTimer = setTimeout(() => {
|
|
480
|
+
if (pendingClick) {
|
|
481
|
+
window.__btActionLog(JSON.stringify(pendingClick));
|
|
482
|
+
}
|
|
483
|
+
pendingClick = null;
|
|
484
|
+
clickTimer = null;
|
|
485
|
+
}, 200);
|
|
486
|
+
},
|
|
487
|
+
true,
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
document.addEventListener(
|
|
491
|
+
"dblclick",
|
|
492
|
+
(event) => {
|
|
493
|
+
if (window.__btApiActionInProgress) return;
|
|
494
|
+
if (clickTimer) {
|
|
495
|
+
clearTimeout(clickTimer);
|
|
496
|
+
clickTimer = null;
|
|
497
|
+
pendingClick = null;
|
|
498
|
+
}
|
|
499
|
+
window.__btActionLog(
|
|
500
|
+
JSON.stringify(buildActionPayload(event, "dblclick")),
|
|
273
501
|
);
|
|
502
|
+
},
|
|
503
|
+
true,
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
const inputTimers = new WeakMap();
|
|
507
|
+
document.addEventListener(
|
|
508
|
+
"input",
|
|
509
|
+
(event) => {
|
|
510
|
+
if (window.__btApiActionInProgress) return;
|
|
511
|
+
const target = isElementNode(event.target) ? event.target : null;
|
|
512
|
+
if (!target) return;
|
|
513
|
+
|
|
514
|
+
if (target.tagName === "SELECT") {
|
|
515
|
+
window.__btActionLog(
|
|
516
|
+
JSON.stringify(
|
|
517
|
+
buildActionPayload(event, "selectOption", {
|
|
518
|
+
value: target.value,
|
|
519
|
+
}),
|
|
520
|
+
),
|
|
521
|
+
);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
274
524
|
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
action: "selectOption",
|
|
286
|
-
selector,
|
|
287
|
-
value: target.value,
|
|
288
|
-
success: true,
|
|
525
|
+
const existing = inputTimers.get(target);
|
|
526
|
+
if (existing) clearTimeout(existing);
|
|
527
|
+
inputTimers.set(
|
|
528
|
+
target,
|
|
529
|
+
setTimeout(() => {
|
|
530
|
+
inputTimers.delete(target);
|
|
531
|
+
window.__btActionLog(
|
|
532
|
+
JSON.stringify(
|
|
533
|
+
buildActionPayload(event, "fill", {
|
|
534
|
+
value: String(target.value || "").slice(0, 100),
|
|
289
535
|
}),
|
|
290
|
-
)
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
const existing = inputTimers.get(target);
|
|
294
|
-
if (existing) clearTimeout(existing);
|
|
295
|
-
inputTimers.set(
|
|
296
|
-
target,
|
|
297
|
-
setTimeout(() => {
|
|
298
|
-
inputTimers.delete(target);
|
|
299
|
-
(window as any).__btActionLog(
|
|
300
|
-
JSON.stringify({
|
|
301
|
-
action: "fill",
|
|
302
|
-
selector,
|
|
303
|
-
value: (target.value || "").slice(0, 100),
|
|
304
|
-
success: true,
|
|
305
|
-
}),
|
|
306
|
-
);
|
|
307
|
-
}, 500),
|
|
536
|
+
),
|
|
308
537
|
);
|
|
309
|
-
},
|
|
310
|
-
true,
|
|
538
|
+
}, 500),
|
|
311
539
|
);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
)
|
|
361
|
-
|
|
362
|
-
true,
|
|
540
|
+
},
|
|
541
|
+
true,
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
const specialKeys = new Set([
|
|
545
|
+
"Enter",
|
|
546
|
+
"Escape",
|
|
547
|
+
"Tab",
|
|
548
|
+
"Backspace",
|
|
549
|
+
"Delete",
|
|
550
|
+
"ArrowUp",
|
|
551
|
+
"ArrowDown",
|
|
552
|
+
"ArrowLeft",
|
|
553
|
+
"ArrowRight",
|
|
554
|
+
"Home",
|
|
555
|
+
"End",
|
|
556
|
+
"PageUp",
|
|
557
|
+
"PageDown",
|
|
558
|
+
"F1",
|
|
559
|
+
"F2",
|
|
560
|
+
"F3",
|
|
561
|
+
"F4",
|
|
562
|
+
"F5",
|
|
563
|
+
"F6",
|
|
564
|
+
"F7",
|
|
565
|
+
"F8",
|
|
566
|
+
"F9",
|
|
567
|
+
"F10",
|
|
568
|
+
"F11",
|
|
569
|
+
"F12",
|
|
570
|
+
]);
|
|
571
|
+
|
|
572
|
+
document.addEventListener(
|
|
573
|
+
"keydown",
|
|
574
|
+
(event) => {
|
|
575
|
+
if (window.__btApiActionInProgress) return;
|
|
576
|
+
const isShortcut = event.ctrlKey || event.metaKey || event.altKey;
|
|
577
|
+
if (!isShortcut && !specialKeys.has(event.key)) return;
|
|
578
|
+
const keyDesc =
|
|
579
|
+
(event.ctrlKey ? "Ctrl+" : "") +
|
|
580
|
+
(event.metaKey ? "Meta+" : "") +
|
|
581
|
+
(event.altKey ? "Alt+" : "") +
|
|
582
|
+
(event.shiftKey ? "Shift+" : "") +
|
|
583
|
+
event.key;
|
|
584
|
+
window.__btActionLog(
|
|
585
|
+
JSON.stringify(
|
|
586
|
+
buildActionPayload(event, "press", {
|
|
587
|
+
value: keyDesc,
|
|
588
|
+
}),
|
|
589
|
+
),
|
|
363
590
|
);
|
|
591
|
+
},
|
|
592
|
+
true,
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
let scrollTimer = null;
|
|
596
|
+
document.addEventListener(
|
|
597
|
+
"scroll",
|
|
598
|
+
() => {
|
|
599
|
+
if (window.__btApiActionInProgress) return;
|
|
600
|
+
if (scrollTimer) clearTimeout(scrollTimer);
|
|
601
|
+
scrollTimer = setTimeout(() => {
|
|
602
|
+
scrollTimer = null;
|
|
603
|
+
window.__btActionLog(
|
|
604
|
+
JSON.stringify({
|
|
605
|
+
action: "scroll",
|
|
606
|
+
bestSemanticSelector: "document",
|
|
607
|
+
success: true,
|
|
608
|
+
value: \`y=\${window.scrollY}\`,
|
|
609
|
+
}),
|
|
610
|
+
);
|
|
611
|
+
}, 300);
|
|
612
|
+
},
|
|
613
|
+
true,
|
|
614
|
+
);
|
|
615
|
+
})();
|
|
616
|
+
`;
|
|
617
|
+
};
|
|
364
618
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}),
|
|
380
|
-
);
|
|
381
|
-
}, 300);
|
|
382
|
-
},
|
|
383
|
-
true,
|
|
384
|
-
);
|
|
619
|
+
const installUserDomTracking = async (
|
|
620
|
+
page: Page,
|
|
621
|
+
pageId: string,
|
|
622
|
+
): Promise<void> => {
|
|
623
|
+
if (exposedPages.has(page)) return;
|
|
624
|
+
exposedPages.add(page);
|
|
625
|
+
|
|
626
|
+
await page.exposeFunction("__btActionLog", (jsonStr: string) => {
|
|
627
|
+
const parsed = JSON.parse(jsonStr) as Record<string, unknown>;
|
|
628
|
+
emitAction({
|
|
629
|
+
pageId,
|
|
630
|
+
source: "user",
|
|
631
|
+
...parsed,
|
|
632
|
+
});
|
|
385
633
|
});
|
|
634
|
+
|
|
635
|
+
await page.addInitScript({ content: createUserDomTrackingInitScript() });
|
|
386
636
|
};
|
|
387
637
|
|
|
388
638
|
const wrapPageActions = (page: Page, pageId: string): void => {
|
|
@@ -425,7 +675,8 @@ export async function installSessionTelemetry(
|
|
|
425
675
|
action: method,
|
|
426
676
|
source: "agent",
|
|
427
677
|
selector: typeof args[0] === "string" ? args[0] : undefined,
|
|
428
|
-
value:
|
|
678
|
+
value:
|
|
679
|
+
args[1] !== undefined ? String(args[1]).slice(0, 100) : undefined,
|
|
429
680
|
duration: Date.now() - start,
|
|
430
681
|
success: true,
|
|
431
682
|
});
|
|
@@ -497,7 +748,8 @@ export async function installSessionTelemetry(
|
|
|
497
748
|
page.on("response", async (response) => {
|
|
498
749
|
const request = response.request();
|
|
499
750
|
const url = request.url();
|
|
500
|
-
if (STATIC_EXT_RE.test(url) || url.startsWith("chrome-extension://"))
|
|
751
|
+
if (STATIC_EXT_RE.test(url) || url.startsWith("chrome-extension://"))
|
|
752
|
+
return;
|
|
501
753
|
emitNetwork({
|
|
502
754
|
pageId,
|
|
503
755
|
method: request.method(),
|