@buoy-gg/network 7.0.36 → 7.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +45 -12
  2. package/lib/commonjs/index.js +26 -1
  3. package/lib/commonjs/index.web.js +26 -1
  4. package/lib/commonjs/network/components/NetworkEventDetailView.js +6 -1
  5. package/lib/commonjs/network/components/NetworkHeaderMenu.js +4 -0
  6. package/lib/commonjs/network/components/NetworkModal.js +24 -8
  7. package/lib/commonjs/network/conditions/NetworkConditionsControl.js +235 -0
  8. package/lib/commonjs/network/conditions/NetworkConditionsControl.test.js +136 -0
  9. package/lib/commonjs/network/conditions/NetworkConditionsIcon.js +33 -0
  10. package/lib/commonjs/network/conditions/NetworkFloatingTool.js +310 -0
  11. package/lib/commonjs/network/conditions/NetworkFloatingTool.test.js +219 -0
  12. package/lib/commonjs/network/conditions/store.js +75 -0
  13. package/lib/commonjs/network/conditions/wait.js +43 -0
  14. package/lib/commonjs/network/conditions/xhr.js +110 -0
  15. package/lib/commonjs/network/hooks/useNetworkEvents.js +1 -1
  16. package/lib/commonjs/network/overrides/applyToXhr.js +10 -8
  17. package/lib/commonjs/network/overrides/engine.js +2 -1
  18. package/lib/commonjs/network/sync/networkSyncAdapter.js +26 -1
  19. package/lib/commonjs/network/utils/generateNetworkCopyText.js +2 -0
  20. package/lib/commonjs/network/utils/networkEventStore.js +5 -77
  21. package/lib/commonjs/network/utils/networkListener.js +96 -51
  22. package/lib/commonjs/network/utils/networkSavedStore.js +2 -2
  23. package/lib/commonjs/network/utils/selectSavedEvents.js +2 -2
  24. package/lib/commonjs/preset.js +2 -1
  25. package/lib/module/index.js +2 -1
  26. package/lib/module/index.web.js +2 -1
  27. package/lib/module/network/components/NetworkEventDetailView.js +6 -1
  28. package/lib/module/network/components/NetworkHeaderMenu.js +4 -0
  29. package/lib/module/network/components/NetworkModal.js +25 -9
  30. package/lib/module/network/conditions/NetworkConditionsControl.js +231 -0
  31. package/lib/module/network/conditions/NetworkConditionsControl.test.js +135 -0
  32. package/lib/module/network/conditions/NetworkConditionsIcon.js +29 -0
  33. package/lib/module/network/conditions/NetworkFloatingTool.js +304 -0
  34. package/lib/module/network/conditions/NetworkFloatingTool.test.js +218 -0
  35. package/lib/module/network/conditions/store.js +65 -0
  36. package/lib/module/network/conditions/wait.js +38 -0
  37. package/lib/module/network/conditions/xhr.js +106 -0
  38. package/lib/module/network/hooks/useNetworkEvents.js +1 -1
  39. package/lib/module/network/overrides/applyToXhr.js +9 -8
  40. package/lib/module/network/overrides/engine.js +2 -1
  41. package/lib/module/network/sync/networkSyncAdapter.js +26 -1
  42. package/lib/module/network/utils/generateNetworkCopyText.js +2 -0
  43. package/lib/module/network/utils/networkEventStore.js +6 -77
  44. package/lib/module/network/utils/networkListener.js +97 -52
  45. package/lib/module/network/utils/networkSavedStore.js +2 -2
  46. package/lib/module/network/utils/selectSavedEvents.js +2 -2
  47. package/lib/module/preset.js +3 -2
  48. package/lib/typescript/index.d.ts +2 -0
  49. package/lib/typescript/index.d.ts.map +1 -1
  50. package/lib/typescript/index.web.d.ts +2 -0
  51. package/lib/typescript/index.web.d.ts.map +1 -1
  52. package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
  53. package/lib/typescript/network/components/NetworkHeaderMenu.d.ts +2 -1
  54. package/lib/typescript/network/components/NetworkHeaderMenu.d.ts.map +1 -1
  55. package/lib/typescript/network/components/NetworkModal.d.ts +3 -0
  56. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
  57. package/lib/typescript/network/conditions/NetworkConditionsControl.d.ts +12 -0
  58. package/lib/typescript/network/conditions/NetworkConditionsControl.d.ts.map +1 -0
  59. package/lib/typescript/network/conditions/NetworkConditionsControl.test.d.ts +2 -0
  60. package/lib/typescript/network/conditions/NetworkConditionsControl.test.d.ts.map +1 -0
  61. package/lib/typescript/network/conditions/NetworkConditionsIcon.d.ts +4 -0
  62. package/lib/typescript/network/conditions/NetworkConditionsIcon.d.ts.map +1 -0
  63. package/lib/typescript/network/conditions/NetworkFloatingTool.d.ts +15 -0
  64. package/lib/typescript/network/conditions/NetworkFloatingTool.d.ts.map +1 -0
  65. package/lib/typescript/network/conditions/NetworkFloatingTool.test.d.ts +2 -0
  66. package/lib/typescript/network/conditions/NetworkFloatingTool.test.d.ts.map +1 -0
  67. package/lib/typescript/network/conditions/store.d.ts +30 -0
  68. package/lib/typescript/network/conditions/store.d.ts.map +1 -0
  69. package/lib/typescript/network/conditions/wait.d.ts +4 -0
  70. package/lib/typescript/network/conditions/wait.d.ts.map +1 -0
  71. package/lib/typescript/network/conditions/xhr.d.ts +5 -0
  72. package/lib/typescript/network/conditions/xhr.d.ts.map +1 -0
  73. package/lib/typescript/network/hooks/useNetworkEvents.d.ts +1 -1
  74. package/lib/typescript/network/overrides/applyToXhr.d.ts +2 -0
  75. package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -1
  76. package/lib/typescript/network/overrides/engine.d.ts +0 -10
  77. package/lib/typescript/network/overrides/engine.d.ts.map +1 -1
  78. package/lib/typescript/network/sync/networkSyncAdapter.d.ts +17 -0
  79. package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
  80. package/lib/typescript/network/types/index.d.ts +1 -0
  81. package/lib/typescript/network/types/index.d.ts.map +1 -1
  82. package/lib/typescript/network/utils/generateNetworkCopyText.d.ts.map +1 -1
  83. package/lib/typescript/network/utils/networkEventStore.d.ts +0 -15
  84. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
  85. package/lib/typescript/network/utils/networkListener.d.ts +2 -1
  86. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
  87. package/lib/typescript/network/utils/networkSavedStore.d.ts +2 -2
  88. package/lib/typescript/network/utils/selectSavedEvents.d.ts +2 -2
  89. package/lib/typescript/preset.d.ts +0 -16
  90. package/lib/typescript/preset.d.ts.map +1 -1
  91. 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 insideOverriddenFetch = false;
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
- const lower = url.toLowerCase();
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 && lower.includes(host)) return true;
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.bind(globalThis);
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
- if (this.ignoredUrls.some(pattern => pattern.test(url))) return true;
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 (out.length >= maxSize) {
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. `_ttfb` is stamped at HEADERS_RECEIVED; it is
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._ttfb;
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 = passthroughFetch;
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 method = init?.method || "GET";
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
- if (init?.headers) {
638
- if (init.headers instanceof Headers) {
639
- init.headers.forEach((value, key) => {
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(init.headers)) {
643
- init.headers.forEach(([key, value]) => {
659
+ } else if (Array.isArray(headers)) {
660
+ headers.forEach(([key, value]) => {
644
661
  requestHeaders[key] = value;
645
662
  });
646
663
  } else {
647
- requestHeaders = init.headers;
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, _applyToFetch.sleep)(outcome.delayMs);
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 `insideOverriddenFetch`. It has
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
- insideOverriddenFetch = outcome != null;
727
+ const previousFetchHandoff = insideFetchHandoff;
728
+ insideFetchHandoff = true;
708
729
  let pending;
709
730
  try {
710
731
  pending = passthroughFetch(input, init);
711
732
  } finally {
712
- insideOverriddenFetch = false;
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: late.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
- // Superseded generations must not re-stamp (a double stamp burns two
878
- // request ids per request) — just pass through.
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._requestId = `xhr_${++self.requestCounter}`;
913
+ xhr._buoyRequestId = `xhr_${++self.requestCounter}`;
882
914
  xhr._method = method;
883
915
  xhr._url = url;
884
- xhr._startTime = Date.now();
916
+ xhr._buoyStartTime = Date.now();
885
917
  xhr._requestHeaders = {};
886
918
  }
887
- return passthroughOpen.call(this, method, url, async ?? true, user, password);
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._requestId;
941
+ const requestId = xhr._buoyRequestId;
907
942
  const method = xhr._method || "GET";
908
943
  const url = xhr._url || "";
909
- const startTime = xhr._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
- * `insideOverriddenFetch` around its pass-through, so one request can't
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 outcome = insideOverriddenFetch ? null : (0, _engine.overrideForRequest)(url, method);
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._ttfb === undefined && xhr._startTime) {
1014
- xhr._ttfb = Date.now() - xhr._startTime;
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 is blind to the anonymous tier. Kept
48
- * because it is part of the package's public exports.
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 there are now three tiers, and a
23
- * boolean cannot express the anonymous one.
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
  /**
@@ -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)(_sharedUi.NetworkIcon, {
76
+ }) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkConditionsIcon.NetworkConditionsIcon, {
76
77
  size: size
77
78
  }),
78
79
  component: _NetworkModal.NetworkModal,
@@ -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";
@@ -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.duration ? /*#__PURE__*/_jsxs(View, {
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: "Toggle network interception",
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: {