@reticlehq/browser 3.6.0 → 3.7.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/dist/actions/action-report.d.ts +54 -0
- package/dist/actions/action-report.js +60 -0
- package/dist/actions/actions-dom.d.ts +1 -0
- package/dist/actions/actions-dom.js +12 -0
- package/dist/actions/actions-press.d.ts +1 -0
- package/dist/actions/actions-press.js +25 -1
- package/dist/actions/actions.d.ts +1 -37
- package/dist/actions/actions.js +24 -66
- package/dist/actions/appeared-text.js +3 -0
- package/dist/actions/danger-context.d.ts +2 -0
- package/dist/actions/danger-context.js +28 -2
- package/dist/actions/value-input.d.ts +1 -0
- package/dist/actions/value-input.js +11 -0
- package/dist/commands/commands.js +4 -0
- package/dist/dom/a11y.js +251 -10
- package/dist/dom/addressing/coverage-markers.d.ts +6 -0
- package/dist/dom/addressing/coverage-markers.js +13 -0
- package/dist/dom/dom-ignore.d.ts +2 -0
- package/dist/dom/dom-ignore.js +15 -2
- package/dist/dom/query.d.ts +1 -1
- package/dist/dom/query.js +78 -16
- package/dist/dom/render-page.d.ts +12 -0
- package/dist/dom/render-page.js +135 -0
- package/dist/dom/snapshot.js +55 -7
- package/dist/observers/dom.js +32 -16
- package/dist/observers/field.js +3 -21
- package/dist/observers/network.js +56 -7
- package/dist/observers/perf.js +53 -1
- package/dist/observers/route.js +6 -5
- package/dist/presenter/carousel/carousel.js +2 -7
- package/dist/presenter/carousel/panel-slides.d.ts +2 -4
- package/dist/presenter/carousel/panel-slides.js +17 -15
- package/dist/presenter/chrome/log-memory.d.ts +1 -0
- package/dist/presenter/chrome/presenter-log.d.ts +7 -0
- package/dist/presenter/chrome/presenter-log.js +48 -8
- package/dist/presenter/chrome/presenter-morph.d.ts +1 -0
- package/dist/presenter/chrome/presenter-morph.js +34 -0
- package/dist/presenter/chrome/presenter-report-copy.d.ts +17 -4
- package/dist/presenter/chrome/presenter-report-copy.js +26 -4
- package/dist/presenter/hud-telemetry.js +55 -4
- package/dist/presenter/presenter-account-styles.js +3 -3
- package/dist/presenter/presenter-account.d.ts +27 -5
- package/dist/presenter/presenter-account.js +83 -6
- package/dist/presenter/presenter-agent-link.d.ts +9 -0
- package/dist/presenter/presenter-agent-link.js +62 -0
- package/dist/presenter/presenter-chat-views.d.ts +27 -5
- package/dist/presenter/presenter-chat-views.js +312 -72
- package/dist/presenter/presenter-config.d.ts +2 -0
- package/dist/presenter/presenter-config.js +1 -0
- package/dist/presenter/presenter-controls.d.ts +0 -1
- package/dist/presenter/presenter-controls.js +13 -91
- package/dist/presenter/presenter-design-tokens.js +1 -1
- package/dist/presenter/presenter-frame.d.ts +1 -1
- package/dist/presenter/presenter-frame.js +8 -10
- package/dist/presenter/presenter-harness-row.d.ts +67 -0
- package/dist/presenter/presenter-harness-row.js +209 -0
- package/dist/presenter/presenter-personas.d.ts +35 -0
- package/dist/presenter/presenter-personas.js +95 -0
- package/dist/presenter/presenter-plan.js +25 -24
- package/dist/presenter/presenter-report-styles.js +40 -22
- package/dist/presenter/presenter-report.d.ts +2 -1
- package/dist/presenter/presenter-report.js +83 -17
- package/dist/presenter/presenter-settings-styles.js +8 -4
- package/dist/presenter/presenter-settings.d.ts +7 -6
- package/dist/presenter/presenter-settings.js +75 -34
- package/dist/presenter/presenter-shell-styles.js +12 -3
- package/dist/presenter/presenter-shell.d.ts +10 -4
- package/dist/presenter/presenter-shell.js +31 -23
- package/dist/presenter/presenter-styles.js +0 -2
- package/dist/presenter/presenter-workspace.js +10 -3
- package/dist/presenter/presenter.d.ts +3 -2
- package/dist/presenter/presenter.js +81 -20
- package/dist/registry/declared-platform.js +2 -0
- package/dist/reticle-inject.js +777 -700
- package/dist/reticle.js +36 -2
- package/dist/storage-keys.d.ts +3 -2
- package/dist/storage-keys.js +7 -3
- package/dist/transport/transport.js +5 -3
- package/dist/transport/unreachable-message.d.ts +1 -1
- package/dist/transport/unreachable-message.js +3 -5
- package/package.json +2 -2
- package/dist/presenter/carousel/offer-card.d.ts +0 -21
- package/dist/presenter/carousel/offer-card.js +0 -56
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { isSensitiveKey, STEP_SCREENSHOT } from '@reticlehq/core';
|
|
2
|
+
import { RETICLE_OVERLAY } from './dom-ignore.js';
|
|
3
|
+
export const MASK = '••••••••';
|
|
4
|
+
const BUDGET_CHARS = 240_000;
|
|
5
|
+
const CHUNK_CHARS = 60_000;
|
|
6
|
+
const SHRINK = 0.75;
|
|
7
|
+
const MAX_ATTEMPTS = 4;
|
|
8
|
+
const LOAD_TIMEOUT_MS = 5_000;
|
|
9
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
10
|
+
const FALLBACK_BACKGROUND = '#ffffff';
|
|
11
|
+
const JPEG_MIME = 'image/jpeg';
|
|
12
|
+
const DATA_URL_PREFIX = /^data:[^,]*,/;
|
|
13
|
+
function isSecretField(el) {
|
|
14
|
+
if (el instanceof HTMLInputElement && 'password' === el.type)
|
|
15
|
+
return true;
|
|
16
|
+
return [el.name, el.id, el.getAttribute('autocomplete') ?? ''].some((key) => key.length > 0 && isSensitiveKey(key));
|
|
17
|
+
}
|
|
18
|
+
function carryState(from, to) {
|
|
19
|
+
if (from instanceof HTMLInputElement || from instanceof HTMLTextAreaElement) {
|
|
20
|
+
const value = isSecretField(from) && from.value.length > 0 ? MASK : from.value;
|
|
21
|
+
if (from instanceof HTMLTextAreaElement)
|
|
22
|
+
to.textContent = value;
|
|
23
|
+
else
|
|
24
|
+
to.setAttribute('value', value);
|
|
25
|
+
}
|
|
26
|
+
if (from instanceof HTMLInputElement) {
|
|
27
|
+
if (from.checked)
|
|
28
|
+
to.setAttribute('checked', '');
|
|
29
|
+
else
|
|
30
|
+
to.removeAttribute('checked');
|
|
31
|
+
}
|
|
32
|
+
if (from instanceof HTMLOptionElement) {
|
|
33
|
+
if (from.selected)
|
|
34
|
+
to.setAttribute('selected', '');
|
|
35
|
+
else
|
|
36
|
+
to.removeAttribute('selected');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function readableCss(doc) {
|
|
40
|
+
const sheets = [...Array.from(doc.styleSheets), ...(doc.adoptedStyleSheets ?? [])];
|
|
41
|
+
const parts = [];
|
|
42
|
+
for (const sheet of sheets) {
|
|
43
|
+
try {
|
|
44
|
+
for (const rule of Array.from(sheet.cssRules))
|
|
45
|
+
parts.push(rule.cssText);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return parts.join('\n');
|
|
51
|
+
}
|
|
52
|
+
export function cloneForRender(doc) {
|
|
53
|
+
const root = doc.documentElement;
|
|
54
|
+
const clone = root.cloneNode(true);
|
|
55
|
+
const live = root.querySelectorAll('*');
|
|
56
|
+
const copied = clone.querySelectorAll('*');
|
|
57
|
+
live.forEach((el, i) => {
|
|
58
|
+
const twin = copied[i];
|
|
59
|
+
if (twin !== undefined)
|
|
60
|
+
carryState(el, twin);
|
|
61
|
+
});
|
|
62
|
+
const dropped = [RETICLE_OVERLAY, 'script', 'noscript', 'link[rel~="stylesheet"]', 'style'];
|
|
63
|
+
clone.querySelectorAll(dropped.join(',')).forEach((el) => el.remove());
|
|
64
|
+
const style = doc.createElement('style');
|
|
65
|
+
style.textContent = readableCss(doc);
|
|
66
|
+
(clone.querySelector('head') ?? clone).append(style);
|
|
67
|
+
return clone;
|
|
68
|
+
}
|
|
69
|
+
function backgroundOf(doc) {
|
|
70
|
+
const view = doc.defaultView;
|
|
71
|
+
for (const el of [doc.body, doc.documentElement]) {
|
|
72
|
+
if (null === view || null === el)
|
|
73
|
+
continue;
|
|
74
|
+
const colour = view.getComputedStyle(el).backgroundColor;
|
|
75
|
+
if (colour.length > 0 && 'transparent' !== colour && 'rgba(0, 0, 0, 0)' !== colour)
|
|
76
|
+
return colour;
|
|
77
|
+
}
|
|
78
|
+
return FALLBACK_BACKGROUND;
|
|
79
|
+
}
|
|
80
|
+
function loadImage(url) {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
const img = new Image();
|
|
83
|
+
const timer = setTimeout(() => reject(new Error('the page did not render in time')), LOAD_TIMEOUT_MS);
|
|
84
|
+
img.onload = () => {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
resolve(img);
|
|
87
|
+
};
|
|
88
|
+
img.onerror = () => {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
reject(new Error('the page could not be drawn as an image'));
|
|
91
|
+
};
|
|
92
|
+
img.src = url;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function chunked(base64) {
|
|
96
|
+
const out = [];
|
|
97
|
+
for (let i = 0; i < base64.length; i += CHUNK_CHARS)
|
|
98
|
+
out.push(base64.slice(i, i + CHUNK_CHARS));
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
export async function renderPage() {
|
|
102
|
+
try {
|
|
103
|
+
const width = Math.max(1, window.innerWidth);
|
|
104
|
+
const height = Math.max(1, window.innerHeight);
|
|
105
|
+
const clone = cloneForRender(document);
|
|
106
|
+
clone.style.transform = `translate(${String(-window.scrollX)}px, ${String(-window.scrollY)}px)`;
|
|
107
|
+
const xhtml = new XMLSerializer().serializeToString(clone);
|
|
108
|
+
const svg = `<svg xmlns="${SVG_NS}" width="${String(width)}" height="${String(height)}">` +
|
|
109
|
+
`<foreignObject x="0" y="0" width="100%" height="100%">${xhtml}</foreignObject></svg>`;
|
|
110
|
+
const img = await loadImage(`data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`);
|
|
111
|
+
const background = backgroundOf(document);
|
|
112
|
+
let scale = Math.min(1, STEP_SCREENSHOT.MAX_SIDE_PX / Math.max(width, height));
|
|
113
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1, scale *= SHRINK) {
|
|
114
|
+
const canvas = document.createElement('canvas');
|
|
115
|
+
canvas.width = Math.max(1, Math.round(width * scale));
|
|
116
|
+
canvas.height = Math.max(1, Math.round(height * scale));
|
|
117
|
+
const ctx = canvas.getContext('2d');
|
|
118
|
+
if (null === ctx)
|
|
119
|
+
return { ok: false, reason: 'this page has no 2D canvas to draw into' };
|
|
120
|
+
ctx.fillStyle = background;
|
|
121
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
122
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
123
|
+
const base64 = canvas
|
|
124
|
+
.toDataURL(JPEG_MIME, STEP_SCREENSHOT.JPEG_QUALITY)
|
|
125
|
+
.replace(DATA_URL_PREFIX, '');
|
|
126
|
+
if (base64.length <= BUDGET_CHARS) {
|
|
127
|
+
return { ok: true, width: canvas.width, height: canvas.height, chunks: chunked(base64) };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { ok: false, reason: 'the rendered page was too large to send, even scaled down' };
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
134
|
+
}
|
|
135
|
+
}
|
package/dist/dom/snapshot.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ElementState, SnapshotMode } from '@reticlehq/core';
|
|
2
2
|
import { capturedRootOf } from './shadow-registry.js';
|
|
3
|
-
import { isFrame } from './realm.js';
|
|
3
|
+
import { isElement, isFrame } from './realm.js';
|
|
4
4
|
import { getAccessibleName, getRole, getStates, getValue, isVisible } from './a11y.js';
|
|
5
5
|
import { refs } from './addressing/refs.js';
|
|
6
6
|
import { isIgnored, isReticleOverlay } from './dom-ignore.js';
|
|
@@ -30,13 +30,44 @@ function announces(el, role) {
|
|
|
30
30
|
}
|
|
31
31
|
const TEXT_MAX = 80;
|
|
32
32
|
function directText(el) {
|
|
33
|
-
|
|
33
|
+
const parts = readingParts(el);
|
|
34
|
+
const collapsed = (parts.own ? parts.text : '').replace(/\s+/g, ' ').trim();
|
|
35
|
+
return collapsed.length > TEXT_MAX ? `${collapsed.slice(0, TEXT_MAX)}…` : collapsed;
|
|
36
|
+
}
|
|
37
|
+
function readingParts(el) {
|
|
38
|
+
let text = '';
|
|
39
|
+
let own = false;
|
|
34
40
|
for (const node of el.childNodes) {
|
|
35
|
-
if (3 === node.nodeType)
|
|
36
|
-
|
|
41
|
+
if (3 === node.nodeType) {
|
|
42
|
+
text += node.textContent ?? '';
|
|
43
|
+
own ||= 0 < (node.textContent ?? '').trim().length;
|
|
44
|
+
}
|
|
45
|
+
else if (isElement(node) && foldsIntoParent(node)) {
|
|
46
|
+
const inner = readingParts(node);
|
|
47
|
+
text += inner.text;
|
|
48
|
+
own ||= inner.own;
|
|
49
|
+
}
|
|
50
|
+
else if (isElement(node) && isInlineIn(node) && INTERACTIVE.has(getRole(node))) {
|
|
51
|
+
text += getAccessibleName(node);
|
|
52
|
+
}
|
|
37
53
|
}
|
|
38
|
-
|
|
39
|
-
|
|
54
|
+
return { text, own };
|
|
55
|
+
}
|
|
56
|
+
function isInlineIn(el) {
|
|
57
|
+
const view = el.ownerDocument.defaultView;
|
|
58
|
+
const parent = el.parentElement;
|
|
59
|
+
if (null === view || null === parent)
|
|
60
|
+
return false;
|
|
61
|
+
const around = view.getComputedStyle(parent).display;
|
|
62
|
+
if (around.includes('flex') || around.includes('grid'))
|
|
63
|
+
return false;
|
|
64
|
+
return 'inline' === view.getComputedStyle(el).display;
|
|
65
|
+
}
|
|
66
|
+
function foldsIntoParent(el) {
|
|
67
|
+
if (skipEarly(el) || !isInlineIn(el))
|
|
68
|
+
return false;
|
|
69
|
+
const role = getRole(el);
|
|
70
|
+
return !INTERACTIVE.has(role) && !announces(el, role) && 0 === getAccessibleName(el).length;
|
|
40
71
|
}
|
|
41
72
|
export const MAX_UNREAD_BRANCHES = 50;
|
|
42
73
|
function hiddenByMarkup(el) {
|
|
@@ -83,6 +114,7 @@ function layoutSignature(style) {
|
|
|
83
114
|
}
|
|
84
115
|
return '';
|
|
85
116
|
}
|
|
117
|
+
const overlayStamped = (el) => el.hasAttribute('data-aria-hidden') && 'true' === el.getAttribute('aria-hidden');
|
|
86
118
|
function recordUnread(branches, ctx) {
|
|
87
119
|
for (const branch of branches) {
|
|
88
120
|
if (ctx.unread.length >= MAX_UNREAD_BRANCHES) {
|
|
@@ -109,7 +141,11 @@ function pierceChildren(parent) {
|
|
|
109
141
|
return out;
|
|
110
142
|
}
|
|
111
143
|
function visit(child, depth, ctx, inLive) {
|
|
112
|
-
|
|
144
|
+
const stampedOnly = true === ctx.throughOverlay &&
|
|
145
|
+
overlayStamped(child) &&
|
|
146
|
+
!isIgnored(child) &&
|
|
147
|
+
!(child instanceof HTMLElement && child.hidden);
|
|
148
|
+
if (!stampedOnly && skipEarly(child)) {
|
|
113
149
|
if (hiddenByMarkup(child))
|
|
114
150
|
ctx.hiddenSkipped += 1;
|
|
115
151
|
return;
|
|
@@ -120,6 +156,10 @@ function visit(child, depth, ctx, inLive) {
|
|
|
120
156
|
ctx.hiddenSkipped += 1;
|
|
121
157
|
return;
|
|
122
158
|
}
|
|
159
|
+
if (style !== null && 'inline' === style.display && !inLive && foldsIntoParent(child)) {
|
|
160
|
+
walk(child, depth, ctx, inLive);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
123
163
|
const role = getRole(child);
|
|
124
164
|
const name = getAccessibleName(child);
|
|
125
165
|
const interactive = INTERACTIVE.has(role);
|
|
@@ -174,6 +214,7 @@ function collectDialogs(root) {
|
|
|
174
214
|
}
|
|
175
215
|
return names;
|
|
176
216
|
}
|
|
217
|
+
const MODAL_SELECTOR = '[role="dialog"], [role="alertdialog"], dialog[open], [aria-modal="true"]';
|
|
177
218
|
function overlayHidingPage(root) {
|
|
178
219
|
if (root !== document.body)
|
|
179
220
|
return undefined;
|
|
@@ -242,6 +283,13 @@ export function buildSnapshot(options = {}) {
|
|
|
242
283
|
visit(root, 0, ctx, false);
|
|
243
284
|
else
|
|
244
285
|
walk(root, 0, ctx);
|
|
286
|
+
if (0 === ctx.nodes && root === document.body) {
|
|
287
|
+
ctx.throughOverlay = true;
|
|
288
|
+
for (const child of Array.from(root.children))
|
|
289
|
+
if (overlayStamped(child) && null !== child.querySelector(MODAL_SELECTOR))
|
|
290
|
+
visit(child, 0, ctx, false);
|
|
291
|
+
ctx.throughOverlay = false;
|
|
292
|
+
}
|
|
245
293
|
return {
|
|
246
294
|
tree: ctx.lines.join('\n'),
|
|
247
295
|
status,
|
package/dist/observers/dom.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readTestId, testIdSelector } from '../dom/addressing/testid-attr.js';
|
|
2
|
-
import { EventType, TruncationChannel } from '@reticlehq/core';
|
|
3
|
-
import { getAccessibleName, getRole, isVisible } from '../dom/a11y.js';
|
|
2
|
+
import { EventType, REDACTED_VALUE, TruncationChannel } from '@reticlehq/core';
|
|
3
|
+
import { getAccessibleName, getRole, isSensitiveField, isVisible } from '../dom/a11y.js';
|
|
4
4
|
import { refs } from '../dom/addressing/refs.js';
|
|
5
5
|
function regionKeyOf(target) {
|
|
6
6
|
const el = isElement(target) ? target : null;
|
|
@@ -9,7 +9,7 @@ function regionKeyOf(target) {
|
|
|
9
9
|
const labelled = el.closest(testIdSelector());
|
|
10
10
|
return (labelled ? readTestId(labelled) : null) ?? refs.refFor(el);
|
|
11
11
|
}
|
|
12
|
-
import {
|
|
12
|
+
import { isUnobserved } from '../dom/dom-ignore.js';
|
|
13
13
|
import { capturedRoots, installShadowRegistry, onShadowRoot } from '../dom/shadow-registry.js';
|
|
14
14
|
import { isElement } from '../dom/realm.js';
|
|
15
15
|
import { observeSameOriginFrames } from './frames.js';
|
|
@@ -51,7 +51,7 @@ function describeMount(node) {
|
|
|
51
51
|
if (scanned >= MOUNT_SCAN_LIMIT)
|
|
52
52
|
return undefined;
|
|
53
53
|
scanned += 1;
|
|
54
|
-
if (
|
|
54
|
+
if (isUnobserved(child))
|
|
55
55
|
continue;
|
|
56
56
|
const childRole = getRole(child);
|
|
57
57
|
const childName = getAccessibleName(child);
|
|
@@ -71,6 +71,18 @@ function textOf(nodes) {
|
|
|
71
71
|
const trimmed = out.trim();
|
|
72
72
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
73
73
|
}
|
|
74
|
+
const SAFE_ON_SECRET = new Set([
|
|
75
|
+
'class',
|
|
76
|
+
'style',
|
|
77
|
+
'type',
|
|
78
|
+
'disabled',
|
|
79
|
+
'readonly',
|
|
80
|
+
'required',
|
|
81
|
+
'hidden',
|
|
82
|
+
]);
|
|
83
|
+
function secretAttr(el, attr) {
|
|
84
|
+
return !SAFE_ON_SECRET.has(attr) && !attr.startsWith('aria-') && isSensitiveField(el);
|
|
85
|
+
}
|
|
74
86
|
export function installDom(emit) {
|
|
75
87
|
const stopRegistry = installShadowRegistry();
|
|
76
88
|
const observer = new MutationObserver((records) => {
|
|
@@ -81,14 +93,15 @@ export function installDom(emit) {
|
|
|
81
93
|
for (const record of records) {
|
|
82
94
|
if ('attributes' === record.type) {
|
|
83
95
|
const target = record.target;
|
|
84
|
-
if (isElement(target) && record.attributeName !== null && !
|
|
96
|
+
if (isElement(target) && record.attributeName !== null && !isUnobserved(target)) {
|
|
85
97
|
if (changed >= MAX_PER_BATCH) {
|
|
86
98
|
dropped += 1;
|
|
87
99
|
continue;
|
|
88
100
|
}
|
|
89
101
|
changed += 1;
|
|
90
|
-
const
|
|
91
|
-
const
|
|
102
|
+
const hide = secretAttr(target, record.attributeName);
|
|
103
|
+
const value = hide ? REDACTED_VALUE : capValue(target.getAttribute(record.attributeName));
|
|
104
|
+
const old = hide && null !== record.oldValue ? REDACTED_VALUE : capValue(record.oldValue);
|
|
92
105
|
emit(EventType.DOM_ATTR, {
|
|
93
106
|
attr: record.attributeName,
|
|
94
107
|
...(value === undefined ? {} : { value }),
|
|
@@ -99,27 +112,31 @@ export function installDom(emit) {
|
|
|
99
112
|
}
|
|
100
113
|
if ('characterData' === record.type) {
|
|
101
114
|
const parent = record.target.parentElement;
|
|
102
|
-
if (parent !== null && !
|
|
115
|
+
if (parent !== null && !isUnobserved(parent)) {
|
|
103
116
|
if (changed >= MAX_PER_BATCH) {
|
|
104
117
|
dropped += 1;
|
|
105
118
|
continue;
|
|
106
119
|
}
|
|
107
120
|
changed += 1;
|
|
108
|
-
const
|
|
109
|
-
const
|
|
121
|
+
const hide = isSensitiveField(parent);
|
|
122
|
+
const text = hide
|
|
123
|
+
? REDACTED_VALUE
|
|
124
|
+
: (record.target.textContent ?? '').trim().slice(0, 80);
|
|
125
|
+
const old = hide ? undefined : capValue(record.oldValue?.trim() ?? null);
|
|
110
126
|
emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }) }, refs.refFor(parent));
|
|
111
127
|
}
|
|
112
128
|
continue;
|
|
113
129
|
}
|
|
114
130
|
if (textOf(record.addedNodes) !== undefined || textOf(record.removedNodes) !== undefined) {
|
|
115
131
|
const parent = isElement(record.target) ? record.target : null;
|
|
116
|
-
if (parent !== null && !
|
|
132
|
+
if (parent !== null && !isUnobserved(parent)) {
|
|
117
133
|
if (changed >= MAX_PER_BATCH)
|
|
118
134
|
dropped += 1;
|
|
119
135
|
else {
|
|
120
136
|
changed += 1;
|
|
121
|
-
const
|
|
122
|
-
const
|
|
137
|
+
const hide = isSensitiveField(parent);
|
|
138
|
+
const text = hide ? REDACTED_VALUE : (parent.textContent ?? '').trim().slice(0, 80);
|
|
139
|
+
const old = hide ? undefined : capValue(textOf(record.removedNodes) ?? null);
|
|
123
140
|
emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }) }, refs.refFor(parent));
|
|
124
141
|
}
|
|
125
142
|
}
|
|
@@ -131,7 +148,7 @@ export function installDom(emit) {
|
|
|
131
148
|
dropped += 1;
|
|
132
149
|
continue;
|
|
133
150
|
}
|
|
134
|
-
if (
|
|
151
|
+
if (isUnobserved(node))
|
|
135
152
|
continue;
|
|
136
153
|
const mounted = describeMount(node);
|
|
137
154
|
if (mounted === undefined)
|
|
@@ -154,8 +171,7 @@ export function installDom(emit) {
|
|
|
154
171
|
dropped += 1;
|
|
155
172
|
continue;
|
|
156
173
|
}
|
|
157
|
-
if (
|
|
158
|
-
(isElement(record.target) && isReticleOverlay(record.target))) {
|
|
174
|
+
if (isUnobserved(node) || (isElement(record.target) && isUnobserved(record.target))) {
|
|
159
175
|
continue;
|
|
160
176
|
}
|
|
161
177
|
const gone = describeMount(node);
|
package/dist/observers/field.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { readTestId } from '../dom/addressing/testid-attr.js';
|
|
2
|
-
import {
|
|
3
|
-
import { getAccessibleName } from '../dom/a11y.js';
|
|
2
|
+
import { EventType } from '@reticlehq/core';
|
|
3
|
+
import { getAccessibleName, isSensitiveField } from '../dom/a11y.js';
|
|
4
4
|
const MAX_VALUE_LEN = 160;
|
|
5
5
|
const BURST_MS = 150;
|
|
6
|
-
const PAYMENT_AUTOCOMPLETE = /^(cc-|current-password|new-password)/i;
|
|
7
6
|
function isField(target) {
|
|
8
7
|
return (target instanceof HTMLInputElement ||
|
|
9
8
|
target instanceof HTMLTextAreaElement ||
|
|
@@ -21,30 +20,13 @@ function fieldName(element) {
|
|
|
21
20
|
const id = element.getAttribute('id');
|
|
22
21
|
return null !== id && id.length > 0 ? id : element.tagName.toLowerCase();
|
|
23
22
|
}
|
|
24
|
-
function isSecret(element) {
|
|
25
|
-
if (element instanceof HTMLInputElement && 'password' === element.type)
|
|
26
|
-
return true;
|
|
27
|
-
const hint = element.getAttribute('autocomplete');
|
|
28
|
-
if (null !== hint && PAYMENT_AUTOCOMPLETE.test(hint))
|
|
29
|
-
return true;
|
|
30
|
-
for (const key of [
|
|
31
|
-
element.name,
|
|
32
|
-
readTestId(element),
|
|
33
|
-
element.getAttribute(DEFAULT_TESTID_ATTR),
|
|
34
|
-
element.getAttribute('id'),
|
|
35
|
-
]) {
|
|
36
|
-
if (null !== key && key.length > 0 && isSensitiveKey(key))
|
|
37
|
-
return true;
|
|
38
|
-
}
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
23
|
export function installField(emit) {
|
|
42
24
|
const ac = new AbortController();
|
|
43
25
|
const { signal } = ac;
|
|
44
26
|
const pending = new Map();
|
|
45
27
|
const report = (element, kind) => {
|
|
46
28
|
const value = element.value;
|
|
47
|
-
const secret =
|
|
29
|
+
const secret = isSensitiveField(element);
|
|
48
30
|
emit(EventType.FIELD_CHANGE, {
|
|
49
31
|
field: fieldName(element),
|
|
50
32
|
kind,
|
|
@@ -63,15 +63,64 @@ function jsonShape(text) {
|
|
|
63
63
|
return ShapeToken.OTHER;
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
const MAX_DECODED_BODY_BYTES = 1 << 20;
|
|
67
|
+
const TAB = 0x09;
|
|
68
|
+
const LINE_FEED = 0x0a;
|
|
69
|
+
const CARRIAGE_RETURN = 0x0d;
|
|
70
|
+
const FIRST_PRINTABLE = 0x20;
|
|
71
|
+
const DELETE = 0x7f;
|
|
72
|
+
function hasControlCharacter(text) {
|
|
73
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
74
|
+
const code = text.charCodeAt(i);
|
|
75
|
+
if (code === DELETE)
|
|
76
|
+
return true;
|
|
77
|
+
if (code < FIRST_PRINTABLE && code !== TAB && code !== LINE_FEED && code !== CARRIAGE_RETURN) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
function textOfBytes(body) {
|
|
84
|
+
if (!(body instanceof ArrayBuffer || ArrayBuffer.isView(body)))
|
|
85
|
+
return undefined;
|
|
86
|
+
if (body.byteLength > MAX_DECODED_BODY_BYTES)
|
|
87
|
+
return undefined;
|
|
88
|
+
try {
|
|
89
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(body);
|
|
90
|
+
return hasControlCharacter(text) ? undefined : text;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function formShape(form) {
|
|
97
|
+
const fields = [];
|
|
98
|
+
let length = 0;
|
|
99
|
+
form.forEach((value, name) => {
|
|
100
|
+
if ('string' === typeof value)
|
|
101
|
+
length += value.length;
|
|
102
|
+
fields.push(JSON.stringify([name, 'string' === typeof value ? 0 : [value.type, value.size]]));
|
|
103
|
+
});
|
|
104
|
+
return 0 === fields.length ? undefined : fingerprintBody(fields.sort().join(), length);
|
|
105
|
+
}
|
|
66
106
|
function projectRequestBody(body, captureBodies) {
|
|
67
107
|
let text;
|
|
68
108
|
let contentType = 'application/json';
|
|
69
109
|
let shape = REQUEST_SHAPE_NONE;
|
|
70
|
-
|
|
71
|
-
|
|
110
|
+
const bytesText = textOfBytes(body);
|
|
111
|
+
const typeOf = () => body.constructor?.name ?? typeof body;
|
|
112
|
+
if ('string' === typeof body || bytesText !== undefined) {
|
|
113
|
+
text = bytesText ?? body;
|
|
72
114
|
if (text.length > 0)
|
|
73
115
|
shape = fingerprintBody(jsonShape(text), text.length);
|
|
74
116
|
}
|
|
117
|
+
else if (typeof FormData !== 'undefined' && body instanceof FormData) {
|
|
118
|
+
const form = formShape(body);
|
|
119
|
+
return {
|
|
120
|
+
...(form === undefined ? {} : { [REQUEST_SHAPE_FIELD]: form }),
|
|
121
|
+
...(captureBodies ? { requestBodyType: typeOf() } : {}),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
75
124
|
else if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
|
|
76
125
|
text = body.toString();
|
|
77
126
|
contentType = 'application/x-www-form-urlencoded';
|
|
@@ -81,8 +130,7 @@ function projectRequestBody(body, captureBodies) {
|
|
|
81
130
|
}
|
|
82
131
|
}
|
|
83
132
|
else if (body !== undefined && body !== null) {
|
|
84
|
-
|
|
85
|
-
return captureBodies ? { requestBodyType: kind } : {};
|
|
133
|
+
return captureBodies ? { requestBodyType: typeOf() } : {};
|
|
86
134
|
}
|
|
87
135
|
const fields = { [REQUEST_SHAPE_FIELD]: shape };
|
|
88
136
|
if (!captureBodies || text === undefined || 0 === text.length)
|
|
@@ -202,6 +250,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
202
250
|
const method = methodOf(input, init);
|
|
203
251
|
const nextAction = nextActionOf(input, init);
|
|
204
252
|
const nextActionFields = nextAction === undefined ? {} : { [NEXT_ACTION_FIELD]: nextAction };
|
|
253
|
+
const bodyFields = observeValue(() => projectRequestBody(init?.body, captureBodies)) ?? {};
|
|
205
254
|
const urlFields = netUrlFields(rawUrl);
|
|
206
255
|
const url = urlFields.url;
|
|
207
256
|
const initiatorStack = observeValue(() => initiatorFrame());
|
|
@@ -236,7 +285,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
236
285
|
...initiatorFields,
|
|
237
286
|
...resourceTiming(rawUrl),
|
|
238
287
|
...netResponseMeta(res.statusText, contentType, res.headers.get('content-length')),
|
|
239
|
-
...
|
|
288
|
+
...bodyFields,
|
|
240
289
|
...nextActionFields,
|
|
241
290
|
...responseBodyFields,
|
|
242
291
|
...(reinterpret?.(url, (name) => res.headers.get(name)) ?? {}),
|
|
@@ -326,7 +375,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
326
375
|
const m = meta.get(this);
|
|
327
376
|
if (m !== undefined) {
|
|
328
377
|
m.start = performance.now();
|
|
329
|
-
m.
|
|
378
|
+
m.reqFields = observeValue(() => projectRequestBody(body ?? null, captureBodies)) ?? {};
|
|
330
379
|
m.initiatorStack = initiatorFrame();
|
|
331
380
|
const initiatorFields = m.initiatorStack === undefined ? {} : { initiatorStack: m.initiatorStack };
|
|
332
381
|
emit(EventType.NET_PENDING, {
|
|
@@ -375,7 +424,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
375
424
|
...(cur.initiatorStack === undefined ? {} : { initiatorStack: cur.initiatorStack }),
|
|
376
425
|
...resourceTiming(cur.rawUrl),
|
|
377
426
|
...netResponseMeta(this.statusText, xhrContentType, this.getResponseHeader('content-length')),
|
|
378
|
-
...
|
|
427
|
+
...cur.reqFields,
|
|
379
428
|
...responseBodyFields,
|
|
380
429
|
});
|
|
381
430
|
});
|
package/dist/observers/perf.js
CHANGED
|
@@ -1,4 +1,50 @@
|
|
|
1
1
|
import { EventType, PerfMetric } from '@reticlehq/core';
|
|
2
|
+
import { refs } from '../dom/addressing/refs.js';
|
|
3
|
+
import { getTestIdAttr, readTestId } from '../dom/addressing/testid-attr.js';
|
|
4
|
+
import { isReticleOverlay } from '../dom/dom-ignore.js';
|
|
5
|
+
import { isElement } from '../dom/realm.js';
|
|
6
|
+
const areaOf = (r) => r === undefined ? 0 : Math.max(0, r.width) * Math.max(0, r.height);
|
|
7
|
+
function cssIdent(value) {
|
|
8
|
+
const css = globalThis.CSS;
|
|
9
|
+
if ('function' === typeof css?.escape)
|
|
10
|
+
return css.escape(value);
|
|
11
|
+
let out = '';
|
|
12
|
+
for (const [i, ch] of [...value].entries()) {
|
|
13
|
+
const startsWithDigit = /\d/.test(ch) && (0 === i || (1 === i && value.startsWith('-')));
|
|
14
|
+
if (startsWithDigit)
|
|
15
|
+
out += `\\${ch.charCodeAt(0).toString(16)} `;
|
|
16
|
+
else if (/[\w-]/.test(ch))
|
|
17
|
+
out += ch;
|
|
18
|
+
else
|
|
19
|
+
out += `\\${ch}`;
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
function shortSelector(el) {
|
|
24
|
+
const testid = readTestId(el);
|
|
25
|
+
if (null !== testid)
|
|
26
|
+
return `[${getTestIdAttr()}="${testid.replace(/["\\]/g, '\\$&')}"]`;
|
|
27
|
+
const tag = el.tagName.toLowerCase();
|
|
28
|
+
if (0 < el.id.length)
|
|
29
|
+
return `${tag}#${cssIdent(el.id)}`;
|
|
30
|
+
const cls = el.classList.item(0);
|
|
31
|
+
return null === cls ? tag : `${tag}.${cssIdent(cls)}`;
|
|
32
|
+
}
|
|
33
|
+
function largestShifted(sources) {
|
|
34
|
+
let best;
|
|
35
|
+
for (const source of sources ?? []) {
|
|
36
|
+
const node = source.node ?? null;
|
|
37
|
+
const el = null === node ? null : isElement(node) ? node : node.parentElement;
|
|
38
|
+
if (null === el || isReticleOverlay(el))
|
|
39
|
+
continue;
|
|
40
|
+
const area = Math.max(areaOf(source.previousRect), areaOf(source.currentRect));
|
|
41
|
+
if (best === undefined || area > best.area)
|
|
42
|
+
best = { el, area };
|
|
43
|
+
}
|
|
44
|
+
return best === undefined
|
|
45
|
+
? undefined
|
|
46
|
+
: { ref: refs.refFor(best.el), selector: shortSelector(best.el) };
|
|
47
|
+
}
|
|
2
48
|
export function installPerf(emit) {
|
|
3
49
|
if (typeof PerformanceObserver !== 'function')
|
|
4
50
|
return () => undefined;
|
|
@@ -29,7 +75,13 @@ export function installPerf(emit) {
|
|
|
29
75
|
if (true === ls.hadRecentInput)
|
|
30
76
|
return;
|
|
31
77
|
cls += ls.value ?? 0;
|
|
32
|
-
|
|
78
|
+
const shifted = largestShifted(ls.sources);
|
|
79
|
+
emit(EventType.PERF, {
|
|
80
|
+
metric: PerfMetric.CLS,
|
|
81
|
+
value: cls,
|
|
82
|
+
at: Math.round(e.startTime),
|
|
83
|
+
...(shifted === undefined ? {} : { shifted }),
|
|
84
|
+
});
|
|
33
85
|
});
|
|
34
86
|
observe('longtask', (e) => {
|
|
35
87
|
emit(EventType.PERF, {
|
package/dist/observers/route.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EventType } from '@reticlehq/core';
|
|
1
|
+
import { EventType, ROUTE_CHANGE_HOW_FIELD, RouteChangeHow } from '@reticlehq/core';
|
|
2
2
|
import { observeSafely } from './types.js';
|
|
3
3
|
import { captureMethod } from '../patching/capture-method.js';
|
|
4
4
|
function snapshotLocation() {
|
|
@@ -16,7 +16,7 @@ export function installRoute(emit) {
|
|
|
16
16
|
const callPush = origPush.bind(history);
|
|
17
17
|
const callReplace = origReplace.bind(history);
|
|
18
18
|
let lastHref = location.href;
|
|
19
|
-
const fire = (from) => {
|
|
19
|
+
const fire = (from, how) => {
|
|
20
20
|
observeSafely(() => {
|
|
21
21
|
const to = snapshotLocation();
|
|
22
22
|
if (to.href === from)
|
|
@@ -28,23 +28,24 @@ export function installRoute(emit) {
|
|
|
28
28
|
pathname: to.pathname,
|
|
29
29
|
search: to.search,
|
|
30
30
|
hash: to.hash,
|
|
31
|
+
[ROUTE_CHANGE_HOW_FIELD]: how,
|
|
31
32
|
});
|
|
32
33
|
});
|
|
33
34
|
};
|
|
34
35
|
const patchedPush = (data, unused, url) => {
|
|
35
36
|
const from = location.href;
|
|
36
37
|
callPush(data, unused, url ?? null);
|
|
37
|
-
fire(from);
|
|
38
|
+
fire(from, RouteChangeHow.PUSH);
|
|
38
39
|
};
|
|
39
40
|
const patchedReplace = (data, unused, url) => {
|
|
40
41
|
const from = location.href;
|
|
41
42
|
callReplace(data, unused, url ?? null);
|
|
42
|
-
fire(from);
|
|
43
|
+
fire(from, RouteChangeHow.REPLACE);
|
|
43
44
|
};
|
|
44
45
|
history.pushState = patchedPush;
|
|
45
46
|
history.replaceState = patchedReplace;
|
|
46
47
|
const onNav = () => {
|
|
47
|
-
fire(lastHref);
|
|
48
|
+
fire(lastHref, RouteChangeHow.POP);
|
|
48
49
|
};
|
|
49
50
|
window.addEventListener('popstate', onNav, { signal: ac.signal });
|
|
50
51
|
window.addEventListener('hashchange', onNav, { signal: ac.signal });
|
|
@@ -135,7 +135,7 @@ function startRotation(log, root, count) {
|
|
|
135
135
|
export const CAROUSEL_CSS = `
|
|
136
136
|
.reticle-carousel{position:relative;margin:8px 10px;padding:10px 12px 8px;border:1px solid var(--reticle-line,#2a2f3a);border-radius:10px;background:var(--reticle-surface-inset,rgba(255,255,255,.03));}
|
|
137
137
|
.reticle-carousel-track{height:100px;overflow:hidden;}
|
|
138
|
-
.reticle-carousel-slide .reticle-
|
|
138
|
+
.reticle-carousel-slide .reticle-talk{margin:0;padding:0;border:0;background:none;}
|
|
139
139
|
.reticle-carousel-close{position:absolute;top:4px;right:6px;width:20px;height:20px;padding:0;border:0;background:none;color:inherit;opacity:.55;cursor:pointer;font-size:14px;line-height:20px;}
|
|
140
140
|
.reticle-carousel-close:hover{opacity:1;}
|
|
141
141
|
.reticle-carousel-dots{display:flex;justify-content:center;gap:6px;margin-top:6px;}
|
|
@@ -144,10 +144,5 @@ export const CAROUSEL_CSS = `
|
|
|
144
144
|
.reticle-carousel-controls{display:flex;align-items:center;gap:8px;margin-top:6px;}
|
|
145
145
|
.reticle-carousel-arrow{width:24px;height:24px;flex:none;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--reticle-line);border-radius:6px;background:rgba(255,255,255,.04);color:var(--reticle-fg);font:inherit;font-size:18px;line-height:1;cursor:pointer;}
|
|
146
146
|
.reticle-carousel-arrow:hover{border-color:var(--reticle-accent);background:var(--reticle-accent-soft);}
|
|
147
|
-
.reticle-carousel-position{margin-left:auto;color:var(--reticle-muted);font-size:
|
|
148
|
-
.reticle-carousel-slide .reticle-offer{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:3px 8px;margin:0;padding:0;border:0;background:none;}
|
|
149
|
-
.reticle-carousel-slide .reticle-offer-title{grid-column:1/-1;overflow:hidden;padding:0;font-size:11px;text-overflow:ellipsis;white-space:nowrap;}
|
|
150
|
-
.reticle-carousel-slide .reticle-offer-body{grid-column:1;grid-row:2;margin:0;overflow:hidden;font-size:10px;line-height:1.2;text-overflow:ellipsis;white-space:nowrap;}
|
|
151
|
-
.reticle-carousel-slide .reticle-offer-claim{grid-column:2;grid-row:2;padding:3px 7px;font-size:10px;white-space:nowrap;}
|
|
152
|
-
.reticle-carousel-slide .reticle-offer-claimed{overflow:hidden;margin:0;text-overflow:ellipsis;white-space:nowrap;}
|
|
147
|
+
.reticle-carousel-position{margin-left:auto;color:var(--reticle-muted);font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap;}
|
|
153
148
|
`;
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { type OfferState } from './offer-card.js';
|
|
2
1
|
import type { Slide } from './carousel.js';
|
|
3
2
|
export declare const SLIDE_ID: {
|
|
4
|
-
readonly HARNESS: "harness-journeys";
|
|
5
3
|
readonly REPLAY: "replay-confidence";
|
|
6
|
-
readonly
|
|
4
|
+
readonly NOTES: "notes-for-agent";
|
|
7
5
|
};
|
|
8
|
-
export declare function panelSlides(
|
|
6
|
+
export declare function panelSlides(notices?: readonly unknown[], base?: string): Slide[];
|