@reticlehq/browser 2.13.0 → 2.14.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 -20
- package/dist/actions/actions-dom.js +0 -66
- package/dist/actions/actions-press.d.ts +10 -0
- package/dist/actions/actions-press.js +77 -0
- package/dist/actions/actions.d.ts +0 -89
- package/dist/actions/actions.js +24 -358
- package/dist/actions/appeared-text.d.ts +0 -20
- package/dist/actions/appeared-text.js +0 -36
- package/dist/actions/danger-context.d.ts +3 -0
- package/dist/actions/danger-context.js +39 -0
- package/dist/actions/scroll.d.ts +0 -9
- package/dist/actions/scroll.js +0 -7
- package/dist/actions/synthetic-input.d.ts +0 -7
- package/dist/actions/synthetic-input.js +0 -27
- package/dist/actions/value-input.d.ts +0 -30
- package/dist/actions/value-input.js +0 -33
- package/dist/commands/commands.d.ts +0 -2
- package/dist/commands/commands.js +0 -67
- package/dist/connect-options.d.ts +2 -96
- package/dist/dom/a11y.d.ts +0 -29
- package/dist/dom/a11y.js +1 -88
- package/dist/dom/auto-anchor.d.ts +0 -40
- package/dist/dom/auto-anchor.js +0 -33
- package/dist/dom/chart.d.ts +0 -61
- package/dist/dom/chart.js +0 -60
- package/dist/dom/desktop-capture.d.ts +0 -30
- package/dist/dom/desktop-capture.js +0 -72
- package/dist/dom/dom-ignore.d.ts +0 -30
- package/dist/dom/dom-ignore.js +0 -47
- package/dist/dom/occlusion.d.ts +0 -7
- package/dist/dom/occlusion.js +0 -7
- package/dist/dom/query.d.ts +0 -14
- package/dist/dom/query.js +48 -214
- package/dist/dom/realm.d.ts +0 -20
- package/dist/dom/realm.js +0 -28
- package/dist/dom/refs.d.ts +0 -76
- package/dist/dom/refs.js +2 -133
- package/dist/dom/shadow-registry.d.ts +0 -33
- package/dist/dom/shadow-registry.js +2 -72
- package/dist/dom/snapshot.d.ts +2 -43
- package/dist/dom/snapshot.js +26 -151
- package/dist/dom/source.d.ts +0 -44
- package/dist/dom/source.js +0 -45
- package/dist/dom/theme.d.ts +0 -37
- package/dist/dom/theme.js +2 -52
- package/dist/dom/virtualized.d.ts +0 -17
- package/dist/dom/virtualized.js +0 -27
- package/dist/edit-epoch.d.ts +0 -22
- package/dist/edit-epoch.js +0 -52
- package/dist/index.js +1 -23
- package/dist/observers/animation.d.ts +0 -1
- package/dist/observers/animation.js +0 -3
- package/dist/observers/blind-spots.d.ts +0 -26
- package/dist/observers/blind-spots.js +2 -61
- package/dist/observers/console.d.ts +1 -2
- package/dist/observers/console.js +41 -30
- package/dist/observers/context-open.d.ts +1 -11
- package/dist/observers/context-open.js +10 -14
- package/dist/observers/departure.d.ts +3 -0
- package/dist/observers/departure.js +52 -0
- package/dist/observers/dialogs.d.ts +2 -0
- package/dist/observers/dialogs.js +39 -0
- package/dist/observers/dom.js +0 -44
- package/dist/observers/download.d.ts +1 -2
- package/dist/observers/download.js +38 -55
- package/dist/observers/focus.d.ts +0 -6
- package/dist/observers/focus.js +0 -7
- package/dist/observers/frames.d.ts +0 -9
- package/dist/observers/frames.js +1 -28
- package/dist/observers/health.d.ts +0 -5
- package/dist/observers/health.js +1 -33
- package/dist/observers/install-all.d.ts +2 -0
- package/dist/observers/install-all.js +26 -27
- package/dist/observers/ipc.d.ts +0 -25
- package/dist/observers/ipc.js +0 -65
- package/dist/observers/navigation.d.ts +2 -0
- package/dist/observers/navigation.js +33 -0
- package/dist/observers/network-body.d.ts +5 -28
- package/dist/observers/network-body.js +11 -108
- package/dist/observers/network-redact.d.ts +1 -20
- package/dist/observers/network-redact.js +1 -80
- package/dist/observers/network-stream.d.ts +0 -21
- package/dist/observers/network-stream.js +1 -44
- package/dist/observers/network.d.ts +2 -26
- package/dist/observers/network.js +177 -294
- package/dist/observers/perf.d.ts +0 -10
- package/dist/observers/perf.js +0 -16
- package/dist/observers/route.d.ts +1 -2
- package/dist/observers/route.js +13 -19
- package/dist/observers/scroll.d.ts +0 -1
- package/dist/observers/scroll.js +0 -9
- package/dist/observers/sdk-failure.d.ts +1 -9
- package/dist/observers/sdk-failure.js +1 -30
- package/dist/observers/state.d.ts +1 -11
- package/dist/observers/state.js +13 -41
- package/dist/observers/storage.d.ts +0 -14
- package/dist/observers/storage.js +10 -34
- package/dist/observers/types.d.ts +0 -15
- package/dist/observers/types.js +0 -14
- package/dist/patching/capture-method.d.ts +0 -24
- package/dist/patching/capture-method.js +0 -26
- package/dist/presenter/overlay.d.ts +0 -1
- package/dist/presenter/overlay.js +0 -1
- package/dist/presenter/presenter-brand.d.ts +0 -19
- package/dist/presenter/presenter-brand.js +0 -21
- package/dist/presenter/presenter-config.d.ts +1 -105
- package/dist/presenter/presenter-config.js +2 -91
- package/dist/presenter/presenter-controls.d.ts +0 -29
- package/dist/presenter/presenter-controls.js +3 -131
- package/dist/presenter/presenter-dock-layout.d.ts +0 -3
- package/dist/presenter/presenter-dock-layout.js +0 -4
- package/dist/presenter/presenter-drag.d.ts +2 -24
- package/dist/presenter/presenter-drag.js +0 -26
- package/dist/presenter/presenter-effects.d.ts +0 -9
- package/dist/presenter/presenter-effects.js +0 -9
- package/dist/presenter/presenter-glow.d.ts +0 -21
- package/dist/presenter/presenter-glow.js +1 -27
- package/dist/presenter/presenter-heroicons-data.d.ts +0 -8
- package/dist/presenter/presenter-heroicons-data.js +0 -8
- package/dist/presenter/presenter-hud-chrome.d.ts +0 -10
- package/dist/presenter/presenter-hud-chrome.js +0 -10
- package/dist/presenter/presenter-icons.d.ts +0 -6
- package/dist/presenter/presenter-icons.js +0 -6
- package/dist/presenter/presenter-log.d.ts +0 -38
- package/dist/presenter/presenter-log.js +1 -45
- package/dist/presenter/presenter-report-copy.d.ts +5 -66
- package/dist/presenter/presenter-report-copy.js +5 -66
- package/dist/presenter/presenter-report-styles.d.ts +0 -6
- package/dist/presenter/presenter-report-styles.js +6 -6
- package/dist/presenter/presenter-report.d.ts +4 -7
- package/dist/presenter/presenter-report.js +18 -76
- package/dist/presenter/presenter-run-state.d.ts +0 -7
- package/dist/presenter/presenter-run-state.js +0 -5
- package/dist/presenter/presenter-settings-styles.d.ts +0 -1
- package/dist/presenter/presenter-settings-styles.js +0 -1
- package/dist/presenter/presenter-settings.d.ts +1 -23
- package/dist/presenter/presenter-settings.js +1 -31
- package/dist/presenter/presenter-shell-styles.js +2 -5
- package/dist/presenter/presenter-shell.d.ts +2 -18
- package/dist/presenter/presenter-shell.js +1 -73
- package/dist/presenter/presenter-styles.d.ts +0 -9
- package/dist/presenter/presenter-styles.js +0 -9
- package/dist/presenter/presenter-tally.d.ts +0 -5
- package/dist/presenter/presenter-tally.js +0 -4
- package/dist/presenter/presenter-test-helpers.d.ts +0 -2
- package/dist/presenter/presenter-test-helpers.js +0 -2
- package/dist/presenter/presenter-verbs.js +0 -7
- package/dist/presenter/presenter-workspace.d.ts +0 -4
- package/dist/presenter/presenter-workspace.js +1 -8
- package/dist/presenter/presenter.d.ts +0 -60
- package/dist/presenter/presenter.js +4 -118
- package/dist/recorder/recorder-styles.d.ts +0 -6
- package/dist/recorder/recorder-styles.js +0 -6
- package/dist/recorder/recorder.d.ts +0 -34
- package/dist/recorder/recorder.js +0 -49
- package/dist/registry/adapters.d.ts +0 -10
- package/dist/registry/adapters.js +0 -10
- package/dist/registry/auto-testids.d.ts +0 -19
- package/dist/registry/auto-testids.js +1 -21
- package/dist/registry/capabilities.d.ts +0 -31
- package/dist/registry/capabilities.js +1 -46
- package/dist/registry/commit-and-signal.d.ts +0 -6
- package/dist/registry/commit-and-signal.js +1 -7
- package/dist/registry/domains.d.ts +0 -8
- package/dist/registry/domains.js +0 -7
- package/dist/registry/emitter.d.ts +0 -14
- package/dist/registry/emitter.js +0 -10
- package/dist/registry/store-adapters.d.ts +0 -186
- package/dist/registry/store-adapters.js +0 -163
- package/dist/registry/stores.d.ts +0 -33
- package/dist/registry/stores.js +0 -59
- package/dist/reticle-presenter-helpers.d.ts +0 -13
- package/dist/reticle-presenter-helpers.js +0 -14
- package/dist/reticle.d.ts +1 -52
- package/dist/reticle.js +20 -145
- package/dist/review/annotator-styles.js +0 -7
- package/dist/review/annotator.d.ts +2 -13
- package/dist/review/annotator.js +0 -40
- package/dist/review/mark-anchor.d.ts +0 -11
- package/dist/review/mark-anchor.js +0 -8
- package/dist/security/serialization.d.ts +0 -20
- package/dist/security/serialization.js +1 -66
- package/dist/session-continuity.d.ts +0 -16
- package/dist/session-continuity.js +1 -20
- package/dist/timers/clock.d.ts +1 -14
- package/dist/timers/clock.js +59 -60
- package/dist/timers/native-console.js +0 -4
- package/dist/timers/native-timers.d.ts +0 -25
- package/dist/timers/native-timers.js +3 -33
- package/dist/transport/transport.d.ts +0 -51
- package/dist/transport/transport.js +3 -129
- package/dist/transport/unreachable-message.d.ts +0 -17
- package/dist/transport/unreachable-message.js +0 -17
- package/dist/util/captured-method.d.ts +0 -35
- package/dist/util/captured-method.js +0 -35
- package/package.json +2 -2
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { BlindSpotKind, EventType, RETICLE_WS_PATH, StreamTransport, StreamDirection, } from '@reticlehq/core';
|
|
2
2
|
import { captureMethod } from '../patching/capture-method.js';
|
|
3
|
-
import {
|
|
3
|
+
import { observeSafely, observeValue } from './types.js';
|
|
4
|
+
import { isCapturableType, projectBody, projectErrorBody, shouldCaptureErrorBody, withBodyDeadline, } from './network-body.js';
|
|
4
5
|
import { redactUrl, netUrlFields } from './network-redact.js';
|
|
5
6
|
import { watchStreamedBody } from './network-stream.js';
|
|
6
7
|
import { requireCapturedMethod } from '../util/captured-method.js';
|
|
7
|
-
// Redaction moved to its own cohesive module (network.ts is at its line cap); re-exported so callers
|
|
8
|
-
// and the existing test suite keep importing it from here.
|
|
9
8
|
export { redactUrl, netUrlFields };
|
|
10
|
-
/** The byte size of a binary frame (ArrayBuffer / Blob / typed-array view), or undefined if unknown. */
|
|
11
9
|
function binaryFrameBytes(data) {
|
|
12
10
|
if (data instanceof ArrayBuffer)
|
|
13
11
|
return data.byteLength;
|
|
@@ -17,8 +15,6 @@ function binaryFrameBytes(data) {
|
|
|
17
15
|
return data.byteLength;
|
|
18
16
|
return undefined;
|
|
19
17
|
}
|
|
20
|
-
/** Project one SSE/WebSocket frame: byte size + shape; the (capped, redacted) payload only when body
|
|
21
|
-
* capture is on and it is a string frame. Binary frames report their byte size, not just a bare type. */
|
|
22
18
|
function frameFields(data, captureBodies) {
|
|
23
19
|
if (typeof data !== 'string') {
|
|
24
20
|
const bytes = binaryFrameBytes(data);
|
|
@@ -35,11 +31,6 @@ function frameFields(data, captureBodies) {
|
|
|
35
31
|
}
|
|
36
32
|
return out;
|
|
37
33
|
}
|
|
38
|
-
/**
|
|
39
|
-
* The request body for the transcript. Plain strings and URLSearchParams are captured (redacted, capped);
|
|
40
|
-
* FormData/Blob/ArrayBuffer/stream aren't text so they get a `requestBodyType` marker (the agent still
|
|
41
|
-
* learns a body existed) rather than being silently dropped. Shared by the fetch and XHR paths.
|
|
42
|
-
*/
|
|
43
34
|
function projectRequestBody(body, captureBodies) {
|
|
44
35
|
if (!captureBodies)
|
|
45
36
|
return {};
|
|
@@ -61,28 +52,9 @@ function projectRequestBody(body, captureBodies) {
|
|
|
61
52
|
const { body: out, truncated } = projectBody(text, contentType);
|
|
62
53
|
return truncated ? { requestBody: out, requestBodyTruncated: true } : { requestBody: out };
|
|
63
54
|
}
|
|
64
|
-
/**
|
|
65
|
-
* Did this response succeed? A REDIRECT DID.
|
|
66
|
-
*
|
|
67
|
-
* The one rule both transports must share. `Response.ok` is true only for 200-299, so reading it
|
|
68
|
-
* straight through stamped every 3xx as a failed request, while the XHR path computed the same field
|
|
69
|
-
* itself and called the identical status a success. Downstream, `ok` is authoritative when present —
|
|
70
|
-
* so a POST-redirect-GET login, the first flow anyone verifies in an app with auth, produced a
|
|
71
|
-
* `ui-advanced-request-failed` contradiction citing its own success path, and the agent had to
|
|
72
|
-
* overrule the verdict by hand. A verdict that gets manually overruled is not deciding anything.
|
|
73
|
-
*
|
|
74
|
-
* 3xx is normal successful navigation. An opaque (no-cors) response reports status 0 and is correctly
|
|
75
|
-
* excluded here, exactly as `Response.ok` excluded it.
|
|
76
|
-
*/
|
|
77
55
|
function statusIsOk(status) {
|
|
78
56
|
return status >= 200 && status < 400;
|
|
79
57
|
}
|
|
80
|
-
/**
|
|
81
|
-
* Response metadata that needs no body capture: HTTP status text, content-type, and byte size (from
|
|
82
|
-
* content-length when the server sent it). Lets an agent tell an HTML error page served as 200 from
|
|
83
|
-
* real JSON, and spot empty/oversized responses. Fields are omitted when absent so a clean call stays
|
|
84
|
-
* token-flat.
|
|
85
|
-
*/
|
|
86
58
|
function netResponseMeta(statusText, contentType, contentLength) {
|
|
87
59
|
const out = {};
|
|
88
60
|
if (statusText !== '')
|
|
@@ -108,41 +80,22 @@ function methodOf(input, init) {
|
|
|
108
80
|
return input.method.toUpperCase();
|
|
109
81
|
return 'GET';
|
|
110
82
|
}
|
|
111
|
-
/**
|
|
112
|
-
* The first app-code frame that fired a request — the in-page answer to a CDP initiator, as file:line.
|
|
113
|
-
* Captured from a fresh Error.stack at call time, skipping Reticle's own wrapper frames. Feeds the
|
|
114
|
-
* causal chain (which code path made this request). Capped; undefined when no stack is available.
|
|
115
|
-
* ponytail: one stack unwind per request — cheap next to fetch itself; revisit only if a profiler flags it.
|
|
116
|
-
*/
|
|
117
|
-
/**
|
|
118
|
-
* Frames to skip: Reticle's own wrappers + engine-internal frames with no app source location.
|
|
119
|
-
* Anchored to Reticle's actual module paths (`@reticlehq/…`, the SDK's own `.ts` files) — a bare
|
|
120
|
-
* `reticle` alternative matched ANY app whose bundle URL merely contained the word (including this
|
|
121
|
-
* repo's own dogfood fixtures served under `/reticle/`), silently dropping their initiatorStack.
|
|
122
|
-
*/
|
|
123
83
|
const NON_APP_FRAME = /@reticlehq|reticle\.ts|network\.ts|transport\.ts|<anonymous>|new Promise|node:internal/i;
|
|
124
|
-
|
|
84
|
+
const DEPENDENCY_FRAME = /node_modules|\/\.vite\/deps\//i;
|
|
125
85
|
export function firstAppFrame(stack) {
|
|
126
86
|
if (stack === undefined)
|
|
127
87
|
return undefined;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
return undefined;
|
|
88
|
+
const frames = stack
|
|
89
|
+
.split('\n')
|
|
90
|
+
.slice(1)
|
|
91
|
+
.map((line) => line.trim())
|
|
92
|
+
.filter((line) => line.length > 0 && !NON_APP_FRAME.test(line));
|
|
93
|
+
const app = frames.find((line) => !DEPENDENCY_FRAME.test(line));
|
|
94
|
+
return (app ?? frames[0])?.slice(0, 300);
|
|
137
95
|
}
|
|
138
96
|
function initiatorFrame() {
|
|
139
97
|
return firstAppFrame(new Error().stack);
|
|
140
98
|
}
|
|
141
|
-
/**
|
|
142
|
-
* Pure: derive TTFB + transfer size from a PerformanceResourceTiming entry. TTFB = responseStart -
|
|
143
|
-
* requestStart; cross-origin responses zero those (Timing-Allow-Origin), so we omit rather than report a
|
|
144
|
-
* bogus 0. Exported for unit testing (jsdom doesn't post real resource entries).
|
|
145
|
-
*/
|
|
146
99
|
export function extractTiming(entry) {
|
|
147
100
|
if (entry === undefined)
|
|
148
101
|
return {};
|
|
@@ -154,7 +107,6 @@ export function extractTiming(entry) {
|
|
|
154
107
|
timing.transferSize = entry.transferSize;
|
|
155
108
|
return timing;
|
|
156
109
|
}
|
|
157
|
-
/** Best-effort lookup of the most-recent resource-timing entry for a URL. */
|
|
158
110
|
function resourceTiming(rawUrl) {
|
|
159
111
|
try {
|
|
160
112
|
const entries = performance.getEntriesByName(rawUrl, 'resource');
|
|
@@ -164,16 +116,6 @@ function resourceTiming(rawUrl) {
|
|
|
164
116
|
return {};
|
|
165
117
|
}
|
|
166
118
|
}
|
|
167
|
-
/** Patch fetch + XMLHttpRequest to emit net.request events. Fully reversible. */
|
|
168
|
-
/**
|
|
169
|
-
* True for Reticle's OWN bridge connection, which must never be instrumented.
|
|
170
|
-
*
|
|
171
|
-
* The transport resolves `WebSocket` at call time, so a reconnect after instrumentation constructs a
|
|
172
|
-
* PATCHED socket. From then on emitting an event calls transport.send -> the patched send emits a
|
|
173
|
-
* NET_STREAM event -> which calls transport.send again, until the stack blows and the page is dead.
|
|
174
|
-
* Keyed on the bridge PATH rather than the host: an app's own socket on the same origin must still be
|
|
175
|
-
* observed, otherwise this guard would blind the stream category it exists to protect.
|
|
176
|
-
*/
|
|
177
119
|
function isBridgeSocket(url) {
|
|
178
120
|
try {
|
|
179
121
|
return new URL(url, location.href).pathname === RETICLE_WS_PATH;
|
|
@@ -182,21 +124,6 @@ function isBridgeSocket(url) {
|
|
|
182
124
|
return url.includes(RETICLE_WS_PATH);
|
|
183
125
|
}
|
|
184
126
|
}
|
|
185
|
-
/**
|
|
186
|
-
* Whether a function is the platform's own fetch rather than someone's wrapper.
|
|
187
|
-
*
|
|
188
|
-
* `Function.prototype.toString` on a built-in yields "[native code]"; a JS wrapper yields its source.
|
|
189
|
-
* Called off the prototype deliberately, so a wrapper cannot hide behind its own `toString`.
|
|
190
|
-
*
|
|
191
|
-
* Two known limits, both stated in tests rather than left to be discovered:
|
|
192
|
-
* - a BOUND wrapper (`window.fetch = mine.bind(x)`) also reports "[native code]", so it is missed.
|
|
193
|
-
* Nothing in the platform separates the two.
|
|
194
|
-
* - a POLYFILLED fetch reads as wrapped and IS reported — correctly, since a polyfill is a layer we
|
|
195
|
-
* cannot see through, though it will look like a false positive to anyone triaging it.
|
|
196
|
-
*
|
|
197
|
-
* Both failure modes are safe: we under-report on the first and over-report on the second, and
|
|
198
|
-
* over-reporting a coverage caveat is the direction this library errs in everywhere else.
|
|
199
|
-
*/
|
|
200
127
|
function isNativeFetch(fn) {
|
|
201
128
|
try {
|
|
202
129
|
return Function.prototype.toString.call(fn).includes('native code');
|
|
@@ -205,40 +132,20 @@ function isNativeFetch(fn) {
|
|
|
205
132
|
return false;
|
|
206
133
|
}
|
|
207
134
|
}
|
|
208
|
-
/** Wrappers this module installed, so a re-install does not report itself as a foreign wrapper. */
|
|
209
135
|
const OURS = new WeakSet();
|
|
210
136
|
export function installNetwork(emit, opts = {}) {
|
|
211
137
|
const captureBodies = true === opts.captureBodies;
|
|
138
|
+
const captureErrorBodies = false !== opts.captureErrorBodies;
|
|
212
139
|
const reinterpret = opts.reinterpret;
|
|
213
|
-
// Keep the true original for teardown identity, plus a window-bound copy to invoke
|
|
214
|
-
// (fetch throws "Illegal invocation" if called with the wrong `this`).
|
|
215
140
|
const origFetch = requireCapturedMethod(window, 'fetch');
|
|
216
141
|
const callFetch = origFetch.bind(window);
|
|
217
|
-
// Declare it if we are not the first to wrap fetch.
|
|
218
|
-
//
|
|
219
|
-
// Wrappers chain outermost-first, and we record `init.body` when OUR wrapper runs — so anything
|
|
220
|
-
// installed EARLIER sits below us and mutates the request after we have read it. That is a real
|
|
221
|
-
// blind spot (an auth/analytics interceptor started before connect(), a polyfill, a service worker
|
|
222
|
-
// is worse still) and it is not fixable from in here: there is no "patch last" primitive, and
|
|
223
|
-
// racing for outermost loses to whatever loads after us.
|
|
224
|
-
//
|
|
225
|
-
// So it is reported rather than hidden, on the same contract as the cross-origin-iframe sensor —
|
|
226
|
-
// say what we cannot see, so a green verdict never implies we saw it. `isOurs` keeps a re-install
|
|
227
|
-
// from blaming the app for our own wrapper.
|
|
228
142
|
if (!isNativeFetch(origFetch) && !OURS.has(origFetch)) {
|
|
229
143
|
emit(EventType.BLIND_SPOT, { kind: BlindSpotKind.WRAPPED_NETWORK, count: 1 });
|
|
230
144
|
}
|
|
231
|
-
// Correlation id so a NET_PENDING (emitted at request START) can be matched to its
|
|
232
|
-
// NET_REQUEST completion. A request that never completes leaves an unmatched NET_PENDING —
|
|
233
|
-
// that is how a hung/in-flight request becomes observable (it never resolves, so the old
|
|
234
|
-
// completion-only emit saw nothing).
|
|
235
145
|
let seq = 0;
|
|
236
146
|
const nextId = () => `n${++seq}`;
|
|
237
147
|
window.fetch = async (input, init) => {
|
|
238
148
|
const rawUrl = urlOf(input);
|
|
239
|
-
// Reticle's own call: pass it straight through, observed by nobody. Emitting even the PENDING
|
|
240
|
-
// half would be wrong twice over — it lands in the agent's network evidence, and `settle` would
|
|
241
|
-
// wait on the SDK's own screenshot as if the app had a request in flight.
|
|
242
149
|
if (true === opts.ignore?.(rawUrl))
|
|
243
150
|
return callFetch(input, init);
|
|
244
151
|
const id = nextId();
|
|
@@ -246,86 +153,75 @@ export function installNetwork(emit, opts = {}) {
|
|
|
246
153
|
const method = methodOf(input, init);
|
|
247
154
|
const urlFields = netUrlFields(rawUrl);
|
|
248
155
|
const url = urlFields.url;
|
|
249
|
-
const initiatorStack = initiatorFrame();
|
|
156
|
+
const initiatorStack = observeValue(() => initiatorFrame());
|
|
250
157
|
const initiatorFields = initiatorStack === undefined ? {} : { initiatorStack };
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
158
|
+
observeSafely(() => {
|
|
159
|
+
emit(EventType.NET_PENDING, {
|
|
160
|
+
id,
|
|
161
|
+
method,
|
|
162
|
+
...urlFields,
|
|
163
|
+
initiator: 'fetch',
|
|
164
|
+
...initiatorFields,
|
|
165
|
+
});
|
|
257
166
|
});
|
|
258
167
|
try {
|
|
259
168
|
const res = await callFetch(input, init);
|
|
260
|
-
// The app's fetch resolves HERE — at headers — like a native fetch. durationMs is measured to
|
|
261
|
-
// headers-received, so it stays honest whether or not we read the body.
|
|
262
169
|
const headersAt = performance.now();
|
|
263
|
-
const contentType = res.headers.get('content-type');
|
|
264
|
-
|
|
265
|
-
|
|
170
|
+
const contentType = observeValue(() => res.headers.get('content-type')) ?? null;
|
|
171
|
+
observeSafely(() => {
|
|
172
|
+
watchStreamedBody(emit, res, id, url, contentType, res.headers.get('content-length'));
|
|
173
|
+
});
|
|
266
174
|
reportedNetUrls.add(rawUrl);
|
|
267
175
|
const emitRequest = (responseBodyFields) => {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
176
|
+
observeSafely(() => {
|
|
177
|
+
emit(EventType.NET_REQUEST, {
|
|
178
|
+
id,
|
|
179
|
+
method,
|
|
180
|
+
...urlFields,
|
|
181
|
+
status: res.status,
|
|
182
|
+
ok: statusIsOk(res.status),
|
|
183
|
+
durationMs: Math.round(headersAt - start),
|
|
184
|
+
initiator: 'fetch',
|
|
185
|
+
...initiatorFields,
|
|
186
|
+
...resourceTiming(rawUrl),
|
|
187
|
+
...netResponseMeta(res.statusText, contentType, res.headers.get('content-length')),
|
|
188
|
+
...projectRequestBody(init?.body, captureBodies),
|
|
189
|
+
...responseBodyFields,
|
|
190
|
+
...(reinterpret?.(url, (name) => res.headers.get(name)) ?? {}),
|
|
191
|
+
});
|
|
284
192
|
});
|
|
285
193
|
};
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
// arbitrarily long). Clone synchronously so the app's stream is untouched, then read + emit from
|
|
289
|
-
// a DETACHED promise — the app already has res, so our bounded read is invisible to it. Without
|
|
290
|
-
// body capture (the DEFAULT) we emit synchronously below, exactly as before: no latency, no
|
|
291
|
-
// deferral, NET_REQUEST in order.
|
|
292
|
-
//
|
|
293
|
-
// Tradeoff, opt-in path only: under captureBodies the NET_REQUEST for THIS call lands after the
|
|
294
|
-
// bounded body read (≤ the deadline), so it can arrive out of order vs a later request's
|
|
295
|
-
// NET_PENDING, and a BARE `assert { net, count }` fired the instant the app's fetch resolves may
|
|
296
|
-
// see the count not-yet-incremented. Use `wait_for` for count/settle assertions when body
|
|
297
|
-
// capture is on (it already polls until the event lands). The default path has neither caveat.
|
|
194
|
+
const wantErrorBody = shouldCaptureErrorBody(res.status, captureErrorBodies);
|
|
195
|
+
if ((captureBodies || wantErrorBody) && isCapturableType(contentType)) {
|
|
298
196
|
let clone;
|
|
299
197
|
try {
|
|
300
198
|
clone = res.clone();
|
|
301
199
|
}
|
|
302
200
|
catch {
|
|
303
|
-
/* already consumed/locked — emit the envelope with no body */
|
|
304
201
|
}
|
|
305
|
-
// The whole IIFE is guarded: emit is the SDK's already-try/catch'd sink today, but a detached
|
|
306
|
-
// promise must never be able to surface an unhandledrejection into the host page even if a
|
|
307
|
-
// future caller passes a throwing emit. A body we can't read is simply dropped.
|
|
308
202
|
void (async () => {
|
|
309
203
|
let responseBodyFields = {};
|
|
310
204
|
if (clone !== undefined) {
|
|
311
205
|
try {
|
|
312
206
|
const text = await withBodyDeadline(clone.text());
|
|
313
207
|
if (text !== undefined) {
|
|
314
|
-
const { body, truncated } =
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
208
|
+
const { body, truncated } = captureBodies
|
|
209
|
+
? projectBody(text, contentType)
|
|
210
|
+
: projectErrorBody(text, contentType);
|
|
211
|
+
responseBodyFields = {
|
|
212
|
+
responseBody: body,
|
|
213
|
+
...(truncated ? { responseBodyTruncated: true } : {}),
|
|
214
|
+
...(captureBodies ? {} : { responseBodyReason: 'error-status' }),
|
|
215
|
+
};
|
|
318
216
|
}
|
|
319
217
|
}
|
|
320
218
|
catch {
|
|
321
|
-
/* body not readable — skip, keep the envelope */
|
|
322
219
|
}
|
|
323
220
|
}
|
|
324
221
|
try {
|
|
325
222
|
emitRequest(responseBodyFields);
|
|
326
223
|
}
|
|
327
224
|
catch {
|
|
328
|
-
/* observation is best-effort; never reject into the page */
|
|
329
225
|
}
|
|
330
226
|
})().catch(() => undefined);
|
|
331
227
|
}
|
|
@@ -335,22 +231,22 @@ export function installNetwork(emit, opts = {}) {
|
|
|
335
231
|
return res;
|
|
336
232
|
}
|
|
337
233
|
catch (error) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
234
|
+
observeSafely(() => {
|
|
235
|
+
emit(EventType.NET_REQUEST, {
|
|
236
|
+
id,
|
|
237
|
+
method,
|
|
238
|
+
url,
|
|
239
|
+
status: 0,
|
|
240
|
+
ok: false,
|
|
241
|
+
error: error instanceof Error ? error.message : String(error),
|
|
242
|
+
durationMs: Math.round(performance.now() - start),
|
|
243
|
+
initiator: 'fetch',
|
|
244
|
+
...initiatorFields,
|
|
245
|
+
});
|
|
348
246
|
});
|
|
349
247
|
throw error;
|
|
350
248
|
}
|
|
351
249
|
};
|
|
352
|
-
// Remember it, so a later install recognises our own wrapper instead of reporting the app.
|
|
353
|
-
// Our own wrapper, read the same way — it is a value we stored a line ago, not a method call.
|
|
354
250
|
const patchedFetch = requireCapturedMethod(window, 'fetch');
|
|
355
251
|
OURS.add(patchedFetch);
|
|
356
252
|
const meta = new WeakMap();
|
|
@@ -359,78 +255,84 @@ export function installNetwork(emit, opts = {}) {
|
|
|
359
255
|
const origSend = captureMethod(proto, 'send');
|
|
360
256
|
const callOpen = origOpen;
|
|
361
257
|
proto.open = function (method, url, ...rest) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
258
|
+
observeSafely(() => {
|
|
259
|
+
meta.set(this, {
|
|
260
|
+
id: nextId(),
|
|
261
|
+
method: method.toUpperCase(),
|
|
262
|
+
url: redactUrl(String(url)),
|
|
263
|
+
rawUrl: String(url),
|
|
264
|
+
start: 0,
|
|
265
|
+
});
|
|
368
266
|
});
|
|
369
267
|
callOpen.call(this, method, url, ...rest);
|
|
370
268
|
};
|
|
371
269
|
const patchedOpen = captureMethod(proto, 'open');
|
|
372
|
-
// A reused XHR calls send repeatedly; attach the completion listener ONCE per instance and read the
|
|
373
|
-
// request identity from `meta` at fire time. Adding a fresh closure each send would leave stale
|
|
374
|
-
// listeners that re-fire on later completions, emitting duplicate, mislabeled events.
|
|
375
270
|
const listenerAttached = new WeakSet();
|
|
376
271
|
proto.send = function (body) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
m
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (!listenerAttached.has(this)) {
|
|
391
|
-
listenerAttached.add(this);
|
|
392
|
-
this.addEventListener('loadend', () => {
|
|
393
|
-
const cur = meta.get(this);
|
|
394
|
-
if (cur === undefined)
|
|
395
|
-
return;
|
|
396
|
-
reportedNetUrls.add(cur.rawUrl);
|
|
397
|
-
const xhrContentType = this.getResponseHeader('content-type');
|
|
398
|
-
let responseBodyFields = {};
|
|
399
|
-
// responseText throws unless responseType is '' or 'text' — guard before reading.
|
|
400
|
-
const textReadable = '' === this.responseType || 'text' === this.responseType;
|
|
401
|
-
if (captureBodies && textReadable && isCapturableType(xhrContentType)) {
|
|
402
|
-
try {
|
|
403
|
-
const { body: rb, truncated } = projectBody(this.responseText, xhrContentType);
|
|
404
|
-
responseBodyFields = truncated
|
|
405
|
-
? { responseBody: rb, responseBodyTruncated: true }
|
|
406
|
-
: { responseBody: rb };
|
|
407
|
-
}
|
|
408
|
-
catch {
|
|
409
|
-
/* unreadable body — skip */
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
emit(EventType.NET_REQUEST, {
|
|
413
|
-
id: cur.id,
|
|
414
|
-
method: cur.method,
|
|
415
|
-
...netUrlFields(cur.rawUrl),
|
|
416
|
-
status: this.status,
|
|
417
|
-
ok: statusIsOk(this.status),
|
|
418
|
-
durationMs: Math.round(performance.now() - cur.start),
|
|
419
|
-
initiator: 'xhr',
|
|
420
|
-
...(cur.initiatorStack === undefined ? {} : { initiatorStack: cur.initiatorStack }),
|
|
421
|
-
...resourceTiming(cur.rawUrl),
|
|
422
|
-
...netResponseMeta(this.statusText, xhrContentType, this.getResponseHeader('content-length')),
|
|
423
|
-
...projectRequestBody(cur.reqBody, captureBodies),
|
|
424
|
-
...responseBodyFields,
|
|
425
|
-
});
|
|
272
|
+
observeSafely(() => {
|
|
273
|
+
const m = meta.get(this);
|
|
274
|
+
if (m !== undefined) {
|
|
275
|
+
m.start = performance.now();
|
|
276
|
+
m.reqBody = body ?? null;
|
|
277
|
+
m.initiatorStack = initiatorFrame();
|
|
278
|
+
const initiatorFields = m.initiatorStack === undefined ? {} : { initiatorStack: m.initiatorStack };
|
|
279
|
+
emit(EventType.NET_PENDING, {
|
|
280
|
+
id: m.id,
|
|
281
|
+
method: m.method,
|
|
282
|
+
...netUrlFields(m.rawUrl),
|
|
283
|
+
initiator: 'xhr',
|
|
284
|
+
...initiatorFields,
|
|
426
285
|
});
|
|
286
|
+
if (!listenerAttached.has(this)) {
|
|
287
|
+
listenerAttached.add(this);
|
|
288
|
+
this.addEventListener('loadend', () => {
|
|
289
|
+
observeSafely(() => {
|
|
290
|
+
const cur = meta.get(this);
|
|
291
|
+
if (cur === undefined)
|
|
292
|
+
return;
|
|
293
|
+
reportedNetUrls.add(cur.rawUrl);
|
|
294
|
+
const xhrContentType = this.getResponseHeader('content-type');
|
|
295
|
+
let responseBodyFields = {};
|
|
296
|
+
const textReadable = '' === this.responseType || 'text' === this.responseType;
|
|
297
|
+
const wantXhrErrorBody = shouldCaptureErrorBody(this.status, captureErrorBodies);
|
|
298
|
+
if ((captureBodies || wantXhrErrorBody) &&
|
|
299
|
+
textReadable &&
|
|
300
|
+
isCapturableType(xhrContentType)) {
|
|
301
|
+
try {
|
|
302
|
+
const { body: rb, truncated } = captureBodies
|
|
303
|
+
? projectBody(this.responseText, xhrContentType)
|
|
304
|
+
: projectErrorBody(this.responseText, xhrContentType);
|
|
305
|
+
responseBodyFields = {
|
|
306
|
+
responseBody: rb,
|
|
307
|
+
...(truncated ? { responseBodyTruncated: true } : {}),
|
|
308
|
+
...(captureBodies ? {} : { responseBodyReason: 'error-status' }),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
emit(EventType.NET_REQUEST, {
|
|
315
|
+
id: cur.id,
|
|
316
|
+
method: cur.method,
|
|
317
|
+
...netUrlFields(cur.rawUrl),
|
|
318
|
+
status: this.status,
|
|
319
|
+
ok: statusIsOk(this.status),
|
|
320
|
+
durationMs: Math.round(performance.now() - cur.start),
|
|
321
|
+
initiator: 'xhr',
|
|
322
|
+
...(cur.initiatorStack === undefined ? {} : { initiatorStack: cur.initiatorStack }),
|
|
323
|
+
...resourceTiming(cur.rawUrl),
|
|
324
|
+
...netResponseMeta(this.statusText, xhrContentType, this.getResponseHeader('content-length')),
|
|
325
|
+
...projectRequestBody(cur.reqBody, captureBodies),
|
|
326
|
+
...responseBodyFields,
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
}
|
|
427
331
|
}
|
|
428
|
-
}
|
|
332
|
+
});
|
|
429
333
|
origSend.call(this, body ?? null);
|
|
430
334
|
};
|
|
431
335
|
const patchedSend = captureMethod(proto, 'send');
|
|
432
|
-
// SSE + WebSocket frame capture — gated behind body capture, since a chatty stream is the
|
|
433
|
-
// high-volume case. Subclass the native constructors so the app's own usage is unchanged.
|
|
434
336
|
const origEventSource = window.EventSource;
|
|
435
337
|
const origWebSocket = window.WebSocket;
|
|
436
338
|
let patchedEventSource;
|
|
@@ -439,18 +341,22 @@ export function installNetwork(emit, opts = {}) {
|
|
|
439
341
|
window.EventSource = class extends origEventSource {
|
|
440
342
|
constructor(u, init) {
|
|
441
343
|
super(u, init);
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
transport: StreamTransport.SSE,
|
|
445
|
-
direction: StreamDirection.OPEN,
|
|
446
|
-
...urlFields,
|
|
447
|
-
});
|
|
448
|
-
this.addEventListener('message', (ev) => {
|
|
344
|
+
observeSafely(() => {
|
|
345
|
+
const urlFields = netUrlFields(String(u));
|
|
449
346
|
emit(EventType.NET_STREAM, {
|
|
450
347
|
transport: StreamTransport.SSE,
|
|
451
|
-
direction: StreamDirection.
|
|
348
|
+
direction: StreamDirection.OPEN,
|
|
452
349
|
...urlFields,
|
|
453
|
-
|
|
350
|
+
});
|
|
351
|
+
this.addEventListener('message', (ev) => {
|
|
352
|
+
observeSafely(() => {
|
|
353
|
+
emit(EventType.NET_STREAM, {
|
|
354
|
+
transport: StreamTransport.SSE,
|
|
355
|
+
direction: StreamDirection.IN,
|
|
356
|
+
...urlFields,
|
|
357
|
+
...frameFields(ev.data, captureBodies),
|
|
358
|
+
});
|
|
359
|
+
});
|
|
454
360
|
});
|
|
455
361
|
});
|
|
456
362
|
}
|
|
@@ -459,89 +365,75 @@ export function installNetwork(emit, opts = {}) {
|
|
|
459
365
|
}
|
|
460
366
|
if (captureBodies && 'function' === typeof origWebSocket) {
|
|
461
367
|
window.WebSocket = class extends origWebSocket {
|
|
462
|
-
/** Reticle's own bridge socket is never observed — see isBridgeSocket. */
|
|
463
368
|
#isBridge;
|
|
464
369
|
constructor(u, protocols) {
|
|
465
370
|
super(u, protocols);
|
|
466
371
|
this.#isBridge = isBridgeSocket(String(u));
|
|
467
372
|
if (this.#isBridge)
|
|
468
373
|
return;
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
transport: StreamTransport.WS,
|
|
472
|
-
direction: StreamDirection.OPEN,
|
|
473
|
-
...urlFields,
|
|
474
|
-
});
|
|
475
|
-
this.addEventListener('message', (ev) => {
|
|
374
|
+
observeSafely(() => {
|
|
375
|
+
const urlFields = netUrlFields(String(u));
|
|
476
376
|
emit(EventType.NET_STREAM, {
|
|
477
377
|
transport: StreamTransport.WS,
|
|
478
|
-
direction: StreamDirection.
|
|
378
|
+
direction: StreamDirection.OPEN,
|
|
479
379
|
...urlFields,
|
|
480
|
-
|
|
380
|
+
});
|
|
381
|
+
this.addEventListener('message', (ev) => {
|
|
382
|
+
observeSafely(() => {
|
|
383
|
+
emit(EventType.NET_STREAM, {
|
|
384
|
+
transport: StreamTransport.WS,
|
|
385
|
+
direction: StreamDirection.IN,
|
|
386
|
+
...urlFields,
|
|
387
|
+
...frameFields(ev.data, captureBodies),
|
|
388
|
+
});
|
|
389
|
+
});
|
|
481
390
|
});
|
|
482
391
|
});
|
|
483
392
|
}
|
|
484
|
-
// `override` is required, not decorative: this shadows WebSocket.prototype.send, and without
|
|
485
|
-
// the keyword a rename or signature drift in the base class would silently turn this from an
|
|
486
|
-
// override into a NEW method — the observer would stop intercepting and report no WebSocket
|
|
487
|
-
// traffic at all, which reads as "the app sends none". Caught by noImplicitOverride.
|
|
488
393
|
send(data) {
|
|
489
394
|
if (this.#isBridge) {
|
|
490
395
|
super.send(data);
|
|
491
396
|
return;
|
|
492
397
|
}
|
|
493
|
-
emit(EventType.NET_STREAM, {
|
|
494
|
-
transport: StreamTransport.WS,
|
|
495
|
-
direction: StreamDirection.OUT,
|
|
496
|
-
...netUrlFields(this.url),
|
|
497
|
-
...frameFields(data, captureBodies),
|
|
498
|
-
});
|
|
499
398
|
super.send(data);
|
|
399
|
+
observeSafely(() => {
|
|
400
|
+
emit(EventType.NET_STREAM, {
|
|
401
|
+
transport: StreamTransport.WS,
|
|
402
|
+
direction: StreamDirection.OUT,
|
|
403
|
+
...netUrlFields(this.url),
|
|
404
|
+
...frameFields(data, captureBodies),
|
|
405
|
+
});
|
|
406
|
+
});
|
|
500
407
|
}
|
|
501
408
|
};
|
|
502
409
|
patchedWebSocket = window.WebSocket;
|
|
503
410
|
}
|
|
504
|
-
// navigator.sendBeacon — fire-and-forget analytics/telemetry, invisible to fetch/XHR wrapping. A page
|
|
505
|
-
// that beacons a "checkout completed" event should show it. Patch the instance's prototype (via
|
|
506
|
-
// getPrototypeOf, robust whether or not the Navigator global exists); the descriptor read avoids an
|
|
507
|
-
// unbound-method access; the send is synchronous so we emit one completed NET_REQUEST with its result.
|
|
508
411
|
const navProto = (typeof navigator !== 'undefined' ? Object.getPrototypeOf(navigator) : null);
|
|
509
412
|
const origBeacon = (null === navProto ? undefined : Object.getOwnPropertyDescriptor(navProto, 'sendBeacon')?.value);
|
|
510
413
|
let patchedBeacon;
|
|
511
414
|
if (navProto !== null && origBeacon !== undefined) {
|
|
512
415
|
patchedBeacon = function (url, data) {
|
|
513
416
|
const id = nextId();
|
|
514
|
-
const urlFields = netUrlFields(String(url));
|
|
515
|
-
const initiatorStack = initiatorFrame();
|
|
417
|
+
const urlFields = observeValue(() => netUrlFields(String(url)));
|
|
418
|
+
const initiatorStack = observeValue(() => initiatorFrame());
|
|
516
419
|
const sent = origBeacon.call(this, url, data);
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
...(initiatorStack === undefined ? {} : { initiatorStack }),
|
|
420
|
+
observeSafely(() => {
|
|
421
|
+
emit(EventType.NET_REQUEST, {
|
|
422
|
+
id,
|
|
423
|
+
method: 'POST',
|
|
424
|
+
...urlFields,
|
|
425
|
+
status: 0,
|
|
426
|
+
ok: sent,
|
|
427
|
+
queued: sent,
|
|
428
|
+
durationMs: 0,
|
|
429
|
+
initiator: 'beacon',
|
|
430
|
+
...(initiatorStack === undefined ? {} : { initiatorStack }),
|
|
431
|
+
});
|
|
530
432
|
});
|
|
531
433
|
return sent;
|
|
532
434
|
};
|
|
533
435
|
navProto.sendBeacon = patchedBeacon;
|
|
534
436
|
}
|
|
535
|
-
// Document-initiated subresources (link/css/img/script/manifest): fetch and XHR patches never see
|
|
536
|
-
// these, so a `{net}` predicate over a favicon or manifest used to read `assertion_failed` — "your
|
|
537
|
-
// change is broken" — when the truthful answer is "not observable". A PerformanceObserver over
|
|
538
|
-
// `resource` entries reports them with no CDP. Status is the known gap: entries carry one only on
|
|
539
|
-
// newer Chromium, so the field is emitted ONLY when readable and the evaluation seam downgrades
|
|
540
|
-
// status assertions it cannot verify instead of guessing.
|
|
541
|
-
//
|
|
542
|
-
// Dedup: any URL the patched transports already reported (or later report) is skipped — resource
|
|
543
|
-
// timing also records fetch/XHR loads. The seen-set is filled eagerly here AND at every
|
|
544
|
-
// NET_REQUEST emit below, so both orders of arrival stay single-reported.
|
|
545
437
|
const reportedNetUrls = new Set();
|
|
546
438
|
let subresourceEvents = 0;
|
|
547
439
|
const SUBRESOURCE_CAP = 200;
|
|
@@ -554,7 +446,6 @@ export function installNetwork(emit, opts = {}) {
|
|
|
554
446
|
if (subresourceEvents >= SUBRESOURCE_CAP)
|
|
555
447
|
return;
|
|
556
448
|
const type = entry.initiatorType || 'other';
|
|
557
|
-
// Patched transports own their records; only document-initiated types are new here.
|
|
558
449
|
if ('fetch' === type || 'xmlhttprequest' === type)
|
|
559
450
|
continue;
|
|
560
451
|
const rawUrl = entry.name;
|
|
@@ -569,8 +460,6 @@ export function installNetwork(emit, opts = {}) {
|
|
|
569
460
|
durationMs: Math.round(entry.duration),
|
|
570
461
|
initiator: type,
|
|
571
462
|
...(entry.transferSize > 0 ? { transferSize: entry.transferSize } : {}),
|
|
572
|
-
// responseStatus is Chromium-only; omitted entirely when unreadable so the wire never
|
|
573
|
-
// carries a guessed status. Its absence is what the server-side seam reads as unknown.
|
|
574
463
|
...((entry.responseStatus ?? 0) > 0
|
|
575
464
|
? {
|
|
576
465
|
status: entry.responseStatus,
|
|
@@ -582,18 +471,12 @@ export function installNetwork(emit, opts = {}) {
|
|
|
582
471
|
});
|
|
583
472
|
subresourceObserver = observer;
|
|
584
473
|
observer.observe({ type: 'resource', buffered: true });
|
|
585
|
-
// Disconnected in the disposer below.
|
|
586
474
|
}
|
|
587
475
|
catch {
|
|
588
|
-
// No observer support: document-initiated loads stay unobserved, stated rather than guessed.
|
|
589
476
|
}
|
|
590
477
|
}
|
|
591
478
|
return () => {
|
|
592
479
|
subresourceObserver?.disconnect();
|
|
593
|
-
// Restore each slot ONLY if it still holds OUR wrapper. Between connect() and disconnect() the app
|
|
594
|
-
// (or Sentry/analytics/a router) may have wrapped fetch/XHR/EventSource/WebSocket/sendBeacon ON TOP
|
|
595
|
-
// of ours; blindly writing the original back would silently uninstall their instrumentation — the
|
|
596
|
-
// dev-only SDK breaking the app it only meant to observe.
|
|
597
480
|
if (window.fetch === patchedFetch)
|
|
598
481
|
window.fetch = origFetch;
|
|
599
482
|
if (captureMethod(proto, 'open') === patchedOpen)
|