@reticlehq/browser 3.5.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/action-report.d.ts +54 -0
- package/dist/actions/action-report.js +60 -0
- package/dist/actions/actions-dom.d.ts +1 -0
- package/dist/actions/actions-dom.js +56 -43
- package/dist/actions/actions-press.d.ts +11 -4
- package/dist/actions/actions-press.js +132 -17
- package/dist/actions/actions.d.ts +1 -35
- package/dist/actions/actions.js +32 -65
- package/dist/actions/appeared-text.js +3 -0
- package/dist/actions/danger-context.d.ts +2 -0
- package/dist/actions/danger-context.js +29 -3
- package/dist/actions/value-input.d.ts +1 -0
- package/dist/actions/value-input.js +11 -0
- package/dist/commands/commands.js +4 -0
- package/dist/connect-options.d.ts +1 -0
- package/dist/dom/a11y.js +264 -14
- package/dist/dom/addressing/coverage-markers.d.ts +6 -0
- package/dist/dom/addressing/coverage-markers.js +13 -0
- package/dist/dom/addressing/refs.js +2 -1
- package/dist/dom/addressing/testid-attr.d.ts +4 -0
- package/dist/dom/addressing/testid-attr.js +14 -0
- package/dist/dom/dom-ignore.d.ts +2 -0
- package/dist/dom/dom-ignore.js +15 -2
- package/dist/dom/query.d.ts +2 -1
- package/dist/dom/query.js +116 -27
- package/dist/dom/render-page.d.ts +12 -0
- package/dist/dom/render-page.js +135 -0
- package/dist/dom/snapshot.js +56 -7
- package/dist/observers/departure.js +51 -9
- package/dist/observers/dom.js +35 -18
- package/dist/observers/field.js +5 -21
- package/dist/observers/health.js +63 -2
- package/dist/observers/network-stream.d.ts +1 -1
- package/dist/observers/network-stream.js +3 -3
- package/dist/observers/network.js +57 -8
- package/dist/observers/perf.js +53 -1
- package/dist/observers/route.js +6 -5
- package/dist/observers/storage.js +6 -1
- package/dist/presenter/carousel/carousel.d.ts +5 -2
- package/dist/presenter/carousel/carousel.js +30 -8
- package/dist/presenter/carousel/panel-slides.d.ts +3 -4
- package/dist/presenter/carousel/panel-slides.js +48 -9
- package/dist/presenter/carousel/talk-card.d.ts +1 -1
- package/dist/presenter/chrome/log-kinds.d.ts +5 -0
- package/dist/presenter/chrome/log-kinds.js +1 -0
- package/dist/presenter/chrome/log-memory.d.ts +13 -0
- package/dist/presenter/chrome/log-memory.js +39 -0
- package/dist/presenter/chrome/presenter-hud-chrome.d.ts +1 -0
- package/dist/presenter/chrome/presenter-hud-chrome.js +7 -1
- package/dist/presenter/chrome/presenter-log.d.ts +12 -4
- package/dist/presenter/chrome/presenter-log.js +100 -10
- package/dist/presenter/chrome/presenter-morph.d.ts +1 -0
- package/dist/presenter/chrome/presenter-morph.js +34 -0
- package/dist/presenter/chrome/presenter-report-copy.d.ts +28 -5
- package/dist/presenter/chrome/presenter-report-copy.js +40 -5
- package/dist/presenter/hud-telemetry.js +83 -3
- package/dist/presenter/icons/presenter-heroicons-data.d.ts +1 -0
- package/dist/presenter/icons/presenter-heroicons-data.js +4 -3
- package/dist/presenter/icons/presenter-icons.d.ts +2 -0
- package/dist/presenter/icons/presenter-icons.js +2 -0
- package/dist/presenter/presenter-account-styles.js +3 -3
- package/dist/presenter/presenter-account.d.ts +31 -8
- package/dist/presenter/presenter-account.js +105 -15
- package/dist/presenter/presenter-agent-link.d.ts +9 -0
- package/dist/presenter/presenter-agent-link.js +62 -0
- package/dist/presenter/presenter-chat-views.d.ts +43 -0
- package/dist/presenter/presenter-chat-views.js +622 -0
- package/dist/presenter/presenter-config.d.ts +5 -2
- package/dist/presenter/presenter-config.js +5 -3
- package/dist/presenter/presenter-controls.d.ts +3 -2
- package/dist/presenter/presenter-controls.js +163 -61
- package/dist/presenter/presenter-design-tokens.d.ts +1 -0
- package/dist/presenter/presenter-design-tokens.js +34 -0
- package/dist/presenter/presenter-dock-layout.js +1 -1
- package/dist/presenter/presenter-frame.d.ts +12 -0
- package/dist/presenter/presenter-frame.js +187 -0
- package/dist/presenter/presenter-harness-row.d.ts +67 -0
- package/dist/presenter/presenter-harness-row.js +209 -0
- package/dist/presenter/presenter-personas.d.ts +35 -0
- package/dist/presenter/presenter-personas.js +95 -0
- package/dist/presenter/presenter-plan.d.ts +10 -0
- package/dist/presenter/presenter-plan.js +186 -0
- package/dist/presenter/presenter-report-styles.js +46 -21
- package/dist/presenter/presenter-report.d.ts +2 -1
- package/dist/presenter/presenter-report.js +123 -20
- package/dist/presenter/presenter-settings-styles.js +18 -4
- package/dist/presenter/presenter-settings.d.ts +8 -2
- package/dist/presenter/presenter-settings.js +134 -33
- package/dist/presenter/presenter-shell-styles.js +48 -42
- package/dist/presenter/presenter-shell.d.ts +17 -4
- package/dist/presenter/presenter-shell.js +139 -79
- package/dist/presenter/presenter-styles.js +9 -3
- package/dist/presenter/presenter-workspace.js +10 -3
- package/dist/presenter/presenter.d.ts +6 -4
- package/dist/presenter/presenter.js +171 -22
- package/dist/presenter/tour/tour.d.ts +4 -1
- package/dist/presenter/tour/tour.js +6 -4
- package/dist/recorder/recorder.js +2 -2
- package/dist/registry/auto-testids.js +3 -4
- package/dist/registry/declared-platform.js +4 -0
- package/dist/reticle-inject.js +938 -494
- package/dist/reticle.d.ts +10 -1
- package/dist/reticle.js +109 -11
- package/dist/review/annotator-styles.d.ts +1 -1
- package/dist/review/annotator.d.ts +7 -0
- package/dist/review/annotator.js +20 -2
- package/dist/review/mark-anchor.js +2 -2
- package/dist/session-continuity.js +2 -1
- package/dist/storage-keys.d.ts +15 -0
- package/dist/storage-keys.js +21 -0
- package/dist/transport/transport.js +5 -3
- package/dist/transport/unreachable-message.d.ts +1 -1
- package/dist/transport/unreachable-message.js +3 -5
- package/package.json +3 -2
- package/dist/presenter/carousel/offer-card.d.ts +0 -21
- package/dist/presenter/carousel/offer-card.js +0 -55
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { EventType, NetInitiator } from '@reticlehq/core';
|
|
2
2
|
import { observeSafely } from './types.js';
|
|
3
3
|
const NON_NAVIGATING = /^(javascript|mailto|tel|sms|blob|data):/i;
|
|
4
|
+
function navigationOf() {
|
|
5
|
+
if ('undefined' === typeof window)
|
|
6
|
+
return undefined;
|
|
7
|
+
const nav = window.navigation;
|
|
8
|
+
return nav instanceof EventTarget ? nav : undefined;
|
|
9
|
+
}
|
|
4
10
|
function anchorFor(target) {
|
|
5
11
|
if (!(target instanceof Element))
|
|
6
12
|
return undefined;
|
|
7
13
|
const anchor = target.closest('a');
|
|
8
14
|
return anchor instanceof HTMLAnchorElement ? anchor : undefined;
|
|
9
15
|
}
|
|
16
|
+
const DUPLICATE_WINDOW_MS = 1000;
|
|
10
17
|
export function isDeparture(href, downloadAttr, here) {
|
|
11
18
|
if (0 === href.length || NON_NAVIGATING.test(href))
|
|
12
19
|
return false;
|
|
@@ -28,6 +35,24 @@ export function installDeparture(emit) {
|
|
|
28
35
|
if ('undefined' === typeof document)
|
|
29
36
|
return () => undefined;
|
|
30
37
|
let seq = 0;
|
|
38
|
+
let lastRecorded;
|
|
39
|
+
const record = (url, download) => {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
if (lastRecorded !== undefined &&
|
|
42
|
+
lastRecorded.url === url &&
|
|
43
|
+
now - lastRecorded.at < DUPLICATE_WINDOW_MS) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
lastRecorded = { url, at: now };
|
|
47
|
+
seq += 1;
|
|
48
|
+
emit(EventType.NET_PENDING, {
|
|
49
|
+
id: `nav-${String(seq)}`,
|
|
50
|
+
method: 'GET',
|
|
51
|
+
url,
|
|
52
|
+
initiator: NetInitiator.NAVIGATION,
|
|
53
|
+
...(download ? { download: true } : {}),
|
|
54
|
+
});
|
|
55
|
+
};
|
|
31
56
|
const onClick = (event) => {
|
|
32
57
|
observeSafely(() => {
|
|
33
58
|
const anchor = anchorFor(event.target);
|
|
@@ -43,18 +68,35 @@ export function installDeparture(emit) {
|
|
|
43
68
|
observeSafely(() => {
|
|
44
69
|
if (event.defaultPrevented)
|
|
45
70
|
return;
|
|
46
|
-
|
|
47
|
-
emit(EventType.NET_PENDING, {
|
|
48
|
-
id: `nav-${String(seq)}`,
|
|
49
|
-
method: 'GET',
|
|
50
|
-
url,
|
|
51
|
-
initiator: NetInitiator.NAVIGATION,
|
|
52
|
-
...(download ? { download: true } : {}),
|
|
53
|
-
});
|
|
71
|
+
record(url, download);
|
|
54
72
|
});
|
|
55
73
|
});
|
|
56
74
|
});
|
|
57
75
|
};
|
|
76
|
+
const onNavigate = (event) => {
|
|
77
|
+
observeSafely(() => {
|
|
78
|
+
const destination = event.destination;
|
|
79
|
+
if (destination === undefined || destination.sameDocument)
|
|
80
|
+
return;
|
|
81
|
+
const url = destination.url;
|
|
82
|
+
if (!isDeparture(url, false, document.location.href))
|
|
83
|
+
return;
|
|
84
|
+
queueMicrotask(() => {
|
|
85
|
+
observeSafely(() => {
|
|
86
|
+
if (event.defaultPrevented)
|
|
87
|
+
return;
|
|
88
|
+
record(url, false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
const navigation = navigationOf();
|
|
94
|
+
if (navigation !== undefined)
|
|
95
|
+
navigation.addEventListener('navigate', onNavigate);
|
|
58
96
|
document.addEventListener('click', onClick, true);
|
|
59
|
-
return () =>
|
|
97
|
+
return () => {
|
|
98
|
+
document.removeEventListener('click', onClick, true);
|
|
99
|
+
if (navigation !== undefined)
|
|
100
|
+
navigation.removeEventListener('navigate', onNavigate);
|
|
101
|
+
};
|
|
60
102
|
}
|
package/dist/observers/dom.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { readTestId, testIdSelector } from '../dom/addressing/testid-attr.js';
|
|
2
|
+
import { EventType, REDACTED_VALUE, TruncationChannel } from '@reticlehq/core';
|
|
3
|
+
import { getAccessibleName, getRole, isSensitiveField, isVisible } from '../dom/a11y.js';
|
|
3
4
|
import { refs } from '../dom/addressing/refs.js';
|
|
4
5
|
function regionKeyOf(target) {
|
|
5
6
|
const el = isElement(target) ? target : null;
|
|
6
7
|
if (null === el)
|
|
7
8
|
return undefined;
|
|
8
|
-
const labelled = el.closest(
|
|
9
|
-
return labelled
|
|
9
|
+
const labelled = el.closest(testIdSelector());
|
|
10
|
+
return (labelled ? readTestId(labelled) : null) ?? refs.refFor(el);
|
|
10
11
|
}
|
|
11
|
-
import {
|
|
12
|
+
import { isUnobserved } from '../dom/dom-ignore.js';
|
|
12
13
|
import { capturedRoots, installShadowRegistry, onShadowRoot } from '../dom/shadow-registry.js';
|
|
13
14
|
import { isElement } from '../dom/realm.js';
|
|
14
15
|
import { observeSameOriginFrames } from './frames.js';
|
|
@@ -50,7 +51,7 @@ function describeMount(node) {
|
|
|
50
51
|
if (scanned >= MOUNT_SCAN_LIMIT)
|
|
51
52
|
return undefined;
|
|
52
53
|
scanned += 1;
|
|
53
|
-
if (
|
|
54
|
+
if (isUnobserved(child))
|
|
54
55
|
continue;
|
|
55
56
|
const childRole = getRole(child);
|
|
56
57
|
const childName = getAccessibleName(child);
|
|
@@ -70,6 +71,18 @@ function textOf(nodes) {
|
|
|
70
71
|
const trimmed = out.trim();
|
|
71
72
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
72
73
|
}
|
|
74
|
+
const SAFE_ON_SECRET = new Set([
|
|
75
|
+
'class',
|
|
76
|
+
'style',
|
|
77
|
+
'type',
|
|
78
|
+
'disabled',
|
|
79
|
+
'readonly',
|
|
80
|
+
'required',
|
|
81
|
+
'hidden',
|
|
82
|
+
]);
|
|
83
|
+
function secretAttr(el, attr) {
|
|
84
|
+
return !SAFE_ON_SECRET.has(attr) && !attr.startsWith('aria-') && isSensitiveField(el);
|
|
85
|
+
}
|
|
73
86
|
export function installDom(emit) {
|
|
74
87
|
const stopRegistry = installShadowRegistry();
|
|
75
88
|
const observer = new MutationObserver((records) => {
|
|
@@ -80,14 +93,15 @@ export function installDom(emit) {
|
|
|
80
93
|
for (const record of records) {
|
|
81
94
|
if ('attributes' === record.type) {
|
|
82
95
|
const target = record.target;
|
|
83
|
-
if (isElement(target) && record.attributeName !== null && !
|
|
96
|
+
if (isElement(target) && record.attributeName !== null && !isUnobserved(target)) {
|
|
84
97
|
if (changed >= MAX_PER_BATCH) {
|
|
85
98
|
dropped += 1;
|
|
86
99
|
continue;
|
|
87
100
|
}
|
|
88
101
|
changed += 1;
|
|
89
|
-
const
|
|
90
|
-
const
|
|
102
|
+
const hide = secretAttr(target, record.attributeName);
|
|
103
|
+
const value = hide ? REDACTED_VALUE : capValue(target.getAttribute(record.attributeName));
|
|
104
|
+
const old = hide && null !== record.oldValue ? REDACTED_VALUE : capValue(record.oldValue);
|
|
91
105
|
emit(EventType.DOM_ATTR, {
|
|
92
106
|
attr: record.attributeName,
|
|
93
107
|
...(value === undefined ? {} : { value }),
|
|
@@ -98,27 +112,31 @@ export function installDom(emit) {
|
|
|
98
112
|
}
|
|
99
113
|
if ('characterData' === record.type) {
|
|
100
114
|
const parent = record.target.parentElement;
|
|
101
|
-
if (parent !== null && !
|
|
115
|
+
if (parent !== null && !isUnobserved(parent)) {
|
|
102
116
|
if (changed >= MAX_PER_BATCH) {
|
|
103
117
|
dropped += 1;
|
|
104
118
|
continue;
|
|
105
119
|
}
|
|
106
120
|
changed += 1;
|
|
107
|
-
const
|
|
108
|
-
const
|
|
121
|
+
const hide = isSensitiveField(parent);
|
|
122
|
+
const text = hide
|
|
123
|
+
? REDACTED_VALUE
|
|
124
|
+
: (record.target.textContent ?? '').trim().slice(0, 80);
|
|
125
|
+
const old = hide ? undefined : capValue(record.oldValue?.trim() ?? null);
|
|
109
126
|
emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }) }, refs.refFor(parent));
|
|
110
127
|
}
|
|
111
128
|
continue;
|
|
112
129
|
}
|
|
113
130
|
if (textOf(record.addedNodes) !== undefined || textOf(record.removedNodes) !== undefined) {
|
|
114
131
|
const parent = isElement(record.target) ? record.target : null;
|
|
115
|
-
if (parent !== null && !
|
|
132
|
+
if (parent !== null && !isUnobserved(parent)) {
|
|
116
133
|
if (changed >= MAX_PER_BATCH)
|
|
117
134
|
dropped += 1;
|
|
118
135
|
else {
|
|
119
136
|
changed += 1;
|
|
120
|
-
const
|
|
121
|
-
const
|
|
137
|
+
const hide = isSensitiveField(parent);
|
|
138
|
+
const text = hide ? REDACTED_VALUE : (parent.textContent ?? '').trim().slice(0, 80);
|
|
139
|
+
const old = hide ? undefined : capValue(textOf(record.removedNodes) ?? null);
|
|
122
140
|
emit(EventType.DOM_TEXT, { text, ...(old === undefined ? {} : { old }) }, refs.refFor(parent));
|
|
123
141
|
}
|
|
124
142
|
}
|
|
@@ -130,7 +148,7 @@ export function installDom(emit) {
|
|
|
130
148
|
dropped += 1;
|
|
131
149
|
continue;
|
|
132
150
|
}
|
|
133
|
-
if (
|
|
151
|
+
if (isUnobserved(node))
|
|
134
152
|
continue;
|
|
135
153
|
const mounted = describeMount(node);
|
|
136
154
|
if (mounted === undefined)
|
|
@@ -153,8 +171,7 @@ export function installDom(emit) {
|
|
|
153
171
|
dropped += 1;
|
|
154
172
|
continue;
|
|
155
173
|
}
|
|
156
|
-
if (
|
|
157
|
-
(isElement(record.target) && isReticleOverlay(record.target))) {
|
|
174
|
+
if (isUnobserved(node) || (isElement(record.target) && isUnobserved(record.target))) {
|
|
158
175
|
continue;
|
|
159
176
|
}
|
|
160
177
|
const gone = describeMount(node);
|
package/dist/observers/field.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { readTestId } from '../dom/addressing/testid-attr.js';
|
|
2
|
+
import { EventType } from '@reticlehq/core';
|
|
3
|
+
import { getAccessibleName, isSensitiveField } from '../dom/a11y.js';
|
|
3
4
|
const MAX_VALUE_LEN = 160;
|
|
4
5
|
const BURST_MS = 150;
|
|
5
|
-
const PAYMENT_AUTOCOMPLETE = /^(cc-|current-password|new-password)/i;
|
|
6
6
|
function isField(target) {
|
|
7
7
|
return (target instanceof HTMLInputElement ||
|
|
8
8
|
target instanceof HTMLTextAreaElement ||
|
|
9
9
|
target instanceof HTMLSelectElement);
|
|
10
10
|
}
|
|
11
11
|
function fieldName(element) {
|
|
12
|
-
const testid = element
|
|
12
|
+
const testid = readTestId(element);
|
|
13
13
|
if (null !== testid && testid.length > 0)
|
|
14
14
|
return testid;
|
|
15
15
|
if (element.name.length > 0)
|
|
@@ -20,29 +20,13 @@ function fieldName(element) {
|
|
|
20
20
|
const id = element.getAttribute('id');
|
|
21
21
|
return null !== id && id.length > 0 ? id : element.tagName.toLowerCase();
|
|
22
22
|
}
|
|
23
|
-
function isSecret(element) {
|
|
24
|
-
if (element instanceof HTMLInputElement && 'password' === element.type)
|
|
25
|
-
return true;
|
|
26
|
-
const hint = element.getAttribute('autocomplete');
|
|
27
|
-
if (null !== hint && PAYMENT_AUTOCOMPLETE.test(hint))
|
|
28
|
-
return true;
|
|
29
|
-
for (const key of [
|
|
30
|
-
element.name,
|
|
31
|
-
element.getAttribute('data-testid'),
|
|
32
|
-
element.getAttribute('id'),
|
|
33
|
-
]) {
|
|
34
|
-
if (null !== key && key.length > 0 && isSensitiveKey(key))
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
23
|
export function installField(emit) {
|
|
40
24
|
const ac = new AbortController();
|
|
41
25
|
const { signal } = ac;
|
|
42
26
|
const pending = new Map();
|
|
43
27
|
const report = (element, kind) => {
|
|
44
28
|
const value = element.value;
|
|
45
|
-
const secret =
|
|
29
|
+
const secret = isSensitiveField(element);
|
|
46
30
|
emit(EventType.FIELD_CHANGE, {
|
|
47
31
|
field: fieldName(element),
|
|
48
32
|
kind,
|
package/dist/observers/health.js
CHANGED
|
@@ -67,25 +67,86 @@ function detectBrand() {
|
|
|
67
67
|
return BrowserBrand.SAFARI;
|
|
68
68
|
return BrowserBrand.OTHER;
|
|
69
69
|
}
|
|
70
|
-
function
|
|
70
|
+
const pageFetch = 'undefined' !== typeof window && 'function' === typeof window.fetch
|
|
71
|
+
? window.fetch.bind(window)
|
|
72
|
+
: undefined;
|
|
73
|
+
function tauriWindowLabel() {
|
|
74
|
+
const internals = window['__TAURI_INTERNALS__'];
|
|
75
|
+
const label = internals?.metadata?.currentWindow?.label;
|
|
76
|
+
return 'string' === typeof label && label.length > 0 ? label : undefined;
|
|
77
|
+
}
|
|
78
|
+
function navigationStatus() {
|
|
79
|
+
try {
|
|
80
|
+
const [entry] = performance.getEntriesByType('navigation');
|
|
81
|
+
const known = entry?.responseStatus;
|
|
82
|
+
return 'number' === typeof known && known > 0 ? known : undefined;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function documentStatus(runtime) {
|
|
89
|
+
const known = navigationStatus();
|
|
90
|
+
if (known !== undefined)
|
|
91
|
+
return known;
|
|
92
|
+
try {
|
|
93
|
+
if ('web' === runtime || pageFetch === undefined || !/^https?:$/.test(location.protocol)) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
return (await pageFetch(location.href, { method: 'HEAD', cache: 'no-store' })).status;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function electronWindowLabel() {
|
|
103
|
+
const channel = window[RETICLE_IPC_GLOBAL];
|
|
104
|
+
if ('function' !== typeof channel?.windowLabel)
|
|
105
|
+
return undefined;
|
|
106
|
+
try {
|
|
107
|
+
const label = await channel.windowLabel();
|
|
108
|
+
return 'string' === typeof label && label.length > 0 ? label : undefined;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function snapshotHealth(windowLabel) {
|
|
71
115
|
return {
|
|
72
116
|
hidden: 'hidden' === document.visibilityState,
|
|
73
117
|
focused: document.hasFocus(),
|
|
74
118
|
runtime: detectRuntime(),
|
|
75
119
|
engine: detectEngine(),
|
|
76
120
|
brand: detectBrand(),
|
|
121
|
+
...(windowLabel === undefined ? {} : { windowLabel }),
|
|
77
122
|
};
|
|
78
123
|
}
|
|
79
124
|
export function installHealth(emit) {
|
|
80
125
|
const ac = new AbortController();
|
|
81
126
|
const { signal } = ac;
|
|
127
|
+
let status;
|
|
128
|
+
let label = tauriWindowLabel();
|
|
82
129
|
const report = (reason) => {
|
|
83
|
-
emit(EventType.PAGE_HEALTH, {
|
|
130
|
+
emit(EventType.PAGE_HEALTH, {
|
|
131
|
+
...snapshotHealth(label),
|
|
132
|
+
...(status === undefined ? {} : { documentStatus: status }),
|
|
133
|
+
reason,
|
|
134
|
+
});
|
|
84
135
|
};
|
|
85
136
|
document.addEventListener('visibilitychange', () => report(HealthReason.VISIBILITY), { signal });
|
|
86
137
|
window.addEventListener('focus', () => report(HealthReason.FOCUS), { signal });
|
|
87
138
|
window.addEventListener('blur', () => report(HealthReason.BLUR), { signal });
|
|
88
139
|
report(HealthReason.INITIAL);
|
|
140
|
+
void Promise.all([
|
|
141
|
+
documentStatus(detectRuntime()),
|
|
142
|
+
label === undefined ? electronWindowLabel() : Promise.resolve(label),
|
|
143
|
+
]).then(([known, named]) => {
|
|
144
|
+
if (signal.aborted || (known === undefined && named === label))
|
|
145
|
+
return;
|
|
146
|
+
status = known;
|
|
147
|
+
label = named;
|
|
148
|
+
report(HealthReason.HEARTBEAT);
|
|
149
|
+
});
|
|
89
150
|
const stopHeartbeat = nativeSetInterval(() => report(HealthReason.HEARTBEAT), SESSION_HEALTH.HEARTBEAT_MS);
|
|
90
151
|
return () => {
|
|
91
152
|
stopHeartbeat();
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { Emit } from './types.js';
|
|
2
|
-
export declare function watchStreamedBody(emit: Emit, res: Response, id: string,
|
|
2
|
+
export declare function watchStreamedBody(emit: Emit, res: Response, id: string, urlFields: Readonly<Record<string, unknown>>, contentType: string | null, contentLength: string | null): void;
|
|
@@ -2,7 +2,7 @@ import { EventType, StreamDirection, StreamTransport } from '@reticlehq/core';
|
|
|
2
2
|
import { isStreamingBody } from './net-detail/network-body.js';
|
|
3
3
|
import { nativeClearTimeout, nativeSetTimeout } from '../timers/native/native-timers.js';
|
|
4
4
|
const STREAM_WATCH_MS = 10_000;
|
|
5
|
-
export function watchStreamedBody(emit, res, id,
|
|
5
|
+
export function watchStreamedBody(emit, res, id, urlFields, contentType, contentLength) {
|
|
6
6
|
if (!isStreamingBody(contentType, contentLength))
|
|
7
7
|
return;
|
|
8
8
|
if (typeof res.body?.getReader !== 'function')
|
|
@@ -20,7 +20,7 @@ export function watchStreamedBody(emit, res, id, url, contentType, contentLength
|
|
|
20
20
|
emit(EventType.NET_STREAM, {
|
|
21
21
|
transport: StreamTransport.FETCH,
|
|
22
22
|
direction: StreamDirection.OPEN,
|
|
23
|
-
|
|
23
|
+
...urlFields,
|
|
24
24
|
id,
|
|
25
25
|
});
|
|
26
26
|
void (async () => {
|
|
@@ -51,7 +51,7 @@ export function watchStreamedBody(emit, res, id, url, contentType, contentLength
|
|
|
51
51
|
emit(EventType.NET_STREAM, {
|
|
52
52
|
transport: StreamTransport.FETCH,
|
|
53
53
|
direction: StreamDirection.CLOSE,
|
|
54
|
-
|
|
54
|
+
...urlFields,
|
|
55
55
|
id,
|
|
56
56
|
...(gaveUp ? { gaveUp: true } : {}),
|
|
57
57
|
});
|
|
@@ -63,15 +63,64 @@ function jsonShape(text) {
|
|
|
63
63
|
return ShapeToken.OTHER;
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
const MAX_DECODED_BODY_BYTES = 1 << 20;
|
|
67
|
+
const TAB = 0x09;
|
|
68
|
+
const LINE_FEED = 0x0a;
|
|
69
|
+
const CARRIAGE_RETURN = 0x0d;
|
|
70
|
+
const FIRST_PRINTABLE = 0x20;
|
|
71
|
+
const DELETE = 0x7f;
|
|
72
|
+
function hasControlCharacter(text) {
|
|
73
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
74
|
+
const code = text.charCodeAt(i);
|
|
75
|
+
if (code === DELETE)
|
|
76
|
+
return true;
|
|
77
|
+
if (code < FIRST_PRINTABLE && code !== TAB && code !== LINE_FEED && code !== CARRIAGE_RETURN) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
function textOfBytes(body) {
|
|
84
|
+
if (!(body instanceof ArrayBuffer || ArrayBuffer.isView(body)))
|
|
85
|
+
return undefined;
|
|
86
|
+
if (body.byteLength > MAX_DECODED_BODY_BYTES)
|
|
87
|
+
return undefined;
|
|
88
|
+
try {
|
|
89
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(body);
|
|
90
|
+
return hasControlCharacter(text) ? undefined : text;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function formShape(form) {
|
|
97
|
+
const fields = [];
|
|
98
|
+
let length = 0;
|
|
99
|
+
form.forEach((value, name) => {
|
|
100
|
+
if ('string' === typeof value)
|
|
101
|
+
length += value.length;
|
|
102
|
+
fields.push(JSON.stringify([name, 'string' === typeof value ? 0 : [value.type, value.size]]));
|
|
103
|
+
});
|
|
104
|
+
return 0 === fields.length ? undefined : fingerprintBody(fields.sort().join(), length);
|
|
105
|
+
}
|
|
66
106
|
function projectRequestBody(body, captureBodies) {
|
|
67
107
|
let text;
|
|
68
108
|
let contentType = 'application/json';
|
|
69
109
|
let shape = REQUEST_SHAPE_NONE;
|
|
70
|
-
|
|
71
|
-
|
|
110
|
+
const bytesText = textOfBytes(body);
|
|
111
|
+
const typeOf = () => body.constructor?.name ?? typeof body;
|
|
112
|
+
if ('string' === typeof body || bytesText !== undefined) {
|
|
113
|
+
text = bytesText ?? body;
|
|
72
114
|
if (text.length > 0)
|
|
73
115
|
shape = fingerprintBody(jsonShape(text), text.length);
|
|
74
116
|
}
|
|
117
|
+
else if (typeof FormData !== 'undefined' && body instanceof FormData) {
|
|
118
|
+
const form = formShape(body);
|
|
119
|
+
return {
|
|
120
|
+
...(form === undefined ? {} : { [REQUEST_SHAPE_FIELD]: form }),
|
|
121
|
+
...(captureBodies ? { requestBodyType: typeOf() } : {}),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
75
124
|
else if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
|
|
76
125
|
text = body.toString();
|
|
77
126
|
contentType = 'application/x-www-form-urlencoded';
|
|
@@ -81,8 +130,7 @@ function projectRequestBody(body, captureBodies) {
|
|
|
81
130
|
}
|
|
82
131
|
}
|
|
83
132
|
else if (body !== undefined && body !== null) {
|
|
84
|
-
|
|
85
|
-
return captureBodies ? { requestBodyType: kind } : {};
|
|
133
|
+
return captureBodies ? { requestBodyType: typeOf() } : {};
|
|
86
134
|
}
|
|
87
135
|
const fields = { [REQUEST_SHAPE_FIELD]: shape };
|
|
88
136
|
if (!captureBodies || text === undefined || 0 === text.length)
|
|
@@ -202,6 +250,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
202
250
|
const method = methodOf(input, init);
|
|
203
251
|
const nextAction = nextActionOf(input, init);
|
|
204
252
|
const nextActionFields = nextAction === undefined ? {} : { [NEXT_ACTION_FIELD]: nextAction };
|
|
253
|
+
const bodyFields = observeValue(() => projectRequestBody(init?.body, captureBodies)) ?? {};
|
|
205
254
|
const urlFields = netUrlFields(rawUrl);
|
|
206
255
|
const url = urlFields.url;
|
|
207
256
|
const initiatorStack = observeValue(() => initiatorFrame());
|
|
@@ -220,7 +269,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
220
269
|
const headersAt = performance.now();
|
|
221
270
|
const contentType = observeValue(() => res.headers.get('content-type')) ?? null;
|
|
222
271
|
observeSafely(() => {
|
|
223
|
-
watchStreamedBody(emit, res, id,
|
|
272
|
+
watchStreamedBody(emit, res, id, urlFields, contentType, res.headers.get('content-length'));
|
|
224
273
|
});
|
|
225
274
|
reportedNetUrls.add(rawUrl);
|
|
226
275
|
const emitRequest = (responseBodyFields) => {
|
|
@@ -236,7 +285,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
236
285
|
...initiatorFields,
|
|
237
286
|
...resourceTiming(rawUrl),
|
|
238
287
|
...netResponseMeta(res.statusText, contentType, res.headers.get('content-length')),
|
|
239
|
-
...
|
|
288
|
+
...bodyFields,
|
|
240
289
|
...nextActionFields,
|
|
241
290
|
...responseBodyFields,
|
|
242
291
|
...(reinterpret?.(url, (name) => res.headers.get(name)) ?? {}),
|
|
@@ -326,7 +375,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
326
375
|
const m = meta.get(this);
|
|
327
376
|
if (m !== undefined) {
|
|
328
377
|
m.start = performance.now();
|
|
329
|
-
m.
|
|
378
|
+
m.reqFields = observeValue(() => projectRequestBody(body ?? null, captureBodies)) ?? {};
|
|
330
379
|
m.initiatorStack = initiatorFrame();
|
|
331
380
|
const initiatorFields = m.initiatorStack === undefined ? {} : { initiatorStack: m.initiatorStack };
|
|
332
381
|
emit(EventType.NET_PENDING, {
|
|
@@ -375,7 +424,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
375
424
|
...(cur.initiatorStack === undefined ? {} : { initiatorStack: cur.initiatorStack }),
|
|
376
425
|
...resourceTiming(cur.rawUrl),
|
|
377
426
|
...netResponseMeta(this.statusText, xhrContentType, this.getResponseHeader('content-length')),
|
|
378
|
-
...
|
|
427
|
+
...cur.reqFields,
|
|
379
428
|
...responseBodyFields,
|
|
380
429
|
});
|
|
381
430
|
});
|
package/dist/observers/perf.js
CHANGED
|
@@ -1,4 +1,50 @@
|
|
|
1
1
|
import { EventType, PerfMetric } from '@reticlehq/core';
|
|
2
|
+
import { refs } from '../dom/addressing/refs.js';
|
|
3
|
+
import { getTestIdAttr, readTestId } from '../dom/addressing/testid-attr.js';
|
|
4
|
+
import { isReticleOverlay } from '../dom/dom-ignore.js';
|
|
5
|
+
import { isElement } from '../dom/realm.js';
|
|
6
|
+
const areaOf = (r) => r === undefined ? 0 : Math.max(0, r.width) * Math.max(0, r.height);
|
|
7
|
+
function cssIdent(value) {
|
|
8
|
+
const css = globalThis.CSS;
|
|
9
|
+
if ('function' === typeof css?.escape)
|
|
10
|
+
return css.escape(value);
|
|
11
|
+
let out = '';
|
|
12
|
+
for (const [i, ch] of [...value].entries()) {
|
|
13
|
+
const startsWithDigit = /\d/.test(ch) && (0 === i || (1 === i && value.startsWith('-')));
|
|
14
|
+
if (startsWithDigit)
|
|
15
|
+
out += `\\${ch.charCodeAt(0).toString(16)} `;
|
|
16
|
+
else if (/[\w-]/.test(ch))
|
|
17
|
+
out += ch;
|
|
18
|
+
else
|
|
19
|
+
out += `\\${ch}`;
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
function shortSelector(el) {
|
|
24
|
+
const testid = readTestId(el);
|
|
25
|
+
if (null !== testid)
|
|
26
|
+
return `[${getTestIdAttr()}="${testid.replace(/["\\]/g, '\\$&')}"]`;
|
|
27
|
+
const tag = el.tagName.toLowerCase();
|
|
28
|
+
if (0 < el.id.length)
|
|
29
|
+
return `${tag}#${cssIdent(el.id)}`;
|
|
30
|
+
const cls = el.classList.item(0);
|
|
31
|
+
return null === cls ? tag : `${tag}.${cssIdent(cls)}`;
|
|
32
|
+
}
|
|
33
|
+
function largestShifted(sources) {
|
|
34
|
+
let best;
|
|
35
|
+
for (const source of sources ?? []) {
|
|
36
|
+
const node = source.node ?? null;
|
|
37
|
+
const el = null === node ? null : isElement(node) ? node : node.parentElement;
|
|
38
|
+
if (null === el || isReticleOverlay(el))
|
|
39
|
+
continue;
|
|
40
|
+
const area = Math.max(areaOf(source.previousRect), areaOf(source.currentRect));
|
|
41
|
+
if (best === undefined || area > best.area)
|
|
42
|
+
best = { el, area };
|
|
43
|
+
}
|
|
44
|
+
return best === undefined
|
|
45
|
+
? undefined
|
|
46
|
+
: { ref: refs.refFor(best.el), selector: shortSelector(best.el) };
|
|
47
|
+
}
|
|
2
48
|
export function installPerf(emit) {
|
|
3
49
|
if (typeof PerformanceObserver !== 'function')
|
|
4
50
|
return () => undefined;
|
|
@@ -29,7 +75,13 @@ export function installPerf(emit) {
|
|
|
29
75
|
if (true === ls.hadRecentInput)
|
|
30
76
|
return;
|
|
31
77
|
cls += ls.value ?? 0;
|
|
32
|
-
|
|
78
|
+
const shifted = largestShifted(ls.sources);
|
|
79
|
+
emit(EventType.PERF, {
|
|
80
|
+
metric: PerfMetric.CLS,
|
|
81
|
+
value: cls,
|
|
82
|
+
at: Math.round(e.startTime),
|
|
83
|
+
...(shifted === undefined ? {} : { shifted }),
|
|
84
|
+
});
|
|
33
85
|
});
|
|
34
86
|
observe('longtask', (e) => {
|
|
35
87
|
emit(EventType.PERF, {
|
package/dist/observers/route.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EventType } from '@reticlehq/core';
|
|
1
|
+
import { EventType, ROUTE_CHANGE_HOW_FIELD, RouteChangeHow } from '@reticlehq/core';
|
|
2
2
|
import { observeSafely } from './types.js';
|
|
3
3
|
import { captureMethod } from '../patching/capture-method.js';
|
|
4
4
|
function snapshotLocation() {
|
|
@@ -16,7 +16,7 @@ export function installRoute(emit) {
|
|
|
16
16
|
const callPush = origPush.bind(history);
|
|
17
17
|
const callReplace = origReplace.bind(history);
|
|
18
18
|
let lastHref = location.href;
|
|
19
|
-
const fire = (from) => {
|
|
19
|
+
const fire = (from, how) => {
|
|
20
20
|
observeSafely(() => {
|
|
21
21
|
const to = snapshotLocation();
|
|
22
22
|
if (to.href === from)
|
|
@@ -28,23 +28,24 @@ export function installRoute(emit) {
|
|
|
28
28
|
pathname: to.pathname,
|
|
29
29
|
search: to.search,
|
|
30
30
|
hash: to.hash,
|
|
31
|
+
[ROUTE_CHANGE_HOW_FIELD]: how,
|
|
31
32
|
});
|
|
32
33
|
});
|
|
33
34
|
};
|
|
34
35
|
const patchedPush = (data, unused, url) => {
|
|
35
36
|
const from = location.href;
|
|
36
37
|
callPush(data, unused, url ?? null);
|
|
37
|
-
fire(from);
|
|
38
|
+
fire(from, RouteChangeHow.PUSH);
|
|
38
39
|
};
|
|
39
40
|
const patchedReplace = (data, unused, url) => {
|
|
40
41
|
const from = location.href;
|
|
41
42
|
callReplace(data, unused, url ?? null);
|
|
42
|
-
fire(from);
|
|
43
|
+
fire(from, RouteChangeHow.REPLACE);
|
|
43
44
|
};
|
|
44
45
|
history.pushState = patchedPush;
|
|
45
46
|
history.replaceState = patchedReplace;
|
|
46
47
|
const onNav = () => {
|
|
47
|
-
fire(lastHref);
|
|
48
|
+
fire(lastHref, RouteChangeHow.POP);
|
|
48
49
|
};
|
|
49
50
|
window.addEventListener('popstate', onNav, { signal: ac.signal });
|
|
50
51
|
window.addEventListener('hashchange', onNav, { signal: ac.signal });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EventType, REDACTED_VALUE, StorageArea } from '@reticlehq/core';
|
|
2
2
|
import { isSensitiveKey, scrubKnownSecrets } from '../security/serialization.js';
|
|
3
|
+
import { isReticleStorageKey } from '../storage-keys.js';
|
|
3
4
|
import { observeSafely, observeValue } from './types.js';
|
|
4
5
|
function safeArea(get) {
|
|
5
6
|
try {
|
|
@@ -95,6 +96,8 @@ export function installStorage(emit) {
|
|
|
95
96
|
origSet.call(this, key, value);
|
|
96
97
|
if (old === value)
|
|
97
98
|
return;
|
|
99
|
+
if (isReticleStorageKey(key))
|
|
100
|
+
return;
|
|
98
101
|
observeSafely(() => {
|
|
99
102
|
emit(EventType.STORAGE_CHANGE, {
|
|
100
103
|
area: areaOf(this),
|
|
@@ -108,6 +111,8 @@ export function installStorage(emit) {
|
|
|
108
111
|
const patchedRemoveItem = function (key) {
|
|
109
112
|
const old = observeValue(() => readOld(this, key)) ?? null;
|
|
110
113
|
origRemove.call(this, key);
|
|
114
|
+
if (isReticleStorageKey(key))
|
|
115
|
+
return;
|
|
111
116
|
observeSafely(() => {
|
|
112
117
|
emit(EventType.STORAGE_CHANGE, {
|
|
113
118
|
area: areaOf(this),
|
|
@@ -123,7 +128,7 @@ export function installStorage(emit) {
|
|
|
123
128
|
try {
|
|
124
129
|
for (let i = 0; i < this.length; i += 1) {
|
|
125
130
|
const key = this.key(i);
|
|
126
|
-
if (key !== null)
|
|
131
|
+
if (key !== null && !isReticleStorageKey(key))
|
|
127
132
|
removed.push({ key, old: readOld(this, key) });
|
|
128
133
|
}
|
|
129
134
|
}
|
|
@@ -2,7 +2,10 @@ export declare const CAROUSEL_ATTR = "data-reticle-carousel";
|
|
|
2
2
|
export declare const CAROUSEL_SLIDE_ATTR = "data-reticle-carousel-slide";
|
|
3
3
|
export declare const CAROUSEL_DOT_ATTR = "data-reticle-carousel-dot";
|
|
4
4
|
export declare const CAROUSEL_CLOSE_ATTR = "data-reticle-carousel-close";
|
|
5
|
-
export declare const
|
|
5
|
+
export declare const CAROUSEL_PREV_ATTR = "data-reticle-carousel-prev";
|
|
6
|
+
export declare const CAROUSEL_NEXT_ATTR = "data-reticle-carousel-next";
|
|
7
|
+
export declare const CAROUSEL_POSITION_ATTR = "data-reticle-carousel-position";
|
|
8
|
+
export declare const CAROUSEL_DISMISSED_KEY: "reticle.carousel.dismissed";
|
|
6
9
|
export declare const ROTATE_MS = 6000;
|
|
7
10
|
export interface Slide {
|
|
8
11
|
id: string;
|
|
@@ -10,4 +13,4 @@ export interface Slide {
|
|
|
10
13
|
}
|
|
11
14
|
export declare function carouselHtml(slides: readonly Slide[], active: number): string;
|
|
12
15
|
export declare function paintCarousel(log: HTMLElement, slides: readonly Slide[], storage: Pick<Storage, 'getItem' | 'setItem'> | undefined): void;
|
|
13
|
-
export declare const CAROUSEL_CSS
|
|
16
|
+
export declare const CAROUSEL_CSS: string;
|