@reticlehq/browser 2.2.1 → 2.3.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/actions-dom.d.ts +0 -1
- package/dist/actions/actions-dom.js +0 -1
- package/dist/actions/actions.d.ts +3 -1
- package/dist/actions/actions.js +55 -36
- package/dist/actions/scroll.d.ts +0 -1
- package/dist/actions/scroll.js +0 -1
- package/dist/actions/value-input.d.ts +33 -0
- package/dist/actions/value-input.js +63 -0
- package/dist/commands/commands.d.ts +0 -1
- package/dist/commands/commands.js +15 -7
- package/dist/dom/a11y.d.ts +0 -2
- package/dist/dom/a11y.js +54 -24
- package/dist/dom/auto-anchor.d.ts +0 -1
- package/dist/dom/auto-anchor.js +0 -1
- package/dist/dom/blind-spots.d.ts +0 -1
- package/dist/dom/blind-spots.js +0 -1
- package/dist/dom/chart.d.ts +0 -1
- package/dist/dom/chart.js +0 -1
- package/dist/dom/desktop-capture.d.ts +36 -0
- package/dist/dom/desktop-capture.js +113 -0
- package/dist/dom/dom-ignore.d.ts +7 -1
- package/dist/dom/dom-ignore.js +1 -2
- package/dist/dom/occlusion.d.ts +0 -1
- package/dist/dom/occlusion.js +0 -1
- package/dist/dom/query.d.ts +0 -1
- package/dist/dom/query.js +61 -13
- package/dist/dom/realm.d.ts +30 -0
- package/dist/dom/realm.js +58 -0
- package/dist/dom/refs.d.ts +0 -1
- package/dist/dom/refs.js +0 -1
- package/dist/dom/shadow-registry.d.ts +40 -0
- package/dist/dom/shadow-registry.js +165 -0
- package/dist/dom/snapshot.d.ts +2 -2
- package/dist/dom/snapshot.js +21 -4
- package/dist/dom/source.d.ts +0 -1
- package/dist/dom/source.js +0 -1
- package/dist/dom/theme.d.ts +0 -1
- package/dist/dom/theme.js +0 -1
- package/dist/dom/virtualized.d.ts +19 -0
- package/dist/dom/virtualized.js +80 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/observers/animation.d.ts +0 -1
- package/dist/observers/animation.js +0 -1
- package/dist/observers/blind-spots.d.ts +16 -4
- package/dist/observers/blind-spots.js +65 -11
- package/dist/observers/console.d.ts +0 -1
- package/dist/observers/console.js +0 -1
- package/dist/observers/dom.d.ts +0 -2
- package/dist/observers/dom.js +67 -9
- package/dist/observers/download.d.ts +6 -0
- package/dist/observers/download.js +103 -0
- package/dist/observers/focus.d.ts +0 -1
- package/dist/observers/focus.js +0 -1
- package/dist/observers/frames.d.ts +12 -0
- package/dist/observers/frames.js +67 -0
- package/dist/observers/health.d.ts +0 -1
- package/dist/observers/health.js +30 -2
- package/dist/observers/install-all.d.ts +5 -0
- package/dist/observers/install-all.js +63 -0
- package/dist/observers/ipc.d.ts +22 -0
- package/dist/observers/ipc.js +121 -0
- package/dist/observers/network-body.d.ts +17 -1
- package/dist/observers/network-body.js +26 -1
- package/dist/observers/network-redact.d.ts +0 -1
- package/dist/observers/network-redact.js +6 -3
- package/dist/observers/network-stream.d.ts +23 -0
- package/dist/observers/network-stream.js +102 -0
- package/dist/observers/network.d.ts +11 -1
- package/dist/observers/network.js +7 -1
- package/dist/observers/perf.d.ts +0 -1
- package/dist/observers/perf.js +0 -1
- package/dist/observers/route.d.ts +0 -1
- package/dist/observers/route.js +0 -1
- package/dist/observers/scroll.d.ts +0 -1
- package/dist/observers/scroll.js +0 -1
- package/dist/observers/sdk-failure.d.ts +28 -0
- package/dist/observers/sdk-failure.js +60 -0
- package/dist/observers/state.d.ts +0 -1
- package/dist/observers/state.js +0 -1
- package/dist/observers/storage.d.ts +0 -1
- package/dist/observers/storage.js +0 -1
- package/dist/observers/types.d.ts +0 -1
- package/dist/observers/types.js +0 -1
- package/dist/patching/capture-method.d.ts +0 -1
- package/dist/patching/capture-method.js +0 -1
- package/dist/presenter/overlay.d.ts +0 -1
- package/dist/presenter/overlay.js +0 -1
- package/dist/presenter/presenter-config.d.ts +0 -1
- package/dist/presenter/presenter-config.js +0 -1
- package/dist/presenter/presenter-controls.d.ts +0 -1
- package/dist/presenter/presenter-controls.js +0 -1
- package/dist/presenter/presenter-effects.d.ts +0 -1
- package/dist/presenter/presenter-effects.js +0 -1
- package/dist/presenter/presenter-glow.d.ts +0 -1
- package/dist/presenter/presenter-glow.js +0 -1
- package/dist/presenter/presenter-log.d.ts +0 -1
- package/dist/presenter/presenter-log.js +0 -1
- package/dist/presenter/presenter-run-state.d.ts +0 -1
- package/dist/presenter/presenter-run-state.js +0 -1
- package/dist/presenter/presenter-styles.d.ts +0 -1
- package/dist/presenter/presenter-styles.js +0 -1
- package/dist/presenter/presenter-tally.d.ts +0 -1
- package/dist/presenter/presenter-tally.js +0 -1
- package/dist/presenter/presenter-test-helpers.d.ts +0 -1
- package/dist/presenter/presenter-test-helpers.js +0 -1
- package/dist/presenter/presenter-verbs.d.ts +0 -1
- package/dist/presenter/presenter-verbs.js +0 -1
- package/dist/presenter/presenter.d.ts +0 -1
- package/dist/presenter/presenter.js +0 -1
- package/dist/recorder/recorder-styles.d.ts +0 -1
- package/dist/recorder/recorder-styles.js +0 -1
- package/dist/recorder/recorder.d.ts +0 -1
- package/dist/recorder/recorder.js +0 -1
- package/dist/registry/adapters.d.ts +0 -1
- package/dist/registry/adapters.js +0 -1
- package/dist/registry/capabilities.d.ts +0 -1
- package/dist/registry/capabilities.js +0 -1
- package/dist/registry/commit-and-signal.d.ts +0 -1
- package/dist/registry/commit-and-signal.js +0 -1
- package/dist/registry/domains.d.ts +0 -1
- package/dist/registry/domains.js +0 -1
- package/dist/registry/emitter.d.ts +0 -1
- package/dist/registry/emitter.js +0 -1
- package/dist/registry/store-adapters.d.ts +14 -6
- package/dist/registry/store-adapters.js +0 -1
- package/dist/registry/stores.d.ts +0 -1
- package/dist/registry/stores.js +8 -1
- package/dist/reticle-presenter-helpers.d.ts +0 -1
- package/dist/reticle-presenter-helpers.js +0 -1
- package/dist/reticle.d.ts +3 -2
- package/dist/reticle.js +10 -31
- package/dist/review/annotator.d.ts +0 -1
- package/dist/review/annotator.js +0 -1
- package/dist/review/mark-anchor.d.ts +0 -1
- package/dist/review/mark-anchor.js +0 -1
- package/dist/security/serialization.d.ts +0 -1
- package/dist/security/serialization.js +0 -1
- package/dist/timers/clock.d.ts +0 -1
- package/dist/timers/clock.js +0 -1
- package/dist/timers/native-console.d.ts +0 -1
- package/dist/timers/native-console.js +0 -1
- package/dist/timers/native-timers.d.ts +0 -1
- package/dist/timers/native-timers.js +0 -1
- package/dist/transport/transport.d.ts +2 -6
- package/dist/transport/transport.js +93 -17
- package/package.json +8 -3
- package/dist/actions/actions-dom.d.ts.map +0 -1
- package/dist/actions/actions-dom.js.map +0 -1
- package/dist/actions/actions.d.ts.map +0 -1
- package/dist/actions/actions.js.map +0 -1
- package/dist/actions/scroll.d.ts.map +0 -1
- package/dist/actions/scroll.js.map +0 -1
- package/dist/commands/commands.d.ts.map +0 -1
- package/dist/commands/commands.js.map +0 -1
- package/dist/dom/a11y.d.ts.map +0 -1
- package/dist/dom/a11y.js.map +0 -1
- package/dist/dom/auto-anchor.d.ts.map +0 -1
- package/dist/dom/auto-anchor.js.map +0 -1
- package/dist/dom/blind-spots.d.ts.map +0 -1
- package/dist/dom/blind-spots.js.map +0 -1
- package/dist/dom/chart.d.ts.map +0 -1
- package/dist/dom/chart.js.map +0 -1
- package/dist/dom/dom-ignore.d.ts.map +0 -1
- package/dist/dom/dom-ignore.js.map +0 -1
- package/dist/dom/occlusion.d.ts.map +0 -1
- package/dist/dom/occlusion.js.map +0 -1
- package/dist/dom/query.d.ts.map +0 -1
- package/dist/dom/query.js.map +0 -1
- package/dist/dom/refs.d.ts.map +0 -1
- package/dist/dom/refs.js.map +0 -1
- package/dist/dom/snapshot.d.ts.map +0 -1
- package/dist/dom/snapshot.js.map +0 -1
- package/dist/dom/source.d.ts.map +0 -1
- package/dist/dom/source.js.map +0 -1
- package/dist/dom/theme.d.ts.map +0 -1
- package/dist/dom/theme.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/observers/animation.d.ts.map +0 -1
- package/dist/observers/animation.js.map +0 -1
- package/dist/observers/blind-spots.d.ts.map +0 -1
- package/dist/observers/blind-spots.js.map +0 -1
- package/dist/observers/console.d.ts.map +0 -1
- package/dist/observers/console.js.map +0 -1
- package/dist/observers/dom.d.ts.map +0 -1
- package/dist/observers/dom.js.map +0 -1
- package/dist/observers/focus.d.ts.map +0 -1
- package/dist/observers/focus.js.map +0 -1
- package/dist/observers/health.d.ts.map +0 -1
- package/dist/observers/health.js.map +0 -1
- package/dist/observers/network-body.d.ts.map +0 -1
- package/dist/observers/network-body.js.map +0 -1
- package/dist/observers/network-redact.d.ts.map +0 -1
- package/dist/observers/network-redact.js.map +0 -1
- package/dist/observers/network.d.ts.map +0 -1
- package/dist/observers/network.js.map +0 -1
- package/dist/observers/perf.d.ts.map +0 -1
- package/dist/observers/perf.js.map +0 -1
- package/dist/observers/route.d.ts.map +0 -1
- package/dist/observers/route.js.map +0 -1
- package/dist/observers/scroll.d.ts.map +0 -1
- package/dist/observers/scroll.js.map +0 -1
- package/dist/observers/state.d.ts.map +0 -1
- package/dist/observers/state.js.map +0 -1
- package/dist/observers/storage.d.ts.map +0 -1
- package/dist/observers/storage.js.map +0 -1
- package/dist/observers/types.d.ts.map +0 -1
- package/dist/observers/types.js.map +0 -1
- package/dist/patching/capture-method.d.ts.map +0 -1
- package/dist/patching/capture-method.js.map +0 -1
- package/dist/presenter/overlay.d.ts.map +0 -1
- package/dist/presenter/overlay.js.map +0 -1
- package/dist/presenter/presenter-config.d.ts.map +0 -1
- package/dist/presenter/presenter-config.js.map +0 -1
- package/dist/presenter/presenter-controls.d.ts.map +0 -1
- package/dist/presenter/presenter-controls.js.map +0 -1
- package/dist/presenter/presenter-effects.d.ts.map +0 -1
- package/dist/presenter/presenter-effects.js.map +0 -1
- package/dist/presenter/presenter-glow.d.ts.map +0 -1
- package/dist/presenter/presenter-glow.js.map +0 -1
- package/dist/presenter/presenter-log.d.ts.map +0 -1
- package/dist/presenter/presenter-log.js.map +0 -1
- package/dist/presenter/presenter-run-state.d.ts.map +0 -1
- package/dist/presenter/presenter-run-state.js.map +0 -1
- package/dist/presenter/presenter-styles.d.ts.map +0 -1
- package/dist/presenter/presenter-styles.js.map +0 -1
- package/dist/presenter/presenter-tally.d.ts.map +0 -1
- package/dist/presenter/presenter-tally.js.map +0 -1
- package/dist/presenter/presenter-test-helpers.d.ts.map +0 -1
- package/dist/presenter/presenter-test-helpers.js.map +0 -1
- package/dist/presenter/presenter-verbs.d.ts.map +0 -1
- package/dist/presenter/presenter-verbs.js.map +0 -1
- package/dist/presenter/presenter.d.ts.map +0 -1
- package/dist/presenter/presenter.js.map +0 -1
- package/dist/recorder/recorder-styles.d.ts.map +0 -1
- package/dist/recorder/recorder-styles.js.map +0 -1
- package/dist/recorder/recorder.d.ts.map +0 -1
- package/dist/recorder/recorder.js.map +0 -1
- package/dist/registry/adapters.d.ts.map +0 -1
- package/dist/registry/adapters.js.map +0 -1
- package/dist/registry/capabilities.d.ts.map +0 -1
- package/dist/registry/capabilities.js.map +0 -1
- package/dist/registry/commit-and-signal.d.ts.map +0 -1
- package/dist/registry/commit-and-signal.js.map +0 -1
- package/dist/registry/domains.d.ts.map +0 -1
- package/dist/registry/domains.js.map +0 -1
- package/dist/registry/emitter.d.ts.map +0 -1
- package/dist/registry/emitter.js.map +0 -1
- package/dist/registry/store-adapters.d.ts.map +0 -1
- package/dist/registry/store-adapters.js.map +0 -1
- package/dist/registry/stores.d.ts.map +0 -1
- package/dist/registry/stores.js.map +0 -1
- package/dist/reticle-presenter-helpers.d.ts.map +0 -1
- package/dist/reticle-presenter-helpers.js.map +0 -1
- package/dist/reticle.d.ts.map +0 -1
- package/dist/reticle.js.map +0 -1
- package/dist/review/annotator.d.ts.map +0 -1
- package/dist/review/annotator.js.map +0 -1
- package/dist/review/mark-anchor.d.ts.map +0 -1
- package/dist/review/mark-anchor.js.map +0 -1
- package/dist/security/serialization.d.ts.map +0 -1
- package/dist/security/serialization.js.map +0 -1
- package/dist/timers/clock.d.ts.map +0 -1
- package/dist/timers/clock.js.map +0 -1
- package/dist/timers/native-console.d.ts.map +0 -1
- package/dist/timers/native-console.js.map +0 -1
- package/dist/timers/native-timers.d.ts.map +0 -1
- package/dist/timers/native-timers.js.map +0 -1
- package/dist/transport/transport.d.ts.map +0 -1
- package/dist/transport/transport.js.map +0 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-origin `<iframe>` documents, wired into the top document's observation.
|
|
3
|
+
*
|
|
4
|
+
* A `MutationObserver` is scoped to the node it is given, and a frame's document is a DIFFERENT node
|
|
5
|
+
* tree — so a click inside an embedded panel changed its text and `observe` reported an empty window.
|
|
6
|
+
* Measured on a console with an embedded SLA panel: the click landed, the panel updated, and the
|
|
7
|
+
* observe window held zero DOM events. That is the false-green shape exactly — "nothing happened" is
|
|
8
|
+
* what an agent concludes, and it is wrong.
|
|
9
|
+
*
|
|
10
|
+
* A MutationObserver CAN observe a node in another same-origin document, so the fix is to hand it the
|
|
11
|
+
* frame bodies. Cross-origin frames stay unreachable and keep their existing blind-spot declaration.
|
|
12
|
+
*
|
|
13
|
+
* Network is NOT covered here. A frame's `fetch` lives in the frame's own realm and the top realm's
|
|
14
|
+
* patch never sees it, so requests it makes are invisible — declared as a blind spot rather than
|
|
15
|
+
* partially instrumented, because a half-seen request channel is worse than an admitted one.
|
|
16
|
+
*/
|
|
17
|
+
/** Frames-inside-frames, bounded. Real embeds nest once, occasionally twice. */
|
|
18
|
+
const DEPTH_MAX = 3;
|
|
19
|
+
function bodyOf(frame) {
|
|
20
|
+
try {
|
|
21
|
+
return frame.contentDocument?.body ?? null;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null; // cross-origin — not ours to read
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Every same-origin frame body at or beneath `root`, in document order. */
|
|
28
|
+
export function sameOriginFrameBodies(root) {
|
|
29
|
+
const found = [];
|
|
30
|
+
const walk = (node, depth) => {
|
|
31
|
+
if (depth >= DEPTH_MAX)
|
|
32
|
+
return;
|
|
33
|
+
for (const frame of node.querySelectorAll('iframe')) {
|
|
34
|
+
const body = bodyOf(frame);
|
|
35
|
+
if (body === null)
|
|
36
|
+
continue;
|
|
37
|
+
found.push(body);
|
|
38
|
+
walk(body, depth + 1);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
walk(root, 0);
|
|
42
|
+
return found;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Call `attach` for every same-origin frame body now and after each frame load.
|
|
46
|
+
*
|
|
47
|
+
* A frame that navigates gets a BRAND NEW document, so the old observation is dead and re-attaching on
|
|
48
|
+
* `load` is not optional — without it, observation silently stops the first time an embedded panel
|
|
49
|
+
* routes. Attaching twice to the same node is harmless (MutationObserver de-duplicates), so no
|
|
50
|
+
* bookkeeping is kept.
|
|
51
|
+
*/
|
|
52
|
+
export function observeSameOriginFrames(attach) {
|
|
53
|
+
const attachAll = () => {
|
|
54
|
+
for (const body of sameOriginFrameBodies(document))
|
|
55
|
+
attach(body);
|
|
56
|
+
};
|
|
57
|
+
attachAll();
|
|
58
|
+
// `load` does not bubble, but it DOES reach a capturing listener on the document.
|
|
59
|
+
const onLoad = (event) => {
|
|
60
|
+
if (event.target instanceof HTMLIFrameElement)
|
|
61
|
+
attachAll();
|
|
62
|
+
};
|
|
63
|
+
document.addEventListener('load', onLoad, true);
|
|
64
|
+
return () => {
|
|
65
|
+
document.removeEventListener('load', onLoad, true);
|
|
66
|
+
};
|
|
67
|
+
}
|
package/dist/observers/health.js
CHANGED
|
@@ -1,9 +1,38 @@
|
|
|
1
|
-
import { EventType, HealthReason, SESSION_HEALTH } from '@reticlehq/core';
|
|
1
|
+
import { EventType, HealthReason, RETICLE_IPC_GLOBAL, SESSION_HEALTH } from '@reticlehq/core';
|
|
2
2
|
import { nativeSetInterval } from '../timers/native-timers.js';
|
|
3
|
+
/**
|
|
4
|
+
* Which shell this page runs in. Reported with health because it is what makes a timeout
|
|
5
|
+
* diagnosable: an occluded WKWebView is SUSPENDED by macOS, so the session stays connected while
|
|
6
|
+
* every command times out — and a URL cannot tell a Tauri dev server from a plain localhost app, so
|
|
7
|
+
* the runtime has to come from the page itself.
|
|
8
|
+
*/
|
|
9
|
+
function detectRuntime() {
|
|
10
|
+
const w = window;
|
|
11
|
+
if (w['__TAURI_INTERNALS__'] !== undefined || w['__TAURI__'] !== undefined)
|
|
12
|
+
return 'tauri';
|
|
13
|
+
if (navigator.userAgent.includes('Electron') || w[RETICLE_IPC_GLOBAL] !== undefined)
|
|
14
|
+
return 'electron';
|
|
15
|
+
return 'web';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The rendering engine, coarse on purpose. Three buckets — never the UA string itself, which is
|
|
19
|
+
* high-cardinality and semi-identifying. Order matters: every Chromium UA also contains "Safari", and
|
|
20
|
+
* Gecko is the only engine that reports a real `Gecko/` build token.
|
|
21
|
+
*/
|
|
22
|
+
function detectEngine() {
|
|
23
|
+
const ua = navigator.userAgent;
|
|
24
|
+
if (/Chrome|Chromium|Edg\//.test(ua))
|
|
25
|
+
return 'blink';
|
|
26
|
+
if (/Gecko\/|Firefox/.test(ua))
|
|
27
|
+
return 'gecko';
|
|
28
|
+
return 'webkit';
|
|
29
|
+
}
|
|
3
30
|
function snapshotHealth() {
|
|
4
31
|
return {
|
|
5
32
|
hidden: document.visibilityState === 'hidden',
|
|
6
33
|
focused: document.hasFocus(),
|
|
34
|
+
runtime: detectRuntime(),
|
|
35
|
+
engine: detectEngine(),
|
|
7
36
|
};
|
|
8
37
|
}
|
|
9
38
|
/**
|
|
@@ -30,4 +59,3 @@ export function installHealth(emit) {
|
|
|
30
59
|
window.removeEventListener('blur', onBlur);
|
|
31
60
|
};
|
|
32
61
|
}
|
|
33
|
-
//# sourceMappingURL=health.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install every page observer, guarded.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `reticle.ts` because it is one job with one rule, and because that file is the SDK's
|
|
5
|
+
* public surface — the more of this that lived there, the harder it was to see what `connect()`
|
|
6
|
+
* actually does.
|
|
7
|
+
*
|
|
8
|
+
* THE RULE: one observer failing must never stop the others. This array used to be built bare, so a
|
|
9
|
+
* single `install*` that threw took the whole `connect()` down with it — one bad patch on one exotic
|
|
10
|
+
* page and the app got no instrumentation at all, silently, because the SDK\'s own catch blocks
|
|
11
|
+
* ensured nobody ever found out. Each install is now wrapped: the healthy observers still run, the
|
|
12
|
+
* failure is reported over the bridge, and the caller gets a teardown list of the same length.
|
|
13
|
+
*/
|
|
14
|
+
import { installDom } from './dom.js';
|
|
15
|
+
import { installStorage } from './storage.js';
|
|
16
|
+
import { installStoreState } from './state.js';
|
|
17
|
+
import { installFocus } from './focus.js';
|
|
18
|
+
import { installBlindSpots } from './blind-spots.js';
|
|
19
|
+
import { installDownload } from './download.js';
|
|
20
|
+
import { installNetwork } from './network.js';
|
|
21
|
+
import { installIpc, ipcNetOverrides } from './ipc.js';
|
|
22
|
+
import { installPerf } from './perf.js';
|
|
23
|
+
import { installRoute } from './route.js';
|
|
24
|
+
import { installConsole } from './console.js';
|
|
25
|
+
import { installAnimation } from './animation.js';
|
|
26
|
+
import { installScroll } from './scroll.js';
|
|
27
|
+
import { installHealth } from './health.js';
|
|
28
|
+
import { reportSdkFailure, SdkSite } from './sdk-failure.js';
|
|
29
|
+
/** Run one install, reporting rather than throwing if it cannot start. */
|
|
30
|
+
function guard(emit, site, install) {
|
|
31
|
+
try {
|
|
32
|
+
return install();
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
reportSdkFailure(emit, site, error);
|
|
36
|
+
return () => { }; // nothing was installed, so there is nothing to tear down
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function installAllObservers(emit, options) {
|
|
40
|
+
return [
|
|
41
|
+
// Composition happens HERE, not inside the network observer: the network observer knows
|
|
42
|
+
// nothing about desktop IPC, and the IPC observer knows nothing about fetch plumbing.
|
|
43
|
+
guard(emit, SdkSite.NETWORK_OBSERVER, () => installNetwork(emit, {
|
|
44
|
+
captureBodies: options.captureBodies,
|
|
45
|
+
reinterpret: ipcNetOverrides,
|
|
46
|
+
})),
|
|
47
|
+
// Desktop backends are reached over IPC, not HTTP — inert on a plain web page.
|
|
48
|
+
guard(emit, SdkSite.IPC_OBSERVER, () => installIpc(emit, { captureBodies: options.captureBodies })),
|
|
49
|
+
guard(emit, SdkSite.ANIMATION_OBSERVER, () => installPerf(emit)),
|
|
50
|
+
guard(emit, SdkSite.ROUTER_OBSERVER, () => installRoute(emit)),
|
|
51
|
+
guard(emit, SdkSite.CONSOLE_OBSERVER, () => installConsole(emit)),
|
|
52
|
+
guard(emit, SdkSite.ANIMATION_OBSERVER, () => installAnimation(emit)),
|
|
53
|
+
guard(emit, SdkSite.DOM_OBSERVER, () => installScroll(emit)),
|
|
54
|
+
guard(emit, SdkSite.DOM_OBSERVER, () => installDom(emit)),
|
|
55
|
+
guard(emit, SdkSite.STORE_ADAPTER, () => installStorage(emit)), // storage WRITES → STORAGE_CHANGE diffs (pull remains the fallback)
|
|
56
|
+
guard(emit, SdkSite.STORE_ADAPTER, () => installStoreState(emit)), // subscribed-store mutations → STATE_CHANGE path diffs
|
|
57
|
+
guard(emit, SdkSite.DOM_OBSERVER, () => installFocus(emit)), // element focus movement → FOCUS_CHANGE (focus-to-body = a regression)
|
|
58
|
+
// Files the app PRODUCES — never cross the network, so no outside-the-page tool can see them.
|
|
59
|
+
guard(emit, SdkSite.NETWORK_OBSERVER, () => installDownload(emit, { capturePreview: options.captureBodies })),
|
|
60
|
+
guard(emit, SdkSite.DOM_OBSERVER, () => installBlindSpots(emit)), // cross-origin iframes the SDK can't see → BLIND_SPOT (coverage: partial)
|
|
61
|
+
guard(emit, SdkSite.HEALTH_OBSERVER, () => installHealth(emit)), // page visibility/focus health + heartbeat
|
|
62
|
+
];
|
|
63
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Emit, Teardown } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* How Tauri v2 reports a command's verdict. The transport answers HTTP 200 whether the Rust command
|
|
4
|
+
* returned Ok or Err — the verdict is in this response header, which Tauri explicitly CORS-exposes so
|
|
5
|
+
* JS may read it. Without translating it, every failed command is recorded as a successful request.
|
|
6
|
+
*/
|
|
7
|
+
export declare const TAURI_RESPONSE_HEADER_NAME = "Tauri-Response";
|
|
8
|
+
/**
|
|
9
|
+
* Fields to override on a NET_REQUEST that turned out to be a Tauri IPC call, or null when the
|
|
10
|
+
* request was ordinary HTTP. Pure — the network observer calls this with the response header it
|
|
11
|
+
* already holds, so IPC knowledge stays in this file.
|
|
12
|
+
*/
|
|
13
|
+
export declare function ipcNetOverrides(url: string, header: (name: string) => string | null): Record<string, unknown> | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Subscribe to the Electron preload shim, if this app installed it. Inert on Tauri (whose calls the
|
|
16
|
+
* network observer handles) and on a plain web page.
|
|
17
|
+
*/
|
|
18
|
+
export interface IpcOptions {
|
|
19
|
+
/** Forward the IPC payloads the preload recorded. Off by default, like the HTTP body capture. */
|
|
20
|
+
captureBodies?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function installIpc(emit: Emit, options?: IpcOptions): Teardown;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { BlindSpotKind, EventType, IPC_URL_SCHEME, IpcStatus, NetInitiator, RETICLE_IPC_GLOBAL, } from '@reticlehq/core';
|
|
2
|
+
import { observeSafely } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* How Tauri v2 reports a command's verdict. The transport answers HTTP 200 whether the Rust command
|
|
5
|
+
* returned Ok or Err — the verdict is in this response header, which Tauri explicitly CORS-exposes so
|
|
6
|
+
* JS may read it. Without translating it, every failed command is recorded as a successful request.
|
|
7
|
+
*/
|
|
8
|
+
export const TAURI_RESPONSE_HEADER_NAME = 'Tauri-Response';
|
|
9
|
+
const TAURI_RESPONSE_ERROR = 'error';
|
|
10
|
+
/** Rust/IPC vocabulary rather than HTTP's, so the text never claims a transport status it lacks. */
|
|
11
|
+
const IPC_STATUS_TEXT = { OK: 'Ok', ERROR: 'Err' };
|
|
12
|
+
/**
|
|
13
|
+
* Tauri's IPC endpoint. The last path segment is the command name.
|
|
14
|
+
*
|
|
15
|
+
* TWO spellings, because Tauri v2 uses a different one per platform and matching only the first is a
|
|
16
|
+
* silent false green on Windows: macOS/Linux get the custom scheme `ipc://localhost/archive_todo`,
|
|
17
|
+
* while Windows (and Android) need a real http origin and get
|
|
18
|
+
* `http://ipc.localhost/archive_todo` — the hostname `desktop-doctor` already tells people to put in
|
|
19
|
+
* their CSP. Unmatched, a Windows `invoke` was recorded as an ordinary HTTP request, so the
|
|
20
|
+
* `Tauri-Response` translation never ran and every command that returned `Err` was banked as a
|
|
21
|
+
* successful 200. `.localhost` is reserved for loopback by RFC 6761, so this can never match a
|
|
22
|
+
* remote host.
|
|
23
|
+
*/
|
|
24
|
+
const TAURI_IPC_URL = /^(?:ipc:\/\/[^/]*|https?:\/\/ipc\.localhost(?::\d+)?)\/(.+)$/;
|
|
25
|
+
/**
|
|
26
|
+
* Fields to override on a NET_REQUEST that turned out to be a Tauri IPC call, or null when the
|
|
27
|
+
* request was ordinary HTTP. Pure — the network observer calls this with the response header it
|
|
28
|
+
* already holds, so IPC knowledge stays in this file.
|
|
29
|
+
*/
|
|
30
|
+
export function ipcNetOverrides(url, header) {
|
|
31
|
+
const match = TAURI_IPC_URL.exec(url);
|
|
32
|
+
const command = match?.[1];
|
|
33
|
+
if (command === undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
const ok = header(TAURI_RESPONSE_HEADER_NAME) !== TAURI_RESPONSE_ERROR;
|
|
36
|
+
return {
|
|
37
|
+
// Normalize `ipc://localhost/archive_todo` to `ipc://archive_todo`, so a Tauri command and an
|
|
38
|
+
// Electron channel read identically to the agent and to a saved flow's assertions.
|
|
39
|
+
url: `${IPC_URL_SCHEME}${command}`,
|
|
40
|
+
initiator: NetInitiator.IPC,
|
|
41
|
+
method: NetInitiator.IPC,
|
|
42
|
+
ok,
|
|
43
|
+
status: ok ? IpcStatus.OK : IpcStatus.ERROR,
|
|
44
|
+
// Overwrite the TRANSPORT's statusText. Tauri's fetch genuinely answered 200/"OK", so letting it
|
|
45
|
+
// through beside a synthetic 500 produced a record that contradicted itself — `status: 500,
|
|
46
|
+
// statusText: "OK"` reads as nonsense to anyone seeing it cold. The record must tell one story,
|
|
47
|
+
// and the story that matters is the command's verdict, not the pipe it travelled down.
|
|
48
|
+
statusText: ok ? IPC_STATUS_TEXT.OK : IPC_STATUS_TEXT.ERROR,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Is this page an Electron renderer? Read off the user agent, which Electron stamps and which is
|
|
53
|
+
* available whether or not the preload ran — the whole point is to recognise a renderer that has
|
|
54
|
+
* NOT been instrumented.
|
|
55
|
+
*/
|
|
56
|
+
function isElectronRenderer() {
|
|
57
|
+
return navigator.userAgent.includes('Electron');
|
|
58
|
+
}
|
|
59
|
+
export function installIpc(emit, options = {}) {
|
|
60
|
+
const channel = window[RETICLE_IPC_GLOBAL];
|
|
61
|
+
if (typeof channel?.subscribe !== 'function') {
|
|
62
|
+
// No shim — but on Electron that is not "nothing to observe", it is "everything unobserved".
|
|
63
|
+
// Tauri needs no shim (its invoke travels as a fetch) and a web page has no IPC at all, so only
|
|
64
|
+
// Electron gets the declaration. Saying nothing here is what let a missing preload line read as
|
|
65
|
+
// a clean, empty network view for a whole app.
|
|
66
|
+
if (isElectronRenderer())
|
|
67
|
+
emit(EventType.BLIND_SPOT, { kind: BlindSpotKind.UNOBSERVED_IPC, count: 1 });
|
|
68
|
+
return () => undefined;
|
|
69
|
+
}
|
|
70
|
+
const token = channel.subscribe((record) => {
|
|
71
|
+
observeSafely(() => {
|
|
72
|
+
const url = `${IPC_URL_SCHEME}${record.channel}`;
|
|
73
|
+
if (record.phase === 'start') {
|
|
74
|
+
emit(EventType.NET_PENDING, {
|
|
75
|
+
id: record.id,
|
|
76
|
+
method: NetInitiator.IPC,
|
|
77
|
+
url,
|
|
78
|
+
initiator: NetInitiator.IPC,
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// A one-way `send` that did not throw has NO verdict — the renderer never learns whether the
|
|
83
|
+
// main process handled it. Emitting `ok/status` anyway would manufacture the success nobody
|
|
84
|
+
// reported, so both are omitted and `oneWay` says why. `reticle_network { ok }` then matches
|
|
85
|
+
// such a record in neither direction, which is the honest answer to "did it work".
|
|
86
|
+
const verdictless = record.oneWay === true && record.ok === undefined;
|
|
87
|
+
// Declared, not inferred: the server cannot tell "no verdict" from "not yet settled" without
|
|
88
|
+
// being told, and a send with nothing to observe must not read as a clean green.
|
|
89
|
+
if (verdictless)
|
|
90
|
+
emit(EventType.BLIND_SPOT, { kind: BlindSpotKind.VERDICTLESS_SEND, count: 1 });
|
|
91
|
+
const ok = record.ok === true;
|
|
92
|
+
emit(EventType.NET_REQUEST, {
|
|
93
|
+
id: record.id,
|
|
94
|
+
method: NetInitiator.IPC,
|
|
95
|
+
url,
|
|
96
|
+
...(verdictless ? {} : { ok, status: ok ? IpcStatus.OK : IpcStatus.ERROR }),
|
|
97
|
+
...(record.oneWay === true ? { oneWay: true } : {}),
|
|
98
|
+
durationMs: record.durationMs ?? 0,
|
|
99
|
+
initiator: NetInitiator.IPC,
|
|
100
|
+
...(record.error === undefined ? {} : { error: record.error }),
|
|
101
|
+
// Size travels ALWAYS, bodies only on opt-in. The split matters: the verdict layer reports "a
|
|
102
|
+
// write returned ok and its payload went unread" from the size alone, and it cannot infer that
|
|
103
|
+
// from an absent field — an unread payload and an empty one would look identical.
|
|
104
|
+
...(record.responseSize === undefined ? {} : { responseSize: record.responseSize }),
|
|
105
|
+
...(options.captureBodies === true
|
|
106
|
+
? {
|
|
107
|
+
...(record.requestBody === undefined ? {} : { requestBody: record.requestBody }),
|
|
108
|
+
...(record.responseBody === undefined ? {} : { responseBody: record.responseBody }),
|
|
109
|
+
...(record.responseBodyTruncated === true ? { responseBodyTruncated: true } : {}),
|
|
110
|
+
}
|
|
111
|
+
: {}),
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
// A REAL removal, so teardown leaves the app as it found it. The preload's `invoke` patch stays
|
|
116
|
+
// deliberately — it is a pass-through with no effect once no subscriber remains, and un-patching
|
|
117
|
+
// mid-flight would strand any call already inside the wrapper.
|
|
118
|
+
return () => {
|
|
119
|
+
channel.unsubscribe?.(token);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is this response body still arriving after the request itself resolved?
|
|
3
|
+
*
|
|
4
|
+
* STRUCTURAL, not a content-type allowlist. The first version listed `x-component|x-ndjson|
|
|
5
|
+
* application/stream` — which is to say "Next.js RSC, plus two guesses". It closed the exact defect
|
|
6
|
+
* it was written against and left every other streamed response as invisible as before: a chunked
|
|
7
|
+
* `application/json` from a token API, a streamed HTML document, an export assembled server-side.
|
|
8
|
+
* Fixing one app's symptom and calling the class solved is how a false green comes back wearing
|
|
9
|
+
* different clothes.
|
|
10
|
+
*
|
|
11
|
+
* The real signal is on the wire: a response with a BODY and no `content-length` is being sent
|
|
12
|
+
* chunked, which is the definition of "the server is still writing". `content-length` present means
|
|
13
|
+
* the size was known up front and the body cannot grow.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately independent of content type, with one exception above for streams that never end.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isStreamingBody(contentType: string | null, contentLength: string | null): boolean;
|
|
1
18
|
/**
|
|
2
19
|
* Resolve with the body text, or `undefined` if it takes too long.
|
|
3
20
|
*
|
|
@@ -16,4 +33,3 @@ export declare function projectBody(rawText: string, contentType: string | null)
|
|
|
16
33
|
body: string;
|
|
17
34
|
truncated: boolean;
|
|
18
35
|
};
|
|
19
|
-
//# sourceMappingURL=network-body.d.ts.map
|
|
@@ -21,6 +21,32 @@ const CAPTURABLE_CONTENT = /application\/json|text\/|application\/xml|x-www-form
|
|
|
21
21
|
* observability SDK. Never read these; the frame observer covers SSE properly.
|
|
22
22
|
*/
|
|
23
23
|
const STREAMING_CONTENT = /event-stream|x-ndjson|application\/stream/i;
|
|
24
|
+
/**
|
|
25
|
+
* Content types that stay OPEN by design — watching them would make every first settle pay the full
|
|
26
|
+
* watch bound for a stream doing exactly its job. Real SSE is covered by the EventSource observer.
|
|
27
|
+
*/
|
|
28
|
+
const ENDLESS_BY_DESIGN = /event-stream/i;
|
|
29
|
+
/**
|
|
30
|
+
* Is this response body still arriving after the request itself resolved?
|
|
31
|
+
*
|
|
32
|
+
* STRUCTURAL, not a content-type allowlist. The first version listed `x-component|x-ndjson|
|
|
33
|
+
* application/stream` — which is to say "Next.js RSC, plus two guesses". It closed the exact defect
|
|
34
|
+
* it was written against and left every other streamed response as invisible as before: a chunked
|
|
35
|
+
* `application/json` from a token API, a streamed HTML document, an export assembled server-side.
|
|
36
|
+
* Fixing one app's symptom and calling the class solved is how a false green comes back wearing
|
|
37
|
+
* different clothes.
|
|
38
|
+
*
|
|
39
|
+
* The real signal is on the wire: a response with a BODY and no `content-length` is being sent
|
|
40
|
+
* chunked, which is the definition of "the server is still writing". `content-length` present means
|
|
41
|
+
* the size was known up front and the body cannot grow.
|
|
42
|
+
*
|
|
43
|
+
* Deliberately independent of content type, with one exception above for streams that never end.
|
|
44
|
+
*/
|
|
45
|
+
export function isStreamingBody(contentType, contentLength) {
|
|
46
|
+
if (contentType !== null && ENDLESS_BY_DESIGN.test(contentType))
|
|
47
|
+
return false;
|
|
48
|
+
return contentLength === null;
|
|
49
|
+
}
|
|
24
50
|
/**
|
|
25
51
|
* Longest the SDK will wait on a response-body clone before emitting without it.
|
|
26
52
|
*
|
|
@@ -140,4 +166,3 @@ export function projectBody(rawText, contentType) {
|
|
|
140
166
|
const truncated = oversized || out.length > MAX_BODY_CHARS;
|
|
141
167
|
return { body: out.length > MAX_BODY_CHARS ? out.slice(0, MAX_BODY_CHARS) : out, truncated };
|
|
142
168
|
}
|
|
143
|
-
//# sourceMappingURL=network-body.js.map
|
|
@@ -30,13 +30,17 @@ export function redactUrl(raw) {
|
|
|
30
30
|
let newQuery = query;
|
|
31
31
|
if (query !== '') {
|
|
32
32
|
const params = new URLSearchParams(query);
|
|
33
|
+
let queryChanged = false;
|
|
33
34
|
for (const key of [...params.keys()]) {
|
|
34
35
|
if (isSensitiveKey(key)) {
|
|
35
36
|
params.set(key, REDACTED_VALUE);
|
|
36
|
-
|
|
37
|
+
queryChanged = true;
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
+
if (queryChanged) {
|
|
41
|
+
newQuery = params.toString();
|
|
42
|
+
changed = true;
|
|
43
|
+
}
|
|
40
44
|
}
|
|
41
45
|
const segments = authority.split('/');
|
|
42
46
|
for (let i = 0; i + 1 < segments.length; i++) {
|
|
@@ -63,4 +67,3 @@ export function redactUrl(raw) {
|
|
|
63
67
|
const queryOut = queryStart === -1 ? '' : `?${newQuery}`;
|
|
64
68
|
return `${segments.join('/')}${queryOut}${newHash}`;
|
|
65
69
|
}
|
|
66
|
-
//# sourceMappingURL=network-redact.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Emit } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Watch a STREAMED response body to its end, so `settled` cannot pass while the server is still
|
|
4
|
+
* writing.
|
|
5
|
+
*
|
|
6
|
+
* `fetch` resolves at HEADERS. On a Next.js App Router page the RSC payload's NET_REQUEST landed
|
|
7
|
+
* 16 ms in, the shell rendered at 3013, and the Suspense boundary's rows arrived at 3902 — 889 ms
|
|
8
|
+
* in which NOTHING was observed. Quiet is exactly what settle tests for, so it passed with the
|
|
9
|
+
* fallback still on screen and zero rows rendered: a green on every streaming boundary.
|
|
10
|
+
*
|
|
11
|
+
* STRUCTURAL, not an allowlist: a response with a body and no `content-length` is being sent
|
|
12
|
+
* chunked, which is the wire's own way of saying the server is still writing. An earlier version
|
|
13
|
+
* gated on a content-type list containing `x-component` — "Next.js RSC, plus two guesses" — which
|
|
14
|
+
* closed the one defect it was written against and left every other streamed response as invisible
|
|
15
|
+
* as before. The clone is DRAINED as it arrives rather than buffered, so watching costs a second
|
|
16
|
+
* read of bytes already in flight, not a second copy held in memory.
|
|
17
|
+
*
|
|
18
|
+
* BOUNDED both ways: an endless stream (SSE over fetch, a live feed) must not hold `settled` open
|
|
19
|
+
* forever, so the watch gives up after STREAM_WATCH_MS and closes the record anyway, marked
|
|
20
|
+
* `gaveUp`. That is the safe direction — a settle that eventually proceeds beats one that never
|
|
21
|
+
* does — and it says which happened rather than pretending the stream ended.
|
|
22
|
+
*/
|
|
23
|
+
export declare function watchStreamedBody(emit: Emit, res: Response, id: string, url: string, contentType: string | null, contentLength: string | null): void;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watching a STREAMED response body to its end, so `settled` cannot pass mid-stream.
|
|
3
|
+
*
|
|
4
|
+
* Split out of network.ts, which was already at its line cap. One question: when did this response
|
|
5
|
+
* actually finish arriving?
|
|
6
|
+
*/
|
|
7
|
+
import { EventType, StreamDirection, StreamTransport } from '@reticlehq/core';
|
|
8
|
+
import { isStreamingBody } from './network-body.js';
|
|
9
|
+
import { nativeClearTimeout, nativeSetTimeout } from '../timers/native-timers.js';
|
|
10
|
+
/**
|
|
11
|
+
* Longest a streamed response body is watched before the record is closed anyway.
|
|
12
|
+
*
|
|
13
|
+
* An endless stream must never hold `settled` open forever. Ten seconds is longer than any
|
|
14
|
+
* page-render stream and short enough that a genuinely endless one costs one settle window, not the
|
|
15
|
+
* session.
|
|
16
|
+
*/
|
|
17
|
+
const STREAM_WATCH_MS = 10_000;
|
|
18
|
+
/**
|
|
19
|
+
* Watch a STREAMED response body to its end, so `settled` cannot pass while the server is still
|
|
20
|
+
* writing.
|
|
21
|
+
*
|
|
22
|
+
* `fetch` resolves at HEADERS. On a Next.js App Router page the RSC payload's NET_REQUEST landed
|
|
23
|
+
* 16 ms in, the shell rendered at 3013, and the Suspense boundary's rows arrived at 3902 — 889 ms
|
|
24
|
+
* in which NOTHING was observed. Quiet is exactly what settle tests for, so it passed with the
|
|
25
|
+
* fallback still on screen and zero rows rendered: a green on every streaming boundary.
|
|
26
|
+
*
|
|
27
|
+
* STRUCTURAL, not an allowlist: a response with a body and no `content-length` is being sent
|
|
28
|
+
* chunked, which is the wire's own way of saying the server is still writing. An earlier version
|
|
29
|
+
* gated on a content-type list containing `x-component` — "Next.js RSC, plus two guesses" — which
|
|
30
|
+
* closed the one defect it was written against and left every other streamed response as invisible
|
|
31
|
+
* as before. The clone is DRAINED as it arrives rather than buffered, so watching costs a second
|
|
32
|
+
* read of bytes already in flight, not a second copy held in memory.
|
|
33
|
+
*
|
|
34
|
+
* BOUNDED both ways: an endless stream (SSE over fetch, a live feed) must not hold `settled` open
|
|
35
|
+
* forever, so the watch gives up after STREAM_WATCH_MS and closes the record anyway, marked
|
|
36
|
+
* `gaveUp`. That is the safe direction — a settle that eventually proceeds beats one that never
|
|
37
|
+
* does — and it says which happened rather than pretending the stream ended.
|
|
38
|
+
*/
|
|
39
|
+
export function watchStreamedBody(emit, res, id, url, contentType, contentLength) {
|
|
40
|
+
// `=== null` is not enough: jsdom's Response has no `body` at all, and older/polyfilled
|
|
41
|
+
// implementations return undefined. Reaching for `.getReader()` on that threw an unhandled
|
|
42
|
+
// rejection out of a detached promise — an observability layer throwing into the host page, from
|
|
43
|
+
// code the host never called. Duck-type the reader instead of trusting the shape.
|
|
44
|
+
if (!isStreamingBody(contentType, contentLength))
|
|
45
|
+
return;
|
|
46
|
+
if (typeof res.body?.getReader !== 'function')
|
|
47
|
+
return;
|
|
48
|
+
let clone;
|
|
49
|
+
try {
|
|
50
|
+
clone = res.clone();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return; // already consumed or locked — the app owns it; observing it would take it away
|
|
54
|
+
}
|
|
55
|
+
const body = clone.body;
|
|
56
|
+
if (body === null)
|
|
57
|
+
return;
|
|
58
|
+
emit(EventType.NET_STREAM, {
|
|
59
|
+
transport: StreamTransport.FETCH,
|
|
60
|
+
direction: StreamDirection.OPEN,
|
|
61
|
+
url,
|
|
62
|
+
id,
|
|
63
|
+
});
|
|
64
|
+
void (async () => {
|
|
65
|
+
const reader = body.getReader();
|
|
66
|
+
let gaveUp = false;
|
|
67
|
+
// The deadline must be a TIMER, not a check after each read. An idle stream parks inside
|
|
68
|
+
// `read()` waiting for a chunk that may never come, so a post-read check never runs and the
|
|
69
|
+
// record never closes — which would hold `settled` open forever on exactly the endless streams
|
|
70
|
+
// the bound exists to protect against.
|
|
71
|
+
let timer;
|
|
72
|
+
const expired = new Promise((resolve) => {
|
|
73
|
+
timer = nativeSetTimeout(() => resolve('expired'), STREAM_WATCH_MS);
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
for (;;) {
|
|
77
|
+
const next = await Promise.race([reader.read(), expired]);
|
|
78
|
+
if (next === 'expired') {
|
|
79
|
+
gaveUp = true;
|
|
80
|
+
void reader.cancel().catch(() => undefined);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
if (next.done)
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
/* aborted or errored — the stream is over either way, which is what we report */
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
if (timer !== undefined)
|
|
92
|
+
nativeClearTimeout(timer);
|
|
93
|
+
}
|
|
94
|
+
emit(EventType.NET_STREAM, {
|
|
95
|
+
transport: StreamTransport.FETCH,
|
|
96
|
+
direction: StreamDirection.CLOSE,
|
|
97
|
+
url,
|
|
98
|
+
id,
|
|
99
|
+
...(gaveUp ? { gaveUp: true } : {}),
|
|
100
|
+
});
|
|
101
|
+
})();
|
|
102
|
+
}
|
|
@@ -2,9 +2,20 @@ import type { Emit, Teardown } from './types.js';
|
|
|
2
2
|
import { redactUrl } from './network-redact.js';
|
|
3
3
|
export { redactUrl };
|
|
4
4
|
/** Config for the network observer. Body capture is OFF by default and dev-only opt-in. */
|
|
5
|
+
/**
|
|
6
|
+
* Reinterpret a completed request's fields from its response.
|
|
7
|
+
*
|
|
8
|
+
* The seam exists so this observer stays ignorant of desktop IPC. A Tauri `invoke` arrives here as an
|
|
9
|
+
* ordinary fetch to an `ipc://` protocol, and the header that says whether the Rust command
|
|
10
|
+
* succeeded means nothing to a network observer — so the knowledge lives in the IPC observer and is
|
|
11
|
+
* passed IN, rather than this module importing it. Returns undefined to leave the record untouched.
|
|
12
|
+
*/
|
|
13
|
+
export type NetResponseReinterpreter = (url: string, header: (name: string) => string | null) => Record<string, unknown> | undefined;
|
|
5
14
|
export interface NetworkOptions {
|
|
6
15
|
/** Capture request/response bodies (text-like content only, redacted, per-body capped). */
|
|
7
16
|
captureBodies?: boolean;
|
|
17
|
+
/** Optional hook that reinterprets a completed request — see NetResponseReinterpreter. */
|
|
18
|
+
reinterpret?: NetResponseReinterpreter;
|
|
8
19
|
}
|
|
9
20
|
/** Pure: the first real app-code frame in a stack string, capped. Exported for unit testing. */
|
|
10
21
|
export declare function firstAppFrame(stack: string | undefined): string | undefined;
|
|
@@ -20,4 +31,3 @@ export interface NetTiming {
|
|
|
20
31
|
*/
|
|
21
32
|
export declare function extractTiming(entry: PerformanceResourceTiming | undefined): NetTiming;
|
|
22
33
|
export declare function installNetwork(emit: Emit, opts?: NetworkOptions): Teardown;
|
|
23
|
-
//# sourceMappingURL=network.d.ts.map
|
|
@@ -2,6 +2,7 @@ import { BlindSpotKind, EventType, RETICLE_WS_PATH, StreamTransport, StreamDirec
|
|
|
2
2
|
import { captureMethod } from '../patching/capture-method.js';
|
|
3
3
|
import { isCapturableType, projectBody, withBodyDeadline } from './network-body.js';
|
|
4
4
|
import { redactUrl } from './network-redact.js';
|
|
5
|
+
import { watchStreamedBody } from './network-stream.js';
|
|
5
6
|
// Redaction moved to its own cohesive module (network.ts is at its line cap); re-exported so callers
|
|
6
7
|
// and the existing test suite keep importing it from here.
|
|
7
8
|
export { redactUrl };
|
|
@@ -191,6 +192,7 @@ function isNativeFetch(fn) {
|
|
|
191
192
|
const OURS = new WeakSet();
|
|
192
193
|
export function installNetwork(emit, opts = {}) {
|
|
193
194
|
const captureBodies = opts.captureBodies === true;
|
|
195
|
+
const reinterpret = opts.reinterpret;
|
|
194
196
|
// Keep the true original for teardown identity, plus a window-bound copy to invoke
|
|
195
197
|
// (fetch throws "Illegal invocation" if called with the wrong `this`).
|
|
196
198
|
const origFetch = window.fetch;
|
|
@@ -230,6 +232,8 @@ export function installNetwork(emit, opts = {}) {
|
|
|
230
232
|
// headers-received, so it stays honest whether or not we read the body.
|
|
231
233
|
const headersAt = performance.now();
|
|
232
234
|
const contentType = res.headers.get('content-type');
|
|
235
|
+
// The request is done; the BODY may not be. Watch it so settle cannot pass mid-stream.
|
|
236
|
+
watchStreamedBody(emit, res, id, url, contentType, res.headers.get('content-length'));
|
|
233
237
|
const emitRequest = (responseBodyFields) => {
|
|
234
238
|
emit(EventType.NET_REQUEST, {
|
|
235
239
|
id,
|
|
@@ -244,6 +248,9 @@ export function installNetwork(emit, opts = {}) {
|
|
|
244
248
|
...netResponseMeta(res.statusText, contentType, res.headers.get('content-length')),
|
|
245
249
|
...projectRequestBody(init?.body, captureBodies),
|
|
246
250
|
...responseBodyFields,
|
|
251
|
+
// Applied LAST so a reinterpreted verdict wins over the transport's own fields — a Tauri
|
|
252
|
+
// command that returned Err still travelled down a fetch that answered HTTP 200.
|
|
253
|
+
...(reinterpret?.(url, (name) => res.headers.get(name)) ?? {}),
|
|
247
254
|
});
|
|
248
255
|
};
|
|
249
256
|
if (captureBodies && isCapturableType(contentType)) {
|
|
@@ -511,4 +518,3 @@ export function installNetwork(emit, opts = {}) {
|
|
|
511
518
|
}
|
|
512
519
|
};
|
|
513
520
|
}
|
|
514
|
-
//# sourceMappingURL=network.js.map
|
package/dist/observers/perf.d.ts
CHANGED
package/dist/observers/perf.js
CHANGED
package/dist/observers/route.js
CHANGED