@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,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.formatGraphQLDisplay = formatGraphQLDisplay;
|
|
7
|
+
exports.formatGraphQLVariables = formatGraphQLVariables;
|
|
8
|
+
exports.searchGraphQLVariables = searchGraphQLVariables;
|
|
9
|
+
/**
|
|
10
|
+
* Format GraphQL variables for display using arrow notation
|
|
11
|
+
*
|
|
12
|
+
* Mimics React Query key display format to provide visual consistency:
|
|
13
|
+
* - React Query: ["pokemon", "Sandshrew"] → "pokemon › Sandshrew"
|
|
14
|
+
* - GraphQL: GetPokemon(id: "Sandshrew") → "GetPokemon › Sandshrew"
|
|
15
|
+
*
|
|
16
|
+
* Extracts only the values from variables (not keys) and formats them
|
|
17
|
+
* for clean, compact display in the network request list.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Format a single variable value for display
|
|
22
|
+
*
|
|
23
|
+
* @param value - The variable value to format
|
|
24
|
+
* @param maxLength - Maximum length for string values (default: 30)
|
|
25
|
+
* @returns Formatted string or null if value should be skipped
|
|
26
|
+
*/
|
|
27
|
+
function formatValue(value, maxLength = 30) {
|
|
28
|
+
// Null/undefined - skip
|
|
29
|
+
if (value === null || value === undefined) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Boolean
|
|
34
|
+
if (typeof value === 'boolean') {
|
|
35
|
+
return String(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Number
|
|
39
|
+
if (typeof value === 'number') {
|
|
40
|
+
return String(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// String
|
|
44
|
+
if (typeof value === 'string') {
|
|
45
|
+
// Truncate long strings
|
|
46
|
+
if (value.length > maxLength) {
|
|
47
|
+
return value.substring(0, maxLength - 1) + '…';
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Array - take first value or show count
|
|
53
|
+
if (Array.isArray(value)) {
|
|
54
|
+
if (value.length === 0) return null;
|
|
55
|
+
if (value.length === 1) {
|
|
56
|
+
return formatValue(value[0], maxLength);
|
|
57
|
+
}
|
|
58
|
+
// Multiple items - show count
|
|
59
|
+
return `${value.length} items`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Object - extract first string/number value
|
|
63
|
+
if (typeof value === 'object') {
|
|
64
|
+
const entries = Object.entries(value);
|
|
65
|
+
if (entries.length === 0) return null;
|
|
66
|
+
|
|
67
|
+
// Find first meaningful value
|
|
68
|
+
for (const [_, v] of entries) {
|
|
69
|
+
const formatted = formatValue(v, maxLength);
|
|
70
|
+
if (formatted) {
|
|
71
|
+
return formatted;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Extract variable values from GraphQL variables object
|
|
81
|
+
*
|
|
82
|
+
* Returns an array of formatted values for display with arrow notation.
|
|
83
|
+
*
|
|
84
|
+
* @param variables - GraphQL variables object
|
|
85
|
+
* @param maxValues - Maximum number of values to show (default: 3)
|
|
86
|
+
* @returns Array of formatted variable values
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* formatGraphQLVariables({ id: "pikachu" })
|
|
90
|
+
* // Returns: ["pikachu"]
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* formatGraphQLVariables({ userId: 123, includeProfile: true })
|
|
94
|
+
* // Returns: ["123", "true"]
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* formatGraphQLVariables({ filter: { status: "active" }, limit: 10 })
|
|
98
|
+
* // Returns: ["active", "10"]
|
|
99
|
+
*/
|
|
100
|
+
function formatGraphQLVariables(variables, maxValues = 3) {
|
|
101
|
+
if (!variables || typeof variables !== 'object') {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const values = [];
|
|
105
|
+
|
|
106
|
+
// Extract values from variables object
|
|
107
|
+
for (const [_, value] of Object.entries(variables)) {
|
|
108
|
+
const formatted = formatValue(value);
|
|
109
|
+
if (formatted) {
|
|
110
|
+
values.push(formatted);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Stop if we've reached max values
|
|
114
|
+
if (values.length >= maxValues) {
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return values.length > 0 ? values : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Combine operation name with variables using arrow notation
|
|
123
|
+
*
|
|
124
|
+
* Matches React Query display pattern: "pokemon › Sandshrew"
|
|
125
|
+
* This provides visual consistency across all dev tools.
|
|
126
|
+
*
|
|
127
|
+
* @param operationName - GraphQL operation name (e.g., "GetPokemon")
|
|
128
|
+
* @param variables - GraphQL variables object
|
|
129
|
+
* @returns Formatted string with arrow notation
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* formatGraphQLDisplay("GetPokemon", { id: "Sandshrew" })
|
|
133
|
+
* // Returns: "GetPokemon › Sandshrew"
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* formatGraphQLDisplay("GetUser", { userId: 123, includeProfile: true })
|
|
137
|
+
* // Returns: "GetUser › 123 › true"
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* formatGraphQLDisplay("GetPosts", { status: "published", limit: 10, offset: 0 })
|
|
141
|
+
* // Returns: "GetPosts › published › 10 › 0" (first 3 values by default)
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* formatGraphQLDisplay("GetCurrentUser", {})
|
|
145
|
+
* // Returns: "GetCurrentUser" (no variables)
|
|
146
|
+
*/
|
|
147
|
+
function formatGraphQLDisplay(operationName, variables) {
|
|
148
|
+
const values = formatGraphQLVariables(variables);
|
|
149
|
+
if (!values || values.length === 0) {
|
|
150
|
+
// No variables - just operation name
|
|
151
|
+
return operationName;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Combine: "GetPokemon › Sandshrew" (matches React Query pattern)
|
|
155
|
+
return [operationName, ...values].join(" › ");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Search GraphQL variables for a given text
|
|
160
|
+
*
|
|
161
|
+
* Recursively searches through all variable values to find matches.
|
|
162
|
+
* Used for filtering GraphQL requests by variable content.
|
|
163
|
+
*
|
|
164
|
+
* @param variables - GraphQL variables object
|
|
165
|
+
* @param searchText - Text to search for (already lowercased)
|
|
166
|
+
* @returns true if search text found in any variable value
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* searchGraphQLVariables({ id: "pikachu" }, "pika")
|
|
170
|
+
* // Returns: true
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* searchGraphQLVariables({ userId: 123, name: "John" }, "123")
|
|
174
|
+
* // Returns: true
|
|
175
|
+
*/
|
|
176
|
+
function searchGraphQLVariables(variables, searchText) {
|
|
177
|
+
if (!variables || typeof variables !== 'object') {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Search through all variable values
|
|
182
|
+
for (const value of Object.values(variables)) {
|
|
183
|
+
if (searchValue(value, searchText)) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Recursively search a value for matching text
|
|
192
|
+
*/
|
|
193
|
+
function searchValue(value, searchText) {
|
|
194
|
+
// String - direct match
|
|
195
|
+
if (typeof value === 'string') {
|
|
196
|
+
return value.toLowerCase().includes(searchText);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Number - convert to string and match
|
|
200
|
+
if (typeof value === 'number') {
|
|
201
|
+
return String(value).includes(searchText);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Boolean - convert to string and match
|
|
205
|
+
if (typeof value === 'boolean') {
|
|
206
|
+
return String(value).includes(searchText);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Array - search each item
|
|
210
|
+
if (Array.isArray(value)) {
|
|
211
|
+
return value.some(item => searchValue(item, searchText));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Object - search nested values
|
|
215
|
+
if (typeof value === 'object' && value !== null) {
|
|
216
|
+
return Object.values(value).some(v => searchValue(v, searchText));
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
Object.defineProperty(exports, "formatBytes", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function () {
|
|
9
|
+
return _sharedUi.formatBytes;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
Object.defineProperty(exports, "formatDuration", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () {
|
|
15
|
+
return _sharedUi.formatDuration;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(exports, "formatHttpStatus", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () {
|
|
21
|
+
return _sharedUi.formatHttpStatus;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(exports, "getMethodColor", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
get: function () {
|
|
27
|
+
return _sharedUi.getMethodColor;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.networkEventStore = void 0;
|
|
7
|
+
var _extractOperationName = require("./extractOperationName");
|
|
8
|
+
/**
|
|
9
|
+
* Network event store for managing captured network requests
|
|
10
|
+
* Works with the Reactotron-style network listener
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
class NetworkEventStore {
|
|
14
|
+
events = [];
|
|
15
|
+
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
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Process a network listener event
|
|
22
|
+
*/
|
|
23
|
+
processNetworkEvent(event) {
|
|
24
|
+
const {
|
|
25
|
+
request
|
|
26
|
+
} = event;
|
|
27
|
+
if (event.type === "request") {
|
|
28
|
+
// Check for duplicate request based on URL, method, and timing
|
|
29
|
+
const requestKey = `${request.method}:${request.url}`;
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
const lastRequestTime = this.recentRequests.get(requestKey);
|
|
32
|
+
|
|
33
|
+
// If same request within 50ms, likely a duplicate from XHR/fetch dual interception
|
|
34
|
+
if (lastRequestTime && now - lastRequestTime < 50) {
|
|
35
|
+
return; // Skip duplicate
|
|
36
|
+
}
|
|
37
|
+
this.recentRequests.set(requestKey, now);
|
|
38
|
+
|
|
39
|
+
// Clean up old entries to prevent memory leak
|
|
40
|
+
if (this.recentRequests.size > 100) {
|
|
41
|
+
const cutoff = now - 5000; // Remove entries older than 5 seconds
|
|
42
|
+
for (const [key, time] of this.recentRequests.entries()) {
|
|
43
|
+
if (time < cutoff) {
|
|
44
|
+
this.recentRequests.delete(key);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Create new network event for request
|
|
50
|
+
// Build full URL with query params if present
|
|
51
|
+
const queryString = request.params ? `?${new URLSearchParams(request.params).toString()}` : "";
|
|
52
|
+
const fullUrl = `${request.url}${queryString}`;
|
|
53
|
+
|
|
54
|
+
// Extract GraphQL operation name and variables for searchability
|
|
55
|
+
let operationName;
|
|
56
|
+
let graphqlVariables;
|
|
57
|
+
if (request.client === 'graphql') {
|
|
58
|
+
const extracted = (0, _extractOperationName.extractOperationName)(request.data);
|
|
59
|
+
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') {
|
|
63
|
+
graphqlVariables = request.data.variables;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const networkEvent = {
|
|
67
|
+
id: request.id,
|
|
68
|
+
method: request.method,
|
|
69
|
+
url: fullUrl,
|
|
70
|
+
// Store FULL URL with query params
|
|
71
|
+
host: this.extractHost(request.url),
|
|
72
|
+
path: this.extractPath(request.url),
|
|
73
|
+
query: queryString,
|
|
74
|
+
timestamp: event.timestamp.getTime(),
|
|
75
|
+
requestHeaders: request.headers || {},
|
|
76
|
+
requestData: request.data,
|
|
77
|
+
requestSize: this.getDataSize(request.data),
|
|
78
|
+
responseHeaders: {},
|
|
79
|
+
requestClient: request.client,
|
|
80
|
+
operationName,
|
|
81
|
+
// GraphQL operation name for search/filter
|
|
82
|
+
graphqlVariables // GraphQL variables for display and search
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Store as pending
|
|
86
|
+
this.pendingRequests.set(request.id, networkEvent);
|
|
87
|
+
|
|
88
|
+
// Add to events list
|
|
89
|
+
this.events = [networkEvent, ...this.events].slice(0, this.maxEvents);
|
|
90
|
+
this.notifyListeners();
|
|
91
|
+
} else if (event.type === "response" || event.type === "error") {
|
|
92
|
+
// Find and update the pending request
|
|
93
|
+
const index = this.events.findIndex(e => e.id === request.id);
|
|
94
|
+
if (index !== -1) {
|
|
95
|
+
const updatedEvent = {
|
|
96
|
+
...this.events[index],
|
|
97
|
+
duration: event.duration
|
|
98
|
+
};
|
|
99
|
+
if (event.response) {
|
|
100
|
+
updatedEvent.status = event.response.status;
|
|
101
|
+
updatedEvent.statusText = event.response.statusText;
|
|
102
|
+
updatedEvent.responseHeaders = event.response.headers || {};
|
|
103
|
+
updatedEvent.responseData = event.response.body;
|
|
104
|
+
updatedEvent.responseSize = event.response.size || 0;
|
|
105
|
+
updatedEvent.responseType = event.response.headers?.["content-type"];
|
|
106
|
+
}
|
|
107
|
+
if (event.error) {
|
|
108
|
+
updatedEvent.error = event.error.message;
|
|
109
|
+
updatedEvent.status = updatedEvent.status || 0;
|
|
110
|
+
}
|
|
111
|
+
this.events[index] = updatedEvent;
|
|
112
|
+
this.pendingRequests.delete(request.id);
|
|
113
|
+
this.notifyListeners();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Extract host from URL
|
|
120
|
+
*/
|
|
121
|
+
extractHost(url) {
|
|
122
|
+
try {
|
|
123
|
+
const urlObj = new URL(url);
|
|
124
|
+
// @ts-ignore - this does exist on native
|
|
125
|
+
return urlObj.hostname;
|
|
126
|
+
} catch {
|
|
127
|
+
return "";
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Extract path from URL
|
|
133
|
+
*/
|
|
134
|
+
extractPath(url) {
|
|
135
|
+
try {
|
|
136
|
+
const urlObj = new URL(url);
|
|
137
|
+
// @ts-ignore - this does exist on native
|
|
138
|
+
return urlObj.pathname;
|
|
139
|
+
} catch {
|
|
140
|
+
return url;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get size of data
|
|
146
|
+
*/
|
|
147
|
+
getDataSize(data) {
|
|
148
|
+
if (!data) return 0;
|
|
149
|
+
if (typeof data === "string") return data.length;
|
|
150
|
+
try {
|
|
151
|
+
return JSON.stringify(data).length;
|
|
152
|
+
} catch {
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get all events
|
|
159
|
+
*/
|
|
160
|
+
getEvents() {
|
|
161
|
+
return [...this.events];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get event by ID
|
|
166
|
+
*/
|
|
167
|
+
getEventById(id) {
|
|
168
|
+
return this.events.find(e => e.id === id);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Clear all events
|
|
173
|
+
*/
|
|
174
|
+
clearEvents() {
|
|
175
|
+
this.events = [];
|
|
176
|
+
this.pendingRequests.clear();
|
|
177
|
+
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
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Notify all listeners of changes
|
|
193
|
+
*/
|
|
194
|
+
notifyListeners() {
|
|
195
|
+
const events = this.getEvents();
|
|
196
|
+
this.listeners.forEach(listener => listener(events));
|
|
197
|
+
}
|
|
198
|
+
|
|
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();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get statistics about network events
|
|
212
|
+
*/
|
|
213
|
+
getStats() {
|
|
214
|
+
const total = this.events.length;
|
|
215
|
+
const successful = this.events.filter(e => e.status && e.status >= 200 && e.status < 300).length;
|
|
216
|
+
const failed = this.events.filter(e => e.error || e.status && e.status >= 400).length;
|
|
217
|
+
const pending = this.events.filter(e => !e.status && !e.error).length;
|
|
218
|
+
const durations = this.events.filter(e => e.duration).map(e => e.duration);
|
|
219
|
+
const avgDuration = durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0;
|
|
220
|
+
const totalSent = this.events.reduce((sum, e) => sum + (e.requestSize || 0), 0);
|
|
221
|
+
const totalReceived = this.events.reduce((sum, e) => sum + (e.responseSize || 0), 0);
|
|
222
|
+
return {
|
|
223
|
+
totalRequests: total,
|
|
224
|
+
successfulRequests: successful,
|
|
225
|
+
failedRequests: failed,
|
|
226
|
+
pendingRequests: pending,
|
|
227
|
+
totalDataSent: totalSent,
|
|
228
|
+
totalDataReceived: totalReceived,
|
|
229
|
+
averageDuration: Math.round(avgDuration)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Filter events by criteria
|
|
235
|
+
*/
|
|
236
|
+
filterEvents(filter) {
|
|
237
|
+
let filtered = [...this.events];
|
|
238
|
+
if (filter.method) {
|
|
239
|
+
filtered = filtered.filter(e => e.method === filter.method);
|
|
240
|
+
}
|
|
241
|
+
if (filter.status) {
|
|
242
|
+
switch (filter.status) {
|
|
243
|
+
case "success":
|
|
244
|
+
filtered = filtered.filter(e => e.status && e.status >= 200 && e.status < 300);
|
|
245
|
+
break;
|
|
246
|
+
case "error":
|
|
247
|
+
filtered = filtered.filter(e => e.error || e.status && e.status >= 400);
|
|
248
|
+
break;
|
|
249
|
+
case "pending":
|
|
250
|
+
filtered = filtered.filter(e => !e.status && !e.error);
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (filter.searchText) {
|
|
255
|
+
const search = filter.searchText.toLowerCase();
|
|
256
|
+
filtered = filtered.filter(e => e.url.toLowerCase().includes(search) || e.method.toLowerCase().includes(search) || e.error && e.error.toLowerCase().includes(search));
|
|
257
|
+
}
|
|
258
|
+
if (filter.host) {
|
|
259
|
+
filtered = filtered.filter(e => e.host === filter.host);
|
|
260
|
+
}
|
|
261
|
+
return filtered;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
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.
|
|
268
|
+
*/
|
|
269
|
+
const networkEventStore = exports.networkEventStore = new NetworkEventStore();
|