@buoy-gg/network 7.0.36 → 7.0.38
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/README.md +45 -12
- package/lib/commonjs/index.js +26 -1
- package/lib/commonjs/index.web.js +26 -1
- package/lib/commonjs/network/components/NetworkEventDetailView.js +6 -1
- package/lib/commonjs/network/components/NetworkHeaderMenu.js +4 -0
- package/lib/commonjs/network/components/NetworkModal.js +24 -8
- package/lib/commonjs/network/conditions/NetworkConditionsControl.js +235 -0
- package/lib/commonjs/network/conditions/NetworkConditionsControl.test.js +136 -0
- package/lib/commonjs/network/conditions/NetworkConditionsIcon.js +33 -0
- package/lib/commonjs/network/conditions/NetworkFloatingTool.js +310 -0
- package/lib/commonjs/network/conditions/NetworkFloatingTool.test.js +219 -0
- package/lib/commonjs/network/conditions/store.js +75 -0
- package/lib/commonjs/network/conditions/wait.js +43 -0
- package/lib/commonjs/network/conditions/xhr.js +110 -0
- package/lib/commonjs/network/hooks/useNetworkEvents.js +1 -1
- package/lib/commonjs/network/overrides/applyToXhr.js +10 -8
- package/lib/commonjs/network/overrides/engine.js +2 -1
- package/lib/commonjs/network/sync/networkSyncAdapter.js +26 -1
- package/lib/commonjs/network/utils/generateNetworkCopyText.js +2 -0
- package/lib/commonjs/network/utils/networkEventStore.js +5 -77
- package/lib/commonjs/network/utils/networkListener.js +96 -51
- package/lib/commonjs/network/utils/networkSavedStore.js +2 -2
- package/lib/commonjs/network/utils/selectSavedEvents.js +2 -2
- package/lib/commonjs/preset.js +2 -1
- package/lib/module/index.js +2 -1
- package/lib/module/index.web.js +2 -1
- package/lib/module/network/components/NetworkEventDetailView.js +6 -1
- package/lib/module/network/components/NetworkHeaderMenu.js +4 -0
- package/lib/module/network/components/NetworkModal.js +25 -9
- package/lib/module/network/conditions/NetworkConditionsControl.js +231 -0
- package/lib/module/network/conditions/NetworkConditionsControl.test.js +135 -0
- package/lib/module/network/conditions/NetworkConditionsIcon.js +29 -0
- package/lib/module/network/conditions/NetworkFloatingTool.js +304 -0
- package/lib/module/network/conditions/NetworkFloatingTool.test.js +218 -0
- package/lib/module/network/conditions/store.js +65 -0
- package/lib/module/network/conditions/wait.js +38 -0
- package/lib/module/network/conditions/xhr.js +106 -0
- package/lib/module/network/hooks/useNetworkEvents.js +1 -1
- package/lib/module/network/overrides/applyToXhr.js +9 -8
- package/lib/module/network/overrides/engine.js +2 -1
- package/lib/module/network/sync/networkSyncAdapter.js +26 -1
- package/lib/module/network/utils/generateNetworkCopyText.js +2 -0
- package/lib/module/network/utils/networkEventStore.js +6 -77
- package/lib/module/network/utils/networkListener.js +97 -52
- package/lib/module/network/utils/networkSavedStore.js +2 -2
- package/lib/module/network/utils/selectSavedEvents.js +2 -2
- package/lib/module/preset.js +3 -2
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +2 -0
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkHeaderMenu.d.ts +2 -1
- package/lib/typescript/network/components/NetworkHeaderMenu.d.ts.map +1 -1
- package/lib/typescript/network/components/NetworkModal.d.ts +3 -0
- package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
- package/lib/typescript/network/conditions/NetworkConditionsControl.d.ts +12 -0
- package/lib/typescript/network/conditions/NetworkConditionsControl.d.ts.map +1 -0
- package/lib/typescript/network/conditions/NetworkConditionsControl.test.d.ts +2 -0
- package/lib/typescript/network/conditions/NetworkConditionsControl.test.d.ts.map +1 -0
- package/lib/typescript/network/conditions/NetworkConditionsIcon.d.ts +4 -0
- package/lib/typescript/network/conditions/NetworkConditionsIcon.d.ts.map +1 -0
- package/lib/typescript/network/conditions/NetworkFloatingTool.d.ts +15 -0
- package/lib/typescript/network/conditions/NetworkFloatingTool.d.ts.map +1 -0
- package/lib/typescript/network/conditions/NetworkFloatingTool.test.d.ts +2 -0
- package/lib/typescript/network/conditions/NetworkFloatingTool.test.d.ts.map +1 -0
- package/lib/typescript/network/conditions/store.d.ts +30 -0
- package/lib/typescript/network/conditions/store.d.ts.map +1 -0
- package/lib/typescript/network/conditions/wait.d.ts +4 -0
- package/lib/typescript/network/conditions/wait.d.ts.map +1 -0
- package/lib/typescript/network/conditions/xhr.d.ts +5 -0
- package/lib/typescript/network/conditions/xhr.d.ts.map +1 -0
- package/lib/typescript/network/hooks/useNetworkEvents.d.ts +1 -1
- package/lib/typescript/network/overrides/applyToXhr.d.ts +2 -0
- package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -1
- package/lib/typescript/network/overrides/engine.d.ts +0 -10
- package/lib/typescript/network/overrides/engine.d.ts.map +1 -1
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts +17 -0
- package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/network/types/index.d.ts +1 -0
- package/lib/typescript/network/types/index.d.ts.map +1 -1
- package/lib/typescript/network/utils/generateNetworkCopyText.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkEventStore.d.ts +0 -15
- package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkListener.d.ts +2 -1
- package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
- package/lib/typescript/network/utils/networkSavedStore.d.ts +2 -2
- package/lib/typescript/network/utils/selectSavedEvents.d.ts +2 -2
- package/lib/typescript/preset.d.ts +0 -16
- package/lib/typescript/preset.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -4,6 +4,9 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
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
|
+
var _xhr = require("../conditions/xhr");
|
|
8
|
+
var _store = require("../conditions/store");
|
|
9
|
+
var _wait = require("../conditions/wait");
|
|
7
10
|
var _engine = require("../overrides/engine");
|
|
8
11
|
var _applyToXhr = require("../overrides/applyToXhr");
|
|
9
12
|
var _applyToFetch = require("../overrides/applyToFetch");
|
|
@@ -26,7 +29,7 @@ var _httpCompletion = require("./httpCompletion");
|
|
|
26
29
|
* executor, so a flag raised across the call expression alone is enough to
|
|
27
30
|
* cover the inner `send()` without leaking across the await.
|
|
28
31
|
*/
|
|
29
|
-
let
|
|
32
|
+
let insideFetchHandoff = false;
|
|
30
33
|
|
|
31
34
|
// Extended XMLHttpRequest interface for monkey-patching
|
|
32
35
|
|
|
@@ -105,9 +108,14 @@ const INTERNAL_HOSTS_KEY = "__BUOY_INTERNAL_HOSTS__";
|
|
|
105
108
|
function isInternalBuoyHost(url) {
|
|
106
109
|
const hosts = globalThis[INTERNAL_HOSTS_KEY];
|
|
107
110
|
if (!(hosts instanceof Set) || hosts.size === 0) return false;
|
|
108
|
-
|
|
111
|
+
let authority;
|
|
112
|
+
try {
|
|
113
|
+
authority = new URL(url).host.toLowerCase();
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
109
117
|
for (const host of hosts) {
|
|
110
|
-
if (typeof host === "string" && host &&
|
|
118
|
+
if (typeof host === "string" && host && authority === host.toLowerCase()) return true;
|
|
111
119
|
}
|
|
112
120
|
return false;
|
|
113
121
|
}
|
|
@@ -116,12 +124,6 @@ class NetworkListener {
|
|
|
116
124
|
isListening = false;
|
|
117
125
|
requestCounter = 1000;
|
|
118
126
|
|
|
119
|
-
// URLs to ignore (Metro bundler, symbolicate, etc.)
|
|
120
|
-
ignoredUrls = [/\/symbolicate$/, /\/logs$/, /\/debugger-proxy/, /\/reload$/, /\/launch-js-devtools/, /localhost:8081/, /100\.64\.\d+\.\d+:8081/,
|
|
121
|
-
// iOS simulator
|
|
122
|
-
/10\.0\.\d+\.\d+:8081/ // Android emulator
|
|
123
|
-
];
|
|
124
|
-
|
|
125
127
|
// Store original methods
|
|
126
128
|
|
|
127
129
|
/**
|
|
@@ -145,7 +147,7 @@ class NetworkListener {
|
|
|
145
147
|
xhrGeneration = 0;
|
|
146
148
|
constructor() {
|
|
147
149
|
// Store original methods
|
|
148
|
-
this.originalFetch = globalThis.fetch
|
|
150
|
+
this.originalFetch = globalThis.fetch;
|
|
149
151
|
this.originalXHROpen = XMLHttpRequest.prototype.open;
|
|
150
152
|
this.originalXHRSend = XMLHttpRequest.prototype.send;
|
|
151
153
|
this.originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
|
|
@@ -161,7 +163,14 @@ class NetworkListener {
|
|
|
161
163
|
* @returns True if the URL should be ignored
|
|
162
164
|
*/
|
|
163
165
|
shouldIgnoreUrl(url) {
|
|
164
|
-
|
|
166
|
+
try {
|
|
167
|
+
const parsed = new URL(url);
|
|
168
|
+
if (parsed.hostname === "keygen.sh" || parsed.hostname.endsWith(".keygen.sh")) return true;
|
|
169
|
+
const local = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "10.0.2.2";
|
|
170
|
+
if (local && ["8081", "8802"].includes(parsed.port)) return true;
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
165
174
|
return isInternalBuoyHost(url);
|
|
166
175
|
}
|
|
167
176
|
|
|
@@ -247,7 +256,7 @@ class NetworkListener {
|
|
|
247
256
|
out += decoder.decode(value, {
|
|
248
257
|
stream: true
|
|
249
258
|
});
|
|
250
|
-
if (
|
|
259
|
+
if (size >= maxSize) {
|
|
251
260
|
out = `${out.slice(0, maxSize)}\n\n... [truncated]`;
|
|
252
261
|
capped = true;
|
|
253
262
|
break;
|
|
@@ -344,14 +353,14 @@ class NetworkListener {
|
|
|
344
353
|
* Handle XMLHttpRequest response processing
|
|
345
354
|
* This method processes the response and emits appropriate events
|
|
346
355
|
*/
|
|
347
|
-
handleXHRResponse(xhr, requestId, cleanUrl, method, requestHeaders, requestData, params, startTime, isError = false, clientType = "xhr", override) {
|
|
356
|
+
handleXHRResponse(xhr, requestId, cleanUrl, method, requestHeaders, requestData, params, startTime, isError = false, clientType = "xhr", override, condition) {
|
|
348
357
|
const duration = startTime ? Date.now() - startTime : 0;
|
|
349
358
|
|
|
350
359
|
// XHR's duration was ALREADY total (measured at readyState 4 / load), so
|
|
351
|
-
// only the split is new. `
|
|
360
|
+
// only the split is new. `_buoyTtfb` is stamped at HEADERS_RECEIVED; it is
|
|
352
361
|
// absent for a request that failed before any headers arrived, and the
|
|
353
362
|
// breakdown is then correctly omitted rather than faked as 0.
|
|
354
|
-
const ttfb = xhr.
|
|
363
|
+
const ttfb = xhr._buoyTtfb;
|
|
355
364
|
const timings = ttfb === undefined ? undefined : {
|
|
356
365
|
ttfb,
|
|
357
366
|
download: Math.max(0, duration - ttfb)
|
|
@@ -374,7 +383,8 @@ class NetworkListener {
|
|
|
374
383
|
error: {
|
|
375
384
|
message: isError ? "Request failed" : "Network error or request aborted"
|
|
376
385
|
},
|
|
377
|
-
override
|
|
386
|
+
override,
|
|
387
|
+
condition
|
|
378
388
|
});
|
|
379
389
|
return;
|
|
380
390
|
}
|
|
@@ -516,7 +526,8 @@ class NetworkListener {
|
|
|
516
526
|
body,
|
|
517
527
|
size: responseSize
|
|
518
528
|
},
|
|
519
|
-
override
|
|
529
|
+
override,
|
|
530
|
+
condition
|
|
520
531
|
});
|
|
521
532
|
} else {
|
|
522
533
|
this.emit({
|
|
@@ -542,7 +553,8 @@ class NetworkListener {
|
|
|
542
553
|
error: {
|
|
543
554
|
message: "Network Error"
|
|
544
555
|
},
|
|
545
|
-
override
|
|
556
|
+
override,
|
|
557
|
+
condition
|
|
546
558
|
});
|
|
547
559
|
}
|
|
548
560
|
}
|
|
@@ -608,7 +620,7 @@ class NetworkListener {
|
|
|
608
620
|
// re-installing after a clobber must call through the replacement, and a
|
|
609
621
|
// stale generation must replay the chain exactly as it stood beneath it.
|
|
610
622
|
const passthroughFetch = globalThis.fetch.bind(globalThis);
|
|
611
|
-
this.originalFetch =
|
|
623
|
+
this.originalFetch = globalThis.fetch;
|
|
612
624
|
const generation = ++this.fetchGeneration;
|
|
613
625
|
const wrappedFetch = async (input, init) => {
|
|
614
626
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
@@ -626,7 +638,11 @@ class NetworkListener {
|
|
|
626
638
|
}
|
|
627
639
|
const startTime = Date.now();
|
|
628
640
|
const requestId = `fetch_${++self.requestCounter}`;
|
|
629
|
-
const
|
|
641
|
+
const inputRequest = typeof input === "object" && "method" in input ? input : undefined;
|
|
642
|
+
const method = init?.method ?? inputRequest?.method ?? "GET";
|
|
643
|
+
const signal = init?.signal !== undefined ? init.signal : inputRequest?.signal;
|
|
644
|
+
(0, _wait.throwIfAborted)(signal);
|
|
645
|
+
const condition = (0, _store.snapshotNetworkCondition)(url);
|
|
630
646
|
const {
|
|
631
647
|
url: cleanUrl,
|
|
632
648
|
params
|
|
@@ -634,17 +650,18 @@ class NetworkListener {
|
|
|
634
650
|
|
|
635
651
|
// Parse request headers
|
|
636
652
|
let requestHeaders = {};
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
653
|
+
const headers = init?.headers ?? inputRequest?.headers;
|
|
654
|
+
if (headers) {
|
|
655
|
+
if (headers instanceof Headers) {
|
|
656
|
+
headers.forEach((value, key) => {
|
|
640
657
|
requestHeaders[key] = value;
|
|
641
658
|
});
|
|
642
|
-
} else if (Array.isArray(
|
|
643
|
-
|
|
659
|
+
} else if (Array.isArray(headers)) {
|
|
660
|
+
headers.forEach(([key, value]) => {
|
|
644
661
|
requestHeaders[key] = value;
|
|
645
662
|
});
|
|
646
663
|
} else {
|
|
647
|
-
requestHeaders =
|
|
664
|
+
requestHeaders = headers;
|
|
648
665
|
}
|
|
649
666
|
}
|
|
650
667
|
|
|
@@ -676,7 +693,7 @@ class NetworkListener {
|
|
|
676
693
|
* Skipped when `canSynthesizeResponse()` is false so a runtime without a
|
|
677
694
|
* `Response` global degrades to a real request rather than a broken one.
|
|
678
695
|
*/
|
|
679
|
-
const outcome = (0, _applyToFetch.canSynthesizeResponse)() ? (0, _engine.overrideForRequest)(url, method) : null;
|
|
696
|
+
const outcome = condition?.profile !== "offline" && (0, _applyToFetch.canSynthesizeResponse)() ? (0, _engine.overrideForRequest)(url, method) : null;
|
|
680
697
|
const override = outcome ? (0, _types.markForOutcome)(outcome) : undefined;
|
|
681
698
|
|
|
682
699
|
// Emit request event
|
|
@@ -692,24 +709,28 @@ class NetworkListener {
|
|
|
692
709
|
params: params || undefined,
|
|
693
710
|
client: clientType
|
|
694
711
|
},
|
|
695
|
-
override
|
|
712
|
+
override,
|
|
713
|
+
condition
|
|
696
714
|
});
|
|
697
715
|
try {
|
|
698
|
-
if (outcome) await (0,
|
|
716
|
+
if (condition || outcome) await (0, _wait.waitBeforeDispatch)((condition?.delayMs ?? 0) + (outcome?.delayMs ?? 0), signal);
|
|
717
|
+
(0, _wait.throwIfAborted)(signal);
|
|
718
|
+
if (condition?.profile === "offline") throw new TypeError("Network request failed (Buoy Offline)");
|
|
699
719
|
if (outcome?.kind === "fail") throw (0, _applyToFetch.synthesizeFetchError)(outcome);
|
|
700
720
|
let response;
|
|
701
721
|
if (outcome?.kind === "respond") {
|
|
702
722
|
response = (0, _applyToFetch.synthesizeResponse)(url, outcome);
|
|
703
723
|
} else {
|
|
704
|
-
// The flag spans the CALL only — see `
|
|
724
|
+
// The flag spans the CALL only — see `insideFetchHandoff`. It has
|
|
705
725
|
// to be cleared before the await, or it would still be raised for
|
|
706
726
|
// every unrelated request that starts while this one is in flight.
|
|
707
|
-
|
|
727
|
+
const previousFetchHandoff = insideFetchHandoff;
|
|
728
|
+
insideFetchHandoff = true;
|
|
708
729
|
let pending;
|
|
709
730
|
try {
|
|
710
731
|
pending = passthroughFetch(input, init);
|
|
711
732
|
} finally {
|
|
712
|
-
|
|
733
|
+
insideFetchHandoff = previousFetchHandoff;
|
|
713
734
|
}
|
|
714
735
|
response = await pending;
|
|
715
736
|
}
|
|
@@ -731,7 +752,7 @@ class NetworkListener {
|
|
|
731
752
|
* known, and the body is filled in when the stream closes: the store
|
|
732
753
|
* merges a second `response` event onto the same request id.
|
|
733
754
|
*/
|
|
734
|
-
const streamed = isStreamedContentType(response.headers?.get?.("content-type"));
|
|
755
|
+
const streamed = typeof response.body?.getReader === "function" || isStreamedContentType(response.headers?.get?.("content-type"));
|
|
735
756
|
|
|
736
757
|
// Clone response to read body with size limits
|
|
737
758
|
const responseClone = response.clone();
|
|
@@ -798,11 +819,18 @@ class NetworkListener {
|
|
|
798
819
|
body,
|
|
799
820
|
size: responseSize
|
|
800
821
|
},
|
|
801
|
-
override
|
|
822
|
+
override,
|
|
823
|
+
condition
|
|
802
824
|
});
|
|
803
825
|
if (streamed) {
|
|
804
826
|
// Bounded, cancelled at the cap, and never awaited by the caller.
|
|
805
827
|
void self.readStreamedBody(responseClone).then(late => {
|
|
828
|
+
let capturedBody = late.body;
|
|
829
|
+
if (response.headers?.get?.("content-type")?.includes("json")) {
|
|
830
|
+
try {
|
|
831
|
+
capturedBody = JSON.parse(late.body);
|
|
832
|
+
} catch {/* Partial or malformed JSON stays text. */}
|
|
833
|
+
}
|
|
806
834
|
self.emit({
|
|
807
835
|
type: "response",
|
|
808
836
|
timestamp: new Date(),
|
|
@@ -821,10 +849,11 @@ class NetworkListener {
|
|
|
821
849
|
status: response.status,
|
|
822
850
|
statusText: response.statusText,
|
|
823
851
|
headers: responseHeaders,
|
|
824
|
-
body:
|
|
852
|
+
body: capturedBody,
|
|
825
853
|
size: late.size
|
|
826
854
|
},
|
|
827
|
-
override
|
|
855
|
+
override,
|
|
856
|
+
condition
|
|
828
857
|
});
|
|
829
858
|
}).catch(() => {
|
|
830
859
|
/* the caller's response is unaffected either way */
|
|
@@ -852,7 +881,8 @@ class NetworkListener {
|
|
|
852
881
|
message: error instanceof Error ? error.message : "Network error",
|
|
853
882
|
stack: error instanceof Error ? error.stack : undefined
|
|
854
883
|
},
|
|
855
|
-
override
|
|
884
|
+
override,
|
|
885
|
+
condition
|
|
856
886
|
});
|
|
857
887
|
throw error;
|
|
858
888
|
}
|
|
@@ -873,18 +903,20 @@ class NetworkListener {
|
|
|
873
903
|
this.originalXHRSend = passthroughSend;
|
|
874
904
|
this.originalXHRSetRequestHeader = passthroughSetRequestHeader;
|
|
875
905
|
const generation = ++this.xhrGeneration;
|
|
906
|
+
const pendingCapture = new WeakMap();
|
|
876
907
|
const patchedOpen = function (method, url, async, user, password) {
|
|
877
|
-
//
|
|
878
|
-
|
|
908
|
+
// Native validation runs before replacing metadata or capture listeners.
|
|
909
|
+
const result = passthroughOpen.call(this, method, url, async ?? true, user, password);
|
|
879
910
|
if (generation === self.xhrGeneration) {
|
|
911
|
+
pendingCapture.get(this)?.();
|
|
880
912
|
const xhr = this;
|
|
881
|
-
xhr.
|
|
913
|
+
xhr._buoyRequestId = `xhr_${++self.requestCounter}`;
|
|
882
914
|
xhr._method = method;
|
|
883
915
|
xhr._url = url;
|
|
884
|
-
xhr.
|
|
916
|
+
xhr._buoyStartTime = Date.now();
|
|
885
917
|
xhr._requestHeaders = {};
|
|
886
918
|
}
|
|
887
|
-
return
|
|
919
|
+
return result;
|
|
888
920
|
};
|
|
889
921
|
|
|
890
922
|
// Track request headers
|
|
@@ -899,14 +931,19 @@ class NetworkListener {
|
|
|
899
931
|
// @ts-ignore - this does exist on native
|
|
900
932
|
data) {
|
|
901
933
|
// Superseded generation: silent pass-through (see the fetch wrapper).
|
|
902
|
-
if (generation !== self.xhrGeneration) {
|
|
934
|
+
if (generation !== self.xhrGeneration || insideFetchHandoff) {
|
|
935
|
+
return passthroughSend.call(this, data);
|
|
936
|
+
}
|
|
937
|
+
if (this.readyState !== 1 || this._sent) {
|
|
903
938
|
return passthroughSend.call(this, data);
|
|
904
939
|
}
|
|
905
940
|
const xhr = this;
|
|
906
|
-
const requestId = xhr.
|
|
941
|
+
const requestId = xhr._buoyRequestId;
|
|
907
942
|
const method = xhr._method || "GET";
|
|
908
943
|
const url = xhr._url || "";
|
|
909
|
-
const startTime =
|
|
944
|
+
const startTime = Date.now();
|
|
945
|
+
xhr._buoyStartTime = startTime;
|
|
946
|
+
xhr._buoyTtfb = undefined;
|
|
910
947
|
const requestHeaders = xhr._requestHeaders || {};
|
|
911
948
|
|
|
912
949
|
// Skip ignored URLs
|
|
@@ -946,14 +983,15 @@ class NetworkListener {
|
|
|
946
983
|
* This is the layer that covers `axios` and raw XMLHttpRequest. It also
|
|
947
984
|
* covers `fetch` on BARE React Native, where the whatwg-fetch polyfill
|
|
948
985
|
* runs through XHR — which is why the fetch wrapper raises
|
|
949
|
-
* `
|
|
986
|
+
* `insideFetchHandoff` around its pass-through, so one request can't
|
|
950
987
|
* be overridden twice.
|
|
951
988
|
*
|
|
952
989
|
* Matching uses the raw `url` (query string included), which is what the
|
|
953
990
|
* developer sees in the list and writes into a rule. No-ops in production
|
|
954
991
|
* builds; see `overrideForRequest`.
|
|
955
992
|
*/
|
|
956
|
-
const
|
|
993
|
+
const condition = (0, _store.snapshotNetworkCondition)(url);
|
|
994
|
+
const outcome = condition?.profile === "offline" ? null : (0, _engine.overrideForRequest)(url, method);
|
|
957
995
|
const override = outcome ? (0, _types.markForOutcome)(outcome) : undefined;
|
|
958
996
|
|
|
959
997
|
// Emit request event
|
|
@@ -969,7 +1007,8 @@ class NetworkListener {
|
|
|
969
1007
|
params: params || undefined,
|
|
970
1008
|
client: clientType
|
|
971
1009
|
},
|
|
972
|
-
override
|
|
1010
|
+
override,
|
|
1011
|
+
condition
|
|
973
1012
|
});
|
|
974
1013
|
|
|
975
1014
|
// Track if we've already processed this request to avoid duplicate events
|
|
@@ -977,18 +1016,20 @@ class NetworkListener {
|
|
|
977
1016
|
const processResponse = (isError = false) => {
|
|
978
1017
|
if (processed) return;
|
|
979
1018
|
processed = true;
|
|
980
|
-
self.handleXHRResponse(this, requestId || "unknown", cleanUrl, method, requestHeaders, requestData, params, startTime || 0, isError, clientType, override);
|
|
1019
|
+
self.handleXHRResponse(this, requestId || "unknown", cleanUrl, method, requestHeaders, requestData, params, startTime || 0, isError, clientType, override, condition);
|
|
981
1020
|
// Clean up event listeners after processing to prevent memory leaks
|
|
982
1021
|
cleanup();
|
|
983
1022
|
};
|
|
984
1023
|
|
|
985
1024
|
// Cleanup function to remove event listeners
|
|
986
1025
|
const cleanup = () => {
|
|
1026
|
+
pendingCapture.delete(this);
|
|
987
1027
|
this.removeEventListener("load", loadListener);
|
|
988
1028
|
this.removeEventListener("error", errorListener);
|
|
989
1029
|
this.removeEventListener("abort", abortListener);
|
|
990
1030
|
this.removeEventListener("readystatechange", readyStateListener);
|
|
991
1031
|
};
|
|
1032
|
+
pendingCapture.set(this, cleanup);
|
|
992
1033
|
|
|
993
1034
|
// Use addEventListener to listen to events WITHOUT replacing user handlers
|
|
994
1035
|
// This is critical because React Native's XMLHttpRequest uses EventTarget
|
|
@@ -1010,8 +1051,8 @@ class NetworkListener {
|
|
|
1010
1051
|
// TTFB here costs one comparison and one clock read.
|
|
1011
1052
|
if (this.readyState === 2) {
|
|
1012
1053
|
const xhr = this;
|
|
1013
|
-
if (xhr.
|
|
1014
|
-
xhr.
|
|
1054
|
+
if (xhr._buoyTtfb === undefined && xhr._buoyStartTime) {
|
|
1055
|
+
xhr._buoyTtfb = Date.now() - xhr._buoyStartTime;
|
|
1015
1056
|
}
|
|
1016
1057
|
}
|
|
1017
1058
|
if (this.readyState === 4 && !processed) {
|
|
@@ -1024,6 +1065,10 @@ class NetworkListener {
|
|
|
1024
1065
|
this.addEventListener("error", errorListener);
|
|
1025
1066
|
this.addEventListener("abort", abortListener);
|
|
1026
1067
|
this.addEventListener("readystatechange", readyStateListener);
|
|
1068
|
+
if (condition) {
|
|
1069
|
+
(0, _xhr.runConditionXhr)(this, url, condition, outcome, () => passthroughSend.call(this, data));
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1027
1072
|
if (outcome) {
|
|
1028
1073
|
// Listeners are attached above FIRST: `runOverride` drives the same
|
|
1029
1074
|
// readyState transitions a real response would, and the interceptor has
|
|
@@ -44,8 +44,8 @@ const MAX_PINNED = exports.MAX_PINNED = 25;
|
|
|
44
44
|
const MAX_SAVED = exports.MAX_SAVED = 50;
|
|
45
45
|
/**
|
|
46
46
|
* @deprecated Read the live limit with `useLimit("network.saved")` — this only
|
|
47
|
-
* reports the FREE tier's value and
|
|
48
|
-
*
|
|
47
|
+
* reports the FREE tier's value and ignores the Weekend Pass. Kept because it
|
|
48
|
+
* is part of the package's public exports.
|
|
49
49
|
*/
|
|
50
50
|
const FREE_TIER_SAVED_LIMIT = exports.FREE_TIER_SAVED_LIMIT = _sharedUi.TIER_LIMITS["network.saved"].free;
|
|
51
51
|
/** Per-body cap on a snapshot. */
|
|
@@ -19,8 +19,8 @@ var _networkSavedStore = require("./networkSavedStore");
|
|
|
19
19
|
function selectSavedEvents(savedRecords,
|
|
20
20
|
/**
|
|
21
21
|
* How many saves this tier may see — `useLimit("network.saved")`. Takes a
|
|
22
|
-
* number rather than `isPro` because
|
|
23
|
-
* boolean cannot express
|
|
22
|
+
* number rather than `isPro` because Pro's cap is 50, not unlimited, and a
|
|
23
|
+
* boolean cannot express that.
|
|
24
24
|
*/
|
|
25
25
|
limit, search) {
|
|
26
26
|
/**
|
package/lib/commonjs/preset.js
CHANGED
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.networkToolPreset = exports.createNetworkTool = void 0;
|
|
7
|
+
var _NetworkConditionsIcon = require("./network/conditions/NetworkConditionsIcon");
|
|
7
8
|
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
8
9
|
var _NetworkModal = require("./network/components/NetworkModal");
|
|
9
10
|
var _overrideRulesStore = require("./network/overrides/overrideRulesStore");
|
|
@@ -72,7 +73,7 @@ const networkToolPreset = exports.networkToolPreset = (0, _sharedUi.defineToolPr
|
|
|
72
73
|
slot: "both",
|
|
73
74
|
icon: ({
|
|
74
75
|
size
|
|
75
|
-
}) => /*#__PURE__*/(0, _jsxRuntime.jsx)(
|
|
76
|
+
}) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkConditionsIcon.NetworkConditionsIcon, {
|
|
76
77
|
size: size
|
|
77
78
|
}),
|
|
78
79
|
component: _NetworkModal.NetworkModal,
|
package/lib/module/index.js
CHANGED
|
@@ -99,4 +99,5 @@ export { networkEventStore } from "./network/utils/networkEventStore";
|
|
|
99
99
|
export { networkSavedStore, flagsForEventId, FREE_TIER_SAVED_LIMIT, MAX_PINNED, MAX_SAVED } from "./network/utils/networkSavedStore";
|
|
100
100
|
export { generateCurl } from "./network/utils/generateCurl";
|
|
101
101
|
/** @internal */
|
|
102
|
-
export { networkListener, addNetworkListener, removeAllNetworkListeners } from "./network/utils/networkListener";
|
|
102
|
+
export { networkListener, addNetworkListener, removeAllNetworkListeners } from "./network/utils/networkListener";
|
|
103
|
+
export { NETWORK_CONDITIONS, getNetworkCondition, setNetworkCondition, subscribeNetworkConditions } from "./network/conditions/store";
|
package/lib/module/index.web.js
CHANGED
|
@@ -78,4 +78,5 @@ export { ruleMatches } from "./network/overrides/matchRule";
|
|
|
78
78
|
export { filterNetworkEvents, applyIgnoredPatterns } from "./network/utils/filterNetworkEvents";
|
|
79
79
|
export { splitPinned } from "./network/utils/withPinnedFirst";
|
|
80
80
|
export { extractOperationName } from "./network/utils/extractOperationName";
|
|
81
|
-
export { generateCurl } from "./network/utils/generateCurl";
|
|
81
|
+
export { generateCurl } from "./network/utils/generateCurl";
|
|
82
|
+
export { NETWORK_CONDITIONS, getNetworkCondition, setNetworkCondition, subscribeNetworkConditions } from "./network/conditions/store";
|
|
@@ -446,7 +446,12 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
|
|
|
446
446
|
style: styles.pendingBadgeText,
|
|
447
447
|
children: "Pending"
|
|
448
448
|
})]
|
|
449
|
-
}) : null, event.
|
|
449
|
+
}) : null, event.condition && /*#__PURE__*/_jsxs(Text, {
|
|
450
|
+
style: {
|
|
451
|
+
color: "#FFCC66"
|
|
452
|
+
},
|
|
453
|
+
children: ["Conditions: ", event.condition.profile, " (+", event.condition.delayMs, " ms)"]
|
|
454
|
+
}), event.duration ? /*#__PURE__*/_jsxs(View, {
|
|
450
455
|
style: styles.httpDuration,
|
|
451
456
|
children: [/*#__PURE__*/_jsx(Clock, {
|
|
452
457
|
size: 10,
|
|
@@ -76,11 +76,15 @@ export function NetworkHeaderMenuItem({
|
|
|
76
76
|
icon,
|
|
77
77
|
label,
|
|
78
78
|
detail,
|
|
79
|
+
testID,
|
|
79
80
|
onPress
|
|
80
81
|
}) {
|
|
81
82
|
return /*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
82
83
|
onPress: onPress,
|
|
83
84
|
"sentry-label": "ignore header menu item",
|
|
85
|
+
testID: testID,
|
|
86
|
+
accessibilityRole: "button",
|
|
87
|
+
accessibilityLabel: label,
|
|
84
88
|
style: styles.item,
|
|
85
89
|
children: [/*#__PURE__*/_jsx(View, {
|
|
86
90
|
style: styles.itemIcon,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
import { createRemoteConditionsSource, openNetworkFloatingTool } from "../conditions/NetworkFloatingTool";
|
|
3
4
|
import { useState, useRef, useEffect, useCallback } from "react";
|
|
4
5
|
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
5
|
-
import { Search, Filter, Bookmark, JsModal, ModalHeader, devToolsStorageKeys, macOSColors, night, persistentStorage, NightSegmented, useFeatureGate, UpgradeModal, PowerToggleButton, useIgnoredPatterns, windowDragProps } from "@buoy-gg/shared-ui";
|
|
6
|
+
import { Search, Wifi, Filter, Bookmark, JsModal, ModalHeader, devToolsStorageKeys, macOSColors, night, persistentStorage, NightSegmented, useFeatureGate, UpgradeModal, PowerToggleButton, useIgnoredPatterns, windowDragProps } from "@buoy-gg/shared-ui";
|
|
6
7
|
import { NetworkFilterViewV3 } from "./NetworkFilterViewV3";
|
|
7
8
|
import { TickProvider } from "../hooks/useTickEveryMinute";
|
|
8
9
|
import { NetworkEventDetailView } from "./NetworkEventDetailView";
|
|
@@ -178,7 +179,8 @@ function NetworkModalInner({
|
|
|
178
179
|
enableSharedModalDimensions = false,
|
|
179
180
|
inline = false,
|
|
180
181
|
paused: pausedProp,
|
|
181
|
-
onPausedChange
|
|
182
|
+
onPausedChange,
|
|
183
|
+
remoteConditions
|
|
182
184
|
}) {
|
|
183
185
|
const {
|
|
184
186
|
isPro
|
|
@@ -208,6 +210,12 @@ function NetworkModalInner({
|
|
|
208
210
|
const [showSavedView, setShowSavedView] = useState(false);
|
|
209
211
|
const [showOverridesView, setShowOverridesView] = useState(false);
|
|
210
212
|
const [showHeaderMenu, setShowHeaderMenu] = useState(false);
|
|
213
|
+
const [minimizeRequest, setMinimizeRequest] = useState(0);
|
|
214
|
+
const [remoteConditionsSource] = useState(createRemoteConditionsSource);
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
remoteConditionsSource.update(remoteConditions);
|
|
217
|
+
}, [remoteConditionsSource, remoteConditions]);
|
|
218
|
+
useEffect(() => () => remoteConditionsSource.update(undefined), [remoteConditionsSource]);
|
|
211
219
|
/**
|
|
212
220
|
* Overrides are a DEVICE capability — a rule runs inside the app's own
|
|
213
221
|
* `fetch`/`XMLHttpRequest`, which the dashboard doesn't have.
|
|
@@ -488,7 +496,7 @@ function NetworkModalInner({
|
|
|
488
496
|
}) : null, /*#__PURE__*/_jsx(PowerToggleButton, {
|
|
489
497
|
isEnabled: !paused,
|
|
490
498
|
onToggle: () => setPaused(prev => !prev),
|
|
491
|
-
accessibilityLabel: "
|
|
499
|
+
accessibilityLabel: "Pause network list",
|
|
492
500
|
testID: "buoy-network-power"
|
|
493
501
|
}), /*#__PURE__*/_jsx(NetworkClearButton, {
|
|
494
502
|
paused: paused
|
|
@@ -586,6 +594,18 @@ function NetworkModalInner({
|
|
|
586
594
|
}), showHeaderMenu ? /*#__PURE__*/_jsxs(NetworkHeaderMenu, {
|
|
587
595
|
onClose: () => setShowHeaderMenu(false),
|
|
588
596
|
children: [/*#__PURE__*/_jsx(NetworkHeaderMenuItem, {
|
|
597
|
+
icon: /*#__PURE__*/_jsx(Wifi, {
|
|
598
|
+
size: 14,
|
|
599
|
+
color: night.color.textSecondary
|
|
600
|
+
}),
|
|
601
|
+
label: "Network throttling",
|
|
602
|
+
testID: "buoy-network-menu-throttling",
|
|
603
|
+
onPress: () => {
|
|
604
|
+
setShowHeaderMenu(false);
|
|
605
|
+
openNetworkFloatingTool(remoteConditions ? remoteConditionsSource : undefined);
|
|
606
|
+
if (!inline) setMinimizeRequest(value => value + 1);
|
|
607
|
+
}
|
|
608
|
+
}), /*#__PURE__*/_jsx(NetworkHeaderMenuItem, {
|
|
589
609
|
icon: /*#__PURE__*/_jsx(Filter, {
|
|
590
610
|
size: 14,
|
|
591
611
|
color: hasActiveFilter ? night.color.accent : night.color.textSecondary
|
|
@@ -624,12 +644,7 @@ function NetworkModalInner({
|
|
|
624
644
|
})]
|
|
625
645
|
}) : null, /*#__PURE__*/_jsx(UpgradeModal, {
|
|
626
646
|
visible: showUpgradeModal,
|
|
627
|
-
onClose: () => setShowUpgradeModal(false)
|
|
628
|
-
// "saved" is a count limit a free key can raise; "overrides" is one it
|
|
629
|
-
// can't (1 on both tiers) and "export" is Pro-only — freeKeyRaises
|
|
630
|
-
// sorts those out, so the mapping here can stay literal.
|
|
631
|
-
,
|
|
632
|
-
limitKey: upgradeReason === "saved" ? "network.saved" : upgradeReason === "overrides" ? "network.overrideRules" : undefined,
|
|
647
|
+
onClose: () => setShowUpgradeModal(false),
|
|
633
648
|
featureName: upgradeReason === "saved" ? "Saved Requests" : upgradeReason === "overrides" ? "Response Overrides" : "Network Export",
|
|
634
649
|
description: upgradeReason === "saved" ? "Keep the requests that matter. Saved requests survive Clear, app restarts, and the 500-request history cap." : upgradeReason === "overrides" ? "Force any endpoint to fail, stall or return something else — no backend changes, no code changes. Free keeps one active rule." : "Export network requests to share with your team, debug APIs, or feed into AI tools.",
|
|
635
650
|
benefits: upgradeReason === "overrides" ? [`Run ${MAX_OVERRIDE_RULES} rules at once (free keeps ${FREE_TIER_OVERRIDE_LIMIT})`, "Force a 500, a 401 or an empty list in one tap", "Simulate offline, timeouts and slow connections", "Rules survive an app reload, so you can test the boot path"] : upgradeReason === "saved" ? [`Save up to ${MAX_SAVED} requests (free keeps ${FREE_TIER_SAVED_LIMIT})`, "Failed requests stay put through a Clear", "Snapshots survive an app restart", "Export saved requests to teammates or AI assistants"] : ["Export to JSON, Markdown, or plain text", "Customizable copy presets (URLs only, LLM-ready, full data)", "Share request details with teammates", "Feed network data directly to AI assistants"]
|
|
@@ -652,6 +667,7 @@ function NetworkModalInner({
|
|
|
652
667
|
visible: visible,
|
|
653
668
|
onClose: onClose,
|
|
654
669
|
onMinimize: onMinimize,
|
|
670
|
+
minimizeRequest: minimizeRequest,
|
|
655
671
|
persistenceKey: persistenceKey,
|
|
656
672
|
variant: "night",
|
|
657
673
|
header: {
|