@buoy-gg/network 7.0.26 → 7.0.28
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/lib/commonjs/network/components/NetworkDetailStepper.js +2 -1
- package/lib/commonjs/network/components/NetworkEventDetailView.js +74 -72
- package/lib/commonjs/network/components/NetworkEventItemCompact.js +26 -20
- package/lib/commonjs/network/components/NetworkEventItemCompact.web.js +24 -6
- package/lib/commonjs/network/components/NetworkFilterViewV3.js +12 -12
- package/lib/commonjs/network/components/NetworkHeaderLive.js +25 -29
- package/lib/commonjs/network/components/NetworkHeaderLive.web.js +24 -6
- package/lib/commonjs/network/components/NetworkHeaderMenu.js +10 -9
- package/lib/commonjs/network/components/NetworkListScreen.js +2 -1
- package/lib/commonjs/network/components/NetworkListScreen.web.js +24 -6
- package/lib/commonjs/network/components/NetworkModal.js +45 -31
- package/lib/commonjs/network/components/NetworkOverrideHeaderButton.js +10 -7
- package/lib/commonjs/network/components/NetworkOverridesButton.js +7 -7
- package/lib/commonjs/network/sync/networkSyncAdapter.js +36 -16
- package/lib/commonjs/network/utils/httpCompletion.js +37 -0
- package/lib/commonjs/network/utils/networkEventStore.js +26 -0
- package/lib/commonjs/network/utils/networkListener.js +180 -43
- package/lib/module/network/components/NetworkDetailStepper.js +2 -1
- package/lib/module/network/components/NetworkEventDetailView.js +75 -73
- package/lib/module/network/components/NetworkEventItemCompact.js +27 -21
- package/lib/module/network/components/NetworkEventItemCompact.web.js +24 -6
- package/lib/module/network/components/NetworkFilterViewV3.js +13 -13
- package/lib/module/network/components/NetworkHeaderLive.js +26 -30
- package/lib/module/network/components/NetworkHeaderLive.web.js +24 -6
- package/lib/module/network/components/NetworkHeaderMenu.js +11 -10
- package/lib/module/network/components/NetworkListScreen.js +3 -2
- package/lib/module/network/components/NetworkListScreen.web.js +24 -6
- package/lib/module/network/components/NetworkModal.js +46 -32
- package/lib/module/network/components/NetworkOverrideHeaderButton.js +11 -8
- package/lib/module/network/components/NetworkOverridesButton.js +8 -8
- package/lib/module/network/sync/networkSyncAdapter.js +37 -17
- package/lib/module/network/utils/httpCompletion.js +32 -0
- package/lib/module/network/utils/networkEventStore.js +26 -0
- package/lib/module/network/utils/networkListener.js +178 -42
- package/lib/typescript/network/components/NetworkDetailStepper.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkEventItemCompact.web.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkFilterViewV3.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkHeaderLive.web.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkListScreen.web.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts +1 -0
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/network/utils/httpCompletion.d.ts +11 -0
- package/lib/typescript/network/utils/httpCompletion.d.ts.map +1 -0
- package/lib/typescript/network/utils/networkEventStore.d.ts +17 -0
- package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkListener.d.ts +61 -5
- package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -79,24 +79,27 @@ function NetworkOverrideHeaderButton({
|
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
const styles = _reactNative.StyleSheet.create({
|
|
82
|
+
// Night: the header's dark-glass chip, with the accent label the night
|
|
83
|
+
// language gives labelled actions (plate + accent ink). Active keeps the
|
|
84
|
+
// warning tint — same "you are not seeing the truth" signal as the flask.
|
|
82
85
|
button: {
|
|
83
86
|
paddingHorizontal: 10,
|
|
84
87
|
paddingVertical: 6,
|
|
85
|
-
borderRadius:
|
|
88
|
+
borderRadius: 8,
|
|
86
89
|
borderWidth: 1,
|
|
87
|
-
borderColor: _sharedUi.
|
|
88
|
-
backgroundColor: _sharedUi.
|
|
90
|
+
borderColor: _sharedUi.night.color.border,
|
|
91
|
+
backgroundColor: _sharedUi.night.color.buttonSurface
|
|
89
92
|
},
|
|
90
93
|
buttonActive: {
|
|
91
|
-
borderColor: _sharedUi.
|
|
92
|
-
backgroundColor: _sharedUi.
|
|
94
|
+
borderColor: _sharedUi.night.color.warning + "77",
|
|
95
|
+
backgroundColor: _sharedUi.night.color.warningSoft
|
|
93
96
|
},
|
|
94
97
|
label: {
|
|
95
98
|
fontSize: 11,
|
|
96
99
|
fontWeight: "700",
|
|
97
|
-
color: _sharedUi.
|
|
100
|
+
color: _sharedUi.night.color.accent
|
|
98
101
|
},
|
|
99
102
|
labelActive: {
|
|
100
|
-
color: _sharedUi.
|
|
103
|
+
color: _sharedUi.night.color.warning
|
|
101
104
|
}
|
|
102
105
|
});
|
|
@@ -35,7 +35,7 @@ function NetworkOverridesButton({
|
|
|
35
35
|
accessibilityLabel: active ? `Response overrides — ${activeCount} active` : "Response overrides",
|
|
36
36
|
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.FlaskConical, {
|
|
37
37
|
size: 14,
|
|
38
|
-
color: active ? _sharedUi.
|
|
38
|
+
color: active ? _sharedUi.night.color.warning : _sharedUi.night.color.textSecondary
|
|
39
39
|
}), active ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
40
40
|
style: styles.badge,
|
|
41
41
|
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
@@ -56,15 +56,15 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
56
56
|
borderRadius: 8,
|
|
57
57
|
alignItems: "center",
|
|
58
58
|
justifyContent: "center",
|
|
59
|
-
backgroundColor: _sharedUi.
|
|
59
|
+
backgroundColor: _sharedUi.night.color.buttonSurface,
|
|
60
60
|
borderWidth: 1,
|
|
61
|
-
borderColor: _sharedUi.
|
|
61
|
+
borderColor: _sharedUi.night.color.border
|
|
62
62
|
},
|
|
63
63
|
// Active fills the whole box, the way the power toggle goes green — the
|
|
64
64
|
// warning now reads at a glance instead of as a tinted glyph.
|
|
65
65
|
buttonActive: {
|
|
66
|
-
backgroundColor: _sharedUi.
|
|
67
|
-
borderColor: _sharedUi.
|
|
66
|
+
backgroundColor: _sharedUi.night.color.warningSoft,
|
|
67
|
+
borderColor: _sharedUi.night.color.warning + "77"
|
|
68
68
|
},
|
|
69
69
|
badge: {
|
|
70
70
|
position: "absolute",
|
|
@@ -78,11 +78,11 @@ const styles = _reactNative.StyleSheet.create({
|
|
|
78
78
|
paddingHorizontal: 3,
|
|
79
79
|
alignItems: "center",
|
|
80
80
|
justifyContent: "center",
|
|
81
|
-
backgroundColor: _sharedUi.
|
|
81
|
+
backgroundColor: _sharedUi.night.color.warning
|
|
82
82
|
},
|
|
83
83
|
badgeText: {
|
|
84
84
|
fontSize: 8,
|
|
85
85
|
fontWeight: "800",
|
|
86
|
-
color:
|
|
86
|
+
color: "#000000"
|
|
87
87
|
}
|
|
88
88
|
});
|
|
@@ -364,22 +364,30 @@ const networkSyncAdapter = exports.networkSyncAdapter = {
|
|
|
364
364
|
// flags, and a total snapshot budget so the list degrades instead of the
|
|
365
365
|
// whole panel being dropped at the emit layer.
|
|
366
366
|
version: 5,
|
|
367
|
-
getSnapshot: () =>
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
367
|
+
getSnapshot: () => {
|
|
368
|
+
// Piggyback the interception health check on the watch loop: while a
|
|
369
|
+
// dashboard/MCP watch is open this runs continuously, so a clobbered
|
|
370
|
+
// fetch/XHR patch (see networkListener.startListening) heals within one
|
|
371
|
+
// snapshot tick instead of staying dead for the session. Two property
|
|
372
|
+
// reads when healthy.
|
|
373
|
+
_networkEventStore.networkEventStore.ensureInterception();
|
|
374
|
+
return {
|
|
375
|
+
events: fitEventsToBudget(_networkEventStore.networkEventStore.getEvents()),
|
|
376
|
+
/**
|
|
377
|
+
* Response-override rules live on the DEVICE — that's the only place they
|
|
378
|
+
* can run, since a rule is applied inside the app's own XMLHttpRequest.
|
|
379
|
+
* Remote surfaces mirror this and forward their edits back as actions.
|
|
380
|
+
*/
|
|
381
|
+
overrides: stripLargeRuleBodies(_overrideRulesStore.overrideRulesStore.getState()),
|
|
382
|
+
// Saved snapshots go through the same body strip. They are few (capped in
|
|
383
|
+
// the store) but they ride on EVERY snapshot, so a fat one would land in
|
|
384
|
+
// the same 200ms loop that the body-stripping exists to protect.
|
|
385
|
+
saved: _networkSavedStore.networkSavedStore.getRecords().map(record => ({
|
|
386
|
+
...record,
|
|
387
|
+
event: toWireEvent(record.event).event
|
|
388
|
+
}))
|
|
389
|
+
};
|
|
390
|
+
},
|
|
383
391
|
subscribe: onChange => {
|
|
384
392
|
// Pinning with no traffic in flight still has to reach the dashboard, so
|
|
385
393
|
// both stores drive the snapshot.
|
|
@@ -590,10 +598,22 @@ const networkSyncAdapter = exports.networkSyncAdapter = {
|
|
|
590
598
|
} catch {
|
|
591
599
|
/* no interceptable runtime here */
|
|
592
600
|
}
|
|
601
|
+
// `interceptorInstalled` is the listener's own flag; `interceptorLive`
|
|
602
|
+
// is whether the interceptors are actually reachable through the global
|
|
603
|
+
// chain. installed:true + live:false = something re-assigned fetch/XHR
|
|
604
|
+
// over the interceptor (it self-heals on the next subscribe/snapshot —
|
|
605
|
+
// see ensureInterception).
|
|
606
|
+
let interceptorLive = null;
|
|
607
|
+
try {
|
|
608
|
+
interceptorLive = (0, _networkListener.isNetworkInterceptionLive)();
|
|
609
|
+
} catch {
|
|
610
|
+
/* no interceptable runtime here */
|
|
611
|
+
}
|
|
593
612
|
return {
|
|
594
613
|
capturing: subscribers > 0,
|
|
595
614
|
subscribers,
|
|
596
615
|
interceptorInstalled,
|
|
616
|
+
interceptorLive,
|
|
597
617
|
listenerCount,
|
|
598
618
|
eventCount: _networkEventStore.networkEventStore.getEvents().length
|
|
599
619
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.httpStatusText = httpStatusText;
|
|
7
|
+
exports.xhrCompletionKind = xhrCompletionKind;
|
|
8
|
+
/**
|
|
9
|
+
* Classify a completed XHR/fetch HTTP status.
|
|
10
|
+
*
|
|
11
|
+
* A 4xx/5xx is a *response*, not a transport error. RN's XHR leaves
|
|
12
|
+
* `statusText` empty, which used to become `event.error = "HTTP 500: "` and
|
|
13
|
+
* then MCP `ERR HTTP 500: no error`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const STATUS_TEXT = {
|
|
17
|
+
400: "Bad Request",
|
|
18
|
+
401: "Unauthorized",
|
|
19
|
+
403: "Forbidden",
|
|
20
|
+
404: "Not Found",
|
|
21
|
+
408: "Request Timeout",
|
|
22
|
+
429: "Too Many Requests",
|
|
23
|
+
500: "Internal Server Error",
|
|
24
|
+
502: "Bad Gateway",
|
|
25
|
+
503: "Service Unavailable",
|
|
26
|
+
504: "Gateway Timeout"
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** status 0 (or missing) means the request never got an HTTP response. */
|
|
30
|
+
function xhrCompletionKind(status) {
|
|
31
|
+
return typeof status === "number" && status > 0 ? "response" : "error";
|
|
32
|
+
}
|
|
33
|
+
function httpStatusText(status, statusText) {
|
|
34
|
+
const trimmed = (statusText ?? "").trim();
|
|
35
|
+
if (trimmed) return trimmed;
|
|
36
|
+
return STATUS_TEXT[status] ?? "";
|
|
37
|
+
}
|
|
@@ -175,6 +175,32 @@ class NetworkEventStore extends _sharedUi.BaseEventStore {
|
|
|
175
175
|
return this.rawListenerUnsubscribe !== null;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Re-assert the interceptor if something re-assigned fetch/XHR since capture
|
|
180
|
+
* started (Expo's runtime, RN DevTools, a second dev server's tooling — see
|
|
181
|
+
* networkListener.startListening). A no-op costing two property reads when
|
|
182
|
+
* everything is healthy, and a no-op entirely unless this store is actively
|
|
183
|
+
* capturing — a mirror-mode dashboard must never install an interceptor in
|
|
184
|
+
* its own process.
|
|
185
|
+
*/
|
|
186
|
+
ensureInterception() {
|
|
187
|
+
if (!this.isCapturing()) return;
|
|
188
|
+
(0, _networkListener.networkListener)().startListening();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Every new consumer — the Network modal opening, a badge mounting, a
|
|
193
|
+
* dashboard watch attaching — is a chance to notice and repair a clobbered
|
|
194
|
+
* interceptor, which matters because a long-lived watch can hold the
|
|
195
|
+
* subscriber count above zero for the whole session (so the 0→1
|
|
196
|
+
* startCapturing path would never run again).
|
|
197
|
+
*/
|
|
198
|
+
subscribeToEvents(listener) {
|
|
199
|
+
const unsubscribe = super.subscribeToEvents(listener);
|
|
200
|
+
this.ensureInterception();
|
|
201
|
+
return unsubscribe;
|
|
202
|
+
}
|
|
203
|
+
|
|
178
204
|
/**
|
|
179
205
|
* Process a network listener event
|
|
180
206
|
*/
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.stopNetworkListener = exports.startNetworkListener = exports.removeAllNetworkListeners = exports.networkListener = exports.isNetworkListening = exports.getNetworkListenerCount = exports.addNetworkListener = void 0;
|
|
6
|
+
exports.stopNetworkListener = exports.startNetworkListener = exports.removeAllNetworkListeners = exports.networkListener = exports.isNetworkListening = exports.isNetworkInterceptionLive = exports.getNetworkListenerCount = exports.addNetworkListener = exports.BUOY_XHR_INTERCEPTOR_FLAG = exports.BUOY_FETCH_INTERCEPTOR_FLAG = void 0;
|
|
7
7
|
var _engine = require("../overrides/engine");
|
|
8
8
|
var _applyToXhr = require("../overrides/applyToXhr");
|
|
9
9
|
var _applyToFetch = require("../overrides/applyToFetch");
|
|
10
10
|
var _types = require("../overrides/types");
|
|
11
|
+
var _httpCompletion = require("./httpCompletion");
|
|
11
12
|
/**
|
|
12
13
|
* Network listener using Reactotron-style event pattern
|
|
13
14
|
* Simple and reliable network interception for React Native
|
|
@@ -31,6 +32,18 @@ let insideOverriddenFetch = false;
|
|
|
31
32
|
|
|
32
33
|
// Event types for network operations
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Markers stamped on the installed interceptor functions so their presence in
|
|
37
|
+
* the live chain stays detectable even after another tool wraps them.
|
|
38
|
+
* Cooperating patchers (@buoy-gg/impersonate) copy every `__buoy*` marker from
|
|
39
|
+
* the function they wrap onto their own wrapper; a global fetch/XHR WITHOUT
|
|
40
|
+
* the marker therefore means our interceptor was clobbered — some runtime
|
|
41
|
+
* re-assigned `globalThis.fetch` over it (Expo's winter runtime, React Native
|
|
42
|
+
* DevTools' network panel, a second dev server's tooling, hot-reload churn all
|
|
43
|
+
* do this in the wild) — and capture must re-install. See startListening.
|
|
44
|
+
*/
|
|
45
|
+
const BUOY_FETCH_INTERCEPTOR_FLAG = exports.BUOY_FETCH_INTERCEPTOR_FLAG = "__buoyNetworkFetchIntercepted";
|
|
46
|
+
const BUOY_XHR_INTERCEPTOR_FLAG = exports.BUOY_XHR_INTERCEPTOR_FLAG = "__buoyNetworkXhrIntercepted";
|
|
34
47
|
/**
|
|
35
48
|
* Network traffic interceptor for React Native applications
|
|
36
49
|
*
|
|
@@ -71,6 +84,25 @@ class NetworkListener {
|
|
|
71
84
|
|
|
72
85
|
// Store original methods
|
|
73
86
|
|
|
87
|
+
/**
|
|
88
|
+
* The wrappers currently installed by THIS instance, for identity checks:
|
|
89
|
+
* `stopListening` restores a global only when it is still literally ours —
|
|
90
|
+
* assigning our saved original over someone who patched after us would wipe
|
|
91
|
+
* their interceptor exactly the way this class gets wiped (see below).
|
|
92
|
+
*/
|
|
93
|
+
patchedFetch = null;
|
|
94
|
+
patchedXHROpen = null;
|
|
95
|
+
patchedXHRSend = null;
|
|
96
|
+
patchedXHRSetRequestHeader = null;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Bumped on every (re)install and on stop. Each wrapper closes over the
|
|
100
|
+
* generation it was born with and goes silent (pure pass-through — no emit,
|
|
101
|
+
* no counter bump) once a newer generation exists, so a wrapper stranded
|
|
102
|
+
* inside another tool's chain can never double-report a request.
|
|
103
|
+
*/
|
|
104
|
+
fetchGeneration = 0;
|
|
105
|
+
xhrGeneration = 0;
|
|
74
106
|
constructor() {
|
|
75
107
|
// Store original methods
|
|
76
108
|
this.originalFetch = globalThis.fetch.bind(globalThis);
|
|
@@ -354,8 +386,10 @@ class NetworkListener {
|
|
|
354
386
|
// Ignore header parsing errors
|
|
355
387
|
}
|
|
356
388
|
|
|
357
|
-
// Emit response or error based on status
|
|
358
|
-
|
|
389
|
+
// Emit response or error based on status. A 4xx/5xx is a completed HTTP
|
|
390
|
+
// response, not a transport error — RN XHR leaves statusText empty, which
|
|
391
|
+
// used to become `event.error = "HTTP 500: "` and MCP `ERR HTTP 500: no error`.
|
|
392
|
+
if ((0, _httpCompletion.xhrCompletionKind)(xhr.status) === "response") {
|
|
359
393
|
this.emit({
|
|
360
394
|
type: "response",
|
|
361
395
|
timestamp: new Date(),
|
|
@@ -372,7 +406,7 @@ class NetworkListener {
|
|
|
372
406
|
},
|
|
373
407
|
response: {
|
|
374
408
|
status: xhr.status,
|
|
375
|
-
statusText: xhr.statusText,
|
|
409
|
+
statusText: (0, _httpCompletion.httpStatusText)(xhr.status, xhr.statusText),
|
|
376
410
|
headers: responseHeaders,
|
|
377
411
|
body,
|
|
378
412
|
size: responseSize
|
|
@@ -401,37 +435,89 @@ class NetworkListener {
|
|
|
401
435
|
size: responseSize
|
|
402
436
|
},
|
|
403
437
|
error: {
|
|
404
|
-
message:
|
|
438
|
+
message: "Network Error"
|
|
405
439
|
},
|
|
406
440
|
override
|
|
407
441
|
});
|
|
408
442
|
}
|
|
409
443
|
}
|
|
410
444
|
|
|
445
|
+
/** Is our fetch interceptor still reachable through the global chain? */
|
|
446
|
+
isFetchInterceptionLive() {
|
|
447
|
+
return globalThis.fetch[BUOY_FETCH_INTERCEPTOR_FLAG] === true;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Is our XHR interceptor still reachable through the prototype chain? */
|
|
451
|
+
isXhrInterceptionLive() {
|
|
452
|
+
return XMLHttpRequest.prototype.send[BUOY_XHR_INTERCEPTOR_FLAG] === true;
|
|
453
|
+
}
|
|
454
|
+
|
|
411
455
|
/**
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
|
|
456
|
+
* Both halves of the interceptor are reachable — requests actually flow
|
|
457
|
+
* through capture. `isListening` alone cannot promise that (see
|
|
458
|
+
* startListening); diagnostics should report THIS.
|
|
459
|
+
*/
|
|
460
|
+
isInterceptionLive() {
|
|
461
|
+
return this.isFetchInterceptionLive() && this.isXhrInterceptionLive();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Start (or repair) interception by swizzling fetch and XMLHttpRequest.
|
|
416
466
|
*
|
|
417
|
-
*
|
|
467
|
+
* NOT guarded by a plain `isListening` flag: `globalThis.fetch` gets
|
|
468
|
+
* re-assigned in the wild — Expo's winter runtime, React Native DevTools'
|
|
469
|
+
* network panel, a second dev server's tooling, hot-reload churn. When that
|
|
470
|
+
* happens the flag still says "listening" while every request bypasses
|
|
471
|
+
* capture, and a flag-only guard makes the outage PERMANENT (observed as
|
|
472
|
+
* "No network events" forever with `interceptorInstalled: true`). So this
|
|
473
|
+
* verifies each interceptor is actually reachable (marker check) and
|
|
474
|
+
* re-installs whichever half was clobbered, wrapping the CURRENT global so
|
|
475
|
+
* whatever replaced us keeps working underneath. When both halves are live
|
|
476
|
+
* this costs two property reads.
|
|
418
477
|
*
|
|
419
478
|
* @performance Uses method swizzling for minimal runtime overhead
|
|
420
479
|
* @performance Includes request deduplication through ignored URL patterns
|
|
421
480
|
*/
|
|
422
481
|
startListening() {
|
|
423
|
-
|
|
482
|
+
const fetchLive = this.isListening && this.isFetchInterceptionLive();
|
|
483
|
+
const xhrLive = this.isListening && this.isXhrInterceptionLive();
|
|
484
|
+
if (fetchLive && xhrLive) {
|
|
424
485
|
return;
|
|
425
486
|
}
|
|
426
|
-
|
|
487
|
+
if (!fetchLive) {
|
|
488
|
+
this.installFetchInterceptor();
|
|
489
|
+
}
|
|
490
|
+
if (!xhrLive) {
|
|
491
|
+
this.installXhrInterceptor();
|
|
492
|
+
}
|
|
493
|
+
this.isListening = true;
|
|
494
|
+
if (__DEV__) {
|
|
495
|
+
// Network listener has started monitoring fetch and XMLHttpRequest operations
|
|
496
|
+
}
|
|
497
|
+
}
|
|
427
498
|
|
|
428
|
-
|
|
429
|
-
|
|
499
|
+
/** Swizzle `globalThis.fetch`, wrapping whatever is installed right now. */
|
|
500
|
+
installFetchInterceptor() {
|
|
501
|
+
const self = this;
|
|
502
|
+
// The pass-through is the CURRENT global (not the construction-time one):
|
|
503
|
+
// re-installing after a clobber must call through the replacement, and a
|
|
504
|
+
// stale generation must replay the chain exactly as it stood beneath it.
|
|
505
|
+
const passthroughFetch = globalThis.fetch.bind(globalThis);
|
|
506
|
+
this.originalFetch = passthroughFetch;
|
|
507
|
+
const generation = ++this.fetchGeneration;
|
|
508
|
+
const wrappedFetch = async (input, init) => {
|
|
430
509
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
431
510
|
|
|
511
|
+
// A newer install supersedes this wrapper. If it is still stranded in
|
|
512
|
+
// another tool's chain it must pass through silently, or one request
|
|
513
|
+
// would be reported once per stranded generation.
|
|
514
|
+
if (generation !== self.fetchGeneration) {
|
|
515
|
+
return passthroughFetch(input, init);
|
|
516
|
+
}
|
|
517
|
+
|
|
432
518
|
// Skip ignored URLs
|
|
433
519
|
if (self.shouldIgnoreUrl(url)) {
|
|
434
|
-
return
|
|
520
|
+
return passthroughFetch(input, init);
|
|
435
521
|
}
|
|
436
522
|
const startTime = Date.now();
|
|
437
523
|
const requestId = `fetch_${++self.requestCounter}`;
|
|
@@ -514,7 +600,7 @@ class NetworkListener {
|
|
|
514
600
|
insideOverriddenFetch = outcome != null;
|
|
515
601
|
let pending;
|
|
516
602
|
try {
|
|
517
|
-
pending =
|
|
603
|
+
pending = passthroughFetch(input, init);
|
|
518
604
|
} finally {
|
|
519
605
|
insideOverriddenFetch = false;
|
|
520
606
|
}
|
|
@@ -617,30 +703,51 @@ class NetworkListener {
|
|
|
617
703
|
throw error;
|
|
618
704
|
}
|
|
619
705
|
};
|
|
706
|
+
wrappedFetch[BUOY_FETCH_INTERCEPTOR_FLAG] = true;
|
|
707
|
+
this.patchedFetch = wrappedFetch;
|
|
708
|
+
globalThis.fetch = this.patchedFetch;
|
|
709
|
+
}
|
|
620
710
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
711
|
+
/** Swizzle the XHR prototype methods, wrapping whatever is installed now. */
|
|
712
|
+
installXhrInterceptor() {
|
|
713
|
+
const self = this;
|
|
714
|
+
// Same contract as the fetch half: pass through to the CURRENT methods.
|
|
715
|
+
const passthroughOpen = XMLHttpRequest.prototype.open;
|
|
716
|
+
const passthroughSend = XMLHttpRequest.prototype.send;
|
|
717
|
+
const passthroughSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
|
|
718
|
+
this.originalXHROpen = passthroughOpen;
|
|
719
|
+
this.originalXHRSend = passthroughSend;
|
|
720
|
+
this.originalXHRSetRequestHeader = passthroughSetRequestHeader;
|
|
721
|
+
const generation = ++this.xhrGeneration;
|
|
722
|
+
const patchedOpen = function (method, url, async, user, password) {
|
|
723
|
+
// Superseded generations must not re-stamp (a double stamp burns two
|
|
724
|
+
// request ids per request) — just pass through.
|
|
725
|
+
if (generation === self.xhrGeneration) {
|
|
726
|
+
const xhr = this;
|
|
727
|
+
xhr._requestId = `xhr_${++self.requestCounter}`;
|
|
728
|
+
xhr._method = method;
|
|
729
|
+
xhr._url = url;
|
|
730
|
+
xhr._startTime = Date.now();
|
|
731
|
+
xhr._requestHeaders = {};
|
|
732
|
+
}
|
|
733
|
+
return passthroughOpen.call(this, method, url, async ?? true, user, password);
|
|
631
734
|
};
|
|
632
735
|
|
|
633
736
|
// Track request headers
|
|
634
|
-
|
|
737
|
+
const patchedSetRequestHeader = function (header, value) {
|
|
635
738
|
const xhr = this;
|
|
636
|
-
if (xhr._requestHeaders) {
|
|
739
|
+
if (generation === self.xhrGeneration && xhr._requestHeaders) {
|
|
637
740
|
xhr._requestHeaders[header] = value;
|
|
638
741
|
}
|
|
639
|
-
return
|
|
742
|
+
return passthroughSetRequestHeader.call(this, header, value);
|
|
640
743
|
};
|
|
641
|
-
|
|
744
|
+
const patchedSend = function (
|
|
642
745
|
// @ts-ignore - this does exist on native
|
|
643
746
|
data) {
|
|
747
|
+
// Superseded generation: silent pass-through (see the fetch wrapper).
|
|
748
|
+
if (generation !== self.xhrGeneration) {
|
|
749
|
+
return passthroughSend.call(this, data);
|
|
750
|
+
}
|
|
644
751
|
const xhr = this;
|
|
645
752
|
const requestId = xhr._requestId;
|
|
646
753
|
const method = xhr._method || "GET";
|
|
@@ -650,7 +757,7 @@ class NetworkListener {
|
|
|
650
757
|
|
|
651
758
|
// Skip ignored URLs
|
|
652
759
|
if (self.shouldIgnoreUrl(url)) {
|
|
653
|
-
return
|
|
760
|
+
return passthroughSend.call(this, data);
|
|
654
761
|
}
|
|
655
762
|
const {
|
|
656
763
|
url: cleanUrl,
|
|
@@ -767,18 +874,21 @@ class NetworkListener {
|
|
|
767
874
|
// Listeners are attached above FIRST: `runOverride` drives the same
|
|
768
875
|
// readyState transitions a real response would, and the interceptor has
|
|
769
876
|
// to see them (readyState 2 is where TTFB is stamped).
|
|
770
|
-
const handled = (0, _applyToXhr.runOverride)(this, url, outcome, () =>
|
|
877
|
+
const handled = (0, _applyToXhr.runOverride)(this, url, outcome, () => passthroughSend.call(this, data));
|
|
771
878
|
// `handled` is false only where this isn't React Native's own
|
|
772
879
|
// XMLHttpRequest and can't be driven by hand. Fall through and send for
|
|
773
880
|
// real rather than dropping the request on the floor.
|
|
774
881
|
if (handled) return;
|
|
775
882
|
}
|
|
776
|
-
return
|
|
883
|
+
return passthroughSend.call(this, data);
|
|
777
884
|
};
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
885
|
+
patchedSend[BUOY_XHR_INTERCEPTOR_FLAG] = true;
|
|
886
|
+
this.patchedXHROpen = patchedOpen;
|
|
887
|
+
this.patchedXHRSend = patchedSend;
|
|
888
|
+
this.patchedXHRSetRequestHeader = patchedSetRequestHeader;
|
|
889
|
+
XMLHttpRequest.prototype.open = this.patchedXHROpen;
|
|
890
|
+
XMLHttpRequest.prototype.send = this.patchedXHRSend;
|
|
891
|
+
XMLHttpRequest.prototype.setRequestHeader = this.patchedXHRSetRequestHeader;
|
|
782
892
|
}
|
|
783
893
|
|
|
784
894
|
/**
|
|
@@ -792,11 +902,30 @@ class NetworkListener {
|
|
|
792
902
|
return;
|
|
793
903
|
}
|
|
794
904
|
|
|
795
|
-
//
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
905
|
+
// Silence any of our wrappers stranded inside another tool's chain — they
|
|
906
|
+
// check the generation on every call and now pass through without emitting.
|
|
907
|
+
this.fetchGeneration++;
|
|
908
|
+
this.xhrGeneration++;
|
|
909
|
+
|
|
910
|
+
// Restore ONLY what is still literally ours. If another tool patched after
|
|
911
|
+
// us (impersonate, a debugger), assigning our saved originals would wipe
|
|
912
|
+
// ITS interceptor — the exact injury startListening defends against.
|
|
913
|
+
if (this.patchedFetch && globalThis.fetch === this.patchedFetch) {
|
|
914
|
+
globalThis.fetch = this.originalFetch;
|
|
915
|
+
}
|
|
916
|
+
if (this.patchedXHROpen && XMLHttpRequest.prototype.open === this.patchedXHROpen) {
|
|
917
|
+
XMLHttpRequest.prototype.open = this.originalXHROpen;
|
|
918
|
+
}
|
|
919
|
+
if (this.patchedXHRSend && XMLHttpRequest.prototype.send === this.patchedXHRSend) {
|
|
920
|
+
XMLHttpRequest.prototype.send = this.originalXHRSend;
|
|
921
|
+
}
|
|
922
|
+
if (this.patchedXHRSetRequestHeader && XMLHttpRequest.prototype.setRequestHeader === this.patchedXHRSetRequestHeader) {
|
|
923
|
+
XMLHttpRequest.prototype.setRequestHeader = this.originalXHRSetRequestHeader;
|
|
924
|
+
}
|
|
925
|
+
this.patchedFetch = null;
|
|
926
|
+
this.patchedXHROpen = null;
|
|
927
|
+
this.patchedXHRSend = null;
|
|
928
|
+
this.patchedXHRSetRequestHeader = null;
|
|
800
929
|
this.isListening = false;
|
|
801
930
|
if (__DEV__) {
|
|
802
931
|
// Network listener has stopped monitoring and restored original methods
|
|
@@ -921,4 +1050,12 @@ const isNetworkListening = () => getNetworkListener().isActive;
|
|
|
921
1050
|
*/
|
|
922
1051
|
exports.isNetworkListening = isNetworkListening;
|
|
923
1052
|
const getNetworkListenerCount = () => getNetworkListener().listenerCount;
|
|
924
|
-
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Whether both interceptors are actually reachable through the live global
|
|
1056
|
+
* chain — the honest version of `isNetworkListening` (whose flag stays true
|
|
1057
|
+
* even after another tool re-assigns `globalThis.fetch` over the wrapper).
|
|
1058
|
+
*/
|
|
1059
|
+
exports.getNetworkListenerCount = getNetworkListenerCount;
|
|
1060
|
+
const isNetworkInterceptionLive = () => getNetworkListener().isInterceptionLive();
|
|
1061
|
+
exports.isNetworkInterceptionLive = isNetworkInterceptionLive;
|
|
@@ -98,6 +98,7 @@ export const NetworkDetailStepper = /*#__PURE__*/memo(function NetworkDetailStep
|
|
|
98
98
|
itemLabel: "Request",
|
|
99
99
|
applySafeAreaInset: applySafeAreaInset,
|
|
100
100
|
onPrevious: stepPrevious,
|
|
101
|
-
onNext: stepNext
|
|
101
|
+
onNext: stepNext,
|
|
102
|
+
variant: "night"
|
|
102
103
|
});
|
|
103
104
|
});
|