@copilotkit/learning 0.0.1 → 1.80.0
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 +63 -3
- package/dist/index.cjs +335 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +6 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +335 -34
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/scripts/check-size.mjs +2 -2
- package/src/__tests__/clicks.test.ts +499 -1
- package/src/__tests__/collector.test.ts +152 -0
- package/src/__tests__/inputs.test.ts +400 -1
- package/src/__tests__/redact.test.ts +4 -2
- package/src/__tests__/trajectory-collector.test.ts +105 -0
- package/src/clicks.ts +308 -3
- package/src/collector.ts +11 -5
- package/src/inputs.ts +167 -19
- package/src/redact.ts +7 -2
- package/src/trajectory-collector.ts +16 -2
- package/src/types.ts +6 -0
- package/CHANGELOG.md +0 -9
package/dist/index.mjs
CHANGED
|
@@ -115,13 +115,17 @@ function createRedactor() {
|
|
|
115
115
|
const toggled = (records) => {
|
|
116
116
|
for (const record of records) if (record.oldValue?.toLowerCase() === "password") seen.add(record.target);
|
|
117
117
|
};
|
|
118
|
-
/**
|
|
119
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Replaces remembered values, raw and in their JSON and URL encodings, plus
|
|
120
|
+
* `normalize(value)` when given and at least 4 characters long.
|
|
121
|
+
*/
|
|
122
|
+
const scrub = (text, normalize) => {
|
|
120
123
|
for (const value of [...remembered.values()].sort((a, b) => b.length - a.length)) for (const form of [
|
|
121
124
|
value,
|
|
122
125
|
JSON.stringify(value).slice(1, -1),
|
|
123
126
|
encodeURIComponent(value),
|
|
124
|
-
new URLSearchParams({ v: value }).toString().slice(2)
|
|
127
|
+
new URLSearchParams({ v: value }).toString().slice(2),
|
|
128
|
+
...[normalize?.(value) ?? ""].filter((normal) => normal.length >= 4)
|
|
125
129
|
]) text = /^\d+$/.test(form) ? text.replace(RegExp(`(?<!\\d)${form}(?!\\d)`, "g"), REDACTED) : text.split(form).join(REDACTED);
|
|
126
130
|
return text;
|
|
127
131
|
};
|
|
@@ -139,6 +143,7 @@ function createRedactor() {
|
|
|
139
143
|
};
|
|
140
144
|
return {
|
|
141
145
|
isPassword,
|
|
146
|
+
scrub,
|
|
142
147
|
watch() {
|
|
143
148
|
try {
|
|
144
149
|
for (const input of document.querySelectorAll("input[type=password]")) seen.add(input);
|
|
@@ -222,6 +227,172 @@ function describeTarget(el, redact) {
|
|
|
222
227
|
}
|
|
223
228
|
};
|
|
224
229
|
}
|
|
230
|
+
const MAX_LABEL_LENGTH = 80;
|
|
231
|
+
const MAX_WALK_NODES = 1e3;
|
|
232
|
+
const MAX_WALK_CHARS = 4 * MAX_LABEL_LENGTH;
|
|
233
|
+
const CONTROL_ROLE = /^(button|link|tab|menuitem(checkbox|radio)?|option|checkbox|switch|radio|treeitem)$/;
|
|
234
|
+
const CONTENTEDITABLE = "[contenteditable]:not([contenteditable=\"false\"])";
|
|
235
|
+
const TEXT_ENTRY = `${CONTENTEDITABLE}, [role~=textbox], [role~=searchbox], [role~=combobox]`;
|
|
236
|
+
const EDITABLE = `input, textarea, select, ${TEXT_ENTRY}`;
|
|
237
|
+
const HEADING = "h1, h2, h3, h4, h5, h6, [role~=heading]";
|
|
238
|
+
const STRUCTURE = `${HEADING}, p, ul, ol, li, dl, table, article, section, blockquote`;
|
|
239
|
+
const SKIPPED = "script, style, template, [hidden], [aria-hidden=\"true\"]";
|
|
240
|
+
const PHRASING = /^(b|i|u|em|strong|mark|small|sub|sup|abbr|code|kbd|s|q|cite|dfn|var|time|data|bdi|bdo|wbr|span)$/;
|
|
241
|
+
const CAPTION_INPUT = /^(submit|button|reset|image)$/;
|
|
242
|
+
const NON_TEXT_INPUT = /^(submit|button|reset|image|checkbox|radio|range|color|file|hidden)$/;
|
|
243
|
+
const IGNORABLE = /\p{Default_Ignorable_Code_Point}/gu;
|
|
244
|
+
const URL_LIKE = /(?:\b[a-z][\w+.-]{0,31}:\/\/|\bwww\.)[^\s<>"']*[^\s<>"'.,;:!?)]/gi;
|
|
245
|
+
const EMAIL = /[\p{L}\p{N}_.%+-]{1,64}@[\p{L}\p{N}-]{1,63}(?:\.[\p{L}\p{N}-]{1,63})+/gu;
|
|
246
|
+
const JWT = /(^|[^\w-])(?:eyJ[\w-]*\.[\w-]*\.[\w-]*|[\w-]{16,}\.[\w-]{16,}\.[\w-]{16,}|(?=[\w-]*\d)[\w-]{8,}\.(?=[\w-]*\d)[\w-]{8,}\.(?=[\w-]*\d)[\w-]{8,})/g;
|
|
247
|
+
const TOKEN_RUN = /[\w+/=-]{24,}/g;
|
|
248
|
+
const roleOf = (element) => (element.getAttribute("role") ?? "").trim().split(/\s+/)[0].toLowerCase();
|
|
249
|
+
const idRefs = (element, name) => (element.getAttribute(name) ?? "").split(/\s+/).filter(Boolean);
|
|
250
|
+
const rootOf = (element) => {
|
|
251
|
+
const root = element.getRootNode();
|
|
252
|
+
return root instanceof Document || root instanceof ShadowRoot ? root : null;
|
|
253
|
+
};
|
|
254
|
+
/** Buttons, links with `href`, `summary`, and elements with a control role. */
|
|
255
|
+
const isControl = (element) => {
|
|
256
|
+
const role = roleOf(element);
|
|
257
|
+
return role ? CONTROL_ROLE.test(role) : element.matches("button, a[href], summary");
|
|
258
|
+
};
|
|
259
|
+
/** Text-like inputs, text areas, selects, and anything inside editable content or a text-entry role. */
|
|
260
|
+
const holdsValue = (element) => element instanceof HTMLInputElement ? !NON_TEXT_INPUT.test(element.type) : element.matches("textarea, select") || element.closest(TEXT_ENTRY) !== null;
|
|
261
|
+
/**
|
|
262
|
+
* The name an image contributes: `alt` of an `img`, or `aria-label` (else a
|
|
263
|
+
* plain `<title>`) of an `svg` or `role="img"` element. `undefined` when the
|
|
264
|
+
* element is not an image.
|
|
265
|
+
*/
|
|
266
|
+
function imageName(element) {
|
|
267
|
+
if (element.localName === "img") return element.getAttribute("alt");
|
|
268
|
+
if (element.localName !== "svg" && roleOf(element) !== "img") return void 0;
|
|
269
|
+
const title = Array.from(element.children).find((child) => child.localName === "title" && child.childElementCount === 0);
|
|
270
|
+
return element.getAttribute("aria-label") ?? title?.textContent ?? null;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Rendered text. Inline formatting joins words; other elements separate them.
|
|
274
|
+
* Skips hidden subtrees, uses image names, and does
|
|
275
|
+
* not walk into images. Yields nothing for editable content or editable
|
|
276
|
+
* descendants. In `strict` mode (a control's own text) it also yields nothing
|
|
277
|
+
* past the walk bounds, for card-like structure, and for text that looks like
|
|
278
|
+
* separate fields: two `div`s that directly hold text, or three sibling
|
|
279
|
+
* elements that each carry text.
|
|
280
|
+
*/
|
|
281
|
+
function safeText(root, strict) {
|
|
282
|
+
if (holdsValue(root)) return "";
|
|
283
|
+
let text = "";
|
|
284
|
+
let nodes = 0;
|
|
285
|
+
let paragraphs = 0;
|
|
286
|
+
let div;
|
|
287
|
+
const bearers = /* @__PURE__ */ new Map();
|
|
288
|
+
const add = (part) => text = (text + part).replace(/\s+/g, " ");
|
|
289
|
+
const bear = (start) => {
|
|
290
|
+
if (!strict) return true;
|
|
291
|
+
for (let child = start, parent = start.parentNode; child !== root && parent !== null; child = parent, parent = parent.parentNode) {
|
|
292
|
+
const siblings = bearers.get(parent) ?? /* @__PURE__ */ new Set();
|
|
293
|
+
bearers.set(parent, siblings);
|
|
294
|
+
if (siblings.has(child)) break;
|
|
295
|
+
if (siblings.add(child).size > 2) return false;
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
};
|
|
299
|
+
let node = root.firstChild;
|
|
300
|
+
while (node !== null) {
|
|
301
|
+
if (++nodes > MAX_WALK_NODES || text.length > MAX_WALK_CHARS) {
|
|
302
|
+
if (strict) return "";
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
let descend = false;
|
|
306
|
+
if (node instanceof Text) {
|
|
307
|
+
const parent = node.parentNode;
|
|
308
|
+
let holder = parent;
|
|
309
|
+
while (holder !== root && PHRASING.test(holder.localName)) holder = holder.parentElement;
|
|
310
|
+
if (/\S/.test(node.data) && (!bear(parent) || strict && holder.localName === "div" && (div ??= holder) !== holder)) return "";
|
|
311
|
+
add(node.data);
|
|
312
|
+
} else if (node instanceof Element) {
|
|
313
|
+
if (node.matches(EDITABLE)) return "";
|
|
314
|
+
if (strict && node.matches(STRUCTURE) && !(root.localName === "summary" && node.matches(HEADING)) && (node.localName !== "p" || ++paragraphs > 1)) return "";
|
|
315
|
+
const name = node.matches(SKIPPED) ? null : imageName(node);
|
|
316
|
+
if (name) {
|
|
317
|
+
if (!bear(node)) return "";
|
|
318
|
+
add(` ${name} `);
|
|
319
|
+
} else if (name === void 0) {
|
|
320
|
+
if (!PHRASING.test(node.localName)) add(" ");
|
|
321
|
+
descend = true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (descend && node.firstChild !== null) node = node.firstChild;
|
|
325
|
+
else {
|
|
326
|
+
while (node !== root && node.nextSibling === null) {
|
|
327
|
+
node = node.parentNode;
|
|
328
|
+
if (!PHRASING.test(node.localName)) add(" ");
|
|
329
|
+
}
|
|
330
|
+
node = node === root ? null : node.nextSibling;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return text;
|
|
334
|
+
}
|
|
335
|
+
/** An option or tree item of an autocomplete or search popup, whose text usually repeats what the user typed. */
|
|
336
|
+
const isSuggestion = (element) => (element.localName === "option" || /^(option|treeitem)$/.test(roleOf(element))) && inSuggestionList(element);
|
|
337
|
+
/** Whether `element` is, or is inside, a combobox, a datalist, or the popup a combobox or autocomplete field controls or owns. */
|
|
338
|
+
function inSuggestionList(element) {
|
|
339
|
+
if (element.closest("[role~=combobox], datalist") !== null) return true;
|
|
340
|
+
const root = rootOf(element);
|
|
341
|
+
return root !== null && Array.from(root.querySelectorAll("[role~=combobox], [aria-autocomplete]")).some((owner) => [...idRefs(owner, "aria-controls"), ...idRefs(owner, "aria-owns")].some((id) => root.getElementById(id)?.contains(element)));
|
|
342
|
+
}
|
|
343
|
+
function labelledBy(element) {
|
|
344
|
+
const root = rootOf(element);
|
|
345
|
+
return root === null ? null : idRefs(element, "aria-labelledby").map((id) => {
|
|
346
|
+
const referenced = root.getElementById(id);
|
|
347
|
+
return referenced === null || inSuggestionList(referenced) ? "" : safeText(referenced, false);
|
|
348
|
+
}).join(" ");
|
|
349
|
+
}
|
|
350
|
+
/** A control's own text, unless it is longer than a label: then it is content, not a name. */
|
|
351
|
+
const normalize = (text) => text.replace(IGNORABLE, "").replace(/\s+/g, " ").trim();
|
|
352
|
+
function contentText(element) {
|
|
353
|
+
const text = safeText(element, true);
|
|
354
|
+
return Array.from(normalize(text)).length > MAX_LABEL_LENGTH ? "" : text;
|
|
355
|
+
}
|
|
356
|
+
/** Removes invisible characters, credentials, emails, URLs, and tokens, then caps the length. */
|
|
357
|
+
function cleanLabel(raw, redact) {
|
|
358
|
+
const chars = Array.from(redact.scrub(normalize(redact.body(raw.replace(IGNORABLE, "")) ?? ""), normalize).replace(URL_LIKE, "[url]").replace(EMAIL, "[email]").replace(JWT, `$1${REDACTED}`).replace(TOKEN_RUN, (run) => /\d/.test(run) ? REDACTED : run));
|
|
359
|
+
return chars.length > MAX_LABEL_LENGTH ? `${chars.slice(0, MAX_LABEL_LENGTH - 1).join("").trimEnd()}…` : chars.join("");
|
|
360
|
+
}
|
|
361
|
+
function firstLabel(sources, redact) {
|
|
362
|
+
for (const source of sources) {
|
|
363
|
+
const label = cleanLabel(source() ?? "", redact);
|
|
364
|
+
if (label) return label;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* A short name for the clicked control, from the first source that yields one:
|
|
369
|
+
* `aria-labelledby`, `aria-label`, an input caption or `alt`, `title` (controls
|
|
370
|
+
* only), then the text of a control named by its content. Containers, cards,
|
|
371
|
+
* and editable content never contribute their text. An autocomplete suggestion,
|
|
372
|
+
* or anything inside one, gets no label from any source; `data-copilotkit-action`
|
|
373
|
+
* names it instead. An icon without its own name takes the name of the control
|
|
374
|
+
* around it.
|
|
375
|
+
*/
|
|
376
|
+
function clickLabel(element, redact) {
|
|
377
|
+
const option = element.closest("option, [role~=option], [role~=treeitem]");
|
|
378
|
+
if (option !== null && isSuggestion(option)) return void 0;
|
|
379
|
+
const named = [() => labelledBy(element), () => element.getAttribute("aria-label")];
|
|
380
|
+
if (/^(img|svg)$/.test(element.localName) || /^(img|presentation|none)$/.test(roleOf(element))) {
|
|
381
|
+
const own = firstLabel([...named, () => imageName(element) ?? null], redact);
|
|
382
|
+
if (own !== void 0) return own;
|
|
383
|
+
let control = element.parentElement;
|
|
384
|
+
while (control !== null && !isControl(control)) control = control.parentElement;
|
|
385
|
+
return control === null ? void 0 : clickLabel(control, redact);
|
|
386
|
+
}
|
|
387
|
+
const control = isControl(element) && !holdsValue(element);
|
|
388
|
+
const captionInput = element instanceof HTMLInputElement && CAPTION_INPUT.test(element.type);
|
|
389
|
+
return firstLabel([
|
|
390
|
+
...named,
|
|
391
|
+
() => captionInput ? element.type === "image" ? element.getAttribute("alt") : element.value : null,
|
|
392
|
+
() => control || captionInput ? element.getAttribute("title") : null,
|
|
393
|
+
() => control ? contentText(element) : null
|
|
394
|
+
], redact);
|
|
395
|
+
}
|
|
225
396
|
/** Snapshot live control properties; HTML attributes do not reflect edited values. */
|
|
226
397
|
function readControlValue(element, redact) {
|
|
227
398
|
if (element instanceof HTMLInputElement) return {
|
|
@@ -239,7 +410,7 @@ function readControlValue(element, redact) {
|
|
|
239
410
|
selectedValues: Array.from(element.selectedOptions, (option) => option.value)
|
|
240
411
|
};
|
|
241
412
|
if (element instanceof HTMLTextAreaElement) return { value: element.value };
|
|
242
|
-
if (element.matches(
|
|
413
|
+
if (element.matches(CONTENTEDITABLE)) return { value: element.textContent ?? "" };
|
|
243
414
|
return {};
|
|
244
415
|
}
|
|
245
416
|
function eventElement(event) {
|
|
@@ -260,12 +431,21 @@ function isNear(origin, event) {
|
|
|
260
431
|
function installClickCapture(params) {
|
|
261
432
|
const { emit, getRoute, enrich, isTrusted = (event) => event.isTrusted, redact = createRedactor() } = params;
|
|
262
433
|
let origin = null;
|
|
434
|
+
const describeClick = (el) => {
|
|
435
|
+
const described = describeTarget(el, redact);
|
|
436
|
+
if (described === null) return null;
|
|
437
|
+
try {
|
|
438
|
+
const label = clickLabel(described.element, redact);
|
|
439
|
+
if (label !== void 0) described.target.label = label;
|
|
440
|
+
} catch {}
|
|
441
|
+
return described;
|
|
442
|
+
};
|
|
263
443
|
const onPointerDown = (event) => {
|
|
264
444
|
try {
|
|
265
445
|
if (event.isPrimary === false) return;
|
|
266
446
|
const el = eventElement(event);
|
|
267
447
|
origin = {
|
|
268
|
-
described: el === null ? null :
|
|
448
|
+
described: el === null ? null : describeClick(el),
|
|
269
449
|
x: event.clientX,
|
|
270
450
|
y: event.clientY
|
|
271
451
|
};
|
|
@@ -280,7 +460,7 @@ function installClickCapture(params) {
|
|
|
280
460
|
if (!isTrusted(event)) return;
|
|
281
461
|
const el = eventElement(event);
|
|
282
462
|
if (el === null) return;
|
|
283
|
-
const described = pointerOrigin !== null && isRootOrDetached(el) && isNear(pointerOrigin, event) ? pointerOrigin.described :
|
|
463
|
+
const described = pointerOrigin !== null && isRootOrDetached(el) && isNear(pointerOrigin, event) ? pointerOrigin.described : describeClick(el);
|
|
284
464
|
if (described === null) return;
|
|
285
465
|
const input = event.detail === 0 ? "keyboard" : "pointer";
|
|
286
466
|
emit("click", {
|
|
@@ -310,41 +490,138 @@ function installClickCapture(params) {
|
|
|
310
490
|
|
|
311
491
|
//#endregion
|
|
312
492
|
//#region src/inputs.ts
|
|
313
|
-
|
|
493
|
+
const TEXT_DEBOUNCE_MS = 300;
|
|
494
|
+
const TEXT_INPUT_TYPES = new Set([
|
|
495
|
+
"text",
|
|
496
|
+
"search",
|
|
497
|
+
"tel",
|
|
498
|
+
"url",
|
|
499
|
+
"email",
|
|
500
|
+
"password",
|
|
501
|
+
"number"
|
|
502
|
+
]);
|
|
503
|
+
/** Records settled user edits without interfering with the host app's controls. */
|
|
314
504
|
function installInputCapture(params) {
|
|
315
505
|
const { emit, isTrusted = (event) => event.isTrusted, redact = createRedactor() } = params;
|
|
316
|
-
|
|
506
|
+
let active = true;
|
|
507
|
+
const pending = /* @__PURE__ */ new Map();
|
|
508
|
+
const changeEcho = /* @__PURE__ */ new WeakMap();
|
|
509
|
+
const composing = /* @__PURE__ */ new WeakMap();
|
|
510
|
+
const cancel = (element) => {
|
|
511
|
+
clearTimeout(pending.get(element)?.timer);
|
|
512
|
+
pending.delete(element);
|
|
513
|
+
};
|
|
514
|
+
const deliver = (element, edit) => {
|
|
317
515
|
try {
|
|
318
|
-
if (!
|
|
319
|
-
|
|
320
|
-
if (
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
516
|
+
if (!active || describeTarget(element, redact) === null) return;
|
|
517
|
+
if (redact.isPassword(element) && edit.state.value !== REDACTED) return;
|
|
518
|
+
if (edit.payload.eventType === "input") changeEcho.set(element, JSON.stringify(edit.state));
|
|
519
|
+
edit.payload.url = redact.url(edit.payload.url);
|
|
520
|
+
edit.payload.target.text = redact.url(edit.payload.target.text);
|
|
521
|
+
edit.payload.target.attributes = Object.fromEntries(Object.entries(edit.payload.target.attributes).map(([key, value]) => [key, redact.url(value)]));
|
|
522
|
+
emit("input", edit.payload);
|
|
523
|
+
} catch {}
|
|
524
|
+
};
|
|
525
|
+
const flush = () => {
|
|
526
|
+
const edits = [...pending];
|
|
527
|
+
for (const [element] of edits) cancel(element);
|
|
528
|
+
for (const [element, edit] of edits) deliver(element, edit);
|
|
529
|
+
};
|
|
530
|
+
const observe = (element, eventType, settle) => {
|
|
531
|
+
if (pending.size > 0 && !pending.has(element)) flush();
|
|
532
|
+
if (!active) return;
|
|
533
|
+
const described = describeTarget(element, redact);
|
|
534
|
+
if (described === null) {
|
|
535
|
+
cancel(element);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const state = readControlValue(element, redact);
|
|
539
|
+
if (!("value" in state)) return;
|
|
540
|
+
const text = element instanceof HTMLTextAreaElement || element instanceof HTMLInputElement && TEXT_INPUT_TYPES.has(element.type) || element.matches("[contenteditable]:not([contenteditable=\"false\"])");
|
|
541
|
+
if (!text) flush();
|
|
542
|
+
const signature = JSON.stringify(state);
|
|
543
|
+
const echo = eventType === "change" && (signature === JSON.stringify(pending.get(element)?.state) || signature === changeEcho.get(element));
|
|
544
|
+
changeEcho.delete(element);
|
|
545
|
+
if (echo) {
|
|
546
|
+
flush();
|
|
547
|
+
changeEcho.delete(element);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
cancel(element);
|
|
551
|
+
const edit = {
|
|
552
|
+
state,
|
|
553
|
+
payload: {
|
|
554
|
+
eventType,
|
|
327
555
|
target: {
|
|
328
556
|
...described.target,
|
|
329
|
-
...
|
|
557
|
+
...state
|
|
330
558
|
},
|
|
331
559
|
url: redact.url(location.href),
|
|
332
560
|
route: location.pathname
|
|
333
|
-
}
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
if (!text) {
|
|
564
|
+
deliver(element, edit);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (settle) edit.timer = setTimeout(() => {
|
|
568
|
+
cancel(element);
|
|
569
|
+
deliver(element, edit);
|
|
570
|
+
}, TEXT_DEBOUNCE_MS);
|
|
571
|
+
pending.set(element, edit);
|
|
572
|
+
};
|
|
573
|
+
const onEdit = (event) => {
|
|
574
|
+
try {
|
|
575
|
+
if (!active || !isTrusted(event)) return;
|
|
576
|
+
const [element] = event.composedPath();
|
|
577
|
+
if (!(element instanceof Element)) return;
|
|
578
|
+
if (event.type === "compositionstart") {
|
|
579
|
+
composing.set(element, true);
|
|
580
|
+
clearTimeout(pending.get(element)?.timer);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (event.type === "compositionend") composing.set(element, false);
|
|
584
|
+
const inComposition = composing.get(element) === true || composing.get(element) !== false && "isComposing" in event && event.isComposing === true;
|
|
585
|
+
observe(element, event.type === "compositionend" ? "input" : event.type, !inComposition);
|
|
586
|
+
if (event.type === "change") flush();
|
|
334
587
|
} catch {}
|
|
335
588
|
};
|
|
336
|
-
|
|
589
|
+
const onAction = (event) => {
|
|
590
|
+
if (event.type !== "pagehide" && !isTrusted(event)) return;
|
|
591
|
+
if (event.type === "keydown") {
|
|
592
|
+
const [element] = event.composedPath();
|
|
593
|
+
if (!(event instanceof KeyboardEvent) || event.key !== "Enter" || event.isComposing || event.keyCode === 229 || element instanceof Element && composing.get(element) === true) return;
|
|
594
|
+
}
|
|
595
|
+
flush();
|
|
596
|
+
};
|
|
597
|
+
const editTypes = [
|
|
598
|
+
"input",
|
|
599
|
+
"change",
|
|
600
|
+
"compositionstart",
|
|
601
|
+
"compositionend"
|
|
602
|
+
];
|
|
603
|
+
const actionTypes = [
|
|
604
|
+
"click",
|
|
605
|
+
"submit",
|
|
606
|
+
"focusout",
|
|
607
|
+
"pagehide",
|
|
608
|
+
"keydown"
|
|
609
|
+
];
|
|
610
|
+
for (const type of editTypes) window.addEventListener(type, onEdit, {
|
|
337
611
|
capture: true,
|
|
338
612
|
passive: true
|
|
339
613
|
});
|
|
340
|
-
window.addEventListener(
|
|
614
|
+
for (const type of actionTypes) window.addEventListener(type, onAction, {
|
|
341
615
|
capture: true,
|
|
342
616
|
passive: true
|
|
343
617
|
});
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
618
|
+
const uninstall = () => {
|
|
619
|
+
active = false;
|
|
620
|
+
for (const element of pending.keys()) cancel(element);
|
|
621
|
+
for (const type of editTypes) window.removeEventListener(type, onEdit, true);
|
|
622
|
+
for (const type of actionTypes) window.removeEventListener(type, onAction, true);
|
|
347
623
|
};
|
|
624
|
+
return Object.assign(uninstall, { flush });
|
|
348
625
|
}
|
|
349
626
|
|
|
350
627
|
//#endregion
|
|
@@ -864,6 +1141,8 @@ function createCollector(options) {
|
|
|
864
1141
|
const record = (name, value) => {
|
|
865
1142
|
const current = session;
|
|
866
1143
|
if (current === null) return;
|
|
1144
|
+
if (name !== "input" && name !== "network") current.flushInputs?.();
|
|
1145
|
+
if (session !== current) return;
|
|
867
1146
|
try {
|
|
868
1147
|
seq += 1;
|
|
869
1148
|
const event = {
|
|
@@ -876,7 +1155,7 @@ function createCollector(options) {
|
|
|
876
1155
|
timestamp: Date.now()
|
|
877
1156
|
};
|
|
878
1157
|
const kept = beforeSend === void 0 ? event : beforeSend(event);
|
|
879
|
-
if (kept === null) return;
|
|
1158
|
+
if (kept === null || session !== current) return;
|
|
880
1159
|
current.queue.push(kept);
|
|
881
1160
|
if (current.queue.length >= FLUSH_SIZE) flush();
|
|
882
1161
|
} catch {}
|
|
@@ -919,10 +1198,14 @@ function createCollector(options) {
|
|
|
919
1198
|
ignoreUrls,
|
|
920
1199
|
redact
|
|
921
1200
|
}));
|
|
922
|
-
if (capture.inputs)
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1201
|
+
if (capture.inputs) {
|
|
1202
|
+
const inputs = installInputCapture({
|
|
1203
|
+
emit: recordCurrent,
|
|
1204
|
+
redact
|
|
1205
|
+
});
|
|
1206
|
+
current.flushInputs = inputs.flush;
|
|
1207
|
+
current.uninstalls.push(inputs);
|
|
1208
|
+
}
|
|
926
1209
|
const onPageHide = () => flush({ beacon: true });
|
|
927
1210
|
window.addEventListener("pagehide", onPageHide);
|
|
928
1211
|
current.uninstalls.push(() => window.removeEventListener("pagehide", onPageHide));
|
|
@@ -937,6 +1220,8 @@ function createCollector(options) {
|
|
|
937
1220
|
const stop = () => {
|
|
938
1221
|
const current = session;
|
|
939
1222
|
if (current === null) return;
|
|
1223
|
+
current.flushInputs?.();
|
|
1224
|
+
if (session !== current) return;
|
|
940
1225
|
clearInterval(current.timer);
|
|
941
1226
|
for (const uninstall of current.uninstalls) uninstall();
|
|
942
1227
|
flush();
|
|
@@ -1016,7 +1301,9 @@ function isJson(value, ancestors = /* @__PURE__ */ new Set()) {
|
|
|
1016
1301
|
*/
|
|
1017
1302
|
function createTrajectoryCollector(options) {
|
|
1018
1303
|
let active = false;
|
|
1304
|
+
let generation = 0;
|
|
1019
1305
|
let uninstalls = [];
|
|
1306
|
+
let flushInputs;
|
|
1020
1307
|
const report = (code, message) => {
|
|
1021
1308
|
try {
|
|
1022
1309
|
options.onError?.({
|
|
@@ -1028,6 +1315,9 @@ function createTrajectoryCollector(options) {
|
|
|
1028
1315
|
const getRoute = () => toRoute(location.pathname);
|
|
1029
1316
|
const record = (name, value) => {
|
|
1030
1317
|
if (!active) return;
|
|
1318
|
+
const current = generation;
|
|
1319
|
+
if (name !== "input" && name !== "network") flushInputs?.();
|
|
1320
|
+
if (!active || generation !== current) return;
|
|
1031
1321
|
let payload;
|
|
1032
1322
|
try {
|
|
1033
1323
|
const event = {
|
|
@@ -1037,7 +1327,7 @@ function createTrajectoryCollector(options) {
|
|
|
1037
1327
|
value
|
|
1038
1328
|
};
|
|
1039
1329
|
const kept = options.beforeSend === void 0 ? event : options.beforeSend(event);
|
|
1040
|
-
if (kept === null || !active) return;
|
|
1330
|
+
if (kept === null || !active || generation !== current) return;
|
|
1041
1331
|
if (kept.type !== "CUSTOM" || typeof kept.name !== "string" || !kept.name.trim() || kept.name.length > 200 || kept.name.includes("\0") || !Number.isFinite(kept.timestamp) || kept.timestamp <= 0 || kept.timestamp >= 0xe677d21fdc00 || !isJson(kept.value)) {
|
|
1042
1332
|
report("INVALID_EVENT", "The event must contain a name, timestamp, and JSON value.");
|
|
1043
1333
|
return;
|
|
@@ -1067,6 +1357,12 @@ function createTrajectoryCollector(options) {
|
|
|
1067
1357
|
record(name, value);
|
|
1068
1358
|
};
|
|
1069
1359
|
const stop = () => {
|
|
1360
|
+
const current = generation;
|
|
1361
|
+
const flush = flushInputs;
|
|
1362
|
+
flushInputs = void 0;
|
|
1363
|
+
flush?.();
|
|
1364
|
+
if (generation !== current) return;
|
|
1365
|
+
generation++;
|
|
1070
1366
|
active = false;
|
|
1071
1367
|
const installed = uninstalls;
|
|
1072
1368
|
uninstalls = [];
|
|
@@ -1076,6 +1372,7 @@ function createTrajectoryCollector(options) {
|
|
|
1076
1372
|
start() {
|
|
1077
1373
|
if (active || typeof window === "undefined") return;
|
|
1078
1374
|
active = true;
|
|
1375
|
+
generation++;
|
|
1079
1376
|
if (typeof window.addEventListener !== "function") return;
|
|
1080
1377
|
const redact = createRedactor();
|
|
1081
1378
|
try {
|
|
@@ -1089,10 +1386,14 @@ function createTrajectoryCollector(options) {
|
|
|
1089
1386
|
emit: capture,
|
|
1090
1387
|
redact
|
|
1091
1388
|
}));
|
|
1092
|
-
if (options.capture?.inputs !== false)
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1389
|
+
if (options.capture?.inputs !== false) {
|
|
1390
|
+
const inputs = installInputCapture({
|
|
1391
|
+
emit: capture,
|
|
1392
|
+
redact
|
|
1393
|
+
});
|
|
1394
|
+
flushInputs = inputs.flush;
|
|
1395
|
+
uninstalls.push(inputs);
|
|
1396
|
+
}
|
|
1096
1397
|
if (options.capture?.network !== false) uninstalls.push(installNetworkCapture({
|
|
1097
1398
|
emit: capture,
|
|
1098
1399
|
ignoreUrls: options.ignoreUrls ?? [],
|