@reticlehq/browser 3.9.0 → 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 +16 -6
- package/dist/actions/actions-dom.js +94 -46
- package/dist/actions/actions-press.d.ts +10 -0
- package/dist/actions/actions-press.js +146 -2
- package/dist/actions/actions.js +52 -22
- 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 +82 -6
- package/dist/connect-options.d.ts +11 -1
- package/dist/connect-options.js +27 -1
- package/dist/dom/a11y.js +4 -3
- 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 +1 -0
- package/dist/dom/query.js +78 -51
- 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 +4 -3
- package/dist/dom/theme.js +6 -7
- package/dist/dom/visibility.d.ts +2 -0
- package/dist/dom/visibility.js +40 -8
- 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 +12 -45
- 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 +597 -597
- package/dist/reticle.d.ts +3 -19
- package/dist/reticle.js +74 -108
- package/dist/security/serialization.js +35 -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
|
@@ -25,6 +25,10 @@ export function stalledFade(el) {
|
|
|
25
25
|
}
|
|
26
26
|
const OVERLAY_ARIA_HIDDEN = 'data-aria-hidden';
|
|
27
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
|
+
}
|
|
28
32
|
const UNPAINTED_TAGS = new Set(['script', 'style', 'template', 'noscript', 'link', 'meta']);
|
|
29
33
|
function hiddenByAnOverlay(el) {
|
|
30
34
|
if (el.hasAttribute(OVERLAY_ARIA_HIDDEN)) {
|
|
@@ -57,7 +61,7 @@ function holdsOpenLayer(portal) {
|
|
|
57
61
|
}
|
|
58
62
|
return false;
|
|
59
63
|
}
|
|
60
|
-
function selfCssHidden(el, style) {
|
|
64
|
+
function selfCssHidden(el, style, includeVisibility = true) {
|
|
61
65
|
if (isHtmlElement(el) && el.hidden) {
|
|
62
66
|
return true;
|
|
63
67
|
}
|
|
@@ -65,9 +69,7 @@ function selfCssHidden(el, style) {
|
|
|
65
69
|
return true;
|
|
66
70
|
}
|
|
67
71
|
if (style !== null) {
|
|
68
|
-
if ('none' === style.display ||
|
|
69
|
-
'hidden' === style.visibility ||
|
|
70
|
-
'collapse' === style.visibility) {
|
|
72
|
+
if ('none' === style.display || (includeVisibility && visibilityHidden(style))) {
|
|
71
73
|
return true;
|
|
72
74
|
}
|
|
73
75
|
if (0 === Number.parseFloat(style.opacity || '1') && !stalledFade(el)) {
|
|
@@ -79,6 +81,9 @@ function selfCssHidden(el, style) {
|
|
|
79
81
|
}
|
|
80
82
|
return false;
|
|
81
83
|
}
|
|
84
|
+
function visibilityHidden(style) {
|
|
85
|
+
return 'hidden' === style?.visibility || 'collapse' === style?.visibility;
|
|
86
|
+
}
|
|
82
87
|
function clippedAway(style) {
|
|
83
88
|
const rect = /^rect\((.*)\)$/.exec(style.getPropertyValue('clip').trim());
|
|
84
89
|
if (null !== rect && ('absolute' === style.position || 'fixed' === style.position)) {
|
|
@@ -106,7 +111,7 @@ function selfHidden(el, style) {
|
|
|
106
111
|
if ('true' === el.getAttribute('aria-hidden') && !hiddenByAnOverlay(el)) {
|
|
107
112
|
return true;
|
|
108
113
|
}
|
|
109
|
-
return selfCssHidden(el, style);
|
|
114
|
+
return selfCssHidden(el, style, false);
|
|
110
115
|
}
|
|
111
116
|
export function isHiddenByAriaOnly(el) {
|
|
112
117
|
let foundAriaHidden = false;
|
|
@@ -116,7 +121,7 @@ export function isHiddenByAriaOnly(el) {
|
|
|
116
121
|
foundAriaHidden = true;
|
|
117
122
|
}
|
|
118
123
|
const style = current.ownerDocument.defaultView?.getComputedStyle(current) ?? null;
|
|
119
|
-
if (selfCssHidden(current, style)) {
|
|
124
|
+
if (selfCssHidden(current, style, current === el)) {
|
|
120
125
|
return false;
|
|
121
126
|
}
|
|
122
127
|
current = parentAcrossShadowBoundary(current);
|
|
@@ -130,7 +135,7 @@ export function isRenderedOutsideAriaHidden(el) {
|
|
|
130
135
|
return false;
|
|
131
136
|
}
|
|
132
137
|
const style = current.ownerDocument.defaultView?.getComputedStyle(current) ?? null;
|
|
133
|
-
if (selfCssHidden(current, style)) {
|
|
138
|
+
if (selfCssHidden(current, style, current === el)) {
|
|
134
139
|
return false;
|
|
135
140
|
}
|
|
136
141
|
current = parentAcrossShadowBoundary(current);
|
|
@@ -235,9 +240,32 @@ function visibilityInfoMemo(memo) {
|
|
|
235
240
|
}
|
|
236
241
|
return info;
|
|
237
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
|
+
}
|
|
238
252
|
function ownVisibilityInfo(el, memo) {
|
|
239
253
|
let info = memo.get(el);
|
|
240
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
|
+
}
|
|
241
269
|
const style = el.ownerDocument.defaultView?.getComputedStyle(el) ?? null;
|
|
242
270
|
if (selfHidden(el, style)) {
|
|
243
271
|
info = {
|
|
@@ -386,6 +414,9 @@ function collapsed(el, memo) {
|
|
|
386
414
|
(own.clipY && 0 === r.height) ||
|
|
387
415
|
(own.clipX && own.clipY && r.width <= 1 && r.height <= 1)));
|
|
388
416
|
}
|
|
417
|
+
export function isHiddenSubtree(el, memo) {
|
|
418
|
+
return ownVisibilityInfo(el, visibilityInfoMemo(memo)).hidden;
|
|
419
|
+
}
|
|
389
420
|
export function isVisible(el, memo) {
|
|
390
421
|
if (!el.isConnected) {
|
|
391
422
|
return false;
|
|
@@ -395,7 +426,8 @@ export function isVisible(el, memo) {
|
|
|
395
426
|
return cached;
|
|
396
427
|
}
|
|
397
428
|
const info = visibilityInfoMemo(memo);
|
|
398
|
-
const result =
|
|
429
|
+
const result = !visibilityHidden(ownVisibilityInfo(el, info).style) &&
|
|
430
|
+
visibleBounds(el, info, undefined, true) !== null &&
|
|
399
431
|
!collapsed(el, info) &&
|
|
400
432
|
!backfaceHidden(el, info);
|
|
401
433
|
if (memo !== undefined) {
|
|
@@ -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) {
|
|
@@ -240,8 +241,11 @@ export function installNetwork(emit, opts = {}) {
|
|
|
240
241
|
}
|
|
241
242
|
let seq = 0;
|
|
242
243
|
const nextId = () => `n${++seq}`;
|
|
244
|
+
const reportedNetUrls = new Set();
|
|
245
|
+
const patchedNetUrls = new Set();
|
|
243
246
|
window.fetch = async (input, init) => {
|
|
244
247
|
const rawUrl = urlOf(input);
|
|
248
|
+
patchedNetUrls.add(absoluteUrl(rawUrl));
|
|
245
249
|
if (true === opts.ignore?.(rawUrl)) {
|
|
246
250
|
return callFetch(input, init);
|
|
247
251
|
}
|
|
@@ -354,6 +358,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
354
358
|
rawUrl: String(url),
|
|
355
359
|
start: 0,
|
|
356
360
|
});
|
|
361
|
+
patchedNetUrls.add(absoluteUrl(String(url)));
|
|
357
362
|
});
|
|
358
363
|
callOpen.call(this, method, url, ...rest);
|
|
359
364
|
};
|
|
@@ -538,51 +543,13 @@ export function installNetwork(emit, opts = {}) {
|
|
|
538
543
|
};
|
|
539
544
|
navProto.sendBeacon = patchedBeacon;
|
|
540
545
|
}
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
for (const raw of list.getEntries()) {
|
|
549
|
-
const entry = raw;
|
|
550
|
-
if (subresourceEvents >= SUBRESOURCE_CAP) {
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
const type = entry.initiatorType || 'other';
|
|
554
|
-
const patched = 'fetch' === type || 'xmlhttprequest' === type;
|
|
555
|
-
if (patched && entry.startTime >= installedAt) {
|
|
556
|
-
continue;
|
|
557
|
-
}
|
|
558
|
-
const rawUrl = entry.name;
|
|
559
|
-
if (reportedNetUrls.has(rawUrl)) {
|
|
560
|
-
continue;
|
|
561
|
-
}
|
|
562
|
-
reportedNetUrls.add(rawUrl);
|
|
563
|
-
subresourceEvents += 1;
|
|
564
|
-
emit(EventType.NET_REQUEST, {
|
|
565
|
-
id: nextId(),
|
|
566
|
-
method: 'GET',
|
|
567
|
-
...netUrlFields(rawUrl),
|
|
568
|
-
durationMs: Math.round(entry.duration),
|
|
569
|
-
initiator: type,
|
|
570
|
-
...(entry.transferSize > 0 ? { transferSize: entry.transferSize } : {}),
|
|
571
|
-
...((entry.responseStatus ?? 0) > 0
|
|
572
|
-
? {
|
|
573
|
-
status: entry.responseStatus,
|
|
574
|
-
ok: (entry.responseStatus ?? 0) >= 200 && (entry.responseStatus ?? 0) < 400,
|
|
575
|
-
}
|
|
576
|
-
: {}),
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
});
|
|
580
|
-
subresourceObserver = observer;
|
|
581
|
-
observer.observe({ type: 'resource', buffered: true });
|
|
582
|
-
}
|
|
583
|
-
catch {
|
|
584
|
-
}
|
|
585
|
-
}
|
|
546
|
+
const subresourceObserver = observeResourceTiming({
|
|
547
|
+
emit,
|
|
548
|
+
installedAt,
|
|
549
|
+
nextId,
|
|
550
|
+
reportedNetUrls,
|
|
551
|
+
patchedNetUrls,
|
|
552
|
+
});
|
|
586
553
|
return () => {
|
|
587
554
|
if (pageSlots()[PAGE_CAPTURE] === owner) {
|
|
588
555
|
delete pageSlots()[PAGE_CAPTURE];
|
package/dist/observers/perf.js
CHANGED
|
@@ -45,6 +45,15 @@ function largestShifted(sources) {
|
|
|
45
45
|
? undefined
|
|
46
46
|
: { ref: refs.refFor(best.el), selector: shortSelector(best.el) };
|
|
47
47
|
}
|
|
48
|
+
function onlyReticleMoved(sources) {
|
|
49
|
+
if (sources === undefined || 0 === sources.length) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return sources.every(({ node }) => {
|
|
53
|
+
const el = isElement(node) ? node : node?.parentElement;
|
|
54
|
+
return el !== null && el !== undefined && isReticleOverlay(el);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
48
57
|
export function installPerf(emit) {
|
|
49
58
|
if (typeof PerformanceObserver !== 'function')
|
|
50
59
|
return () => undefined;
|
|
@@ -74,11 +83,16 @@ export function installPerf(emit) {
|
|
|
74
83
|
const ls = e;
|
|
75
84
|
if (true === ls.hadRecentInput)
|
|
76
85
|
return;
|
|
77
|
-
|
|
86
|
+
if (onlyReticleMoved(ls.sources)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const shift = ls.value ?? 0;
|
|
90
|
+
cls += shift;
|
|
78
91
|
const shifted = largestShifted(ls.sources);
|
|
79
92
|
emit(EventType.PERF, {
|
|
80
93
|
metric: PerfMetric.CLS,
|
|
81
94
|
value: cls,
|
|
95
|
+
shift,
|
|
82
96
|
at: Math.round(e.startTime),
|
|
83
97
|
...(shifted === undefined ? {} : { shifted }),
|
|
84
98
|
});
|
|
@@ -54,6 +54,7 @@ export declare const GLOW_OFF = "0";
|
|
|
54
54
|
export declare const DATA_ON = "data-on";
|
|
55
55
|
export declare const STATE_ATTR = "data-reticle-state";
|
|
56
56
|
export declare const UNREACHABLE_STATE = "unreachable";
|
|
57
|
+
export declare const NARROW_VIEWPORT_PX = 480;
|
|
57
58
|
export declare const MIN_ATTR = "data-reticle-min";
|
|
58
59
|
export declare const CHAT_ATTR = "data-reticle-chat";
|
|
59
60
|
export declare const DOCK_ATTR = "data-reticle-dock";
|
|
@@ -35,6 +35,7 @@ export const GLOW_OFF = '0';
|
|
|
35
35
|
export const DATA_ON = 'data-on';
|
|
36
36
|
export const STATE_ATTR = 'data-reticle-state';
|
|
37
37
|
export const UNREACHABLE_STATE = 'unreachable';
|
|
38
|
+
export const NARROW_VIEWPORT_PX = 480;
|
|
38
39
|
export const MIN_ATTR = 'data-reticle-min';
|
|
39
40
|
export const CHAT_ATTR = 'data-reticle-chat';
|
|
40
41
|
export const DOCK_ATTR = 'data-reticle-dock';
|
|
@@ -53,4 +53,5 @@ export declare function wasLeftOpen(): boolean;
|
|
|
53
53
|
export declare function rememberLeftOpen(open: boolean): void;
|
|
54
54
|
export declare function shouldReopenPanel(): boolean;
|
|
55
55
|
export declare function shouldAutoOpenChat(autoOpenChat: boolean): boolean;
|
|
56
|
+
export declare function isNarrowViewport(): boolean;
|
|
56
57
|
export {};
|
|
@@ -3,7 +3,7 @@ import { PresenterReport, reportPanelHtml } from './presenter-report.js';
|
|
|
3
3
|
import { paintToolbarAccount, TOOLBAR_ACCOUNT_ATTR } from './presenter-workspace.js';
|
|
4
4
|
import { mountAccountControl, TEAM_SLOT_ATTR } from './presenter-account.js';
|
|
5
5
|
import { paintSettingsAccount } from './presenter-settings.js';
|
|
6
|
-
import { CHAT_MIN_ATTR, CHAT_PILL_ATTR, REPORT_ATTR, ANNOTATE_BTN_ATTR, CHAT_ATTR, CHAT_TOGGLE_ATTR, CHAT_PANEL_ATTR, DOCK_ATTR, FAB_ATTR, MIN_ATTR, SETTINGS_ATTR, SETTINGS_BTN_ATTR, MINIMISED_STORAGE_KEY, OPEN_STORAGE_KEY, } from './presenter-config.js';
|
|
6
|
+
import { CHAT_MIN_ATTR, CHAT_PILL_ATTR, REPORT_ATTR, ANNOTATE_BTN_ATTR, CHAT_ATTR, CHAT_TOGGLE_ATTR, CHAT_PANEL_ATTR, DOCK_ATTR, FAB_ATTR, MIN_ATTR, NARROW_VIEWPORT_PX, SETTINGS_ATTR, SETTINGS_BTN_ATTR, MINIMISED_STORAGE_KEY, OPEN_STORAGE_KEY, } from './presenter-config.js';
|
|
7
7
|
import { paintCarousel } from './carousel/carousel.js';
|
|
8
8
|
import { panelSlides } from './carousel/panel-slides.js';
|
|
9
9
|
import { paintRail, RAIL_HTML, RAIL_PROMO_ATTR, RAIL_SIGNIN_HTML } from './presenter-frame.js';
|
|
@@ -513,8 +513,11 @@ export function rememberLeftOpen(open) {
|
|
|
513
513
|
}
|
|
514
514
|
}
|
|
515
515
|
export function shouldReopenPanel() {
|
|
516
|
-
return wasLeftOpen() && !wasMinimised();
|
|
516
|
+
return !isNarrowViewport() && wasLeftOpen() && !wasMinimised();
|
|
517
517
|
}
|
|
518
518
|
export function shouldAutoOpenChat(autoOpenChat) {
|
|
519
|
-
return autoOpenChat && !wasMinimised();
|
|
519
|
+
return autoOpenChat && !isNarrowViewport() && !wasMinimised();
|
|
520
|
+
}
|
|
521
|
+
export function isNarrowViewport() {
|
|
522
|
+
return globalThis.innerWidth < NARROW_VIEWPORT_PX;
|
|
520
523
|
}
|
|
@@ -225,6 +225,10 @@ export function mountTour(deps) {
|
|
|
225
225
|
const onKey = (event) => {
|
|
226
226
|
if (!open)
|
|
227
227
|
return;
|
|
228
|
+
if (deps.isDriving()) {
|
|
229
|
+
remove();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
228
232
|
if (appModalOpen(doc))
|
|
229
233
|
return;
|
|
230
234
|
const step = 'ArrowRight' === event.key ? 1 : 'ArrowLeft' === event.key ? -1 : 0;
|