@buoy-gg/network 1.7.2
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 +381 -0
- package/lib/commonjs/index.js +34 -0
- package/lib/commonjs/network/components/NetworkCopySettingsView.js +867 -0
- package/lib/commonjs/network/components/NetworkEventDetailView.js +837 -0
- package/lib/commonjs/network/components/NetworkEventItemCompact.js +323 -0
- package/lib/commonjs/network/components/NetworkFilterViewV3.js +297 -0
- package/lib/commonjs/network/components/NetworkModal.js +937 -0
- package/lib/commonjs/network/hooks/useNetworkEvents.js +320 -0
- package/lib/commonjs/network/hooks/useTickEveryMinute.js +34 -0
- package/lib/commonjs/network/index.js +102 -0
- package/lib/commonjs/network/types/index.js +1 -0
- package/lib/commonjs/network/utils/extractOperationName.js +80 -0
- package/lib/commonjs/network/utils/formatGraphQLVariables.js +219 -0
- package/lib/commonjs/network/utils/formatting.js +30 -0
- package/lib/commonjs/network/utils/networkEventStore.js +269 -0
- package/lib/commonjs/network/utils/networkListener.js +801 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/preset.js +83 -0
- package/lib/module/index.js +7 -0
- package/lib/module/network/components/NetworkCopySettingsView.js +862 -0
- package/lib/module/network/components/NetworkEventDetailView.js +834 -0
- package/lib/module/network/components/NetworkEventItemCompact.js +320 -0
- package/lib/module/network/components/NetworkFilterViewV3.js +293 -0
- package/lib/module/network/components/NetworkModal.js +933 -0
- package/lib/module/network/hooks/useNetworkEvents.js +316 -0
- package/lib/module/network/hooks/useTickEveryMinute.js +29 -0
- package/lib/module/network/index.js +20 -0
- package/lib/module/network/types/index.js +1 -0
- package/lib/module/network/utils/extractOperationName.js +76 -0
- package/lib/module/network/utils/formatGraphQLVariables.js +213 -0
- package/lib/module/network/utils/formatting.js +9 -0
- package/lib/module/network/utils/networkEventStore.js +265 -0
- package/lib/module/network/utils/networkListener.js +791 -0
- package/lib/module/preset.js +79 -0
- package/lib/typescript/index.d.ts +3 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkCopySettingsView.d.ts +26 -0
- package/lib/typescript/network/components/NetworkCopySettingsView.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkEventDetailView.d.ts +13 -0
- package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +12 -0
- package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkFilterViewV3.d.ts +22 -0
- package/lib/typescript/network/components/NetworkFilterViewV3.d.ts.map +1 -0
- package/lib/typescript/network/components/NetworkModal.d.ts +14 -0
- package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -0
- package/lib/typescript/network/hooks/useNetworkEvents.d.ts +72 -0
- package/lib/typescript/network/hooks/useNetworkEvents.d.ts.map +1 -0
- package/lib/typescript/network/hooks/useTickEveryMinute.d.ts +9 -0
- package/lib/typescript/network/hooks/useTickEveryMinute.d.ts.map +1 -0
- package/lib/typescript/network/index.d.ts +12 -0
- package/lib/typescript/network/index.d.ts.map +1 -0
- package/lib/typescript/network/types/index.d.ts +88 -0
- package/lib/typescript/network/types/index.d.ts.map +1 -0
- package/lib/typescript/network/utils/extractOperationName.d.ts +41 -0
- package/lib/typescript/network/utils/extractOperationName.d.ts.map +1 -0
- package/lib/typescript/network/utils/formatGraphQLVariables.d.ts +79 -0
- package/lib/typescript/network/utils/formatGraphQLVariables.d.ts.map +1 -0
- package/lib/typescript/network/utils/formatting.d.ts +6 -0
- package/lib/typescript/network/utils/formatting.d.ts.map +1 -0
- package/lib/typescript/network/utils/networkEventStore.d.ts +81 -0
- package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -0
- package/lib/typescript/network/utils/networkListener.d.ts +191 -0
- package/lib/typescript/network/utils/networkListener.d.ts.map +1 -0
- package/lib/typescript/preset.d.ts +76 -0
- package/lib/typescript/preset.d.ts.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook for accessing network events and controls
|
|
5
|
+
* Uses Reactotron-style listener pattern
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect, useCallback, useMemo } from "react";
|
|
9
|
+
import { networkEventStore } from "../utils/networkEventStore";
|
|
10
|
+
import { networkListener, startNetworkListener, stopNetworkListener, addNetworkListener } from "../utils/networkListener";
|
|
11
|
+
import { searchGraphQLVariables } from "../utils/formatGraphQLVariables";
|
|
12
|
+
|
|
13
|
+
/** Free tier limit for network requests */
|
|
14
|
+
export const FREE_TIER_REQUEST_LIMIT = 25;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Custom hook for accessing network events and controls
|
|
18
|
+
*
|
|
19
|
+
* This hook provides a complete interface for network monitoring, including
|
|
20
|
+
* event filtering, statistics calculation, and interception control. It uses
|
|
21
|
+
* the Reactotron-style listener pattern for network event handling.
|
|
22
|
+
*
|
|
23
|
+
* @returns Object containing filtered events, statistics, controls, and utilities
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* function NetworkMonitor() {
|
|
28
|
+
* const {
|
|
29
|
+
* events,
|
|
30
|
+
* stats,
|
|
31
|
+
* filter,
|
|
32
|
+
* setFilter,
|
|
33
|
+
* clearEvents,
|
|
34
|
+
* toggleInterception,
|
|
35
|
+
* isEnabled
|
|
36
|
+
* } = useNetworkEvents();
|
|
37
|
+
*
|
|
38
|
+
* return (
|
|
39
|
+
* <div>
|
|
40
|
+
* <p>Total requests: {stats.totalRequests}</p>
|
|
41
|
+
* <p>Success rate: {stats.successfulRequests}/{stats.totalRequests}</p>
|
|
42
|
+
* <button onClick={toggleInterception}>
|
|
43
|
+
* {isEnabled ? 'Stop' : 'Start'} Monitoring
|
|
44
|
+
* </button>
|
|
45
|
+
* </div>
|
|
46
|
+
* );
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @performance Uses memoization for expensive filtering and statistics calculations
|
|
51
|
+
* @performance Optimizes string operations and array processing for large datasets
|
|
52
|
+
* @performance Includes Set-based lookups for O(1) filter matching
|
|
53
|
+
*
|
|
54
|
+
* @param options.isPro - Whether user has Pro access (unlimited events). Defaults to false (limited to FREE_TIER_REQUEST_LIMIT)
|
|
55
|
+
*/
|
|
56
|
+
export function useNetworkEvents(options = {}) {
|
|
57
|
+
const {
|
|
58
|
+
isPro = false
|
|
59
|
+
} = options;
|
|
60
|
+
const [events, setEvents] = useState([]);
|
|
61
|
+
const [filter, setFilter] = useState({});
|
|
62
|
+
const [isEnabled, setIsEnabled] = useState(false);
|
|
63
|
+
|
|
64
|
+
// Subscribe to event store changes
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
// Subscribe to store changes
|
|
67
|
+
const unsubscribeStore = networkEventStore.subscribe(setEvents);
|
|
68
|
+
|
|
69
|
+
// Add listener to network events
|
|
70
|
+
const unsubscribeListener = addNetworkListener(event => {
|
|
71
|
+
// Only log in development and for non-ignored URLs
|
|
72
|
+
if (__DEV__ && !event.request.url.includes("symbolicate") && !event.request.url.includes(":8081")) {
|
|
73
|
+
// Network event processed: [event.type] [method] [url] - available for debugging if needed
|
|
74
|
+
}
|
|
75
|
+
networkEventStore.processNetworkEvent(event);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Check if already listening
|
|
79
|
+
setIsEnabled(networkListener().isActive);
|
|
80
|
+
|
|
81
|
+
// Start listening if not already
|
|
82
|
+
if (!networkListener().isActive) {
|
|
83
|
+
startNetworkListener();
|
|
84
|
+
setIsEnabled(true);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Load initial events
|
|
88
|
+
setEvents(networkEventStore.getEvents());
|
|
89
|
+
return () => {
|
|
90
|
+
unsubscribeStore();
|
|
91
|
+
unsubscribeListener();
|
|
92
|
+
};
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
// Clear all events
|
|
96
|
+
const clearEvents = useCallback(() => {
|
|
97
|
+
networkEventStore.clearEvents();
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
// Toggle interception
|
|
101
|
+
const toggleInterception = useCallback(() => {
|
|
102
|
+
if (isEnabled) {
|
|
103
|
+
stopNetworkListener();
|
|
104
|
+
setIsEnabled(false);
|
|
105
|
+
} else {
|
|
106
|
+
startNetworkListener();
|
|
107
|
+
setIsEnabled(true);
|
|
108
|
+
}
|
|
109
|
+
}, [isEnabled]);
|
|
110
|
+
|
|
111
|
+
// Memoize search text processing to avoid repeated toLowerCase calls
|
|
112
|
+
// Performance: Expensive string operations repeated for every event on every filter
|
|
113
|
+
const searchLower = useMemo(() => {
|
|
114
|
+
return filter.searchText ? filter.searchText.toLowerCase() : null;
|
|
115
|
+
}, [filter.searchText]);
|
|
116
|
+
|
|
117
|
+
// Memoize method filter Set for O(1) lookup instead of Array.includes
|
|
118
|
+
// Performance: Converting array.includes to Set.has for faster lookups with large method lists
|
|
119
|
+
const methodSet = useMemo(() => {
|
|
120
|
+
return filter.method && filter.method.length > 0 ? new Set(filter.method) : null;
|
|
121
|
+
}, [filter.method]);
|
|
122
|
+
|
|
123
|
+
// Memoize content type Set for O(1) lookup
|
|
124
|
+
// Performance: Converting array.some to Set.has for faster content type matching
|
|
125
|
+
const contentTypeSet = useMemo(() => {
|
|
126
|
+
return filter.contentType && filter.contentType.length > 0 ? new Set(filter.contentType) : null;
|
|
127
|
+
}, [filter.contentType]);
|
|
128
|
+
|
|
129
|
+
// Filter events with optimized string operations and Set lookups
|
|
130
|
+
// Performance: Complex multi-stage filtering with string operations and content type matching
|
|
131
|
+
const filteredEvents = useMemo(() => {
|
|
132
|
+
let filtered = [...events];
|
|
133
|
+
if (methodSet) {
|
|
134
|
+
filtered = filtered.filter(e => methodSet.has(e.method));
|
|
135
|
+
}
|
|
136
|
+
if (filter.status && filter.status !== "all") {
|
|
137
|
+
switch (filter.status) {
|
|
138
|
+
case "success":
|
|
139
|
+
filtered = filtered.filter(e => e.status && e.status >= 200 && e.status < 300);
|
|
140
|
+
break;
|
|
141
|
+
case "error":
|
|
142
|
+
filtered = filtered.filter(e => e.error || e.status && e.status >= 400);
|
|
143
|
+
break;
|
|
144
|
+
case "pending":
|
|
145
|
+
filtered = filtered.filter(e => !e.status && !e.error);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (searchLower) {
|
|
150
|
+
filtered = filtered.filter(e => e.url.toLowerCase().includes(searchLower) || e.method.toLowerCase().includes(searchLower) || e.path?.toLowerCase().includes(searchLower) || e.host?.toLowerCase().includes(searchLower) || e.error && e.error.toLowerCase().includes(searchLower) ||
|
|
151
|
+
// Search by GraphQL operation name (e.g., "GetUser", "CreatePost")
|
|
152
|
+
e.operationName && e.operationName.toLowerCase().includes(searchLower) ||
|
|
153
|
+
// Search by GraphQL variable values (e.g., "Sandshrew", "123", "true")
|
|
154
|
+
// This enables finding specific requests like "GetPokemon › Sandshrew" by typing "Sandshrew"
|
|
155
|
+
searchGraphQLVariables(e.graphqlVariables, searchLower));
|
|
156
|
+
}
|
|
157
|
+
if (filter.host) {
|
|
158
|
+
filtered = filtered.filter(e => e.host === filter.host);
|
|
159
|
+
}
|
|
160
|
+
if (contentTypeSet) {
|
|
161
|
+
filtered = filtered.filter(e => {
|
|
162
|
+
const headers = e.responseHeaders || e.requestHeaders;
|
|
163
|
+
const contentType = headers?.["content-type"] || headers?.["Content-Type"] || "";
|
|
164
|
+
for (const type of contentTypeSet) {
|
|
165
|
+
switch (type) {
|
|
166
|
+
case "JSON":
|
|
167
|
+
if (contentType.includes("json")) return true;
|
|
168
|
+
break;
|
|
169
|
+
case "XML":
|
|
170
|
+
if (contentType.includes("xml")) return true;
|
|
171
|
+
break;
|
|
172
|
+
case "HTML":
|
|
173
|
+
if (contentType.includes("html")) return true;
|
|
174
|
+
break;
|
|
175
|
+
case "TEXT":
|
|
176
|
+
if (contentType.includes("text")) return true;
|
|
177
|
+
break;
|
|
178
|
+
case "IMAGE":
|
|
179
|
+
if (contentType.includes("image")) return true;
|
|
180
|
+
break;
|
|
181
|
+
case "VIDEO":
|
|
182
|
+
if (contentType.includes("video")) return true;
|
|
183
|
+
break;
|
|
184
|
+
case "AUDIO":
|
|
185
|
+
if (contentType.includes("audio")) return true;
|
|
186
|
+
break;
|
|
187
|
+
case "FORM":
|
|
188
|
+
if (contentType.includes("form")) return true;
|
|
189
|
+
break;
|
|
190
|
+
case "OTHER":
|
|
191
|
+
if (!contentType || !contentType.includes("json") && !contentType.includes("xml") && !contentType.includes("html") && !contentType.includes("text") && !contentType.includes("image") && !contentType.includes("video") && !contentType.includes("audio") && !contentType.includes("form")) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return filtered;
|
|
201
|
+
}, [events, filter, searchLower, methodSet, contentTypeSet]);
|
|
202
|
+
|
|
203
|
+
// For free users, determine which events are "locked" (beyond the limit)
|
|
204
|
+
// Based on original event order, not filtered results - so locked events stay locked even when searching
|
|
205
|
+
const lockedEventIds = useMemo(() => {
|
|
206
|
+
if (isPro) {
|
|
207
|
+
return new Set();
|
|
208
|
+
}
|
|
209
|
+
// Events beyond the limit in the ORIGINAL events array are locked
|
|
210
|
+
const lockedIds = new Set();
|
|
211
|
+
events.slice(FREE_TIER_REQUEST_LIMIT).forEach(event => {
|
|
212
|
+
lockedIds.add(event.id);
|
|
213
|
+
});
|
|
214
|
+
return lockedIds;
|
|
215
|
+
}, [events, isPro]);
|
|
216
|
+
|
|
217
|
+
// Check if there are locked events in the current filtered results
|
|
218
|
+
const lockedEventsInFilter = useMemo(() => {
|
|
219
|
+
return filteredEvents.filter(event => lockedEventIds.has(event.id));
|
|
220
|
+
}, [filteredEvents, lockedEventIds]);
|
|
221
|
+
const hasLockedEvents = lockedEventsInFilter.length > 0;
|
|
222
|
+
const lockedEventCount = lockedEventsInFilter.length;
|
|
223
|
+
|
|
224
|
+
// Helper to check if a specific event is locked
|
|
225
|
+
const isEventLocked = useCallback(eventId => {
|
|
226
|
+
return lockedEventIds.has(eventId);
|
|
227
|
+
}, [lockedEventIds]);
|
|
228
|
+
|
|
229
|
+
// Memoize expensive statistics calculation by categorizing events in single pass
|
|
230
|
+
// Performance: Multiple array.filter operations replaced with single loop for better performance
|
|
231
|
+
const stats = useMemo(() => {
|
|
232
|
+
let successful = 0;
|
|
233
|
+
let failed = 0;
|
|
234
|
+
let pending = 0;
|
|
235
|
+
let totalSent = 0;
|
|
236
|
+
let totalReceived = 0;
|
|
237
|
+
let durationSum = 0;
|
|
238
|
+
let durationCount = 0;
|
|
239
|
+
|
|
240
|
+
// Single pass through events for all statistics
|
|
241
|
+
for (const event of events) {
|
|
242
|
+
// Categorize status
|
|
243
|
+
if (event.status && event.status >= 200 && event.status < 300) {
|
|
244
|
+
successful++;
|
|
245
|
+
} else if (event.error || event.status && event.status >= 400) {
|
|
246
|
+
failed++;
|
|
247
|
+
} else if (!event.status && !event.error) {
|
|
248
|
+
pending++;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Accumulate data sizes
|
|
252
|
+
totalSent += event.requestSize || 0;
|
|
253
|
+
totalReceived += event.responseSize || 0;
|
|
254
|
+
|
|
255
|
+
// Accumulate durations
|
|
256
|
+
if (event.duration) {
|
|
257
|
+
durationSum += event.duration;
|
|
258
|
+
durationCount++;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const avgDuration = durationCount > 0 ? durationSum / durationCount : 0;
|
|
262
|
+
return {
|
|
263
|
+
totalRequests: events.length,
|
|
264
|
+
successfulRequests: successful,
|
|
265
|
+
failedRequests: failed,
|
|
266
|
+
pendingRequests: pending,
|
|
267
|
+
totalDataSent: totalSent,
|
|
268
|
+
totalDataReceived: totalReceived,
|
|
269
|
+
averageDuration: Math.round(avgDuration)
|
|
270
|
+
};
|
|
271
|
+
}, [events]);
|
|
272
|
+
|
|
273
|
+
// Memoize unique hosts extraction with single pass instead of map + filter + Set
|
|
274
|
+
// Performance: Avoiding array.map().filter() chain, using single loop with Set for deduplication
|
|
275
|
+
const hosts = useMemo(() => {
|
|
276
|
+
const hostSet = new Set();
|
|
277
|
+
for (const event of events) {
|
|
278
|
+
if (event.host) {
|
|
279
|
+
hostSet.add(event.host);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return Array.from(hostSet);
|
|
283
|
+
}, [events]);
|
|
284
|
+
|
|
285
|
+
// Memoize unique methods extraction with single pass
|
|
286
|
+
// Performance: Avoiding array.map() + Set constructor, using single loop for better performance
|
|
287
|
+
const methods = useMemo(() => {
|
|
288
|
+
const methodSet = new Set();
|
|
289
|
+
for (const event of events) {
|
|
290
|
+
methodSet.add(event.method);
|
|
291
|
+
}
|
|
292
|
+
return Array.from(methodSet);
|
|
293
|
+
}, [events]);
|
|
294
|
+
return {
|
|
295
|
+
/** Filtered events (all shown, but some may be locked for free users) */
|
|
296
|
+
events: filteredEvents,
|
|
297
|
+
/** All events before filtering (for stats) */
|
|
298
|
+
allEvents: events,
|
|
299
|
+
stats,
|
|
300
|
+
filter,
|
|
301
|
+
setFilter,
|
|
302
|
+
clearEvents,
|
|
303
|
+
isEnabled,
|
|
304
|
+
toggleInterception,
|
|
305
|
+
hosts,
|
|
306
|
+
methods,
|
|
307
|
+
/** Whether there are locked events due to free tier limit */
|
|
308
|
+
hasLockedEvents,
|
|
309
|
+
/** Number of events locked due to free tier limit */
|
|
310
|
+
lockedEventCount,
|
|
311
|
+
/** Check if a specific event is locked (by ID) */
|
|
312
|
+
isEventLocked,
|
|
313
|
+
/** Free tier request limit */
|
|
314
|
+
requestLimit: FREE_TIER_REQUEST_LIMIT
|
|
315
|
+
};
|
|
316
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
5
|
+
const TickContext = /*#__PURE__*/createContext(Date.now());
|
|
6
|
+
export function TickProvider({
|
|
7
|
+
children,
|
|
8
|
+
intervalMs = 60_000
|
|
9
|
+
}) {
|
|
10
|
+
const [tick, setTick] = useState(() => Date.now());
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const id = setInterval(() => {
|
|
13
|
+
setTick(Date.now());
|
|
14
|
+
}, intervalMs);
|
|
15
|
+
return () => {
|
|
16
|
+
clearInterval(id);
|
|
17
|
+
};
|
|
18
|
+
}, [intervalMs]);
|
|
19
|
+
return /*#__PURE__*/_jsx(TickContext.Provider, {
|
|
20
|
+
value: tick,
|
|
21
|
+
children: children
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export function useTickEveryMinute() {
|
|
25
|
+
const tick = useContext(TickContext);
|
|
26
|
+
|
|
27
|
+
// Expose stable object so consumers can memoize on value changes
|
|
28
|
+
return useMemo(() => tick, [tick]);
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Network monitoring section for React Native dev tools
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Components
|
|
8
|
+
export { NetworkModal } from "./components/NetworkModal";
|
|
9
|
+
export { NetworkEventDetailView } from "./components/NetworkEventDetailView";
|
|
10
|
+
export { NetworkEventItemCompact } from "./components/NetworkEventItemCompact";
|
|
11
|
+
|
|
12
|
+
// Hooks
|
|
13
|
+
export { useNetworkEvents } from "./hooks/useNetworkEvents";
|
|
14
|
+
|
|
15
|
+
// Utils
|
|
16
|
+
export { networkListener, startNetworkListener, stopNetworkListener, addNetworkListener, removeAllNetworkListeners, isNetworkListening, getNetworkListenerCount } from "./utils/networkListener";
|
|
17
|
+
export { networkEventStore } from "./utils/networkEventStore";
|
|
18
|
+
export { formatBytes, formatDuration, formatHttpStatus } from "./utils/formatting";
|
|
19
|
+
|
|
20
|
+
// Types
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract GraphQL operation name from request data
|
|
5
|
+
*
|
|
6
|
+
* This utility extracts the operation name from GraphQL requests to enable
|
|
7
|
+
* searching and filtering by operation name rather than just URL paths.
|
|
8
|
+
*
|
|
9
|
+
* For GraphQL, all requests typically go to the same endpoint (e.g., /graphql),
|
|
10
|
+
* making URL-based search ineffective. This function extracts the actual
|
|
11
|
+
* operation name (e.g., "GetUser", "CreatePost") from the request payload.
|
|
12
|
+
*
|
|
13
|
+
* Extraction Methods (tried in order):
|
|
14
|
+
* 1. Direct operationName field in request data
|
|
15
|
+
* 2. Parse from query string using regex pattern
|
|
16
|
+
*
|
|
17
|
+
* @param requestData - The request data object (typically GraphQL query payload)
|
|
18
|
+
* @returns Operation name string or null if not found
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // Method 1: Explicit operationName field
|
|
22
|
+
* const data1 = {
|
|
23
|
+
* operationName: "GetUser",
|
|
24
|
+
* query: "query GetUser { user { id name } }"
|
|
25
|
+
* };
|
|
26
|
+
* extractOperationName(data1); // Returns "GetUser"
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // Method 2: Parse from query string
|
|
30
|
+
* const data2 = {
|
|
31
|
+
* query: "mutation CreatePost { createPost(title: \"Hello\") { id } }"
|
|
32
|
+
* };
|
|
33
|
+
* extractOperationName(data2); // Returns "CreatePost"
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // No operation name (anonymous query)
|
|
37
|
+
* const data3 = {
|
|
38
|
+
* query: "{ user { id name } }"
|
|
39
|
+
* };
|
|
40
|
+
* extractOperationName(data3); // Returns null
|
|
41
|
+
*/
|
|
42
|
+
export function extractOperationName(requestData) {
|
|
43
|
+
// Validate input is an object
|
|
44
|
+
if (!requestData || typeof requestData !== 'object') {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Method 1: Check for explicit operationName field
|
|
49
|
+
// This is the standard GraphQL request format
|
|
50
|
+
if ('operationName' in requestData && requestData.operationName) {
|
|
51
|
+
const opName = requestData.operationName;
|
|
52
|
+
// Ensure it's a string and not empty
|
|
53
|
+
if (typeof opName === 'string' && opName.trim().length > 0) {
|
|
54
|
+
return opName.trim();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Method 2: Parse from query string
|
|
59
|
+
// Handles cases where operationName field is missing or null
|
|
60
|
+
if ('query' in requestData && typeof requestData.query === 'string') {
|
|
61
|
+
const query = requestData.query;
|
|
62
|
+
|
|
63
|
+
// Match: query OperationName or mutation OperationName or subscription OperationName
|
|
64
|
+
// Pattern explanation:
|
|
65
|
+
// - (?:query|mutation|subscription) - Match operation type (non-capturing group)
|
|
66
|
+
// - \s+ - One or more whitespace characters
|
|
67
|
+
// - (\w+) - Capture operation name (letters, numbers, underscore)
|
|
68
|
+
const match = query.match(/(?:query|mutation|subscription)\s+(\w+)/);
|
|
69
|
+
if (match && match[1]) {
|
|
70
|
+
return match[1];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// No operation name found
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Format GraphQL variables for display using arrow notation
|
|
5
|
+
*
|
|
6
|
+
* Mimics React Query key display format to provide visual consistency:
|
|
7
|
+
* - React Query: ["pokemon", "Sandshrew"] → "pokemon › Sandshrew"
|
|
8
|
+
* - GraphQL: GetPokemon(id: "Sandshrew") → "GetPokemon › Sandshrew"
|
|
9
|
+
*
|
|
10
|
+
* Extracts only the values from variables (not keys) and formats them
|
|
11
|
+
* for clean, compact display in the network request list.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Format a single variable value for display
|
|
16
|
+
*
|
|
17
|
+
* @param value - The variable value to format
|
|
18
|
+
* @param maxLength - Maximum length for string values (default: 30)
|
|
19
|
+
* @returns Formatted string or null if value should be skipped
|
|
20
|
+
*/
|
|
21
|
+
function formatValue(value, maxLength = 30) {
|
|
22
|
+
// Null/undefined - skip
|
|
23
|
+
if (value === null || value === undefined) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Boolean
|
|
28
|
+
if (typeof value === 'boolean') {
|
|
29
|
+
return String(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Number
|
|
33
|
+
if (typeof value === 'number') {
|
|
34
|
+
return String(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// String
|
|
38
|
+
if (typeof value === 'string') {
|
|
39
|
+
// Truncate long strings
|
|
40
|
+
if (value.length > maxLength) {
|
|
41
|
+
return value.substring(0, maxLength - 1) + '…';
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Array - take first value or show count
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
if (value.length === 0) return null;
|
|
49
|
+
if (value.length === 1) {
|
|
50
|
+
return formatValue(value[0], maxLength);
|
|
51
|
+
}
|
|
52
|
+
// Multiple items - show count
|
|
53
|
+
return `${value.length} items`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Object - extract first string/number value
|
|
57
|
+
if (typeof value === 'object') {
|
|
58
|
+
const entries = Object.entries(value);
|
|
59
|
+
if (entries.length === 0) return null;
|
|
60
|
+
|
|
61
|
+
// Find first meaningful value
|
|
62
|
+
for (const [_, v] of entries) {
|
|
63
|
+
const formatted = formatValue(v, maxLength);
|
|
64
|
+
if (formatted) {
|
|
65
|
+
return formatted;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extract variable values from GraphQL variables object
|
|
75
|
+
*
|
|
76
|
+
* Returns an array of formatted values for display with arrow notation.
|
|
77
|
+
*
|
|
78
|
+
* @param variables - GraphQL variables object
|
|
79
|
+
* @param maxValues - Maximum number of values to show (default: 3)
|
|
80
|
+
* @returns Array of formatted variable values
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* formatGraphQLVariables({ id: "pikachu" })
|
|
84
|
+
* // Returns: ["pikachu"]
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* formatGraphQLVariables({ userId: 123, includeProfile: true })
|
|
88
|
+
* // Returns: ["123", "true"]
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* formatGraphQLVariables({ filter: { status: "active" }, limit: 10 })
|
|
92
|
+
* // Returns: ["active", "10"]
|
|
93
|
+
*/
|
|
94
|
+
export function formatGraphQLVariables(variables, maxValues = 3) {
|
|
95
|
+
if (!variables || typeof variables !== 'object') {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const values = [];
|
|
99
|
+
|
|
100
|
+
// Extract values from variables object
|
|
101
|
+
for (const [_, value] of Object.entries(variables)) {
|
|
102
|
+
const formatted = formatValue(value);
|
|
103
|
+
if (formatted) {
|
|
104
|
+
values.push(formatted);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Stop if we've reached max values
|
|
108
|
+
if (values.length >= maxValues) {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return values.length > 0 ? values : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Combine operation name with variables using arrow notation
|
|
117
|
+
*
|
|
118
|
+
* Matches React Query display pattern: "pokemon › Sandshrew"
|
|
119
|
+
* This provides visual consistency across all dev tools.
|
|
120
|
+
*
|
|
121
|
+
* @param operationName - GraphQL operation name (e.g., "GetPokemon")
|
|
122
|
+
* @param variables - GraphQL variables object
|
|
123
|
+
* @returns Formatted string with arrow notation
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* formatGraphQLDisplay("GetPokemon", { id: "Sandshrew" })
|
|
127
|
+
* // Returns: "GetPokemon › Sandshrew"
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* formatGraphQLDisplay("GetUser", { userId: 123, includeProfile: true })
|
|
131
|
+
* // Returns: "GetUser › 123 › true"
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* formatGraphQLDisplay("GetPosts", { status: "published", limit: 10, offset: 0 })
|
|
135
|
+
* // Returns: "GetPosts › published › 10 › 0" (first 3 values by default)
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* formatGraphQLDisplay("GetCurrentUser", {})
|
|
139
|
+
* // Returns: "GetCurrentUser" (no variables)
|
|
140
|
+
*/
|
|
141
|
+
export function formatGraphQLDisplay(operationName, variables) {
|
|
142
|
+
const values = formatGraphQLVariables(variables);
|
|
143
|
+
if (!values || values.length === 0) {
|
|
144
|
+
// No variables - just operation name
|
|
145
|
+
return operationName;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Combine: "GetPokemon › Sandshrew" (matches React Query pattern)
|
|
149
|
+
return [operationName, ...values].join(" › ");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Search GraphQL variables for a given text
|
|
154
|
+
*
|
|
155
|
+
* Recursively searches through all variable values to find matches.
|
|
156
|
+
* Used for filtering GraphQL requests by variable content.
|
|
157
|
+
*
|
|
158
|
+
* @param variables - GraphQL variables object
|
|
159
|
+
* @param searchText - Text to search for (already lowercased)
|
|
160
|
+
* @returns true if search text found in any variable value
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* searchGraphQLVariables({ id: "pikachu" }, "pika")
|
|
164
|
+
* // Returns: true
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* searchGraphQLVariables({ userId: 123, name: "John" }, "123")
|
|
168
|
+
* // Returns: true
|
|
169
|
+
*/
|
|
170
|
+
export function searchGraphQLVariables(variables, searchText) {
|
|
171
|
+
if (!variables || typeof variables !== 'object') {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Search through all variable values
|
|
176
|
+
for (const value of Object.values(variables)) {
|
|
177
|
+
if (searchValue(value, searchText)) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Recursively search a value for matching text
|
|
186
|
+
*/
|
|
187
|
+
function searchValue(value, searchText) {
|
|
188
|
+
// String - direct match
|
|
189
|
+
if (typeof value === 'string') {
|
|
190
|
+
return value.toLowerCase().includes(searchText);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Number - convert to string and match
|
|
194
|
+
if (typeof value === 'number') {
|
|
195
|
+
return String(value).includes(searchText);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Boolean - convert to string and match
|
|
199
|
+
if (typeof value === 'boolean') {
|
|
200
|
+
return String(value).includes(searchText);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Array - search each item
|
|
204
|
+
if (Array.isArray(value)) {
|
|
205
|
+
return value.some(item => searchValue(item, searchText));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Object - search nested values
|
|
209
|
+
if (typeof value === 'object' && value !== null) {
|
|
210
|
+
return Object.values(value).some(v => searchValue(v, searchText));
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Formatting utilities for network events
|
|
5
|
+
* Re-exports shared utilities to maintain backward compatibility
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Re-export shared formatting utilities
|
|
9
|
+
export { formatBytes, formatDuration, formatHttpStatus, getMethodColor } from "@buoy-gg/shared-ui";
|