@reticlehq/browser 3.7.0 → 3.8.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 +10 -0
- package/dist/actions/action-report.js +29 -2
- package/dist/actions/actions-dom.d.ts +4 -0
- package/dist/actions/actions-dom.js +20 -4
- package/dist/actions/actions.d.ts +1 -0
- package/dist/actions/actions.js +35 -17
- package/dist/actions/appeared-text.js +2 -2
- package/dist/actions/click-target.d.ts +1 -0
- package/dist/actions/click-target.js +14 -0
- package/dist/actions/danger-context.d.ts +2 -1
- package/dist/actions/danger-context.js +20 -5
- package/dist/activate.d.ts +14 -0
- package/dist/activate.js +43 -0
- package/dist/busy.d.ts +3 -0
- package/dist/busy.js +42 -0
- package/dist/commands/commands.js +45 -17
- package/dist/connect-options.d.ts +12 -1
- package/dist/connect-options.js +18 -1
- package/dist/dom/a11y.d.ts +2 -0
- package/dist/dom/a11y.js +191 -159
- package/dist/dom/chart.d.ts +3 -2
- package/dist/dom/chart.js +3 -2
- package/dist/dom/dom-ignore.d.ts +6 -0
- package/dist/dom/dom-ignore.js +43 -1
- package/dist/dom/layout-audit.d.ts +15 -0
- package/dist/dom/layout-audit.js +208 -0
- package/dist/dom/paint-context.d.ts +15 -0
- package/dist/dom/paint-context.js +78 -0
- package/dist/dom/query.d.ts +2 -1
- package/dist/dom/query.js +97 -34
- package/dist/dom/shadow-registry.d.ts +1 -0
- package/dist/dom/shadow-registry.js +27 -0
- package/dist/dom/snapshot.d.ts +5 -0
- package/dist/dom/snapshot.js +85 -34
- package/dist/dom/stable-selector.d.ts +1 -0
- package/dist/dom/stable-selector.js +19 -0
- package/dist/dom/text-match.d.ts +12 -0
- package/dist/dom/text-match.js +115 -0
- package/dist/dom/theme.d.ts +1 -0
- package/dist/dom/theme.js +21 -9
- package/dist/dom/virtualized.js +5 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -0
- package/dist/observers/blind-spots.js +3 -2
- package/dist/observers/canvas.d.ts +18 -0
- package/dist/observers/canvas.js +76 -0
- package/dist/observers/dom.js +66 -18
- package/dist/observers/field.d.ts +11 -0
- package/dist/observers/field.js +117 -13
- package/dist/observers/install-all.js +4 -0
- package/dist/observers/media.d.ts +2 -0
- package/dist/observers/media.js +32 -0
- package/dist/observers/net-detail/network-body.d.ts +2 -0
- package/dist/observers/net-detail/network-body.js +8 -1
- package/dist/observers/network.js +26 -6
- package/dist/observers/route.d.ts +2 -0
- package/dist/observers/route.js +33 -4
- package/dist/presenter/chrome/presenter-report-copy.d.ts +0 -5
- package/dist/presenter/chrome/presenter-report-copy.js +0 -14
- package/dist/presenter/hud-telemetry.js +1 -1
- package/dist/presenter/presenter-chat-views.d.ts +3 -4
- package/dist/presenter/presenter-chat-views.js +47 -20
- package/dist/presenter/presenter-config.d.ts +1 -0
- package/dist/presenter/presenter-config.js +1 -0
- package/dist/presenter/presenter-frame.d.ts +3 -2
- package/dist/presenter/presenter-frame.js +24 -15
- package/dist/presenter/presenter-harness-row.d.ts +4 -15
- package/dist/presenter/presenter-harness-row.js +11 -50
- package/dist/presenter/presenter-report-styles.js +0 -17
- package/dist/presenter/presenter-report.d.ts +1 -2
- package/dist/presenter/presenter-report.js +4 -54
- package/dist/presenter/presenter-run-state.js +2 -1
- package/dist/presenter/presenter-settings.d.ts +2 -3
- package/dist/presenter/presenter-settings.js +14 -17
- package/dist/presenter/presenter-shell.d.ts +6 -4
- package/dist/presenter/presenter-shell.js +37 -7
- package/dist/presenter/presenter.js +32 -11
- package/dist/registry/store-adapters.d.ts +5 -2
- package/dist/registry/store-adapters.js +40 -10
- package/dist/reticle-inject.js +602 -611
- package/dist/reticle.d.ts +4 -2
- package/dist/reticle.js +36 -6
- package/dist/security/page-location.d.ts +2 -0
- package/dist/security/page-location.js +7 -0
- package/dist/security/serialization.js +1 -1
- package/dist/storage-keys.d.ts +1 -0
- package/dist/storage-keys.js +1 -0
- package/dist/timers/native/native-timers.d.ts +1 -0
- package/dist/timers/native/native-timers.js +5 -0
- package/dist/transport/transport.d.ts +1 -0
- package/dist/transport/transport.js +18 -0
- package/package.json +6 -2
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { CanvasTrigger, EventType } from '@reticlehq/core';
|
|
2
|
+
export { CanvasTrigger };
|
|
3
|
+
import { refs } from '../dom/addressing/refs.js';
|
|
4
|
+
import { nativeSetInterval } from '../timers/native/native-timers.js';
|
|
5
|
+
import { observeSafely, observeValue } from './types.js';
|
|
6
|
+
const SIDE = 16;
|
|
7
|
+
const POLL_MS = 500;
|
|
8
|
+
const MAX_CANVASES = 4;
|
|
9
|
+
const FNV_OFFSET = 0x811c9dc5;
|
|
10
|
+
const FNV_PRIME = 0x01000193;
|
|
11
|
+
const QUANTUM = 8;
|
|
12
|
+
export function digestPixels(data) {
|
|
13
|
+
let hash = FNV_OFFSET;
|
|
14
|
+
const sum = [0, 0, 0];
|
|
15
|
+
const pixels = Math.max(1, data.length / 4);
|
|
16
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
17
|
+
for (let c = 0; c < 3; c += 1) {
|
|
18
|
+
const value = data[i + c] ?? 0;
|
|
19
|
+
sum[c] = (sum[c] ?? 0) + value;
|
|
20
|
+
hash = Math.imul(hash ^ Math.floor(value / QUANTUM), FNV_PRIME) >>> 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const mean = sum.map((s) => Math.round(s / pixels));
|
|
24
|
+
return { digest: hash.toString(16).padStart(8, '0'), mean };
|
|
25
|
+
}
|
|
26
|
+
function sampleCanvas(canvas) {
|
|
27
|
+
if (0 === canvas.width || 0 === canvas.height) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const scratch = document.createElement('canvas');
|
|
31
|
+
scratch.width = SIDE;
|
|
32
|
+
scratch.height = SIDE;
|
|
33
|
+
const ctx = scratch.getContext('2d');
|
|
34
|
+
if (null === ctx) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const data = observeValue(() => {
|
|
38
|
+
ctx.drawImage(canvas, 0, 0, SIDE, SIDE);
|
|
39
|
+
return ctx.getImageData(0, 0, SIDE, SIDE).data;
|
|
40
|
+
});
|
|
41
|
+
return data === undefined ? undefined : { data, width: canvas.width, height: canvas.height };
|
|
42
|
+
}
|
|
43
|
+
let sampleAll;
|
|
44
|
+
export function canvasAfterAct() {
|
|
45
|
+
sampleAll?.(CanvasTrigger.ACT);
|
|
46
|
+
}
|
|
47
|
+
export function installCanvas(emit, opts = {}) {
|
|
48
|
+
const sample = opts.sample ?? sampleCanvas;
|
|
49
|
+
const every = opts.every ?? nativeSetInterval;
|
|
50
|
+
const last = new WeakMap();
|
|
51
|
+
const look = (trigger) => {
|
|
52
|
+
observeSafely(() => {
|
|
53
|
+
const canvases = Array.from(document.querySelectorAll('canvas')).slice(0, MAX_CANVASES);
|
|
54
|
+
for (const canvas of canvases) {
|
|
55
|
+
const read = sample(canvas);
|
|
56
|
+
if (read === undefined) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const { digest, mean } = digestPixels(read.data);
|
|
60
|
+
if (CanvasTrigger.POLL === trigger && last.get(canvas) === digest) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
last.set(canvas, digest);
|
|
64
|
+
emit(EventType.CANVAS_DIGEST, { digest, width: read.width, height: read.height, mean, trigger }, refs.refFor(canvas));
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
sampleAll = look;
|
|
69
|
+
const stop = every(() => look(CanvasTrigger.POLL), POLL_MS);
|
|
70
|
+
return () => {
|
|
71
|
+
stop();
|
|
72
|
+
if (sampleAll === look) {
|
|
73
|
+
sampleAll = undefined;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
package/dist/observers/dom.js
CHANGED
|
@@ -4,8 +4,9 @@ import { getAccessibleName, getRole, isSensitiveField, isVisible } from '../dom/
|
|
|
4
4
|
import { refs } from '../dom/addressing/refs.js';
|
|
5
5
|
function regionKeyOf(target) {
|
|
6
6
|
const el = isElement(target) ? target : null;
|
|
7
|
-
if (null === el)
|
|
7
|
+
if (null === el) {
|
|
8
8
|
return undefined;
|
|
9
|
+
}
|
|
9
10
|
const labelled = el.closest(testIdSelector());
|
|
10
11
|
return (labelled ? readTestId(labelled) : null) ?? refs.refFor(el);
|
|
11
12
|
}
|
|
@@ -30,8 +31,9 @@ const WATCHED_ATTRS = [
|
|
|
30
31
|
];
|
|
31
32
|
const MAX_ATTR_VALUE_LEN = 120;
|
|
32
33
|
function capValue(value) {
|
|
33
|
-
if (null === value)
|
|
34
|
+
if (null === value) {
|
|
34
35
|
return undefined;
|
|
36
|
+
}
|
|
35
37
|
return value.length > MAX_ATTR_VALUE_LEN ? `${value.slice(0, MAX_ATTR_VALUE_LEN)}…` : value;
|
|
36
38
|
}
|
|
37
39
|
const DIALOG_ROLES = new Set(['dialog', 'alertdialog']);
|
|
@@ -44,19 +46,23 @@ const MOUNT_SCAN_LIMIT = 40;
|
|
|
44
46
|
function describeMount(node) {
|
|
45
47
|
const role = getRole(node);
|
|
46
48
|
const name = getAccessibleName(node);
|
|
47
|
-
if (isMeaningful(role, name))
|
|
49
|
+
if (isMeaningful(role, name)) {
|
|
48
50
|
return { role, name, el: node };
|
|
51
|
+
}
|
|
49
52
|
let scanned = 0;
|
|
50
53
|
for (const child of node.querySelectorAll('*')) {
|
|
51
|
-
if (scanned >= MOUNT_SCAN_LIMIT)
|
|
54
|
+
if (scanned >= MOUNT_SCAN_LIMIT) {
|
|
52
55
|
return undefined;
|
|
56
|
+
}
|
|
53
57
|
scanned += 1;
|
|
54
|
-
if (isUnobserved(child))
|
|
58
|
+
if (isUnobserved(child)) {
|
|
55
59
|
continue;
|
|
60
|
+
}
|
|
56
61
|
const childRole = getRole(child);
|
|
57
62
|
const childName = getAccessibleName(child);
|
|
58
|
-
if (isMeaningful(childRole, childName))
|
|
63
|
+
if (isMeaningful(childRole, childName)) {
|
|
59
64
|
return { role: childRole, name: childName, el: child };
|
|
65
|
+
}
|
|
60
66
|
}
|
|
61
67
|
return undefined;
|
|
62
68
|
}
|
|
@@ -64,8 +70,9 @@ const TEXT_NODE = 3;
|
|
|
64
70
|
function textOf(nodes) {
|
|
65
71
|
let out = '';
|
|
66
72
|
for (const node of nodes) {
|
|
67
|
-
if (node.nodeType !== TEXT_NODE)
|
|
73
|
+
if (node.nodeType !== TEXT_NODE) {
|
|
68
74
|
continue;
|
|
75
|
+
}
|
|
69
76
|
out += node.nodeValue ?? '';
|
|
70
77
|
}
|
|
71
78
|
const trimmed = out.trim();
|
|
@@ -83,12 +90,30 @@ const SAFE_ON_SECRET = new Set([
|
|
|
83
90
|
function secretAttr(el, attr) {
|
|
84
91
|
return !SAFE_ON_SECRET.has(attr) && !attr.startsWith('aria-') && isSensitiveField(el);
|
|
85
92
|
}
|
|
93
|
+
const TESTID_DEPTH = 3;
|
|
94
|
+
const MAX_INITIAL_TEXTS = 50;
|
|
95
|
+
function testidNear(el) {
|
|
96
|
+
let at = el;
|
|
97
|
+
for (let depth = 0; at !== null && depth <= TESTID_DEPTH; depth += 1) {
|
|
98
|
+
const id = readTestId(at);
|
|
99
|
+
if (id !== null && id.length > 0) {
|
|
100
|
+
return id;
|
|
101
|
+
}
|
|
102
|
+
at = at.parentElement;
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const named = (el) => {
|
|
107
|
+
const testid = testidNear(el);
|
|
108
|
+
return testid === undefined ? {} : { testid };
|
|
109
|
+
};
|
|
86
110
|
export function installDom(emit) {
|
|
87
111
|
const stopRegistry = installShadowRegistry();
|
|
88
112
|
const observer = new MutationObserver((records) => {
|
|
89
113
|
let added = 0;
|
|
90
114
|
let removed = 0;
|
|
91
115
|
let changed = 0;
|
|
116
|
+
let initials = 0;
|
|
92
117
|
let dropped = 0;
|
|
93
118
|
for (const record of records) {
|
|
94
119
|
if ('attributes' === record.type) {
|
|
@@ -123,36 +148,54 @@ export function installDom(emit) {
|
|
|
123
148
|
? REDACTED_VALUE
|
|
124
149
|
: (record.target.textContent ?? '').trim().slice(0, 80);
|
|
125
150
|
const old = hide ? undefined : capValue(record.oldValue?.trim() ?? null);
|
|
126
|
-
emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }) }, refs.refFor(parent));
|
|
151
|
+
emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }), ...named(parent) }, refs.refFor(parent));
|
|
127
152
|
}
|
|
128
153
|
continue;
|
|
129
154
|
}
|
|
130
155
|
if (textOf(record.addedNodes) !== undefined || textOf(record.removedNodes) !== undefined) {
|
|
131
156
|
const parent = isElement(record.target) ? record.target : null;
|
|
132
157
|
if (parent !== null && !isUnobserved(parent)) {
|
|
133
|
-
if (changed >= MAX_PER_BATCH)
|
|
158
|
+
if (changed >= MAX_PER_BATCH) {
|
|
134
159
|
dropped += 1;
|
|
160
|
+
}
|
|
135
161
|
else {
|
|
136
162
|
changed += 1;
|
|
137
163
|
const hide = isSensitiveField(parent);
|
|
138
164
|
const text = hide ? REDACTED_VALUE : (parent.textContent ?? '').trim().slice(0, 80);
|
|
139
165
|
const old = hide ? undefined : capValue(textOf(record.removedNodes) ?? null);
|
|
140
|
-
emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }) }, refs.refFor(parent));
|
|
166
|
+
emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }), ...named(parent) }, refs.refFor(parent));
|
|
141
167
|
}
|
|
142
168
|
}
|
|
143
169
|
}
|
|
144
170
|
for (const node of record.addedNodes) {
|
|
145
|
-
if (!isElement(node))
|
|
171
|
+
if (!isElement(node)) {
|
|
146
172
|
continue;
|
|
173
|
+
}
|
|
147
174
|
if (added >= MAX_PER_BATCH) {
|
|
148
175
|
dropped += 1;
|
|
149
176
|
continue;
|
|
150
177
|
}
|
|
151
|
-
if (isUnobserved(node))
|
|
178
|
+
if (isUnobserved(node)) {
|
|
152
179
|
continue;
|
|
180
|
+
}
|
|
181
|
+
const testids = [node, ...Array.from(node.querySelectorAll(testIdSelector()))].filter((el) => readTestId(el) !== null);
|
|
182
|
+
for (const el of testids) {
|
|
183
|
+
const text = (el.textContent ?? '').trim();
|
|
184
|
+
if (0 === text.length) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (initials >= MAX_INITIAL_TEXTS) {
|
|
188
|
+
dropped += 1;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
initials += 1;
|
|
192
|
+
const shown = isSensitiveField(el) ? REDACTED_VALUE : text.slice(0, 80);
|
|
193
|
+
emit(EventType.DOM_TEXT, { text: shown, testid: readTestId(el) ?? '', initial: true }, refs.refFor(el));
|
|
194
|
+
}
|
|
153
195
|
const mounted = describeMount(node);
|
|
154
|
-
if (mounted === undefined)
|
|
196
|
+
if (mounted === undefined) {
|
|
155
197
|
continue;
|
|
198
|
+
}
|
|
156
199
|
const { role, name } = mounted;
|
|
157
200
|
added += 1;
|
|
158
201
|
const ref = refs.refFor(mounted.el);
|
|
@@ -160,13 +203,15 @@ export function installDom(emit) {
|
|
|
160
203
|
if (DIALOG_ROLES.has(role) ||
|
|
161
204
|
LIVE_ROLES.has(role) ||
|
|
162
205
|
'true' === node.getAttribute('aria-modal')) {
|
|
163
|
-
if (isVisible(node))
|
|
206
|
+
if (isVisible(node)) {
|
|
164
207
|
emit(EventType.VISIBLE_SHOWN, { role, name }, ref);
|
|
208
|
+
}
|
|
165
209
|
}
|
|
166
210
|
}
|
|
167
211
|
for (const node of record.removedNodes) {
|
|
168
|
-
if (!isElement(node))
|
|
212
|
+
if (!isElement(node)) {
|
|
169
213
|
continue;
|
|
214
|
+
}
|
|
170
215
|
if (removed >= MAX_PER_BATCH) {
|
|
171
216
|
dropped += 1;
|
|
172
217
|
continue;
|
|
@@ -175,15 +220,17 @@ export function installDom(emit) {
|
|
|
175
220
|
continue;
|
|
176
221
|
}
|
|
177
222
|
const gone = describeMount(node);
|
|
178
|
-
if (gone === undefined)
|
|
223
|
+
if (gone === undefined) {
|
|
179
224
|
continue;
|
|
225
|
+
}
|
|
180
226
|
const { role, name } = gone;
|
|
181
227
|
removed += 1;
|
|
182
228
|
emit(EventType.DOM_REMOVED, { role, name, region: regionKeyOf(record.target) });
|
|
183
229
|
}
|
|
184
230
|
}
|
|
185
|
-
if (dropped > 0)
|
|
231
|
+
if (dropped > 0) {
|
|
186
232
|
emit(EventType.TRUNCATED, { channel: TruncationChannel.DOM, dropped });
|
|
233
|
+
}
|
|
187
234
|
});
|
|
188
235
|
const options = {
|
|
189
236
|
subtree: true,
|
|
@@ -195,8 +242,9 @@ export function installDom(emit) {
|
|
|
195
242
|
characterDataOldValue: true,
|
|
196
243
|
};
|
|
197
244
|
observer.observe(document.documentElement, options);
|
|
198
|
-
for (const root of capturedRoots())
|
|
245
|
+
for (const root of capturedRoots()) {
|
|
199
246
|
observer.observe(root, options);
|
|
247
|
+
}
|
|
200
248
|
const unsubscribe = onShadowRoot((root) => observer.observe(root, options));
|
|
201
249
|
const stopFrames = observeSameOriginFrames((body) => observer.observe(body, options));
|
|
202
250
|
return () => {
|
|
@@ -1,2 +1,13 @@
|
|
|
1
1
|
import type { Emit, Teardown } from './types.js';
|
|
2
|
+
export interface FieldReading {
|
|
3
|
+
value: string;
|
|
4
|
+
checked?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface ActedField {
|
|
7
|
+
before: FieldReading;
|
|
8
|
+
stop: () => void;
|
|
9
|
+
written: () => boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function watchActedField(el: Element): ActedField | undefined;
|
|
12
|
+
export declare function publishSettledField(el: Element, acted: ActedField | undefined): void;
|
|
2
13
|
export declare function installField(emit: Emit): Teardown;
|
package/dist/observers/field.js
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
import { readTestId } from '../dom/addressing/testid-attr.js';
|
|
2
|
-
import { EventType } from '@reticlehq/core';
|
|
2
|
+
import { EventType, FieldChangeField, FieldChangeKind } from '@reticlehq/core';
|
|
3
3
|
import { getAccessibleName, isSensitiveField } from '../dom/a11y.js';
|
|
4
4
|
const MAX_VALUE_LEN = 160;
|
|
5
5
|
const BURST_MS = 150;
|
|
6
|
+
const CHECKBOX_TYPE = 'checkbox';
|
|
7
|
+
const RADIO_TYPE = 'radio';
|
|
8
|
+
let settledEmit;
|
|
6
9
|
function isField(target) {
|
|
7
10
|
return (target instanceof HTMLInputElement ||
|
|
8
11
|
target instanceof HTMLTextAreaElement ||
|
|
9
12
|
target instanceof HTMLSelectElement);
|
|
10
13
|
}
|
|
14
|
+
function isCheckable(element) {
|
|
15
|
+
return (element instanceof HTMLInputElement &&
|
|
16
|
+
(CHECKBOX_TYPE === element.type || RADIO_TYPE === element.type));
|
|
17
|
+
}
|
|
18
|
+
function readingOf(element) {
|
|
19
|
+
if (isCheckable(element))
|
|
20
|
+
return { value: element.value, checked: element.checked };
|
|
21
|
+
return { value: element.value };
|
|
22
|
+
}
|
|
23
|
+
function readingMoved(before, after) {
|
|
24
|
+
if (before.checked !== undefined || after.checked !== undefined) {
|
|
25
|
+
return before.checked !== after.checked;
|
|
26
|
+
}
|
|
27
|
+
return before.value !== after.value;
|
|
28
|
+
}
|
|
29
|
+
function clip(value) {
|
|
30
|
+
return value.length > MAX_VALUE_LEN ? `${value.slice(0, MAX_VALUE_LEN)}…` : value;
|
|
31
|
+
}
|
|
11
32
|
function fieldName(element) {
|
|
12
33
|
const testid = readTestId(element);
|
|
13
34
|
if (null !== testid && testid.length > 0)
|
|
@@ -20,21 +41,102 @@ function fieldName(element) {
|
|
|
20
41
|
const id = element.getAttribute('id');
|
|
21
42
|
return null !== id && id.length > 0 ? id : element.tagName.toLowerCase();
|
|
22
43
|
}
|
|
44
|
+
function fieldPayload(element, kind, after, before) {
|
|
45
|
+
const secret = isSensitiveField(element);
|
|
46
|
+
const payload = {
|
|
47
|
+
[FieldChangeField.FIELD]: fieldName(element),
|
|
48
|
+
[FieldChangeField.KIND]: kind,
|
|
49
|
+
[FieldChangeField.LENGTH]: after.value.length,
|
|
50
|
+
};
|
|
51
|
+
if (after.checked !== undefined)
|
|
52
|
+
payload[FieldChangeField.CHECKED] = after.checked;
|
|
53
|
+
if (before?.checked !== undefined)
|
|
54
|
+
payload[FieldChangeField.PREVIOUS_CHECKED] = before.checked;
|
|
55
|
+
if (secret) {
|
|
56
|
+
payload[FieldChangeField.REDACTED] = true;
|
|
57
|
+
return payload;
|
|
58
|
+
}
|
|
59
|
+
payload[FieldChangeField.VALUE] = clip(after.value);
|
|
60
|
+
if (before !== undefined && before.checked === undefined) {
|
|
61
|
+
payload[FieldChangeField.PREVIOUS] = clip(before.value);
|
|
62
|
+
}
|
|
63
|
+
return payload;
|
|
64
|
+
}
|
|
65
|
+
function settledProperty(el) {
|
|
66
|
+
return isCheckable(el) ? 'checked' : 'value';
|
|
67
|
+
}
|
|
68
|
+
function inheritedDescriptor(el, prop) {
|
|
69
|
+
for (let proto = Object.getPrototypeOf(el); proto !== null;) {
|
|
70
|
+
const found = Object.getOwnPropertyDescriptor(proto, prop);
|
|
71
|
+
if (found !== undefined)
|
|
72
|
+
return found;
|
|
73
|
+
proto = Object.getPrototypeOf(proto);
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
function watchWrites(el, prop) {
|
|
78
|
+
const own = Object.getOwnPropertyDescriptor(el, prop);
|
|
79
|
+
const target = own ?? inheritedDescriptor(el, prop);
|
|
80
|
+
const get = target === undefined ? undefined : Reflect.get(target, 'get');
|
|
81
|
+
const set = target === undefined ? undefined : Reflect.get(target, 'set');
|
|
82
|
+
let written = false;
|
|
83
|
+
const usable = 'function' === typeof get && 'function' === typeof set;
|
|
84
|
+
if (!usable || false === own?.configurable) {
|
|
85
|
+
return { stop: () => { }, written: () => written };
|
|
86
|
+
}
|
|
87
|
+
Object.defineProperty(el, prop, {
|
|
88
|
+
configurable: true,
|
|
89
|
+
enumerable: target?.enumerable ?? false,
|
|
90
|
+
get() {
|
|
91
|
+
return Reflect.apply(get, this, []);
|
|
92
|
+
},
|
|
93
|
+
set(next) {
|
|
94
|
+
written = true;
|
|
95
|
+
Reflect.apply(set, this, [next]);
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
let stopped = false;
|
|
99
|
+
return {
|
|
100
|
+
stop: () => {
|
|
101
|
+
if (stopped)
|
|
102
|
+
return;
|
|
103
|
+
stopped = true;
|
|
104
|
+
if (own === undefined)
|
|
105
|
+
Reflect.deleteProperty(el, prop);
|
|
106
|
+
else
|
|
107
|
+
Object.defineProperty(el, prop, own);
|
|
108
|
+
},
|
|
109
|
+
written: () => written,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function watchActedField(el) {
|
|
113
|
+
if (!isField(el) || el instanceof HTMLSelectElement)
|
|
114
|
+
return undefined;
|
|
115
|
+
return { before: readingOf(el), ...watchWrites(el, settledProperty(el)) };
|
|
116
|
+
}
|
|
117
|
+
export function publishSettledField(el, acted) {
|
|
118
|
+
const emit = settledEmit;
|
|
119
|
+
if (acted === undefined)
|
|
120
|
+
return;
|
|
121
|
+
acted.stop();
|
|
122
|
+
if (emit === undefined || !isField(el) || !el.isConnected || !acted.written())
|
|
123
|
+
return;
|
|
124
|
+
const before = acted.before;
|
|
125
|
+
if (isSensitiveField(el) && before.checked === undefined)
|
|
126
|
+
return;
|
|
127
|
+
const after = readingOf(el);
|
|
128
|
+
if (!readingMoved(before, after))
|
|
129
|
+
return;
|
|
130
|
+
emit(EventType.FIELD_CHANGE, fieldPayload(el, FieldChangeKind.SETTLED, after, before));
|
|
131
|
+
}
|
|
23
132
|
export function installField(emit) {
|
|
24
133
|
const ac = new AbortController();
|
|
25
134
|
const { signal } = ac;
|
|
135
|
+
const previousEmit = settledEmit;
|
|
136
|
+
settledEmit = emit;
|
|
26
137
|
const pending = new Map();
|
|
27
138
|
const report = (element, kind) => {
|
|
28
|
-
|
|
29
|
-
const secret = isSensitiveField(element);
|
|
30
|
-
emit(EventType.FIELD_CHANGE, {
|
|
31
|
-
field: fieldName(element),
|
|
32
|
-
kind,
|
|
33
|
-
length: value.length,
|
|
34
|
-
...(secret
|
|
35
|
-
? { redacted: true }
|
|
36
|
-
: { value: value.length > MAX_VALUE_LEN ? `${value.slice(0, MAX_VALUE_LEN)}…` : value }),
|
|
37
|
-
});
|
|
139
|
+
emit(EventType.FIELD_CHANGE, fieldPayload(element, kind, readingOf(element)));
|
|
38
140
|
};
|
|
39
141
|
const clear = (element) => {
|
|
40
142
|
const timer = pending.get(element);
|
|
@@ -49,7 +151,7 @@ export function installField(emit) {
|
|
|
49
151
|
clear(element);
|
|
50
152
|
pending.set(element, setTimeout(() => {
|
|
51
153
|
pending.delete(element);
|
|
52
|
-
report(element,
|
|
154
|
+
report(element, FieldChangeKind.INPUT);
|
|
53
155
|
}, BURST_MS));
|
|
54
156
|
};
|
|
55
157
|
const onChange = (event) => {
|
|
@@ -57,7 +159,7 @@ export function installField(emit) {
|
|
|
57
159
|
if (!isField(element))
|
|
58
160
|
return;
|
|
59
161
|
clear(element);
|
|
60
|
-
report(element,
|
|
162
|
+
report(element, FieldChangeKind.CHANGE);
|
|
61
163
|
};
|
|
62
164
|
document.addEventListener('input', onInput, { signal, capture: true });
|
|
63
165
|
document.addEventListener('change', onChange, { signal, capture: true });
|
|
@@ -65,6 +167,8 @@ export function installField(emit) {
|
|
|
65
167
|
for (const timer of pending.values())
|
|
66
168
|
clearTimeout(timer);
|
|
67
169
|
pending.clear();
|
|
170
|
+
if (settledEmit === emit)
|
|
171
|
+
settledEmit = previousEmit;
|
|
68
172
|
ac.abort();
|
|
69
173
|
};
|
|
70
174
|
}
|
|
@@ -17,6 +17,8 @@ import { installDialogs } from './dialogs.js';
|
|
|
17
17
|
import { installAnimation } from './animation.js';
|
|
18
18
|
import { installScroll } from './scroll.js';
|
|
19
19
|
import { installHealth } from './health.js';
|
|
20
|
+
import { installMedia } from './media.js';
|
|
21
|
+
import { installCanvas } from './canvas.js';
|
|
20
22
|
import { reportSdkFailure, SdkSite } from './sdk-failure.js';
|
|
21
23
|
function guard(emit, site, install) {
|
|
22
24
|
try {
|
|
@@ -65,5 +67,7 @@ export function installAllObservers(emit, options) {
|
|
|
65
67
|
guard(emit, SdkSite.DOM_OBSERVER, () => installBlindSpots(emit)),
|
|
66
68
|
guard(emit, SdkSite.HEALTH_OBSERVER, () => installHealth(emit)),
|
|
67
69
|
guard(emit, SdkSite.DOM_OBSERVER, () => installContextOpen(emit)),
|
|
70
|
+
guard(emit, SdkSite.DOM_OBSERVER, () => installMedia(emit)),
|
|
71
|
+
guard(emit, SdkSite.DOM_OBSERVER, () => installCanvas(emit)),
|
|
68
72
|
];
|
|
69
73
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { EventType, MediaEvent } from '@reticlehq/core';
|
|
2
|
+
import { refs } from '../dom/addressing/refs.js';
|
|
3
|
+
import { observeSafely } from './types.js';
|
|
4
|
+
const TAGS = { VIDEO: 'video', AUDIO: 'audio' };
|
|
5
|
+
export function installMedia(emit) {
|
|
6
|
+
const ac = new AbortController();
|
|
7
|
+
const onMedia = (event) => {
|
|
8
|
+
observeSafely(() => {
|
|
9
|
+
const el = event.target;
|
|
10
|
+
if (!(el instanceof HTMLMediaElement)) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const tag = TAGS[el.tagName];
|
|
14
|
+
if (tag === undefined) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const duration = Number.isFinite(el.duration) ? el.duration : undefined;
|
|
18
|
+
emit(EventType.MEDIA_STATE, {
|
|
19
|
+
event: event.type,
|
|
20
|
+
tag,
|
|
21
|
+
currentTime: el.currentTime,
|
|
22
|
+
...(duration === undefined ? {} : { duration }),
|
|
23
|
+
readyState: el.readyState,
|
|
24
|
+
paused: el.paused,
|
|
25
|
+
}, refs.refFor(el));
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
for (const type of Object.values(MediaEvent)) {
|
|
29
|
+
document.addEventListener(type, onMedia, { capture: true, signal: ac.signal });
|
|
30
|
+
}
|
|
31
|
+
return () => ac.abort();
|
|
32
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { ResponseBodyReason } from '@reticlehq/core';
|
|
1
2
|
export declare function isStreamingBody(contentType: string | null, contentLength: string | null): boolean;
|
|
2
3
|
export declare function withBodyDeadline(read: Promise<string>): Promise<string | undefined>;
|
|
4
|
+
export declare function skippedBodyReason(contentType: string | null): ResponseBodyReason | undefined;
|
|
3
5
|
export declare function isCapturableType(contentType: string | null): boolean;
|
|
4
6
|
export declare function setNetworkBodyMaxChars(chars: number): void;
|
|
5
7
|
export declare function networkBodyMaxChars(): number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { REDACTED_VALUE } from '@reticlehq/core';
|
|
1
|
+
import { REDACTED_VALUE, ResponseBodyReason } from '@reticlehq/core';
|
|
2
2
|
import { isSensitiveKey, safeStringify, scrubKnownSecrets } from '../../security/serialization.js';
|
|
3
3
|
import { nativeClearTimeout, nativeSetTimeout } from '../../timers/native/native-timers.js';
|
|
4
4
|
const CAPTURABLE_CONTENT = /application\/json|text\/|application\/xml|x-www-form-urlencoded|graphql/i;
|
|
@@ -25,6 +25,13 @@ export async function withBodyDeadline(read) {
|
|
|
25
25
|
nativeClearTimeout(timer);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
export function skippedBodyReason(contentType) {
|
|
29
|
+
if (null === contentType)
|
|
30
|
+
return undefined;
|
|
31
|
+
return STREAMING_CONTENT.test(contentType)
|
|
32
|
+
? ResponseBodyReason.STREAMING
|
|
33
|
+
: ResponseBodyReason.NOT_TEXT;
|
|
34
|
+
}
|
|
28
35
|
export function isCapturableType(contentType) {
|
|
29
36
|
if (null === contentType)
|
|
30
37
|
return false;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { BlindSpotKind, EventType, NEXT_ACTION_FIELD, NEXT_ACTION_HEADER, REQUEST_SHAPE_FIELD, REQUEST_SHAPE_NONE, RETICLE_WS_PATH, StreamTransport, StreamDirection, fnv1a, } from '@reticlehq/core';
|
|
1
|
+
import { BlindSpotKind, EventType, NEXT_ACTION_FIELD, NEXT_ACTION_HEADER, REQUEST_SHAPE_FIELD, REQUEST_SHAPE_NONE, RETICLE_WS_PATH, ResponseBodyReason, StreamTransport, StreamDirection, fnv1a, } from '@reticlehq/core';
|
|
2
2
|
import { captureMethod } from '../patching/capture-method.js';
|
|
3
3
|
import { observeSafely, observeValue } from './types.js';
|
|
4
|
-
import { isCapturableType, projectBody, projectErrorBody, shouldCaptureErrorBody, withBodyDeadline, } from './net-detail/network-body.js';
|
|
4
|
+
import { isCapturableType, projectBody, projectErrorBody, shouldCaptureErrorBody, skippedBodyReason, withBodyDeadline, } from './net-detail/network-body.js';
|
|
5
5
|
import { redactUrl, netUrlFields } from './net-detail/network-redact.js';
|
|
6
6
|
import { watchStreamedBody } from './network-stream.js';
|
|
7
7
|
import { requireCapturedMethod } from '../util/captured-method.js';
|
|
@@ -230,7 +230,13 @@ function isNativeFetch(fn) {
|
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
const OURS = new WeakSet();
|
|
233
|
+
const PAGE_CAPTURE = Symbol.for('reticle.network');
|
|
234
|
+
const pageSlots = () => window;
|
|
233
235
|
export function installNetwork(emit, opts = {}) {
|
|
236
|
+
if (undefined !== pageSlots()[PAGE_CAPTURE])
|
|
237
|
+
return () => undefined;
|
|
238
|
+
const owner = {};
|
|
239
|
+
pageSlots()[PAGE_CAPTURE] = owner;
|
|
234
240
|
const captureBodies = true === opts.captureBodies;
|
|
235
241
|
const captureErrorBodies = false !== opts.captureErrorBodies;
|
|
236
242
|
const reinterpret = opts.reinterpret;
|
|
@@ -301,7 +307,9 @@ export function installNetwork(emit, opts = {}) {
|
|
|
301
307
|
catch {
|
|
302
308
|
}
|
|
303
309
|
void (async () => {
|
|
304
|
-
let responseBodyFields =
|
|
310
|
+
let responseBodyFields = captureBodies
|
|
311
|
+
? { responseBodyReason: ResponseBodyReason.UNREADABLE }
|
|
312
|
+
: {};
|
|
305
313
|
if (clone !== undefined) {
|
|
306
314
|
try {
|
|
307
315
|
const text = await withBodyDeadline(clone.text());
|
|
@@ -312,7 +320,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
312
320
|
responseBodyFields = {
|
|
313
321
|
responseBody: body,
|
|
314
322
|
...(truncated ? { responseBodyTruncated: true } : {}),
|
|
315
|
-
...(captureBodies ? {} : { responseBodyReason:
|
|
323
|
+
...(captureBodies ? {} : { responseBodyReason: ResponseBodyReason.ERROR_STATUS }),
|
|
316
324
|
};
|
|
317
325
|
}
|
|
318
326
|
}
|
|
@@ -327,7 +335,8 @@ export function installNetwork(emit, opts = {}) {
|
|
|
327
335
|
})().catch(() => undefined);
|
|
328
336
|
}
|
|
329
337
|
else {
|
|
330
|
-
|
|
338
|
+
const skipped = captureBodies ? skippedBodyReason(contentType) : undefined;
|
|
339
|
+
emitRequest(skipped === undefined ? {} : { responseBodyReason: skipped });
|
|
331
340
|
}
|
|
332
341
|
return res;
|
|
333
342
|
}
|
|
@@ -397,6 +406,13 @@ export function installNetwork(emit, opts = {}) {
|
|
|
397
406
|
let responseBodyFields = {};
|
|
398
407
|
const textReadable = '' === this.responseType || 'text' === this.responseType;
|
|
399
408
|
const wantXhrErrorBody = shouldCaptureErrorBody(this.status, captureErrorBodies);
|
|
409
|
+
if (captureBodies) {
|
|
410
|
+
const skipped = isCapturableType(xhrContentType)
|
|
411
|
+
? ResponseBodyReason.UNREADABLE
|
|
412
|
+
: skippedBodyReason(xhrContentType);
|
|
413
|
+
if (skipped !== undefined)
|
|
414
|
+
responseBodyFields = { responseBodyReason: skipped };
|
|
415
|
+
}
|
|
400
416
|
if ((captureBodies || wantXhrErrorBody) &&
|
|
401
417
|
textReadable &&
|
|
402
418
|
isCapturableType(xhrContentType)) {
|
|
@@ -407,7 +423,9 @@ export function installNetwork(emit, opts = {}) {
|
|
|
407
423
|
responseBodyFields = {
|
|
408
424
|
responseBody: rb,
|
|
409
425
|
...(truncated ? { responseBodyTruncated: true } : {}),
|
|
410
|
-
...(captureBodies
|
|
426
|
+
...(captureBodies
|
|
427
|
+
? {}
|
|
428
|
+
: { responseBodyReason: ResponseBodyReason.ERROR_STATUS }),
|
|
411
429
|
};
|
|
412
430
|
}
|
|
413
431
|
catch {
|
|
@@ -579,6 +597,8 @@ export function installNetwork(emit, opts = {}) {
|
|
|
579
597
|
}
|
|
580
598
|
}
|
|
581
599
|
return () => {
|
|
600
|
+
if (pageSlots()[PAGE_CAPTURE] === owner)
|
|
601
|
+
delete pageSlots()[PAGE_CAPTURE];
|
|
582
602
|
subresourceObserver?.disconnect();
|
|
583
603
|
if (window.fetch === patchedFetch)
|
|
584
604
|
window.fetch = origFetch;
|