@reticlehq/browser 3.8.2 → 3.10.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 +14 -0
- package/dist/actions/action-report.js +95 -22
- package/dist/actions/actions-dom.d.ts +17 -6
- package/dist/actions/actions-dom.js +98 -47
- package/dist/actions/actions-press.d.ts +10 -0
- package/dist/actions/actions-press.js +146 -2
- package/dist/actions/actions.js +64 -24
- package/dist/actions/insert-text.d.ts +3 -0
- package/dist/actions/insert-text.js +90 -0
- package/dist/actions/scroll.d.ts +1 -1
- package/dist/actions/scroll.js +25 -1
- package/dist/actions/value-input.d.ts +2 -0
- package/dist/actions/value-input.js +30 -9
- package/dist/commands/commands.js +85 -9
- package/dist/connect-options.d.ts +11 -1
- package/dist/connect-options.js +27 -1
- package/dist/dom/a11y.d.ts +1 -1
- package/dist/dom/a11y.js +11 -10
- package/dist/dom/chart-geometry.d.ts +7 -0
- package/dist/dom/chart-geometry.js +39 -0
- package/dist/dom/occlusion.js +21 -0
- package/dist/dom/query-finders.d.ts +1 -1
- package/dist/dom/query-finders.js +10 -7
- package/dist/dom/query.d.ts +6 -2
- package/dist/dom/query.js +165 -54
- package/dist/dom/scope-roots.d.ts +8 -0
- package/dist/dom/scope-roots.js +44 -0
- package/dist/dom/snapshot.d.ts +3 -0
- package/dist/dom/snapshot.js +9 -2
- package/dist/dom/text-match.js +6 -4
- package/dist/dom/theme.js +6 -7
- package/dist/dom/visibility.d.ts +3 -0
- package/dist/dom/visibility.js +125 -10
- package/dist/observers/canvas.d.ts +1 -0
- package/dist/observers/canvas.js +10 -1
- package/dist/observers/console.js +46 -35
- package/dist/observers/dom.js +28 -4
- package/dist/observers/net-detail/resource-timing.d.ts +12 -0
- package/dist/observers/net-detail/resource-timing.js +69 -0
- package/dist/observers/network.js +13 -44
- package/dist/observers/perf.js +15 -1
- package/dist/presenter/chrome/presenter-verbs.js +1 -0
- package/dist/presenter/presenter-config.d.ts +1 -0
- package/dist/presenter/presenter-config.js +1 -0
- package/dist/presenter/presenter-shell.d.ts +1 -0
- package/dist/presenter/presenter-shell.js +6 -3
- package/dist/presenter/tour/tour.js +4 -0
- package/dist/recorder/recorder.js +82 -39
- package/dist/reticle-inject.js +618 -618
- package/dist/reticle.d.ts +3 -19
- package/dist/reticle.js +76 -110
- package/dist/review/annotator.js +4 -0
- package/dist/security/serialization.js +35 -0
- package/dist/session-continuity.d.ts +1 -0
- package/dist/session-continuity.js +18 -0
- package/dist/storage-keys.js +2 -1
- package/dist/timers/native/native-timers.js +21 -4
- package/dist/transport/bridge-url.d.ts +8 -0
- package/dist/transport/bridge-url.js +52 -0
- package/package.json +10 -10
package/dist/dom/visibility.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RETICLE_OVERLAY_SELECTOR } from '@reticlehq/core';
|
|
1
2
|
import { isHtmlElement } from './realm.js';
|
|
2
3
|
import { parentAcrossShadowBoundary } from './shadow-registry.js';
|
|
3
4
|
import { clippingBox, overflowAxes, scrolledInto, } from './paint-context.js';
|
|
@@ -16,17 +17,51 @@ function hiddenInsideClosedDetails(el) {
|
|
|
16
17
|
const summary = Array.from(details.children).find((child) => HTML_SUMMARY_TAG === child.localName);
|
|
17
18
|
return summary === undefined || !summary.contains(el);
|
|
18
19
|
}
|
|
19
|
-
function stalledFade(el) {
|
|
20
|
+
export function stalledFade(el) {
|
|
20
21
|
if (!el.ownerDocument.hidden || !('getAnimations' in el)) {
|
|
21
22
|
return false;
|
|
22
23
|
}
|
|
23
24
|
return el.getAnimations().some((a) => 'running' === a.playState || a.pending);
|
|
24
25
|
}
|
|
25
26
|
const OVERLAY_ARIA_HIDDEN = 'data-aria-hidden';
|
|
27
|
+
const OPEN_LAYER = '[role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"], dialog[open], [aria-modal="true"]';
|
|
28
|
+
const NON_RENDERED_TAGS = ['script', 'style', 'template', 'noscript'];
|
|
29
|
+
export function isNonRendered(el) {
|
|
30
|
+
return NON_RENDERED_TAGS.includes(el.tagName.toLowerCase());
|
|
31
|
+
}
|
|
32
|
+
const UNPAINTED_TAGS = new Set(['script', 'style', 'template', 'noscript', 'link', 'meta']);
|
|
26
33
|
function hiddenByAnOverlay(el) {
|
|
27
|
-
|
|
34
|
+
if (el.hasAttribute(OVERLAY_ARIA_HIDDEN)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return hiddenByAModalManager(el);
|
|
38
|
+
}
|
|
39
|
+
function hiddenByAModalManager(el) {
|
|
40
|
+
const body = el.ownerDocument.body;
|
|
41
|
+
if (el.parentElement !== body) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const others = Array.from(body.children).filter((child) => child !== el &&
|
|
45
|
+
!UNPAINTED_TAGS.has(child.localName) &&
|
|
46
|
+
!child.matches(RETICLE_OVERLAY_SELECTOR));
|
|
47
|
+
const open = others.filter((child) => 'true' !== child.getAttribute('aria-hidden'));
|
|
48
|
+
const layer = open[0];
|
|
49
|
+
return 1 === open.length && layer !== undefined && holdsOpenLayer(layer);
|
|
50
|
+
}
|
|
51
|
+
function holdsOpenLayer(portal) {
|
|
52
|
+
const layer = portal.matches(OPEN_LAYER) ? portal : portal.querySelector(OPEN_LAYER);
|
|
53
|
+
for (let node = layer; null !== node; node = node.parentElement) {
|
|
54
|
+
const style = node.ownerDocument.defaultView?.getComputedStyle(node) ?? null;
|
|
55
|
+
if (selfCssHidden(node, style)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (node === portal) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
28
63
|
}
|
|
29
|
-
function selfCssHidden(el, style) {
|
|
64
|
+
function selfCssHidden(el, style, includeVisibility = true) {
|
|
30
65
|
if (isHtmlElement(el) && el.hidden) {
|
|
31
66
|
return true;
|
|
32
67
|
}
|
|
@@ -34,9 +69,7 @@ function selfCssHidden(el, style) {
|
|
|
34
69
|
return true;
|
|
35
70
|
}
|
|
36
71
|
if (style !== null) {
|
|
37
|
-
if ('none' === style.display ||
|
|
38
|
-
'hidden' === style.visibility ||
|
|
39
|
-
'collapse' === style.visibility) {
|
|
72
|
+
if ('none' === style.display || (includeVisibility && visibilityHidden(style))) {
|
|
40
73
|
return true;
|
|
41
74
|
}
|
|
42
75
|
if (0 === Number.parseFloat(style.opacity || '1') && !stalledFade(el)) {
|
|
@@ -48,6 +81,9 @@ function selfCssHidden(el, style) {
|
|
|
48
81
|
}
|
|
49
82
|
return false;
|
|
50
83
|
}
|
|
84
|
+
function visibilityHidden(style) {
|
|
85
|
+
return 'hidden' === style?.visibility || 'collapse' === style?.visibility;
|
|
86
|
+
}
|
|
51
87
|
function clippedAway(style) {
|
|
52
88
|
const rect = /^rect\((.*)\)$/.exec(style.getPropertyValue('clip').trim());
|
|
53
89
|
if (null !== rect && ('absolute' === style.position || 'fixed' === style.position)) {
|
|
@@ -75,7 +111,7 @@ function selfHidden(el, style) {
|
|
|
75
111
|
if ('true' === el.getAttribute('aria-hidden') && !hiddenByAnOverlay(el)) {
|
|
76
112
|
return true;
|
|
77
113
|
}
|
|
78
|
-
return selfCssHidden(el, style);
|
|
114
|
+
return selfCssHidden(el, style, false);
|
|
79
115
|
}
|
|
80
116
|
export function isHiddenByAriaOnly(el) {
|
|
81
117
|
let foundAriaHidden = false;
|
|
@@ -85,7 +121,7 @@ export function isHiddenByAriaOnly(el) {
|
|
|
85
121
|
foundAriaHidden = true;
|
|
86
122
|
}
|
|
87
123
|
const style = current.ownerDocument.defaultView?.getComputedStyle(current) ?? null;
|
|
88
|
-
if (selfCssHidden(current, style)) {
|
|
124
|
+
if (selfCssHidden(current, style, current === el)) {
|
|
89
125
|
return false;
|
|
90
126
|
}
|
|
91
127
|
current = parentAcrossShadowBoundary(current);
|
|
@@ -99,13 +135,63 @@ export function isRenderedOutsideAriaHidden(el) {
|
|
|
99
135
|
return false;
|
|
100
136
|
}
|
|
101
137
|
const style = current.ownerDocument.defaultView?.getComputedStyle(current) ?? null;
|
|
102
|
-
if (selfCssHidden(current, style)) {
|
|
138
|
+
if (selfCssHidden(current, style, current === el)) {
|
|
103
139
|
return false;
|
|
104
140
|
}
|
|
105
141
|
current = parentAcrossShadowBoundary(current);
|
|
106
142
|
}
|
|
107
143
|
return true;
|
|
108
144
|
}
|
|
145
|
+
const BACKFACE_VISIBILITY_HIDDEN = 'hidden';
|
|
146
|
+
function localTransform(style) {
|
|
147
|
+
if ('undefined' === typeof DOMMatrix) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
const transform = style?.transform;
|
|
152
|
+
return undefined === transform || '' === transform || 'none' === transform
|
|
153
|
+
? new DOMMatrix()
|
|
154
|
+
: new DOMMatrix(transform);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function backFaceTowardViewer(matrix) {
|
|
161
|
+
return Number.isFinite(matrix.m33) && 0 > matrix.m33;
|
|
162
|
+
}
|
|
163
|
+
function backfaceHidden(el, memo) {
|
|
164
|
+
const chain = [];
|
|
165
|
+
let declares = false;
|
|
166
|
+
for (let current = el; null !== current; current = parentAcrossShadowBoundary(current)) {
|
|
167
|
+
const style = ownVisibilityInfo(current, memo).style;
|
|
168
|
+
if (BACKFACE_VISIBILITY_HIDDEN === style?.backfaceVisibility) {
|
|
169
|
+
declares = true;
|
|
170
|
+
}
|
|
171
|
+
chain.push(current);
|
|
172
|
+
}
|
|
173
|
+
if (!declares) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
let composed = null;
|
|
177
|
+
for (let index = chain.length - 1; index >= 0; index -= 1) {
|
|
178
|
+
const current = chain[index];
|
|
179
|
+
if (undefined === current) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
const style = ownVisibilityInfo(current, memo).style;
|
|
183
|
+
const local = localTransform(style);
|
|
184
|
+
if (null === local) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
composed = null === composed ? local : composed.multiply(local);
|
|
188
|
+
if (BACKFACE_VISIBILITY_HIDDEN === style?.backfaceVisibility &&
|
|
189
|
+
backFaceTowardViewer(composed)) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
109
195
|
const CLIPPING_OVERFLOW = new Set(['hidden', 'clip']);
|
|
110
196
|
const SCROLLING_OVERFLOW = new Set(['auto', 'scroll']);
|
|
111
197
|
const NON_CLIPPING_DISPLAY = new Set([
|
|
@@ -154,9 +240,32 @@ function visibilityInfoMemo(memo) {
|
|
|
154
240
|
}
|
|
155
241
|
return info;
|
|
156
242
|
}
|
|
243
|
+
function underKnownHidden(el, memo) {
|
|
244
|
+
for (let up = parentAcrossShadowBoundary(el); null !== up; up = parentAcrossShadowBoundary(up)) {
|
|
245
|
+
const known = memo.get(up);
|
|
246
|
+
if (known !== undefined) {
|
|
247
|
+
return known.hidden;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
157
252
|
function ownVisibilityInfo(el, memo) {
|
|
158
253
|
let info = memo.get(el);
|
|
159
254
|
if (info === undefined) {
|
|
255
|
+
if (underKnownHidden(el, memo)) {
|
|
256
|
+
info = {
|
|
257
|
+
hidden: true,
|
|
258
|
+
style: null,
|
|
259
|
+
parent: null,
|
|
260
|
+
clipX: false,
|
|
261
|
+
clipY: false,
|
|
262
|
+
scrollX: false,
|
|
263
|
+
scrollY: false,
|
|
264
|
+
hasClipping: false,
|
|
265
|
+
};
|
|
266
|
+
memo.set(el, info);
|
|
267
|
+
return info;
|
|
268
|
+
}
|
|
160
269
|
const style = el.ownerDocument.defaultView?.getComputedStyle(el) ?? null;
|
|
161
270
|
if (selfHidden(el, style)) {
|
|
162
271
|
info = {
|
|
@@ -305,6 +414,9 @@ function collapsed(el, memo) {
|
|
|
305
414
|
(own.clipY && 0 === r.height) ||
|
|
306
415
|
(own.clipX && own.clipY && r.width <= 1 && r.height <= 1)));
|
|
307
416
|
}
|
|
417
|
+
export function isHiddenSubtree(el, memo) {
|
|
418
|
+
return ownVisibilityInfo(el, visibilityInfoMemo(memo)).hidden;
|
|
419
|
+
}
|
|
308
420
|
export function isVisible(el, memo) {
|
|
309
421
|
if (!el.isConnected) {
|
|
310
422
|
return false;
|
|
@@ -314,7 +426,10 @@ export function isVisible(el, memo) {
|
|
|
314
426
|
return cached;
|
|
315
427
|
}
|
|
316
428
|
const info = visibilityInfoMemo(memo);
|
|
317
|
-
const result =
|
|
429
|
+
const result = !visibilityHidden(ownVisibilityInfo(el, info).style) &&
|
|
430
|
+
visibleBounds(el, info, undefined, true) !== null &&
|
|
431
|
+
!collapsed(el, info) &&
|
|
432
|
+
!backfaceHidden(el, info);
|
|
318
433
|
if (memo !== undefined) {
|
|
319
434
|
memo.set(el, result);
|
|
320
435
|
}
|
|
@@ -13,6 +13,7 @@ export declare function digestPixels(data: Uint8ClampedArray): {
|
|
|
13
13
|
interface CanvasOptions {
|
|
14
14
|
sample?: (canvas: HTMLCanvasElement) => CanvasSample | undefined;
|
|
15
15
|
every?: (run: () => void, ms: number) => () => void;
|
|
16
|
+
onScreen?: (canvas: HTMLCanvasElement) => boolean;
|
|
16
17
|
}
|
|
17
18
|
export declare function canvasAfterAct(): void;
|
|
18
19
|
export declare function installCanvas(emit: Emit, opts?: CanvasOptions): Teardown;
|
package/dist/observers/canvas.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CanvasTrigger, EventType } from '@reticlehq/core';
|
|
2
2
|
export { CanvasTrigger };
|
|
3
3
|
import { refs } from '../dom/addressing/refs.js';
|
|
4
|
+
import { isInViewport } from '../dom/visibility.js';
|
|
4
5
|
import { nativeSetInterval } from '../timers/native/native-timers.js';
|
|
5
6
|
import { observeSafely, observeValue } from './types.js';
|
|
6
7
|
const SIDE = 16;
|
|
@@ -47,6 +48,7 @@ export function canvasAfterAct() {
|
|
|
47
48
|
export function installCanvas(emit, opts = {}) {
|
|
48
49
|
const sample = opts.sample ?? sampleCanvas;
|
|
49
50
|
const every = opts.every ?? nativeSetInterval;
|
|
51
|
+
const onScreen = opts.onScreen ?? ((canvas) => isInViewport(canvas));
|
|
50
52
|
const last = new WeakMap();
|
|
51
53
|
const look = (trigger) => {
|
|
52
54
|
observeSafely(() => {
|
|
@@ -61,7 +63,14 @@ export function installCanvas(emit, opts = {}) {
|
|
|
61
63
|
continue;
|
|
62
64
|
}
|
|
63
65
|
last.set(canvas, digest);
|
|
64
|
-
emit(EventType.CANVAS_DIGEST, {
|
|
66
|
+
emit(EventType.CANVAS_DIGEST, {
|
|
67
|
+
digest,
|
|
68
|
+
width: read.width,
|
|
69
|
+
height: read.height,
|
|
70
|
+
mean,
|
|
71
|
+
trigger,
|
|
72
|
+
visible: onScreen(canvas),
|
|
73
|
+
}, refs.refFor(canvas));
|
|
65
74
|
}
|
|
66
75
|
});
|
|
67
76
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EventType, TRANSPORT_LIMITS } from '@reticlehq/core';
|
|
1
|
+
import { DROPPED_PRE_ATTACH_FIELD, EventType, PRE_ATTACH_FIELD, RETICLE_PRE_ATTACH_ERRORS, TRANSPORT_LIMITS, } from '@reticlehq/core';
|
|
2
2
|
import { observeSafely } from './types.js';
|
|
3
3
|
import { safeStringify } from '../security/serialization.js';
|
|
4
4
|
import { requireCapturedMethod } from '../util/captured-method.js';
|
|
@@ -15,44 +15,48 @@ const METHOD_EVENT = {
|
|
|
15
15
|
function stringifyArgs(args) {
|
|
16
16
|
return args
|
|
17
17
|
.map((a) => {
|
|
18
|
-
if ('string' === typeof a)
|
|
18
|
+
if ('string' === typeof a) {
|
|
19
19
|
return a;
|
|
20
|
-
|
|
20
|
+
}
|
|
21
|
+
if (a instanceof Error) {
|
|
21
22
|
return a.message;
|
|
23
|
+
}
|
|
22
24
|
return safeStringify(a);
|
|
23
25
|
})
|
|
24
26
|
.join(' ');
|
|
25
27
|
}
|
|
26
28
|
const MAX_STACK_LEN = TRANSPORT_LIMITS.MAX_STACK_LENGTH;
|
|
27
29
|
function capStack(stack) {
|
|
28
|
-
if (stack === undefined || 0 === stack.length)
|
|
30
|
+
if (stack === undefined || 0 === stack.length) {
|
|
29
31
|
return undefined;
|
|
32
|
+
}
|
|
30
33
|
return stack.length > MAX_STACK_LEN ? stack.slice(0, MAX_STACK_LEN) : stack;
|
|
31
34
|
}
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return capStack(arg.stack);
|
|
36
|
-
}
|
|
37
|
-
return undefined;
|
|
35
|
+
function stackOf(value) {
|
|
36
|
+
const stack = value instanceof Error ? capStack(value.stack) : undefined;
|
|
37
|
+
return stack === undefined ? {} : { stack };
|
|
38
38
|
}
|
|
39
39
|
export function installConsole(emit) {
|
|
40
40
|
const methods = ['log', 'warn', 'error', 'info', 'debug'];
|
|
41
41
|
const originals = new Map();
|
|
42
42
|
const patched = new Map();
|
|
43
|
+
let marks = {};
|
|
44
|
+
const report = (method, args) => {
|
|
45
|
+
observeSafely(() => {
|
|
46
|
+
emit(METHOD_EVENT[method], {
|
|
47
|
+
...marks,
|
|
48
|
+
message: stringifyArgs(args),
|
|
49
|
+
...('error' === method ? stackOf(args.find((arg) => arg instanceof Error)) : {}),
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
};
|
|
43
53
|
for (const method of methods) {
|
|
44
54
|
const original = requireCapturedMethod(console, method);
|
|
45
55
|
originals.set(method, original);
|
|
46
56
|
const callOriginal = original.bind(console);
|
|
47
57
|
const wrapper = (...args) => {
|
|
48
58
|
callOriginal(...args);
|
|
49
|
-
|
|
50
|
-
const stack = 'error' === method ? firstErrorStack(args) : undefined;
|
|
51
|
-
emit(METHOD_EVENT[method], {
|
|
52
|
-
message: stringifyArgs(args),
|
|
53
|
-
...(stack === undefined ? {} : { stack }),
|
|
54
|
-
});
|
|
55
|
-
});
|
|
59
|
+
report(method, args);
|
|
56
60
|
};
|
|
57
61
|
patched.set(method, wrapper);
|
|
58
62
|
console[method] = wrapper;
|
|
@@ -60,9 +64,11 @@ export function installConsole(emit) {
|
|
|
60
64
|
const repeats = createRepeatLimiter();
|
|
61
65
|
const emitError = (payload) => {
|
|
62
66
|
const decision = repeats.admit(payload);
|
|
63
|
-
if (!decision.emit)
|
|
67
|
+
if (!decision.emit) {
|
|
64
68
|
return;
|
|
69
|
+
}
|
|
65
70
|
emit(EventType.ERROR_UNCAUGHT, {
|
|
71
|
+
...marks,
|
|
66
72
|
...payload,
|
|
67
73
|
...(undefined === decision.repeats ? {} : { repeats: decision.repeats }),
|
|
68
74
|
});
|
|
@@ -70,8 +76,9 @@ export function installConsole(emit) {
|
|
|
70
76
|
const onError = (event) => {
|
|
71
77
|
if (!(event instanceof ErrorEvent)) {
|
|
72
78
|
const target = event.target;
|
|
73
|
-
if (!(target instanceof Element) || target === document)
|
|
79
|
+
if (!(target instanceof Element) || target === document) {
|
|
74
80
|
return;
|
|
81
|
+
}
|
|
75
82
|
const tag = target.tagName.toLowerCase();
|
|
76
83
|
const url = target.getAttribute('src') ?? target.getAttribute('href') ?? '';
|
|
77
84
|
observeSafely(() => {
|
|
@@ -81,37 +88,32 @@ export function installConsole(emit) {
|
|
|
81
88
|
return;
|
|
82
89
|
}
|
|
83
90
|
observeSafely(() => {
|
|
84
|
-
|
|
85
|
-
const payload = {
|
|
91
|
+
emitError({
|
|
86
92
|
message: event.message,
|
|
87
93
|
source: event.filename,
|
|
88
94
|
line: event.lineno,
|
|
89
|
-
...(
|
|
90
|
-
};
|
|
91
|
-
emitError(payload);
|
|
95
|
+
...stackOf(event.error),
|
|
96
|
+
});
|
|
92
97
|
});
|
|
93
98
|
};
|
|
94
99
|
const onRejection = (event) => {
|
|
95
100
|
observeSafely(() => {
|
|
96
101
|
const reason = event.reason;
|
|
97
|
-
|
|
98
|
-
const payload = {
|
|
102
|
+
emitError({
|
|
99
103
|
message: reason instanceof Error ? reason.message : String(reason),
|
|
100
104
|
kind: 'unhandledrejection',
|
|
101
|
-
...(
|
|
102
|
-
};
|
|
103
|
-
emitError(payload);
|
|
105
|
+
...stackOf(reason),
|
|
106
|
+
});
|
|
104
107
|
});
|
|
105
108
|
};
|
|
106
109
|
const onViolation = (event) => {
|
|
107
|
-
const
|
|
110
|
+
const uri = event.blockedURI;
|
|
111
|
+
const what = 0 === uri.length ? 'inline content' : uri;
|
|
108
112
|
const reportOnly = REPORT_ONLY === event.disposition;
|
|
109
113
|
emit(reportOnly ? EventType.CONSOLE_WARN : EventType.CONSOLE_ERROR, {
|
|
110
|
-
message: reportOnly
|
|
111
|
-
? `[Report Only] Content Security Policy directive: ${event.violatedDirective} would block ${what}`
|
|
112
|
-
: `Content Security Policy directive: ${event.violatedDirective} blocked ${what}`,
|
|
114
|
+
message: `${reportOnly ? '[Report Only] ' : ''}Content Security Policy directive: ${event.violatedDirective} ${reportOnly ? 'would block' : 'blocked'} ${what}`,
|
|
113
115
|
kind: CSP_VIOLATION_EVENT,
|
|
114
|
-
...(0 ===
|
|
116
|
+
...(0 === uri.length ? {} : { source: uri }),
|
|
115
117
|
});
|
|
116
118
|
};
|
|
117
119
|
const listeners = new AbortController();
|
|
@@ -119,10 +121,19 @@ export function installConsole(emit) {
|
|
|
119
121
|
window.addEventListener('error', onError, { capture: true, signal });
|
|
120
122
|
window.addEventListener('unhandledrejection', onRejection, { signal });
|
|
121
123
|
window.addEventListener(CSP_VIOLATION_EVENT, onViolation, { capture: true, signal });
|
|
124
|
+
observeSafely(() => {
|
|
125
|
+
const mark = { [PRE_ATTACH_FIELD]: true };
|
|
126
|
+
marks = mark;
|
|
127
|
+
globalThis[RETICLE_PRE_ATTACH_ERRORS]?.drain(onError, onRejection, report, (count) => {
|
|
128
|
+
mark[DROPPED_PRE_ATTACH_FIELD] = Number.isInteger(count) && count > 0 ? count : undefined;
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
marks = {};
|
|
122
132
|
return () => {
|
|
123
133
|
for (const [method, original] of originals) {
|
|
124
|
-
if (console[method] === patched.get(method))
|
|
134
|
+
if (console[method] === patched.get(method)) {
|
|
125
135
|
console[method] = original;
|
|
136
|
+
}
|
|
126
137
|
}
|
|
127
138
|
listeners.abort();
|
|
128
139
|
};
|
package/dist/observers/dom.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readTestId, testIdSelector } from '../dom/addressing/testid-attr.js';
|
|
2
|
-
import { EventType, REDACTED_VALUE, TruncationChannel } from '@reticlehq/core';
|
|
2
|
+
import { EventType, LivePoliteness, REDACTED_VALUE, TruncationChannel } from '@reticlehq/core';
|
|
3
3
|
import { getAccessibleName, getRole, isSensitiveField } from '../dom/a11y.js';
|
|
4
4
|
import { isVisible } from '../dom/visibility.js';
|
|
5
5
|
import { refs } from '../dom/addressing/refs.js';
|
|
@@ -108,6 +108,24 @@ const named = (el) => {
|
|
|
108
108
|
const testid = testidNear(el);
|
|
109
109
|
return testid === undefined ? {} : { testid };
|
|
110
110
|
};
|
|
111
|
+
const ALERT_REGION = '[role="alert"]';
|
|
112
|
+
const LIVE_REGION = `[aria-live],${ALERT_REGION},[role="status"]`;
|
|
113
|
+
const liveOf = (el) => {
|
|
114
|
+
const region = el.closest(LIVE_REGION);
|
|
115
|
+
if (null === region) {
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
const level = region.getAttribute('aria-live');
|
|
119
|
+
if (level === LivePoliteness.ASSERTIVE || level === LivePoliteness.POLITE) {
|
|
120
|
+
return { live: level };
|
|
121
|
+
}
|
|
122
|
+
if (level !== null) {
|
|
123
|
+
return {};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
live: region.matches(ALERT_REGION) ? LivePoliteness.ASSERTIVE : LivePoliteness.POLITE,
|
|
127
|
+
};
|
|
128
|
+
};
|
|
111
129
|
export function installDom(emit) {
|
|
112
130
|
const stopRegistry = installShadowRegistry();
|
|
113
131
|
const observer = new MutationObserver((records) => {
|
|
@@ -149,7 +167,7 @@ export function installDom(emit) {
|
|
|
149
167
|
? REDACTED_VALUE
|
|
150
168
|
: (record.target.textContent ?? '').trim().slice(0, 80);
|
|
151
169
|
const old = hide ? undefined : capValue(record.oldValue?.trim() ?? null);
|
|
152
|
-
emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }), ...named(parent) }, refs.refFor(parent));
|
|
170
|
+
emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }), ...named(parent), ...liveOf(parent) }, refs.refFor(parent));
|
|
153
171
|
}
|
|
154
172
|
continue;
|
|
155
173
|
}
|
|
@@ -164,7 +182,7 @@ export function installDom(emit) {
|
|
|
164
182
|
const hide = isSensitiveField(parent);
|
|
165
183
|
const text = hide ? REDACTED_VALUE : (parent.textContent ?? '').trim().slice(0, 80);
|
|
166
184
|
const old = hide ? undefined : capValue(textOf(record.removedNodes) ?? null);
|
|
167
|
-
emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }), ...named(parent) }, refs.refFor(parent));
|
|
185
|
+
emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }), ...named(parent), ...liveOf(parent) }, refs.refFor(parent));
|
|
168
186
|
}
|
|
169
187
|
}
|
|
170
188
|
}
|
|
@@ -226,7 +244,13 @@ export function installDom(emit) {
|
|
|
226
244
|
}
|
|
227
245
|
const { role, name } = gone;
|
|
228
246
|
removed += 1;
|
|
229
|
-
|
|
247
|
+
const testid = readTestId(gone.el) ?? readTestId(node);
|
|
248
|
+
emit(EventType.DOM_REMOVED, {
|
|
249
|
+
role,
|
|
250
|
+
name,
|
|
251
|
+
region: regionKeyOf(record.target),
|
|
252
|
+
...(null === testid ? {} : { testid }),
|
|
253
|
+
});
|
|
230
254
|
}
|
|
231
255
|
}
|
|
232
256
|
if (dropped > 0) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { EventType } from '@reticlehq/core';
|
|
2
|
+
type EmitNet = (type: EventType, data: Record<string, unknown>) => void;
|
|
3
|
+
export declare function absoluteUrl(raw: string): string;
|
|
4
|
+
export interface ResourceTimingInput {
|
|
5
|
+
emit: EmitNet;
|
|
6
|
+
installedAt: number;
|
|
7
|
+
nextId: () => string;
|
|
8
|
+
reportedNetUrls: Set<string>;
|
|
9
|
+
patchedNetUrls: ReadonlySet<string>;
|
|
10
|
+
}
|
|
11
|
+
export declare function observeResourceTiming(input: ResourceTimingInput): PerformanceObserver | undefined;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { EventType } from '@reticlehq/core';
|
|
2
|
+
import { netUrlFields } from './network-redact.js';
|
|
3
|
+
const RESOURCE_CAP = 200;
|
|
4
|
+
export function absoluteUrl(raw) {
|
|
5
|
+
try {
|
|
6
|
+
return new URL(raw, location.href).href;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return raw;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function statusFields(entry, withOk) {
|
|
13
|
+
const status = entry.responseStatus ?? 0;
|
|
14
|
+
if (status <= 0) {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
return withOk ? { status, ok: status >= 200 && status < 400 } : { status };
|
|
18
|
+
}
|
|
19
|
+
export function observeResourceTiming(input) {
|
|
20
|
+
const { emit, installedAt, nextId, reportedNetUrls, patchedNetUrls } = input;
|
|
21
|
+
if ('undefined' === typeof PerformanceObserver) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
let subresourceEvents = 0;
|
|
25
|
+
let unpatchedEvents = 0;
|
|
26
|
+
const onEntry = (entry) => {
|
|
27
|
+
const type = entry.initiatorType || 'other';
|
|
28
|
+
const rawUrl = entry.name;
|
|
29
|
+
const patched = 'fetch' === type || 'xmlhttprequest' === type;
|
|
30
|
+
if (patched && entry.startTime >= installedAt) {
|
|
31
|
+
if (patchedNetUrls.has(absoluteUrl(rawUrl)) || unpatchedEvents >= RESOURCE_CAP) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
unpatchedEvents += 1;
|
|
35
|
+
emit(EventType.NET_UNPATCHED, {
|
|
36
|
+
...netUrlFields(rawUrl),
|
|
37
|
+
initiator: type,
|
|
38
|
+
...statusFields(entry, false),
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (reportedNetUrls.has(rawUrl) || subresourceEvents >= RESOURCE_CAP) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
reportedNetUrls.add(rawUrl);
|
|
46
|
+
subresourceEvents += 1;
|
|
47
|
+
emit(EventType.NET_REQUEST, {
|
|
48
|
+
id: nextId(),
|
|
49
|
+
method: 'GET',
|
|
50
|
+
...netUrlFields(rawUrl),
|
|
51
|
+
durationMs: Math.round(entry.duration),
|
|
52
|
+
initiator: type,
|
|
53
|
+
...(entry.transferSize > 0 ? { transferSize: entry.transferSize } : {}),
|
|
54
|
+
...statusFields(entry, true),
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
try {
|
|
58
|
+
const observer = new PerformanceObserver((list) => {
|
|
59
|
+
for (const raw of list.getEntries()) {
|
|
60
|
+
onEntry(raw);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
observer.observe({ type: 'resource', buffered: true });
|
|
64
|
+
return observer;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -7,6 +7,7 @@ import { redactUrl, netUrlFields } from './net-detail/network-redact.js';
|
|
|
7
7
|
import { watchStreamedBody } from './network-stream.js';
|
|
8
8
|
import { frameFields, isBridgeSocket } from './net-detail/network-frames.js';
|
|
9
9
|
import { requireCapturedMethod } from '../util/captured-method.js';
|
|
10
|
+
import { absoluteUrl, observeResourceTiming } from './net-detail/resource-timing.js';
|
|
10
11
|
export { redactUrl, netUrlFields };
|
|
11
12
|
const ShapeToken = { NULL: '0', STRING: 's', NUMBER: 'n', BOOLEAN: 'b', OTHER: '?' };
|
|
12
13
|
function shapeOf(value) {
|
|
@@ -228,6 +229,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
228
229
|
}
|
|
229
230
|
const owner = {};
|
|
230
231
|
pageSlots()[PAGE_CAPTURE] = owner;
|
|
232
|
+
const installedAt = performance.now();
|
|
231
233
|
const captureBodies = true === opts.captureBodies;
|
|
232
234
|
const bodyCapture = new AbortController();
|
|
233
235
|
const captureErrorBodies = false !== opts.captureErrorBodies;
|
|
@@ -239,8 +241,11 @@ export function installNetwork(emit, opts = {}) {
|
|
|
239
241
|
}
|
|
240
242
|
let seq = 0;
|
|
241
243
|
const nextId = () => `n${++seq}`;
|
|
244
|
+
const reportedNetUrls = new Set();
|
|
245
|
+
const patchedNetUrls = new Set();
|
|
242
246
|
window.fetch = async (input, init) => {
|
|
243
247
|
const rawUrl = urlOf(input);
|
|
248
|
+
patchedNetUrls.add(absoluteUrl(rawUrl));
|
|
244
249
|
if (true === opts.ignore?.(rawUrl)) {
|
|
245
250
|
return callFetch(input, init);
|
|
246
251
|
}
|
|
@@ -353,6 +358,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
353
358
|
rawUrl: String(url),
|
|
354
359
|
start: 0,
|
|
355
360
|
});
|
|
361
|
+
patchedNetUrls.add(absoluteUrl(String(url)));
|
|
356
362
|
});
|
|
357
363
|
callOpen.call(this, method, url, ...rest);
|
|
358
364
|
};
|
|
@@ -537,50 +543,13 @@ export function installNetwork(emit, opts = {}) {
|
|
|
537
543
|
};
|
|
538
544
|
navProto.sendBeacon = patchedBeacon;
|
|
539
545
|
}
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
for (const raw of list.getEntries()) {
|
|
548
|
-
const entry = raw;
|
|
549
|
-
if (subresourceEvents >= SUBRESOURCE_CAP) {
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
const type = entry.initiatorType || 'other';
|
|
553
|
-
if ('fetch' === type || 'xmlhttprequest' === type) {
|
|
554
|
-
continue;
|
|
555
|
-
}
|
|
556
|
-
const rawUrl = entry.name;
|
|
557
|
-
if (reportedNetUrls.has(rawUrl)) {
|
|
558
|
-
continue;
|
|
559
|
-
}
|
|
560
|
-
reportedNetUrls.add(rawUrl);
|
|
561
|
-
subresourceEvents += 1;
|
|
562
|
-
emit(EventType.NET_REQUEST, {
|
|
563
|
-
id: nextId(),
|
|
564
|
-
method: 'GET',
|
|
565
|
-
...netUrlFields(rawUrl),
|
|
566
|
-
durationMs: Math.round(entry.duration),
|
|
567
|
-
initiator: type,
|
|
568
|
-
...(entry.transferSize > 0 ? { transferSize: entry.transferSize } : {}),
|
|
569
|
-
...((entry.responseStatus ?? 0) > 0
|
|
570
|
-
? {
|
|
571
|
-
status: entry.responseStatus,
|
|
572
|
-
ok: (entry.responseStatus ?? 0) >= 200 && (entry.responseStatus ?? 0) < 400,
|
|
573
|
-
}
|
|
574
|
-
: {}),
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
});
|
|
578
|
-
subresourceObserver = observer;
|
|
579
|
-
observer.observe({ type: 'resource', buffered: true });
|
|
580
|
-
}
|
|
581
|
-
catch {
|
|
582
|
-
}
|
|
583
|
-
}
|
|
546
|
+
const subresourceObserver = observeResourceTiming({
|
|
547
|
+
emit,
|
|
548
|
+
installedAt,
|
|
549
|
+
nextId,
|
|
550
|
+
reportedNetUrls,
|
|
551
|
+
patchedNetUrls,
|
|
552
|
+
});
|
|
584
553
|
return () => {
|
|
585
554
|
if (pageSlots()[PAGE_CAPTURE] === owner) {
|
|
586
555
|
delete pageSlots()[PAGE_CAPTURE];
|