@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.
Files changed (271) hide show
  1. package/dist/actions/actions-dom.d.ts +0 -1
  2. package/dist/actions/actions-dom.js +0 -1
  3. package/dist/actions/actions.d.ts +3 -1
  4. package/dist/actions/actions.js +55 -36
  5. package/dist/actions/scroll.d.ts +0 -1
  6. package/dist/actions/scroll.js +0 -1
  7. package/dist/actions/value-input.d.ts +33 -0
  8. package/dist/actions/value-input.js +63 -0
  9. package/dist/commands/commands.d.ts +0 -1
  10. package/dist/commands/commands.js +15 -7
  11. package/dist/dom/a11y.d.ts +0 -2
  12. package/dist/dom/a11y.js +54 -24
  13. package/dist/dom/auto-anchor.d.ts +0 -1
  14. package/dist/dom/auto-anchor.js +0 -1
  15. package/dist/dom/blind-spots.d.ts +0 -1
  16. package/dist/dom/blind-spots.js +0 -1
  17. package/dist/dom/chart.d.ts +0 -1
  18. package/dist/dom/chart.js +0 -1
  19. package/dist/dom/desktop-capture.d.ts +36 -0
  20. package/dist/dom/desktop-capture.js +113 -0
  21. package/dist/dom/dom-ignore.d.ts +7 -1
  22. package/dist/dom/dom-ignore.js +1 -2
  23. package/dist/dom/occlusion.d.ts +0 -1
  24. package/dist/dom/occlusion.js +0 -1
  25. package/dist/dom/query.d.ts +0 -1
  26. package/dist/dom/query.js +61 -13
  27. package/dist/dom/realm.d.ts +30 -0
  28. package/dist/dom/realm.js +58 -0
  29. package/dist/dom/refs.d.ts +0 -1
  30. package/dist/dom/refs.js +0 -1
  31. package/dist/dom/shadow-registry.d.ts +40 -0
  32. package/dist/dom/shadow-registry.js +165 -0
  33. package/dist/dom/snapshot.d.ts +2 -2
  34. package/dist/dom/snapshot.js +21 -4
  35. package/dist/dom/source.d.ts +0 -1
  36. package/dist/dom/source.js +0 -1
  37. package/dist/dom/theme.d.ts +0 -1
  38. package/dist/dom/theme.js +0 -1
  39. package/dist/dom/virtualized.d.ts +19 -0
  40. package/dist/dom/virtualized.js +80 -0
  41. package/dist/index.d.ts +0 -1
  42. package/dist/index.js +0 -1
  43. package/dist/observers/animation.d.ts +0 -1
  44. package/dist/observers/animation.js +0 -1
  45. package/dist/observers/blind-spots.d.ts +16 -4
  46. package/dist/observers/blind-spots.js +65 -11
  47. package/dist/observers/console.d.ts +0 -1
  48. package/dist/observers/console.js +0 -1
  49. package/dist/observers/dom.d.ts +0 -2
  50. package/dist/observers/dom.js +67 -9
  51. package/dist/observers/download.d.ts +6 -0
  52. package/dist/observers/download.js +103 -0
  53. package/dist/observers/focus.d.ts +0 -1
  54. package/dist/observers/focus.js +0 -1
  55. package/dist/observers/frames.d.ts +12 -0
  56. package/dist/observers/frames.js +67 -0
  57. package/dist/observers/health.d.ts +0 -1
  58. package/dist/observers/health.js +30 -2
  59. package/dist/observers/install-all.d.ts +5 -0
  60. package/dist/observers/install-all.js +63 -0
  61. package/dist/observers/ipc.d.ts +22 -0
  62. package/dist/observers/ipc.js +121 -0
  63. package/dist/observers/network-body.d.ts +17 -1
  64. package/dist/observers/network-body.js +26 -1
  65. package/dist/observers/network-redact.d.ts +0 -1
  66. package/dist/observers/network-redact.js +6 -3
  67. package/dist/observers/network-stream.d.ts +23 -0
  68. package/dist/observers/network-stream.js +102 -0
  69. package/dist/observers/network.d.ts +11 -1
  70. package/dist/observers/network.js +7 -1
  71. package/dist/observers/perf.d.ts +0 -1
  72. package/dist/observers/perf.js +0 -1
  73. package/dist/observers/route.d.ts +0 -1
  74. package/dist/observers/route.js +0 -1
  75. package/dist/observers/scroll.d.ts +0 -1
  76. package/dist/observers/scroll.js +0 -1
  77. package/dist/observers/sdk-failure.d.ts +28 -0
  78. package/dist/observers/sdk-failure.js +60 -0
  79. package/dist/observers/state.d.ts +0 -1
  80. package/dist/observers/state.js +0 -1
  81. package/dist/observers/storage.d.ts +0 -1
  82. package/dist/observers/storage.js +0 -1
  83. package/dist/observers/types.d.ts +0 -1
  84. package/dist/observers/types.js +0 -1
  85. package/dist/patching/capture-method.d.ts +0 -1
  86. package/dist/patching/capture-method.js +0 -1
  87. package/dist/presenter/overlay.d.ts +0 -1
  88. package/dist/presenter/overlay.js +0 -1
  89. package/dist/presenter/presenter-config.d.ts +0 -1
  90. package/dist/presenter/presenter-config.js +0 -1
  91. package/dist/presenter/presenter-controls.d.ts +0 -1
  92. package/dist/presenter/presenter-controls.js +0 -1
  93. package/dist/presenter/presenter-effects.d.ts +0 -1
  94. package/dist/presenter/presenter-effects.js +0 -1
  95. package/dist/presenter/presenter-glow.d.ts +0 -1
  96. package/dist/presenter/presenter-glow.js +0 -1
  97. package/dist/presenter/presenter-log.d.ts +0 -1
  98. package/dist/presenter/presenter-log.js +0 -1
  99. package/dist/presenter/presenter-run-state.d.ts +0 -1
  100. package/dist/presenter/presenter-run-state.js +0 -1
  101. package/dist/presenter/presenter-styles.d.ts +0 -1
  102. package/dist/presenter/presenter-styles.js +0 -1
  103. package/dist/presenter/presenter-tally.d.ts +0 -1
  104. package/dist/presenter/presenter-tally.js +0 -1
  105. package/dist/presenter/presenter-test-helpers.d.ts +0 -1
  106. package/dist/presenter/presenter-test-helpers.js +0 -1
  107. package/dist/presenter/presenter-verbs.d.ts +0 -1
  108. package/dist/presenter/presenter-verbs.js +0 -1
  109. package/dist/presenter/presenter.d.ts +0 -1
  110. package/dist/presenter/presenter.js +0 -1
  111. package/dist/recorder/recorder-styles.d.ts +0 -1
  112. package/dist/recorder/recorder-styles.js +0 -1
  113. package/dist/recorder/recorder.d.ts +0 -1
  114. package/dist/recorder/recorder.js +0 -1
  115. package/dist/registry/adapters.d.ts +0 -1
  116. package/dist/registry/adapters.js +0 -1
  117. package/dist/registry/capabilities.d.ts +0 -1
  118. package/dist/registry/capabilities.js +0 -1
  119. package/dist/registry/commit-and-signal.d.ts +0 -1
  120. package/dist/registry/commit-and-signal.js +0 -1
  121. package/dist/registry/domains.d.ts +0 -1
  122. package/dist/registry/domains.js +0 -1
  123. package/dist/registry/emitter.d.ts +0 -1
  124. package/dist/registry/emitter.js +0 -1
  125. package/dist/registry/store-adapters.d.ts +14 -6
  126. package/dist/registry/store-adapters.js +0 -1
  127. package/dist/registry/stores.d.ts +0 -1
  128. package/dist/registry/stores.js +8 -1
  129. package/dist/reticle-presenter-helpers.d.ts +0 -1
  130. package/dist/reticle-presenter-helpers.js +0 -1
  131. package/dist/reticle.d.ts +3 -2
  132. package/dist/reticle.js +10 -31
  133. package/dist/review/annotator.d.ts +0 -1
  134. package/dist/review/annotator.js +0 -1
  135. package/dist/review/mark-anchor.d.ts +0 -1
  136. package/dist/review/mark-anchor.js +0 -1
  137. package/dist/security/serialization.d.ts +0 -1
  138. package/dist/security/serialization.js +0 -1
  139. package/dist/timers/clock.d.ts +0 -1
  140. package/dist/timers/clock.js +0 -1
  141. package/dist/timers/native-console.d.ts +0 -1
  142. package/dist/timers/native-console.js +0 -1
  143. package/dist/timers/native-timers.d.ts +0 -1
  144. package/dist/timers/native-timers.js +0 -1
  145. package/dist/transport/transport.d.ts +2 -6
  146. package/dist/transport/transport.js +93 -17
  147. package/package.json +8 -3
  148. package/dist/actions/actions-dom.d.ts.map +0 -1
  149. package/dist/actions/actions-dom.js.map +0 -1
  150. package/dist/actions/actions.d.ts.map +0 -1
  151. package/dist/actions/actions.js.map +0 -1
  152. package/dist/actions/scroll.d.ts.map +0 -1
  153. package/dist/actions/scroll.js.map +0 -1
  154. package/dist/commands/commands.d.ts.map +0 -1
  155. package/dist/commands/commands.js.map +0 -1
  156. package/dist/dom/a11y.d.ts.map +0 -1
  157. package/dist/dom/a11y.js.map +0 -1
  158. package/dist/dom/auto-anchor.d.ts.map +0 -1
  159. package/dist/dom/auto-anchor.js.map +0 -1
  160. package/dist/dom/blind-spots.d.ts.map +0 -1
  161. package/dist/dom/blind-spots.js.map +0 -1
  162. package/dist/dom/chart.d.ts.map +0 -1
  163. package/dist/dom/chart.js.map +0 -1
  164. package/dist/dom/dom-ignore.d.ts.map +0 -1
  165. package/dist/dom/dom-ignore.js.map +0 -1
  166. package/dist/dom/occlusion.d.ts.map +0 -1
  167. package/dist/dom/occlusion.js.map +0 -1
  168. package/dist/dom/query.d.ts.map +0 -1
  169. package/dist/dom/query.js.map +0 -1
  170. package/dist/dom/refs.d.ts.map +0 -1
  171. package/dist/dom/refs.js.map +0 -1
  172. package/dist/dom/snapshot.d.ts.map +0 -1
  173. package/dist/dom/snapshot.js.map +0 -1
  174. package/dist/dom/source.d.ts.map +0 -1
  175. package/dist/dom/source.js.map +0 -1
  176. package/dist/dom/theme.d.ts.map +0 -1
  177. package/dist/dom/theme.js.map +0 -1
  178. package/dist/index.d.ts.map +0 -1
  179. package/dist/index.js.map +0 -1
  180. package/dist/observers/animation.d.ts.map +0 -1
  181. package/dist/observers/animation.js.map +0 -1
  182. package/dist/observers/blind-spots.d.ts.map +0 -1
  183. package/dist/observers/blind-spots.js.map +0 -1
  184. package/dist/observers/console.d.ts.map +0 -1
  185. package/dist/observers/console.js.map +0 -1
  186. package/dist/observers/dom.d.ts.map +0 -1
  187. package/dist/observers/dom.js.map +0 -1
  188. package/dist/observers/focus.d.ts.map +0 -1
  189. package/dist/observers/focus.js.map +0 -1
  190. package/dist/observers/health.d.ts.map +0 -1
  191. package/dist/observers/health.js.map +0 -1
  192. package/dist/observers/network-body.d.ts.map +0 -1
  193. package/dist/observers/network-body.js.map +0 -1
  194. package/dist/observers/network-redact.d.ts.map +0 -1
  195. package/dist/observers/network-redact.js.map +0 -1
  196. package/dist/observers/network.d.ts.map +0 -1
  197. package/dist/observers/network.js.map +0 -1
  198. package/dist/observers/perf.d.ts.map +0 -1
  199. package/dist/observers/perf.js.map +0 -1
  200. package/dist/observers/route.d.ts.map +0 -1
  201. package/dist/observers/route.js.map +0 -1
  202. package/dist/observers/scroll.d.ts.map +0 -1
  203. package/dist/observers/scroll.js.map +0 -1
  204. package/dist/observers/state.d.ts.map +0 -1
  205. package/dist/observers/state.js.map +0 -1
  206. package/dist/observers/storage.d.ts.map +0 -1
  207. package/dist/observers/storage.js.map +0 -1
  208. package/dist/observers/types.d.ts.map +0 -1
  209. package/dist/observers/types.js.map +0 -1
  210. package/dist/patching/capture-method.d.ts.map +0 -1
  211. package/dist/patching/capture-method.js.map +0 -1
  212. package/dist/presenter/overlay.d.ts.map +0 -1
  213. package/dist/presenter/overlay.js.map +0 -1
  214. package/dist/presenter/presenter-config.d.ts.map +0 -1
  215. package/dist/presenter/presenter-config.js.map +0 -1
  216. package/dist/presenter/presenter-controls.d.ts.map +0 -1
  217. package/dist/presenter/presenter-controls.js.map +0 -1
  218. package/dist/presenter/presenter-effects.d.ts.map +0 -1
  219. package/dist/presenter/presenter-effects.js.map +0 -1
  220. package/dist/presenter/presenter-glow.d.ts.map +0 -1
  221. package/dist/presenter/presenter-glow.js.map +0 -1
  222. package/dist/presenter/presenter-log.d.ts.map +0 -1
  223. package/dist/presenter/presenter-log.js.map +0 -1
  224. package/dist/presenter/presenter-run-state.d.ts.map +0 -1
  225. package/dist/presenter/presenter-run-state.js.map +0 -1
  226. package/dist/presenter/presenter-styles.d.ts.map +0 -1
  227. package/dist/presenter/presenter-styles.js.map +0 -1
  228. package/dist/presenter/presenter-tally.d.ts.map +0 -1
  229. package/dist/presenter/presenter-tally.js.map +0 -1
  230. package/dist/presenter/presenter-test-helpers.d.ts.map +0 -1
  231. package/dist/presenter/presenter-test-helpers.js.map +0 -1
  232. package/dist/presenter/presenter-verbs.d.ts.map +0 -1
  233. package/dist/presenter/presenter-verbs.js.map +0 -1
  234. package/dist/presenter/presenter.d.ts.map +0 -1
  235. package/dist/presenter/presenter.js.map +0 -1
  236. package/dist/recorder/recorder-styles.d.ts.map +0 -1
  237. package/dist/recorder/recorder-styles.js.map +0 -1
  238. package/dist/recorder/recorder.d.ts.map +0 -1
  239. package/dist/recorder/recorder.js.map +0 -1
  240. package/dist/registry/adapters.d.ts.map +0 -1
  241. package/dist/registry/adapters.js.map +0 -1
  242. package/dist/registry/capabilities.d.ts.map +0 -1
  243. package/dist/registry/capabilities.js.map +0 -1
  244. package/dist/registry/commit-and-signal.d.ts.map +0 -1
  245. package/dist/registry/commit-and-signal.js.map +0 -1
  246. package/dist/registry/domains.d.ts.map +0 -1
  247. package/dist/registry/domains.js.map +0 -1
  248. package/dist/registry/emitter.d.ts.map +0 -1
  249. package/dist/registry/emitter.js.map +0 -1
  250. package/dist/registry/store-adapters.d.ts.map +0 -1
  251. package/dist/registry/store-adapters.js.map +0 -1
  252. package/dist/registry/stores.d.ts.map +0 -1
  253. package/dist/registry/stores.js.map +0 -1
  254. package/dist/reticle-presenter-helpers.d.ts.map +0 -1
  255. package/dist/reticle-presenter-helpers.js.map +0 -1
  256. package/dist/reticle.d.ts.map +0 -1
  257. package/dist/reticle.js.map +0 -1
  258. package/dist/review/annotator.d.ts.map +0 -1
  259. package/dist/review/annotator.js.map +0 -1
  260. package/dist/review/mark-anchor.d.ts.map +0 -1
  261. package/dist/review/mark-anchor.js.map +0 -1
  262. package/dist/security/serialization.d.ts.map +0 -1
  263. package/dist/security/serialization.js.map +0 -1
  264. package/dist/timers/clock.d.ts.map +0 -1
  265. package/dist/timers/clock.js.map +0 -1
  266. package/dist/timers/native-console.d.ts.map +0 -1
  267. package/dist/timers/native-console.js.map +0 -1
  268. package/dist/timers/native-timers.d.ts.map +0 -1
  269. package/dist/timers/native-timers.js.map +0 -1
  270. package/dist/transport/transport.d.ts.map +0 -1
  271. 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
+ }
@@ -5,4 +5,3 @@ import type { Emit, Teardown } from './types.js';
5
5
  * tab blind. Uses a native (pre-bound) timer so a frozen app clock (reticle_clock) never stalls it.
6
6
  */
7
7
  export declare function installHealth(emit: Emit): Teardown;
8
- //# sourceMappingURL=health.d.ts.map
@@ -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,5 @@
1
+ import type { Emit, Teardown } from './types.js';
2
+ export interface InstallOptions {
3
+ captureBodies: boolean;
4
+ }
5
+ export declare function installAllObservers(emit: Emit, options: InstallOptions): Teardown[];
@@ -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
@@ -5,4 +5,3 @@
5
5
  * The hash is preserved and the URL is returned byte-for-byte when nothing matched.
6
6
  */
7
7
  export declare function redactUrl(raw: string): string;
8
- //# sourceMappingURL=network-redact.d.ts.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
- changed = true;
37
+ queryChanged = true;
37
38
  }
38
39
  }
39
- newQuery = params.toString();
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
@@ -10,4 +10,3 @@ import type { Emit, Teardown } from './types.js';
10
10
  * would otherwise be install time.
11
11
  */
12
12
  export declare function installPerf(emit: Emit): Teardown;
13
- //# sourceMappingURL=perf.d.ts.map
@@ -59,4 +59,3 @@ export function installPerf(emit) {
59
59
  po.disconnect();
60
60
  };
61
61
  }
62
- //# sourceMappingURL=perf.js.map
@@ -1,4 +1,3 @@
1
1
  import type { Emit, Teardown } from './types.js';
2
2
  /** Patch the History API + listen to popstate/hashchange to emit route.change. */
3
3
  export declare function installRoute(emit: Emit): Teardown;
4
- //# sourceMappingURL=route.d.ts.map
@@ -62,4 +62,3 @@ export function installRoute(emit) {
62
62
  window.removeEventListener('hashchange', onNav);
63
63
  };
64
64
  }
65
- //# sourceMappingURL=route.js.map
@@ -1,4 +1,3 @@
1
1
  import type { Emit, Teardown } from './types.js';
2
2
  /** Observe scroll position + reveal-on-scroll for modern scroll-reactive UIs. */
3
3
  export declare function installScroll(emit: Emit): Teardown;
4
- //# sourceMappingURL=scroll.d.ts.map