@buoy-gg/network 2.1.2 → 2.1.4-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -49,8 +49,9 @@ function NetworkModalInner({
49
49
  filter,
50
50
  setFilter,
51
51
  clearEvents,
52
- isEnabled,
53
- toggleInterception,
52
+ isCapturing,
53
+ pauseCapturing,
54
+ resumeCapturing,
54
55
  hasLockedEvents,
55
56
  lockedEventCount,
56
57
  isEventLocked,
@@ -58,6 +59,14 @@ function NetworkModalInner({
58
59
  } = (0, _useNetworkEvents.useNetworkEvents)({
59
60
  isPro
60
61
  });
62
+ const isEnabled = isCapturing;
63
+ const toggleInterception = (0, _react.useCallback)(() => {
64
+ if (isCapturing) {
65
+ pauseCapturing();
66
+ } else {
67
+ resumeCapturing();
68
+ }
69
+ }, [isCapturing, pauseCapturing, resumeCapturing]);
61
70
  const handleModeChange = (0, _react.useCallback)(_mode => {
62
71
  // Mode changes handled by JsModal
63
72
  }, []);
@@ -67,7 +76,8 @@ function NetworkModalInner({
67
76
  const [searchText, setSearchText] = (0, _react.useState)("");
68
77
  const [isSearchActive, setIsSearchActive] = (0, _react.useState)(false);
69
78
  const searchInputRef = (0, _react.useRef)(null);
70
- const [ignoredPatterns, setIgnoredPatterns] = (0, _react.useState)(new Set());
79
+ const [ignoredPatterns, setIgnoredPatterns] = (0, _react.useState)(new Set(["api.keygen.sh"]) // Auto-hide Buoy license API requests by default
80
+ );
71
81
  const [copySettings, setCopySettings] = (0, _react.useState)(_NetworkCopySettingsView.DEFAULT_COPY_SETTINGS);
72
82
  const flatListRef = (0, _react.useRef)(null);
73
83
  const hasLoadedFilters = (0, _react.useRef)(false);
@@ -90,6 +100,10 @@ function NetworkModalInner({
90
100
  const storedPatterns = await _sharedUi.persistentStorage.getItem(_sharedUi.devToolsStorageKeys.network.ignoredDomains());
91
101
  if (storedPatterns) {
92
102
  const patterns = JSON.parse(storedPatterns);
103
+ // Always ensure Buoy license API is hidden
104
+ if (!patterns.includes("api.keygen.sh")) {
105
+ patterns.push("api.keygen.sh");
106
+ }
93
107
  setIgnoredPatterns(new Set(patterns));
94
108
  }
95
109
  } catch (_error) {
@@ -318,6 +332,27 @@ function NetworkModalInner({
318
332
  return generateCopyText();
319
333
  }, [filteredEvents.length, generateCopyText]);
320
334
 
335
+ // Compute badge stats from filtered events (respects ignored patterns)
336
+ const badgeStats = (0, _react.useMemo)(() => {
337
+ let successful = 0;
338
+ let failed = 0;
339
+ let pending = 0;
340
+ for (const event of filteredEvents) {
341
+ if (event.status && event.status >= 200 && event.status < 300) {
342
+ successful++;
343
+ } else if (event.error || event.status && event.status >= 400) {
344
+ failed++;
345
+ } else if (!event.status && !event.error) {
346
+ pending++;
347
+ }
348
+ }
349
+ return {
350
+ successful,
351
+ failed,
352
+ pending
353
+ };
354
+ }, [filteredEvents]);
355
+
321
356
  // FlatList optimization - only keep what's needed for FlatList performance
322
357
  const keyExtractor = item => item.id;
323
358
 
@@ -458,7 +493,7 @@ function NetworkModalInner({
458
493
  style: [styles.headerChipValue, {
459
494
  color: _sharedUi.macOSColors.semantic.success
460
495
  }],
461
- children: stats.successfulRequests
496
+ children: badgeStats.successful
462
497
  })]
463
498
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
464
499
  style: [styles.headerChip, filter.status === "error" && styles.headerChipActive],
@@ -473,7 +508,7 @@ function NetworkModalInner({
473
508
  style: [styles.headerChipValue, {
474
509
  color: _sharedUi.macOSColors.semantic.error
475
510
  }],
476
- children: stats.failedRequests
511
+ children: badgeStats.failed
477
512
  })]
478
513
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
479
514
  style: [styles.headerChip, filter.status === "pending" && styles.headerChipActive],
@@ -488,7 +523,7 @@ function NetworkModalInner({
488
523
  style: [styles.headerChipValue, {
489
524
  color: _sharedUi.macOSColors.semantic.warning
490
525
  }],
491
- children: stats.pendingRequests
526
+ children: badgeStats.pending
492
527
  })]
493
528
  })]
494
529
  })
@@ -586,7 +621,7 @@ function NetworkModalInner({
586
621
  onSettingsChange: setCopySettings,
587
622
  events: filteredEvents
588
623
  }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkFilterViewV.NetworkFilterViewV3, {
589
- events: events,
624
+ events: filteredEvents,
590
625
  filter: filter,
591
626
  onFilterChange: setFilter,
592
627
  ignoredPatterns: ignoredPatterns,
@@ -7,11 +7,12 @@ exports.FREE_TIER_REQUEST_LIMIT = void 0;
7
7
  exports.useNetworkEvents = useNetworkEvents;
8
8
  var _react = require("react");
9
9
  var _networkEventStore = require("../utils/networkEventStore");
10
- var _networkListener = require("../utils/networkListener");
11
10
  var _formatGraphQLVariables = require("../utils/formatGraphQLVariables");
12
11
  /**
13
12
  * Hook for accessing network events and controls
14
- * Uses Reactotron-style listener pattern
13
+ *
14
+ * Uses self-managing Subscribable pattern - the network listener automatically
15
+ * starts when this hook mounts and stops when all subscribers unmount.
15
16
  */
16
17
 
17
18
  /** Free tier limit for network requests */
@@ -19,13 +20,15 @@ const FREE_TIER_REQUEST_LIMIT = exports.FREE_TIER_REQUEST_LIMIT = 25;
19
20
 
20
21
  /**
21
22
  * Custom hook for accessing network events and controls
22
- *
23
+ *
23
24
  * This hook provides a complete interface for network monitoring, including
24
- * event filtering, statistics calculation, and interception control. It uses
25
- * the Reactotron-style listener pattern for network event handling.
26
- *
25
+ * event filtering, statistics calculation, and interception control.
26
+ *
27
+ * The network listener automatically starts when this hook mounts and stops
28
+ * when all components using it unmount (Subscribable pattern from TanStack Query).
29
+ *
27
30
  * @returns Object containing filtered events, statistics, controls, and utilities
28
- *
31
+ *
29
32
  * @example
30
33
  * ```typescript
31
34
  * function NetworkMonitor() {
@@ -35,22 +38,19 @@ const FREE_TIER_REQUEST_LIMIT = exports.FREE_TIER_REQUEST_LIMIT = 25;
35
38
  * filter,
36
39
  * setFilter,
37
40
  * clearEvents,
38
- * toggleInterception,
39
- * isEnabled
41
+ * isCapturing
40
42
  * } = useNetworkEvents();
41
- *
43
+ *
42
44
  * return (
43
45
  * <div>
44
46
  * <p>Total requests: {stats.totalRequests}</p>
45
47
  * <p>Success rate: {stats.successfulRequests}/{stats.totalRequests}</p>
46
- * <button onClick={toggleInterception}>
47
- * {isEnabled ? 'Stop' : 'Start'} Monitoring
48
- * </button>
48
+ * <p>Capturing: {isCapturing ? 'Yes' : 'No'}</p>
49
49
  * </div>
50
50
  * );
51
51
  * }
52
52
  * ```
53
- *
53
+ *
54
54
  * @performance Uses memoization for expensive filtering and statistics calculations
55
55
  * @performance Optimizes string operations and array processing for large datasets
56
56
  * @performance Includes Set-based lookups for O(1) filter matching
@@ -63,54 +63,39 @@ function useNetworkEvents(options = {}) {
63
63
  } = options;
64
64
  const [events, setEvents] = (0, _react.useState)([]);
65
65
  const [filter, setFilter] = (0, _react.useState)({});
66
- const [isEnabled, setIsEnabled] = (0, _react.useState)(false);
66
+ const [isCapturing, setIsCapturing] = (0, _react.useState)(true);
67
+ const unsubscribeRef = (0, _react.useRef)(null);
67
68
 
68
- // Subscribe to event store changes
69
+ // Subscribe/unsubscribe to event store based on isCapturing state.
70
+ // Each subscriber is independent — pausing this hook's subscription
71
+ // does not affect other subscribers (e.g. the events tool).
69
72
  (0, _react.useEffect)(() => {
70
- // Subscribe to store changes
71
- const unsubscribeStore = _networkEventStore.networkEventStore.subscribe(setEvents);
72
-
73
- // Add listener to network events
74
- const unsubscribeListener = (0, _networkListener.addNetworkListener)(event => {
75
- // Only log in development and for non-ignored URLs
76
- if (__DEV__ && !event.request.url.includes("symbolicate") && !event.request.url.includes(":8081")) {
77
- // Network event processed: [event.type] [method] [url] - available for debugging if needed
78
- }
79
- _networkEventStore.networkEventStore.processNetworkEvent(event);
80
- });
81
-
82
- // Check if already listening
83
- setIsEnabled((0, _networkListener.networkListener)().isActive);
84
-
85
- // Start listening if not already
86
- if (!(0, _networkListener.networkListener)().isActive) {
87
- (0, _networkListener.startNetworkListener)();
88
- setIsEnabled(true);
73
+ if (isCapturing) {
74
+ unsubscribeRef.current = _networkEventStore.networkEventStore.subscribeToEvents(setEvents);
89
75
  }
90
-
91
- // Load initial events
92
- setEvents(_networkEventStore.networkEventStore.getEvents());
93
76
  return () => {
94
- unsubscribeStore();
95
- unsubscribeListener();
77
+ if (unsubscribeRef.current) {
78
+ unsubscribeRef.current();
79
+ unsubscribeRef.current = null;
80
+ }
96
81
  };
97
- }, []);
82
+ }, [isCapturing]);
98
83
 
99
- // Clear all events
84
+ // Clear all events (also update local state in case we're paused/unsubscribed)
100
85
  const clearEvents = (0, _react.useCallback)(() => {
101
86
  _networkEventStore.networkEventStore.clearEvents();
87
+ setEvents([]);
102
88
  }, []);
103
89
 
104
- // Toggle interception
105
- const toggleInterception = (0, _react.useCallback)(() => {
106
- if (isEnabled) {
107
- (0, _networkListener.stopNetworkListener)();
108
- setIsEnabled(false);
109
- } else {
110
- (0, _networkListener.startNetworkListener)();
111
- setIsEnabled(true);
112
- }
113
- }, [isEnabled]);
90
+ // Pause: unsubscribe this hook from the store (other subscribers unaffected)
91
+ const pauseCapturing = (0, _react.useCallback)(() => {
92
+ setIsCapturing(false);
93
+ }, []);
94
+
95
+ // Resume: resubscribe this hook to the store
96
+ const resumeCapturing = (0, _react.useCallback)(() => {
97
+ setIsCapturing(true);
98
+ }, []);
114
99
 
115
100
  // Memoize search text processing to avoid repeated toLowerCase calls
116
101
  // Performance: Expensive string operations repeated for every event on every filter
@@ -304,8 +289,12 @@ function useNetworkEvents(options = {}) {
304
289
  filter,
305
290
  setFilter,
306
291
  clearEvents,
307
- isEnabled,
308
- toggleInterception,
292
+ /** Whether this hook is subscribed and receiving network events */
293
+ isCapturing,
294
+ /** Unsubscribe this hook from the network store (other subscribers unaffected) */
295
+ pauseCapturing,
296
+ /** Resubscribe this hook to the network store */
297
+ resumeCapturing,
309
298
  hosts,
310
299
  methods,
311
300
  /** Whether there are locked events due to free tier limit */
@@ -315,6 +304,8 @@ function useNetworkEvents(options = {}) {
315
304
  /** Check if a specific event is locked (by ID) */
316
305
  isEventLocked,
317
306
  /** Free tier request limit */
318
- requestLimit: FREE_TIER_REQUEST_LIMIT
307
+ requestLimit: FREE_TIER_REQUEST_LIMIT,
308
+ /** Get subscriber counts for debugging */
309
+ getSubscriberCounts: () => _networkEventStore.networkEventStore.getSubscriberCounts()
319
310
  };
320
311
  }
@@ -4,18 +4,91 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.networkEventStore = void 0;
7
+ var _sharedUi = require("@buoy-gg/shared-ui");
7
8
  var _extractOperationName = require("./extractOperationName");
9
+ var _networkListener = require("./networkListener");
8
10
  /**
9
- * Network event store for managing captured network requests
10
- * Works with the Reactotron-style network listener
11
+ * Network Event Store
12
+ *
13
+ * Store for managing captured network requests. Uses BaseEventStore pattern -
14
+ * the network listener automatically starts when the first subscriber joins
15
+ * and stops when the last subscriber leaves.
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * import { networkEventStore } from '@buoy-gg/network';
20
+ *
21
+ * // Subscribe to network events - automatically starts capturing!
22
+ * const unsubscribe = networkEventStore.subscribeToEvents((events) => {
23
+ * console.log('Network events:', events);
24
+ * });
25
+ *
26
+ * // Later, clean up - automatically stops if no other subscribers
27
+ * unsubscribe();
28
+ * ```
11
29
  */
12
30
 
13
- class NetworkEventStore {
14
- events = [];
31
+ /**
32
+ * Callback type for individual event notifications
33
+ */
34
+
35
+ /**
36
+ * Callback type for full events array notifications
37
+ */
38
+
39
+ class NetworkEventStore extends _sharedUi.BaseEventStore {
15
40
  pendingRequests = new Map();
16
- listeners = new Set();
17
- maxEvents = 500; // Configurable max events to prevent memory issues
18
- recentRequests = new Map(); // Track recent requests to detect duplicates
41
+ recentRequests = new Map();
42
+ rawListenerUnsubscribe = null;
43
+ constructor() {
44
+ super({
45
+ storeName: "network",
46
+ maxEvents: 500
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Start capturing network events
52
+ */
53
+ startCapturing() {
54
+ const listener = (0, _networkListener.networkListener)();
55
+
56
+ // Start the network interceptor if not already active
57
+ if (!listener.isActive) {
58
+ listener.startListening();
59
+ }
60
+
61
+ // Add our listener to push raw events into the store
62
+ if (!this.rawListenerUnsubscribe) {
63
+ this.rawListenerUnsubscribe = (0, _networkListener.addNetworkListener)(event => {
64
+ this.processNetworkEvent(event);
65
+ });
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Stop capturing network events
71
+ */
72
+ stopCapturing() {
73
+ // Remove our raw listener
74
+ if (this.rawListenerUnsubscribe) {
75
+ this.rawListenerUnsubscribe();
76
+ this.rawListenerUnsubscribe = null;
77
+ }
78
+
79
+ // If no other listeners exist, stop the network interceptor
80
+ const listener = (0, _networkListener.networkListener)();
81
+ if (listener.listenerCount === 0) {
82
+ listener.stopListening();
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Check if the store is actively capturing network events
88
+ */
89
+ isCapturing() {
90
+ return this.rawListenerUnsubscribe !== null;
91
+ }
19
92
 
20
93
  /**
21
94
  * Process a network listener event
@@ -32,13 +105,13 @@ class NetworkEventStore {
32
105
 
33
106
  // If same request within 50ms, likely a duplicate from XHR/fetch dual interception
34
107
  if (lastRequestTime && now - lastRequestTime < 50) {
35
- return; // Skip duplicate
108
+ return;
36
109
  }
37
110
  this.recentRequests.set(requestKey, now);
38
111
 
39
112
  // Clean up old entries to prevent memory leak
40
113
  if (this.recentRequests.size > 100) {
41
- const cutoff = now - 5000; // Remove entries older than 5 seconds
114
+ const cutoff = now - 5000;
42
115
  for (const [key, time] of this.recentRequests.entries()) {
43
116
  if (time < cutoff) {
44
117
  this.recentRequests.delete(key);
@@ -46,7 +119,6 @@ class NetworkEventStore {
46
119
  }
47
120
  }
48
121
 
49
- // Create new network event for request
50
122
  // Build full URL with query params if present
51
123
  const queryString = request.params ? `?${new URLSearchParams(request.params).toString()}` : "";
52
124
  const fullUrl = `${request.url}${queryString}`;
@@ -54,12 +126,10 @@ class NetworkEventStore {
54
126
  // Extract GraphQL operation name and variables for searchability
55
127
  let operationName;
56
128
  let graphqlVariables;
57
- if (request.client === 'graphql') {
129
+ if (request.client === "graphql") {
58
130
  const extracted = (0, _extractOperationName.extractOperationName)(request.data);
59
131
  operationName = extracted || undefined;
60
-
61
- // Extract variables from GraphQL request
62
- if (request.data && typeof request.data === 'object' && 'variables' in request.data && request.data.variables && typeof request.data.variables === 'object') {
132
+ if (request.data && typeof request.data === "object" && "variables" in request.data && request.data.variables && typeof request.data.variables === "object") {
63
133
  graphqlVariables = request.data.variables;
64
134
  }
65
135
  }
@@ -67,7 +137,6 @@ class NetworkEventStore {
67
137
  id: request.id,
68
138
  method: request.method,
69
139
  url: fullUrl,
70
- // Store FULL URL with query params
71
140
  host: this.extractHost(request.url),
72
141
  path: this.extractPath(request.url),
73
142
  query: queryString,
@@ -78,18 +147,13 @@ class NetworkEventStore {
78
147
  responseHeaders: {},
79
148
  requestClient: request.client,
80
149
  operationName,
81
- // GraphQL operation name for search/filter
82
- graphqlVariables // GraphQL variables for display and search
150
+ graphqlVariables
83
151
  };
84
-
85
- // Store as pending
86
152
  this.pendingRequests.set(request.id, networkEvent);
87
-
88
- // Add to events list
89
153
  this.events = [networkEvent, ...this.events].slice(0, this.maxEvents);
90
- this.notifyListeners();
154
+ this.notify(networkEvent);
155
+ this.notifyArrayListeners();
91
156
  } else if (event.type === "response" || event.type === "error") {
92
- // Find and update the pending request
93
157
  const index = this.events.findIndex(e => e.id === request.id);
94
158
  if (index !== -1) {
95
159
  const updatedEvent = {
@@ -108,16 +172,13 @@ class NetworkEventStore {
108
172
  updatedEvent.error = event.error.message;
109
173
  updatedEvent.status = updatedEvent.status || 0;
110
174
  }
111
- this.events[index] = updatedEvent;
175
+ this.events = this.events.map((e, i) => i === index ? updatedEvent : e);
112
176
  this.pendingRequests.delete(request.id);
113
- this.notifyListeners();
177
+ this.notify(updatedEvent);
178
+ this.notifyArrayListeners();
114
179
  }
115
180
  }
116
181
  }
117
-
118
- /**
119
- * Extract host from URL
120
- */
121
182
  extractHost(url) {
122
183
  try {
123
184
  const urlObj = new URL(url);
@@ -127,10 +188,6 @@ class NetworkEventStore {
127
188
  return "";
128
189
  }
129
190
  }
130
-
131
- /**
132
- * Extract path from URL
133
- */
134
191
  extractPath(url) {
135
192
  try {
136
193
  const urlObj = new URL(url);
@@ -140,10 +197,6 @@ class NetworkEventStore {
140
197
  return url;
141
198
  }
142
199
  }
143
-
144
- /**
145
- * Get size of data
146
- */
147
200
  getDataSize(data) {
148
201
  if (!data) return 0;
149
202
  if (typeof data === "string") return data.length;
@@ -153,63 +206,38 @@ class NetworkEventStore {
153
206
  return 0;
154
207
  }
155
208
  }
156
-
157
- /**
158
- * Get all events
159
- */
160
- getEvents() {
161
- return [...this.events];
162
- }
163
-
164
- /**
165
- * Get event by ID
166
- */
167
209
  getEventById(id) {
168
210
  return this.events.find(e => e.id === id);
169
211
  }
170
212
 
171
213
  /**
172
- * Clear all events
214
+ * Override clearEvents to also clear pending requests
173
215
  */
174
216
  clearEvents() {
175
- this.events = [];
217
+ super.clearEvents();
176
218
  this.pendingRequests.clear();
177
219
  this.recentRequests.clear();
178
- this.notifyListeners();
179
- }
180
-
181
- /**
182
- * Subscribe to event changes
183
- */
184
- subscribe(listener) {
185
- this.listeners.add(listener);
186
- return () => {
187
- this.listeners.delete(listener);
188
- };
189
220
  }
190
221
 
191
222
  /**
192
- * Notify all listeners of changes
223
+ * Subscribe to individual events with optional replay of existing events
193
224
  */
194
- notifyListeners() {
195
- const events = this.getEvents();
196
- this.listeners.forEach(listener => listener(events));
197
- }
225
+ onEvent(callback, replayExisting = true) {
226
+ const unsubscribe = this.subscribe(callback);
198
227
 
199
- /**
200
- * Set maximum number of events to store
201
- */
202
- setMaxEvents(max) {
203
- this.maxEvents = max;
204
- if (this.events.length > max) {
205
- this.events = this.events.slice(0, max);
206
- this.notifyListeners();
228
+ // Replay existing events if requested
229
+ if (replayExisting && this.events.length > 0) {
230
+ const existingEvents = [...this.events].reverse();
231
+ for (const event of existingEvents) {
232
+ try {
233
+ callback(event);
234
+ } catch {
235
+ // Ignore callback errors
236
+ }
237
+ }
207
238
  }
239
+ return unsubscribe;
208
240
  }
209
-
210
- /**
211
- * Get statistics about network events
212
- */
213
241
  getStats() {
214
242
  const total = this.events.length;
215
243
  const successful = this.events.filter(e => e.status && e.status >= 200 && e.status < 300).length;
@@ -229,10 +257,6 @@ class NetworkEventStore {
229
257
  averageDuration: Math.round(avgDuration)
230
258
  };
231
259
  }
232
-
233
- /**
234
- * Filter events by criteria
235
- */
236
260
  filterEvents(filter) {
237
261
  let filtered = [...this.events];
238
262
  if (filter.method) {
@@ -263,7 +287,7 @@ class NetworkEventStore {
263
287
  }
264
288
 
265
289
  /**
266
- * Singleton store that aggregates captured network traffic. Components and hooks consume this
267
- * store to render histories, derive stats, and subscribe to real-time updates.
290
+ * Singleton store that aggregates captured network traffic.
291
+ * Automatically starts/stops capturing based on subscriber count.
268
292
  */
269
293
  const networkEventStore = exports.networkEventStore = new NetworkEventStore();
@@ -45,8 +45,9 @@ function NetworkModalInner({
45
45
  filter,
46
46
  setFilter,
47
47
  clearEvents,
48
- isEnabled,
49
- toggleInterception,
48
+ isCapturing,
49
+ pauseCapturing,
50
+ resumeCapturing,
50
51
  hasLockedEvents,
51
52
  lockedEventCount,
52
53
  isEventLocked,
@@ -54,6 +55,14 @@ function NetworkModalInner({
54
55
  } = useNetworkEvents({
55
56
  isPro
56
57
  });
58
+ const isEnabled = isCapturing;
59
+ const toggleInterception = useCallback(() => {
60
+ if (isCapturing) {
61
+ pauseCapturing();
62
+ } else {
63
+ resumeCapturing();
64
+ }
65
+ }, [isCapturing, pauseCapturing, resumeCapturing]);
57
66
  const handleModeChange = useCallback(_mode => {
58
67
  // Mode changes handled by JsModal
59
68
  }, []);
@@ -63,7 +72,8 @@ function NetworkModalInner({
63
72
  const [searchText, setSearchText] = useState("");
64
73
  const [isSearchActive, setIsSearchActive] = useState(false);
65
74
  const searchInputRef = useRef(null);
66
- const [ignoredPatterns, setIgnoredPatterns] = useState(new Set());
75
+ const [ignoredPatterns, setIgnoredPatterns] = useState(new Set(["api.keygen.sh"]) // Auto-hide Buoy license API requests by default
76
+ );
67
77
  const [copySettings, setCopySettings] = useState(DEFAULT_COPY_SETTINGS);
68
78
  const flatListRef = useRef(null);
69
79
  const hasLoadedFilters = useRef(false);
@@ -86,6 +96,10 @@ function NetworkModalInner({
86
96
  const storedPatterns = await persistentStorage.getItem(devToolsStorageKeys.network.ignoredDomains());
87
97
  if (storedPatterns) {
88
98
  const patterns = JSON.parse(storedPatterns);
99
+ // Always ensure Buoy license API is hidden
100
+ if (!patterns.includes("api.keygen.sh")) {
101
+ patterns.push("api.keygen.sh");
102
+ }
89
103
  setIgnoredPatterns(new Set(patterns));
90
104
  }
91
105
  } catch (_error) {
@@ -314,6 +328,27 @@ function NetworkModalInner({
314
328
  return generateCopyText();
315
329
  }, [filteredEvents.length, generateCopyText]);
316
330
 
331
+ // Compute badge stats from filtered events (respects ignored patterns)
332
+ const badgeStats = useMemo(() => {
333
+ let successful = 0;
334
+ let failed = 0;
335
+ let pending = 0;
336
+ for (const event of filteredEvents) {
337
+ if (event.status && event.status >= 200 && event.status < 300) {
338
+ successful++;
339
+ } else if (event.error || event.status && event.status >= 400) {
340
+ failed++;
341
+ } else if (!event.status && !event.error) {
342
+ pending++;
343
+ }
344
+ }
345
+ return {
346
+ successful,
347
+ failed,
348
+ pending
349
+ };
350
+ }, [filteredEvents]);
351
+
317
352
  // FlatList optimization - only keep what's needed for FlatList performance
318
353
  const keyExtractor = item => item.id;
319
354
 
@@ -454,7 +489,7 @@ function NetworkModalInner({
454
489
  style: [styles.headerChipValue, {
455
490
  color: macOSColors.semantic.success
456
491
  }],
457
- children: stats.successfulRequests
492
+ children: badgeStats.successful
458
493
  })]
459
494
  }), /*#__PURE__*/_jsxs(TouchableOpacity, {
460
495
  style: [styles.headerChip, filter.status === "error" && styles.headerChipActive],
@@ -469,7 +504,7 @@ function NetworkModalInner({
469
504
  style: [styles.headerChipValue, {
470
505
  color: macOSColors.semantic.error
471
506
  }],
472
- children: stats.failedRequests
507
+ children: badgeStats.failed
473
508
  })]
474
509
  }), /*#__PURE__*/_jsxs(TouchableOpacity, {
475
510
  style: [styles.headerChip, filter.status === "pending" && styles.headerChipActive],
@@ -484,7 +519,7 @@ function NetworkModalInner({
484
519
  style: [styles.headerChipValue, {
485
520
  color: macOSColors.semantic.warning
486
521
  }],
487
- children: stats.pendingRequests
522
+ children: badgeStats.pending
488
523
  })]
489
524
  })]
490
525
  })
@@ -582,7 +617,7 @@ function NetworkModalInner({
582
617
  onSettingsChange: setCopySettings,
583
618
  events: filteredEvents
584
619
  }) : /*#__PURE__*/_jsx(NetworkFilterViewV3, {
585
- events: events,
620
+ events: filteredEvents,
586
621
  filter: filter,
587
622
  onFilterChange: setFilter,
588
623
  ignoredPatterns: ignoredPatterns,