@rozenite/network-activity-plugin 1.5.0 → 1.6.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.
- package/CHANGELOG.md +19 -0
- package/README.md +2 -0
- package/dist/boot-recording.cjs +1 -1
- package/dist/boot-recording.js +3 -1084
- package/dist/rozenite.json +1 -1
- package/dist/src/react-native/agent/__tests__/network-activity-agent-state.test.d.ts +1 -0
- package/dist/src/react-native/agent/state.d.ts +733 -0
- package/dist/src/react-native/agent/tools.d.ts +11 -0
- package/dist/src/react-native/agent/use-network-activity-agent-tools.d.ts +13 -0
- package/dist/useNetworkActivityDevTools.cjs +931 -0
- package/dist/useNetworkActivityDevTools.js +932 -1
- package/package.json +5 -4
- package/src/react-native/agent/__tests__/network-activity-agent-state.test.ts +250 -0
- package/src/react-native/agent/state.ts +869 -0
- package/src/react-native/agent/tools.ts +146 -0
- package/src/react-native/agent/use-network-activity-agent-tools.ts +244 -0
- package/src/react-native/http/http-inspector.ts +0 -1
- package/src/react-native/useNetworkActivityDevTools.ts +11 -0
- package/tsconfig.json +3 -0
|
@@ -3,6 +3,928 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
3
3
|
const react = require("react");
|
|
4
4
|
const pluginBridge = require("@rozenite/plugin-bridge");
|
|
5
5
|
const bootRecording = require("./boot-recording.cjs");
|
|
6
|
+
const agentBridge = require("@rozenite/agent-bridge");
|
|
7
|
+
const DEFAULT_PAGE_LIMIT = 20;
|
|
8
|
+
const MAX_PAGE_LIMIT = 100;
|
|
9
|
+
const HTTP_BUFFER_CAPACITY = 500;
|
|
10
|
+
const REALTIME_BUFFER_CAPACITY = 200;
|
|
11
|
+
const createInitialState = () => ({
|
|
12
|
+
isRecording: false,
|
|
13
|
+
generation: 0,
|
|
14
|
+
httpOrder: [],
|
|
15
|
+
httpRecords: /* @__PURE__ */ new Map(),
|
|
16
|
+
httpTotalRecorded: 0,
|
|
17
|
+
httpEvictedCount: 0,
|
|
18
|
+
httpTruncated: false,
|
|
19
|
+
realtimeOrder: [],
|
|
20
|
+
realtimeRecords: /* @__PURE__ */ new Map(),
|
|
21
|
+
realtimeTotalRecorded: 0,
|
|
22
|
+
realtimeEvictedCount: 0,
|
|
23
|
+
realtimeTruncated: false,
|
|
24
|
+
nextMessageId: 1,
|
|
25
|
+
recordingMetadata: {
|
|
26
|
+
enabledInspectors: {
|
|
27
|
+
http: true,
|
|
28
|
+
websocket: true,
|
|
29
|
+
sse: true
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
const getLimit = (value) => {
|
|
34
|
+
if (typeof value !== "number" || !Number.isInteger(value) || !Number.isFinite(value) || value < 1) {
|
|
35
|
+
return DEFAULT_PAGE_LIMIT;
|
|
36
|
+
}
|
|
37
|
+
return Math.min(value, MAX_PAGE_LIMIT);
|
|
38
|
+
};
|
|
39
|
+
const encodeCursor = (scope, index) => {
|
|
40
|
+
return `${scope}:${index}`;
|
|
41
|
+
};
|
|
42
|
+
const decodeCursor = (cursor, scope) => {
|
|
43
|
+
const [cursorScope, rawIndex] = cursor.split(":", 2);
|
|
44
|
+
if (cursorScope !== scope || !rawIndex) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"Cursor does not match the requested listing. Run the command again."
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const index = Number(rawIndex);
|
|
50
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
51
|
+
throw new Error("Cursor is invalid. Run the command again.");
|
|
52
|
+
}
|
|
53
|
+
return index;
|
|
54
|
+
};
|
|
55
|
+
const paginate = (rows, scope, limit, cursor) => {
|
|
56
|
+
const startIndex = cursor ? decodeCursor(cursor, scope) : 0;
|
|
57
|
+
const endIndex = Math.min(startIndex + limit, rows.length);
|
|
58
|
+
const hasMore = endIndex < rows.length;
|
|
59
|
+
return {
|
|
60
|
+
items: rows.slice(startIndex, endIndex),
|
|
61
|
+
page: {
|
|
62
|
+
limit,
|
|
63
|
+
hasMore,
|
|
64
|
+
...hasMore ? { nextCursor: encodeCursor(scope, endIndex) } : {}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
const serializeRequestBody = (requestId, postData) => {
|
|
69
|
+
if (!postData) {
|
|
70
|
+
return {
|
|
71
|
+
requestId,
|
|
72
|
+
available: false,
|
|
73
|
+
reason: "No request body is available for this request."
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (postData.type === "text") {
|
|
77
|
+
return {
|
|
78
|
+
requestId,
|
|
79
|
+
available: true,
|
|
80
|
+
body: postData.value,
|
|
81
|
+
base64Encoded: false
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
requestId,
|
|
86
|
+
available: true,
|
|
87
|
+
body: bootRecording.safeStringify(postData),
|
|
88
|
+
base64Encoded: false
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
const createHttpSummary = (record) => ({
|
|
92
|
+
requestId: record.requestId,
|
|
93
|
+
method: record.request.method,
|
|
94
|
+
url: record.request.url,
|
|
95
|
+
status: record.response?.status ?? null,
|
|
96
|
+
type: record.resourceType,
|
|
97
|
+
startTimeMs: record.startTimeMs,
|
|
98
|
+
endTimeMs: record.endTimeMs ?? null,
|
|
99
|
+
durationMs: record.durationMs ?? null,
|
|
100
|
+
transferSize: record.size ?? null,
|
|
101
|
+
encodedDataLength: record.response?.size ?? null,
|
|
102
|
+
outcome: record.status === "failed" ? "failed" : record.status === "finished" ? "success" : "in-flight"
|
|
103
|
+
});
|
|
104
|
+
const getRealtimeSummary = (record) => {
|
|
105
|
+
if (record.kind === "websocket") {
|
|
106
|
+
return {
|
|
107
|
+
requestId: record.requestId,
|
|
108
|
+
kind: record.kind,
|
|
109
|
+
url: record.url,
|
|
110
|
+
status: record.status,
|
|
111
|
+
startedAt: record.startedAt,
|
|
112
|
+
endedAt: record.endedAt ?? null,
|
|
113
|
+
durationMs: record.durationMs ?? null,
|
|
114
|
+
messageCount: record.messages.length,
|
|
115
|
+
error: record.error ?? null,
|
|
116
|
+
closeCode: record.closeCode ?? null
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
requestId: record.requestId,
|
|
121
|
+
kind: record.kind,
|
|
122
|
+
url: record.request?.url ?? record.response?.url ?? null,
|
|
123
|
+
status: record.status,
|
|
124
|
+
startedAt: record.startedAt,
|
|
125
|
+
endedAt: record.endedAt ?? null,
|
|
126
|
+
durationMs: record.durationMs ?? null,
|
|
127
|
+
messageCount: record.messages.length,
|
|
128
|
+
error: record.error ?? null,
|
|
129
|
+
httpStatus: record.response?.status ?? null
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
const trimMap = (order, records, capacity) => {
|
|
133
|
+
let evicted = 0;
|
|
134
|
+
while (order.length > capacity) {
|
|
135
|
+
const oldestId = order.shift();
|
|
136
|
+
if (!oldestId) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
records.delete(oldestId);
|
|
140
|
+
evicted += 1;
|
|
141
|
+
}
|
|
142
|
+
return evicted;
|
|
143
|
+
};
|
|
144
|
+
const createNetworkActivityAgentState = () => {
|
|
145
|
+
const state = createInitialState();
|
|
146
|
+
const getStatus = () => ({
|
|
147
|
+
recording: {
|
|
148
|
+
isRecording: state.isRecording,
|
|
149
|
+
startedAt: state.startedAt ?? null,
|
|
150
|
+
stoppedAt: state.stoppedAt ?? null,
|
|
151
|
+
httpRequestCount: state.httpOrder.length,
|
|
152
|
+
realtimeConnectionCount: state.realtimeOrder.length,
|
|
153
|
+
http: {
|
|
154
|
+
totalRecorded: state.httpTotalRecorded,
|
|
155
|
+
evictedCount: state.httpEvictedCount,
|
|
156
|
+
truncated: state.httpTruncated,
|
|
157
|
+
capacity: HTTP_BUFFER_CAPACITY
|
|
158
|
+
},
|
|
159
|
+
realtime: {
|
|
160
|
+
totalRecorded: state.realtimeTotalRecorded,
|
|
161
|
+
evictedCount: state.realtimeEvictedCount,
|
|
162
|
+
truncated: state.realtimeTruncated,
|
|
163
|
+
capacity: REALTIME_BUFFER_CAPACITY
|
|
164
|
+
},
|
|
165
|
+
generation: state.generation,
|
|
166
|
+
enabledInspectors: state.recordingMetadata.enabledInspectors
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
const ensureHttpRecord = (requestId, fallback) => {
|
|
170
|
+
const existing = state.httpRecords.get(requestId);
|
|
171
|
+
if (existing) {
|
|
172
|
+
return existing;
|
|
173
|
+
}
|
|
174
|
+
const record = {
|
|
175
|
+
requestId,
|
|
176
|
+
request: fallback?.request || {
|
|
177
|
+
url: "",
|
|
178
|
+
method: "GET",
|
|
179
|
+
headers: {}
|
|
180
|
+
},
|
|
181
|
+
resourceType: fallback?.resourceType || "Other",
|
|
182
|
+
initiator: fallback?.initiator || { type: "other" },
|
|
183
|
+
startTimeMs: fallback?.startTimeMs ?? Date.now(),
|
|
184
|
+
status: fallback?.status || "pending"
|
|
185
|
+
};
|
|
186
|
+
state.httpRecords.set(requestId, record);
|
|
187
|
+
state.httpOrder.push(requestId);
|
|
188
|
+
state.httpTotalRecorded += 1;
|
|
189
|
+
const evicted = trimMap(
|
|
190
|
+
state.httpOrder,
|
|
191
|
+
state.httpRecords,
|
|
192
|
+
HTTP_BUFFER_CAPACITY
|
|
193
|
+
);
|
|
194
|
+
if (evicted > 0) {
|
|
195
|
+
state.httpEvictedCount += evicted;
|
|
196
|
+
state.httpTruncated = true;
|
|
197
|
+
}
|
|
198
|
+
return record;
|
|
199
|
+
};
|
|
200
|
+
const ensureRealtimeRecord = (requestId, createRecord) => {
|
|
201
|
+
const existing = state.realtimeRecords.get(requestId);
|
|
202
|
+
if (existing) {
|
|
203
|
+
return existing;
|
|
204
|
+
}
|
|
205
|
+
const record = createRecord();
|
|
206
|
+
state.realtimeRecords.set(requestId, record);
|
|
207
|
+
state.realtimeOrder.push(requestId);
|
|
208
|
+
state.realtimeTotalRecorded += 1;
|
|
209
|
+
const evicted = trimMap(
|
|
210
|
+
state.realtimeOrder,
|
|
211
|
+
state.realtimeRecords,
|
|
212
|
+
REALTIME_BUFFER_CAPACITY
|
|
213
|
+
);
|
|
214
|
+
if (evicted > 0) {
|
|
215
|
+
state.realtimeEvictedCount += evicted;
|
|
216
|
+
state.realtimeTruncated = true;
|
|
217
|
+
}
|
|
218
|
+
return record;
|
|
219
|
+
};
|
|
220
|
+
const nextMessageId = (prefix) => {
|
|
221
|
+
const id = `${prefix}-${state.nextMessageId}`;
|
|
222
|
+
state.nextMessageId += 1;
|
|
223
|
+
return id;
|
|
224
|
+
};
|
|
225
|
+
return {
|
|
226
|
+
startRecording(metadata) {
|
|
227
|
+
state.isRecording = true;
|
|
228
|
+
state.startedAt = Date.now();
|
|
229
|
+
state.stoppedAt = void 0;
|
|
230
|
+
state.generation += 1;
|
|
231
|
+
state.httpOrder = [];
|
|
232
|
+
state.httpRecords.clear();
|
|
233
|
+
state.httpTotalRecorded = 0;
|
|
234
|
+
state.httpEvictedCount = 0;
|
|
235
|
+
state.httpTruncated = false;
|
|
236
|
+
state.realtimeOrder = [];
|
|
237
|
+
state.realtimeRecords.clear();
|
|
238
|
+
state.realtimeTotalRecorded = 0;
|
|
239
|
+
state.realtimeEvictedCount = 0;
|
|
240
|
+
state.realtimeTruncated = false;
|
|
241
|
+
state.recordingMetadata = {
|
|
242
|
+
enabledInspectors: {
|
|
243
|
+
...state.recordingMetadata.enabledInspectors,
|
|
244
|
+
...metadata?.enabledInspectors
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
return getStatus();
|
|
248
|
+
},
|
|
249
|
+
stopRecording() {
|
|
250
|
+
if (!state.isRecording) {
|
|
251
|
+
throw new Error("No active network recording for this plugin session");
|
|
252
|
+
}
|
|
253
|
+
state.isRecording = false;
|
|
254
|
+
state.stoppedAt = Date.now();
|
|
255
|
+
return getStatus();
|
|
256
|
+
},
|
|
257
|
+
getStatus,
|
|
258
|
+
getHttpRecord(requestId) {
|
|
259
|
+
return state.httpRecords.get(requestId) || null;
|
|
260
|
+
},
|
|
261
|
+
getRealtimeRecord(requestId) {
|
|
262
|
+
return state.realtimeRecords.get(requestId) || null;
|
|
263
|
+
},
|
|
264
|
+
listRequests(input) {
|
|
265
|
+
const limit = getLimit(input.limit);
|
|
266
|
+
const rows = state.httpOrder.map((requestId) => state.httpRecords.get(requestId)).filter((record) => !!record).reverse().map(createHttpSummary);
|
|
267
|
+
return {
|
|
268
|
+
...getStatus(),
|
|
269
|
+
...paginate(rows, `http-${state.generation}`, limit, input.cursor)
|
|
270
|
+
};
|
|
271
|
+
},
|
|
272
|
+
listRealtimeConnections(input) {
|
|
273
|
+
const limit = getLimit(input.limit);
|
|
274
|
+
const rows = state.realtimeOrder.map((requestId) => state.realtimeRecords.get(requestId)).filter((record) => !!record).reverse().map(getRealtimeSummary);
|
|
275
|
+
return {
|
|
276
|
+
...getStatus(),
|
|
277
|
+
...paginate(rows, `realtime-${state.generation}`, limit, input.cursor)
|
|
278
|
+
};
|
|
279
|
+
},
|
|
280
|
+
getRequestDetails(requestId) {
|
|
281
|
+
const record = state.httpRecords.get(requestId);
|
|
282
|
+
if (!record) {
|
|
283
|
+
throw new Error(`Unknown request "${requestId}"`);
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
...getStatus(),
|
|
287
|
+
request: {
|
|
288
|
+
requestId: record.requestId,
|
|
289
|
+
method: record.request.method,
|
|
290
|
+
url: record.request.url,
|
|
291
|
+
type: record.resourceType,
|
|
292
|
+
initiator: record.initiator,
|
|
293
|
+
startTimeMs: record.startTimeMs,
|
|
294
|
+
endTimeMs: record.endTimeMs ?? null,
|
|
295
|
+
durationMs: record.durationMs ?? null,
|
|
296
|
+
request: record.request,
|
|
297
|
+
response: record.response ?? null,
|
|
298
|
+
loadingFinished: record.status === "finished",
|
|
299
|
+
loadingFailed: record.status === "failed",
|
|
300
|
+
failureText: record.error ?? null,
|
|
301
|
+
canceled: record.canceled ?? false,
|
|
302
|
+
progress: record.progress ? {
|
|
303
|
+
loaded: record.progress.loaded,
|
|
304
|
+
total: record.progress.total,
|
|
305
|
+
lengthComputable: record.progress.lengthComputable
|
|
306
|
+
} : null,
|
|
307
|
+
ttfb: record.ttfb ?? null,
|
|
308
|
+
size: record.size ?? null
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
},
|
|
312
|
+
getRealtimeConnectionDetails(requestId) {
|
|
313
|
+
const record = state.realtimeRecords.get(requestId);
|
|
314
|
+
if (!record) {
|
|
315
|
+
throw new Error(`Unknown realtime connection "${requestId}"`);
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
...getStatus(),
|
|
319
|
+
connection: record.kind === "websocket" ? {
|
|
320
|
+
requestId: record.requestId,
|
|
321
|
+
kind: record.kind,
|
|
322
|
+
url: record.url,
|
|
323
|
+
socketId: record.socketId,
|
|
324
|
+
status: record.status,
|
|
325
|
+
startedAt: record.startedAt,
|
|
326
|
+
endedAt: record.endedAt ?? null,
|
|
327
|
+
durationMs: record.durationMs ?? null,
|
|
328
|
+
protocols: record.protocols ?? null,
|
|
329
|
+
options: record.options ?? [],
|
|
330
|
+
error: record.error ?? null,
|
|
331
|
+
closeCode: record.closeCode ?? null,
|
|
332
|
+
closeReason: record.closeReason ?? null,
|
|
333
|
+
messages: record.messages
|
|
334
|
+
} : {
|
|
335
|
+
requestId: record.requestId,
|
|
336
|
+
kind: record.kind,
|
|
337
|
+
status: record.status,
|
|
338
|
+
startedAt: record.startedAt,
|
|
339
|
+
endedAt: record.endedAt ?? null,
|
|
340
|
+
durationMs: record.durationMs ?? null,
|
|
341
|
+
request: record.request ?? null,
|
|
342
|
+
response: record.response ?? null,
|
|
343
|
+
initiator: record.initiator ?? null,
|
|
344
|
+
resourceType: record.resourceType ?? null,
|
|
345
|
+
error: record.error ?? null,
|
|
346
|
+
messages: record.messages
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
},
|
|
350
|
+
getRequestBody(requestId) {
|
|
351
|
+
const record = state.httpRecords.get(requestId);
|
|
352
|
+
if (!record) {
|
|
353
|
+
throw new Error(`Unknown request "${requestId}"`);
|
|
354
|
+
}
|
|
355
|
+
return serializeRequestBody(requestId, record.request.postData);
|
|
356
|
+
},
|
|
357
|
+
onRequestSent(event) {
|
|
358
|
+
if (!state.isRecording) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const record = ensureHttpRecord(event.requestId, {
|
|
362
|
+
request: event.request,
|
|
363
|
+
resourceType: event.type,
|
|
364
|
+
initiator: event.initiator,
|
|
365
|
+
startTimeMs: event.timestamp,
|
|
366
|
+
status: "pending"
|
|
367
|
+
});
|
|
368
|
+
record.request = event.request;
|
|
369
|
+
record.resourceType = event.type;
|
|
370
|
+
record.initiator = event.initiator;
|
|
371
|
+
record.startTimeMs = event.timestamp;
|
|
372
|
+
record.status = "pending";
|
|
373
|
+
record.response = void 0;
|
|
374
|
+
record.endTimeMs = void 0;
|
|
375
|
+
record.durationMs = void 0;
|
|
376
|
+
record.size = void 0;
|
|
377
|
+
record.ttfb = void 0;
|
|
378
|
+
record.error = void 0;
|
|
379
|
+
record.canceled = void 0;
|
|
380
|
+
record.progress = void 0;
|
|
381
|
+
},
|
|
382
|
+
onRequestProgress(event) {
|
|
383
|
+
if (!state.isRecording) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const record = ensureHttpRecord(event.requestId);
|
|
387
|
+
record.progress = event;
|
|
388
|
+
record.status = "loading";
|
|
389
|
+
},
|
|
390
|
+
onResponseReceived(event) {
|
|
391
|
+
if (!state.isRecording) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const record = ensureHttpRecord(event.requestId);
|
|
395
|
+
record.response = event.response;
|
|
396
|
+
record.status = "loading";
|
|
397
|
+
},
|
|
398
|
+
onRequestCompleted(event) {
|
|
399
|
+
if (!state.isRecording) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const record = ensureHttpRecord(event.requestId);
|
|
403
|
+
record.status = "finished";
|
|
404
|
+
record.endTimeMs = event.timestamp;
|
|
405
|
+
record.durationMs = event.duration;
|
|
406
|
+
record.size = event.size;
|
|
407
|
+
record.ttfb = event.ttfb;
|
|
408
|
+
},
|
|
409
|
+
onRequestFailed(event) {
|
|
410
|
+
if (!state.isRecording) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const record = ensureHttpRecord(event.requestId);
|
|
414
|
+
record.status = "failed";
|
|
415
|
+
record.endTimeMs = event.timestamp;
|
|
416
|
+
record.error = event.error;
|
|
417
|
+
record.canceled = event.canceled;
|
|
418
|
+
},
|
|
419
|
+
onWebSocketConnect(event) {
|
|
420
|
+
if (!state.isRecording) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
ensureRealtimeRecord(`ws-${event.socketId}`, () => ({
|
|
424
|
+
requestId: `ws-${event.socketId}`,
|
|
425
|
+
kind: "websocket",
|
|
426
|
+
url: event.url,
|
|
427
|
+
socketId: event.socketId,
|
|
428
|
+
status: "connecting",
|
|
429
|
+
startedAt: event.timestamp,
|
|
430
|
+
protocols: event.protocols,
|
|
431
|
+
options: event.options,
|
|
432
|
+
messages: []
|
|
433
|
+
}));
|
|
434
|
+
},
|
|
435
|
+
onWebSocketOpen(event) {
|
|
436
|
+
if (!state.isRecording) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const record = ensureRealtimeRecord(`ws-${event.socketId}`, () => ({
|
|
440
|
+
requestId: `ws-${event.socketId}`,
|
|
441
|
+
kind: "websocket",
|
|
442
|
+
url: event.url,
|
|
443
|
+
socketId: event.socketId,
|
|
444
|
+
status: "connecting",
|
|
445
|
+
startedAt: event.timestamp,
|
|
446
|
+
messages: []
|
|
447
|
+
}));
|
|
448
|
+
record.status = "open";
|
|
449
|
+
},
|
|
450
|
+
onWebSocketClose(event) {
|
|
451
|
+
if (!state.isRecording) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const record = ensureRealtimeRecord(`ws-${event.socketId}`, () => ({
|
|
455
|
+
requestId: `ws-${event.socketId}`,
|
|
456
|
+
kind: "websocket",
|
|
457
|
+
url: event.url,
|
|
458
|
+
socketId: event.socketId,
|
|
459
|
+
status: "connecting",
|
|
460
|
+
startedAt: event.timestamp,
|
|
461
|
+
messages: []
|
|
462
|
+
}));
|
|
463
|
+
record.status = "closed";
|
|
464
|
+
record.endedAt = event.timestamp;
|
|
465
|
+
record.durationMs = event.timestamp - record.startedAt;
|
|
466
|
+
record.closeCode = event.code;
|
|
467
|
+
record.closeReason = event.reason;
|
|
468
|
+
},
|
|
469
|
+
onWebSocketMessageSent(event) {
|
|
470
|
+
if (!state.isRecording) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const record = ensureRealtimeRecord(`ws-${event.socketId}`, () => ({
|
|
474
|
+
requestId: `ws-${event.socketId}`,
|
|
475
|
+
kind: "websocket",
|
|
476
|
+
url: event.url,
|
|
477
|
+
socketId: event.socketId,
|
|
478
|
+
status: "connecting",
|
|
479
|
+
startedAt: event.timestamp,
|
|
480
|
+
messages: []
|
|
481
|
+
}));
|
|
482
|
+
const message = {
|
|
483
|
+
id: nextMessageId(record.requestId),
|
|
484
|
+
direction: "sent",
|
|
485
|
+
data: event.data,
|
|
486
|
+
messageType: event.messageType,
|
|
487
|
+
timestamp: event.timestamp
|
|
488
|
+
};
|
|
489
|
+
record.messages = [...record.messages, message].slice(
|
|
490
|
+
-32
|
|
491
|
+
);
|
|
492
|
+
},
|
|
493
|
+
onWebSocketMessageReceived(event) {
|
|
494
|
+
if (!state.isRecording) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const record = ensureRealtimeRecord(`ws-${event.socketId}`, () => ({
|
|
498
|
+
requestId: `ws-${event.socketId}`,
|
|
499
|
+
kind: "websocket",
|
|
500
|
+
url: event.url,
|
|
501
|
+
socketId: event.socketId,
|
|
502
|
+
status: "connecting",
|
|
503
|
+
startedAt: event.timestamp,
|
|
504
|
+
messages: []
|
|
505
|
+
}));
|
|
506
|
+
const message = {
|
|
507
|
+
id: nextMessageId(record.requestId),
|
|
508
|
+
direction: "received",
|
|
509
|
+
data: event.data,
|
|
510
|
+
messageType: event.messageType,
|
|
511
|
+
timestamp: event.timestamp
|
|
512
|
+
};
|
|
513
|
+
record.messages = [...record.messages, message].slice(
|
|
514
|
+
-32
|
|
515
|
+
);
|
|
516
|
+
},
|
|
517
|
+
onWebSocketError(event) {
|
|
518
|
+
if (!state.isRecording) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const record = ensureRealtimeRecord(`ws-${event.socketId}`, () => ({
|
|
522
|
+
requestId: `ws-${event.socketId}`,
|
|
523
|
+
kind: "websocket",
|
|
524
|
+
url: event.url,
|
|
525
|
+
socketId: event.socketId,
|
|
526
|
+
status: "connecting",
|
|
527
|
+
startedAt: event.timestamp,
|
|
528
|
+
messages: []
|
|
529
|
+
}));
|
|
530
|
+
record.status = "error";
|
|
531
|
+
record.error = event.error;
|
|
532
|
+
},
|
|
533
|
+
onWebSocketConnectionStatusChanged(event) {
|
|
534
|
+
if (!state.isRecording) {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const record = ensureRealtimeRecord(`ws-${event.socketId}`, () => ({
|
|
538
|
+
requestId: `ws-${event.socketId}`,
|
|
539
|
+
kind: "websocket",
|
|
540
|
+
url: event.url,
|
|
541
|
+
socketId: event.socketId,
|
|
542
|
+
status: "connecting",
|
|
543
|
+
startedAt: event.timestamp,
|
|
544
|
+
messages: []
|
|
545
|
+
}));
|
|
546
|
+
record.status = event.status;
|
|
547
|
+
},
|
|
548
|
+
onSSEOpen(event) {
|
|
549
|
+
if (!state.isRecording) {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const httpRecord = state.httpRecords.get(event.requestId);
|
|
553
|
+
ensureRealtimeRecord(event.requestId, () => ({
|
|
554
|
+
requestId: event.requestId,
|
|
555
|
+
kind: "sse",
|
|
556
|
+
status: "open",
|
|
557
|
+
startedAt: httpRecord?.startTimeMs ?? event.timestamp,
|
|
558
|
+
request: httpRecord?.request,
|
|
559
|
+
response: event.response,
|
|
560
|
+
initiator: httpRecord?.initiator,
|
|
561
|
+
resourceType: httpRecord?.resourceType,
|
|
562
|
+
messages: []
|
|
563
|
+
}));
|
|
564
|
+
},
|
|
565
|
+
onSSEMessage(event) {
|
|
566
|
+
if (!state.isRecording) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const record = ensureRealtimeRecord(event.requestId, () => ({
|
|
570
|
+
requestId: event.requestId,
|
|
571
|
+
kind: "sse",
|
|
572
|
+
status: "connecting",
|
|
573
|
+
startedAt: event.timestamp,
|
|
574
|
+
messages: []
|
|
575
|
+
}));
|
|
576
|
+
record.messages = [
|
|
577
|
+
...record.messages,
|
|
578
|
+
{
|
|
579
|
+
id: nextMessageId(record.requestId),
|
|
580
|
+
type: event.payload.type,
|
|
581
|
+
data: event.payload.data,
|
|
582
|
+
timestamp: event.timestamp
|
|
583
|
+
}
|
|
584
|
+
].slice(-32);
|
|
585
|
+
},
|
|
586
|
+
onSSEError(event) {
|
|
587
|
+
if (!state.isRecording) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const record = ensureRealtimeRecord(event.requestId, () => ({
|
|
591
|
+
requestId: event.requestId,
|
|
592
|
+
kind: "sse",
|
|
593
|
+
status: "connecting",
|
|
594
|
+
startedAt: event.timestamp,
|
|
595
|
+
messages: []
|
|
596
|
+
}));
|
|
597
|
+
record.status = "error";
|
|
598
|
+
record.error = event.error.message;
|
|
599
|
+
},
|
|
600
|
+
onSSEClose(event) {
|
|
601
|
+
if (!state.isRecording) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const record = ensureRealtimeRecord(event.requestId, () => ({
|
|
605
|
+
requestId: event.requestId,
|
|
606
|
+
kind: "sse",
|
|
607
|
+
status: "connecting",
|
|
608
|
+
startedAt: event.timestamp,
|
|
609
|
+
messages: []
|
|
610
|
+
}));
|
|
611
|
+
record.status = "closed";
|
|
612
|
+
record.endedAt = event.timestamp;
|
|
613
|
+
record.durationMs = event.timestamp - record.startedAt;
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
};
|
|
617
|
+
const getNetworkActivityAgentState = /* @__PURE__ */ (() => {
|
|
618
|
+
let instance = null;
|
|
619
|
+
return () => {
|
|
620
|
+
if (!instance) {
|
|
621
|
+
instance = createNetworkActivityAgentState();
|
|
622
|
+
}
|
|
623
|
+
return instance;
|
|
624
|
+
};
|
|
625
|
+
})();
|
|
626
|
+
const startRecordingTool = {
|
|
627
|
+
name: "startRecording",
|
|
628
|
+
description: "Start recording network activity in the fallback network activity plugin.",
|
|
629
|
+
inputSchema: {
|
|
630
|
+
type: "object",
|
|
631
|
+
properties: {}
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
const stopRecordingTool = {
|
|
635
|
+
name: "stopRecording",
|
|
636
|
+
description: "Stop recording network activity without clearing the captured plugin buffer.",
|
|
637
|
+
inputSchema: {
|
|
638
|
+
type: "object",
|
|
639
|
+
properties: {}
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
const getRecordingStatusTool = {
|
|
643
|
+
name: "getRecordingStatus",
|
|
644
|
+
description: "Return network activity plugin recording state and buffer metadata.",
|
|
645
|
+
inputSchema: {
|
|
646
|
+
type: "object",
|
|
647
|
+
properties: {}
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
const listRequestsTool = {
|
|
651
|
+
name: "listRequests",
|
|
652
|
+
description: "List captured HTTP request summaries with cursor pagination from the fallback plugin.",
|
|
653
|
+
inputSchema: {
|
|
654
|
+
type: "object",
|
|
655
|
+
properties: {
|
|
656
|
+
limit: {
|
|
657
|
+
type: "number",
|
|
658
|
+
description: "Maximum number of requests to return. Defaults to 20."
|
|
659
|
+
},
|
|
660
|
+
cursor: {
|
|
661
|
+
type: "string",
|
|
662
|
+
description: "Opaque pagination cursor from a previous listRequests call."
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
const getRequestDetailsTool = {
|
|
668
|
+
name: "getRequestDetails",
|
|
669
|
+
description: "Return detailed metadata for a captured HTTP request without fetching response body.",
|
|
670
|
+
inputSchema: {
|
|
671
|
+
type: "object",
|
|
672
|
+
properties: {
|
|
673
|
+
requestId: {
|
|
674
|
+
type: "string",
|
|
675
|
+
description: "Captured plugin request ID to inspect."
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
required: ["requestId"]
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
const getRequestBodyTool = {
|
|
682
|
+
name: "getRequestBody",
|
|
683
|
+
description: "Return the captured request body for a plugin-recorded HTTP request when available.",
|
|
684
|
+
inputSchema: {
|
|
685
|
+
type: "object",
|
|
686
|
+
properties: {
|
|
687
|
+
requestId: {
|
|
688
|
+
type: "string",
|
|
689
|
+
description: "Captured plugin request ID to inspect."
|
|
690
|
+
}
|
|
691
|
+
},
|
|
692
|
+
required: ["requestId"]
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
const getResponseBodyTool = {
|
|
696
|
+
name: "getResponseBody",
|
|
697
|
+
description: "Return the captured response body for a plugin-recorded HTTP request when available.",
|
|
698
|
+
inputSchema: {
|
|
699
|
+
type: "object",
|
|
700
|
+
properties: {
|
|
701
|
+
requestId: {
|
|
702
|
+
type: "string",
|
|
703
|
+
description: "Captured plugin request ID to inspect."
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
required: ["requestId"]
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
const listRealtimeConnectionsTool = {
|
|
710
|
+
name: "listRealtimeConnections",
|
|
711
|
+
description: "List captured WebSocket and SSE connections with cursor pagination.",
|
|
712
|
+
inputSchema: {
|
|
713
|
+
type: "object",
|
|
714
|
+
properties: {
|
|
715
|
+
limit: {
|
|
716
|
+
type: "number",
|
|
717
|
+
description: "Maximum number of realtime connections to return. Defaults to 20."
|
|
718
|
+
},
|
|
719
|
+
cursor: {
|
|
720
|
+
type: "string",
|
|
721
|
+
description: "Opaque pagination cursor from a previous listRealtimeConnections call."
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
const getRealtimeConnectionDetailsTool = {
|
|
727
|
+
name: "getRealtimeConnectionDetails",
|
|
728
|
+
description: "Return details for a captured WebSocket or SSE connection, including recent messages.",
|
|
729
|
+
inputSchema: {
|
|
730
|
+
type: "object",
|
|
731
|
+
properties: {
|
|
732
|
+
requestId: {
|
|
733
|
+
type: "string",
|
|
734
|
+
description: "Captured realtime request ID to inspect."
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
required: ["requestId"]
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
const pluginId = "@rozenite/network-activity-plugin";
|
|
741
|
+
const useNetworkActivityAgentTools = ({
|
|
742
|
+
client,
|
|
743
|
+
networkInspector,
|
|
744
|
+
enabledInspectors
|
|
745
|
+
}) => {
|
|
746
|
+
const state = getNetworkActivityAgentState();
|
|
747
|
+
react.useEffect(() => {
|
|
748
|
+
const unsubscribe = [
|
|
749
|
+
networkInspector.http.on(
|
|
750
|
+
"request-sent",
|
|
751
|
+
(event) => state.onRequestSent(event)
|
|
752
|
+
),
|
|
753
|
+
networkInspector.http.on(
|
|
754
|
+
"request-progress",
|
|
755
|
+
(event) => state.onRequestProgress(event)
|
|
756
|
+
),
|
|
757
|
+
networkInspector.http.on(
|
|
758
|
+
"response-received",
|
|
759
|
+
(event) => state.onResponseReceived(event)
|
|
760
|
+
),
|
|
761
|
+
networkInspector.http.on(
|
|
762
|
+
"request-completed",
|
|
763
|
+
(event) => state.onRequestCompleted(event)
|
|
764
|
+
),
|
|
765
|
+
networkInspector.http.on(
|
|
766
|
+
"request-failed",
|
|
767
|
+
(event) => state.onRequestFailed(event)
|
|
768
|
+
),
|
|
769
|
+
networkInspector.websocket.on(
|
|
770
|
+
"websocket-connect",
|
|
771
|
+
(event) => state.onWebSocketConnect(event)
|
|
772
|
+
),
|
|
773
|
+
networkInspector.websocket.on(
|
|
774
|
+
"websocket-open",
|
|
775
|
+
(event) => state.onWebSocketOpen(event)
|
|
776
|
+
),
|
|
777
|
+
networkInspector.websocket.on(
|
|
778
|
+
"websocket-close",
|
|
779
|
+
(event) => state.onWebSocketClose(event)
|
|
780
|
+
),
|
|
781
|
+
networkInspector.websocket.on(
|
|
782
|
+
"websocket-message-sent",
|
|
783
|
+
(event) => state.onWebSocketMessageSent(event)
|
|
784
|
+
),
|
|
785
|
+
networkInspector.websocket.on(
|
|
786
|
+
"websocket-message-received",
|
|
787
|
+
(event) => state.onWebSocketMessageReceived(event)
|
|
788
|
+
),
|
|
789
|
+
networkInspector.websocket.on(
|
|
790
|
+
"websocket-error",
|
|
791
|
+
(event) => state.onWebSocketError(event)
|
|
792
|
+
),
|
|
793
|
+
networkInspector.websocket.on(
|
|
794
|
+
"websocket-connection-status-changed",
|
|
795
|
+
(event) => state.onWebSocketConnectionStatusChanged(event)
|
|
796
|
+
),
|
|
797
|
+
networkInspector.sse.on("sse-open", (event) => state.onSSEOpen(event)),
|
|
798
|
+
networkInspector.sse.on("sse-message", (event) => state.onSSEMessage(event)),
|
|
799
|
+
networkInspector.sse.on("sse-error", (event) => state.onSSEError(event)),
|
|
800
|
+
networkInspector.sse.on("sse-close", (event) => state.onSSEClose(event))
|
|
801
|
+
];
|
|
802
|
+
return () => {
|
|
803
|
+
unsubscribe.forEach((remove) => remove());
|
|
804
|
+
};
|
|
805
|
+
}, [networkInspector, state]);
|
|
806
|
+
react.useEffect(() => {
|
|
807
|
+
if (!client) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const subscriptions = [
|
|
811
|
+
client.onMessage("network-enable", () => {
|
|
812
|
+
state.startRecording({ enabledInspectors });
|
|
813
|
+
}),
|
|
814
|
+
client.onMessage("network-disable", () => {
|
|
815
|
+
if (state.getStatus().recording.isRecording) {
|
|
816
|
+
state.stopRecording();
|
|
817
|
+
}
|
|
818
|
+
})
|
|
819
|
+
];
|
|
820
|
+
return () => {
|
|
821
|
+
subscriptions.forEach((subscription) => subscription.remove());
|
|
822
|
+
};
|
|
823
|
+
}, [client, enabledInspectors, state]);
|
|
824
|
+
agentBridge.useRozenitePluginAgentTool({
|
|
825
|
+
pluginId,
|
|
826
|
+
tool: startRecordingTool,
|
|
827
|
+
handler: () => {
|
|
828
|
+
networkInspector.http.getNetworkRequestsRegistry().clear();
|
|
829
|
+
const result = state.startRecording({ enabledInspectors });
|
|
830
|
+
networkInspector.enable(enabledInspectors);
|
|
831
|
+
return {
|
|
832
|
+
started: true,
|
|
833
|
+
...result
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
agentBridge.useRozenitePluginAgentTool({
|
|
838
|
+
pluginId,
|
|
839
|
+
tool: stopRecordingTool,
|
|
840
|
+
handler: () => {
|
|
841
|
+
const result = state.stopRecording();
|
|
842
|
+
networkInspector.disable();
|
|
843
|
+
return {
|
|
844
|
+
stopped: true,
|
|
845
|
+
...result
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
agentBridge.useRozenitePluginAgentTool({
|
|
850
|
+
pluginId,
|
|
851
|
+
tool: getRecordingStatusTool,
|
|
852
|
+
handler: () => state.getStatus()
|
|
853
|
+
});
|
|
854
|
+
agentBridge.useRozenitePluginAgentTool({
|
|
855
|
+
pluginId,
|
|
856
|
+
tool: listRequestsTool,
|
|
857
|
+
handler: (input = {}) => state.listRequests(input)
|
|
858
|
+
});
|
|
859
|
+
agentBridge.useRozenitePluginAgentTool({
|
|
860
|
+
pluginId,
|
|
861
|
+
tool: getRequestDetailsTool,
|
|
862
|
+
handler: ({ requestId }) => state.getRequestDetails(requestId)
|
|
863
|
+
});
|
|
864
|
+
agentBridge.useRozenitePluginAgentTool({
|
|
865
|
+
pluginId,
|
|
866
|
+
tool: getRequestBodyTool,
|
|
867
|
+
handler: ({ requestId }) => state.getRequestBody(requestId)
|
|
868
|
+
});
|
|
869
|
+
agentBridge.useRozenitePluginAgentTool({
|
|
870
|
+
pluginId,
|
|
871
|
+
tool: getResponseBodyTool,
|
|
872
|
+
handler: async ({ requestId }) => {
|
|
873
|
+
const record = state.getHttpRecord(requestId);
|
|
874
|
+
if (!record) {
|
|
875
|
+
throw new Error(`Unknown request "${requestId}"`);
|
|
876
|
+
}
|
|
877
|
+
if (record.status === "failed") {
|
|
878
|
+
return {
|
|
879
|
+
requestId,
|
|
880
|
+
available: false,
|
|
881
|
+
reason: "Response body is unavailable because the request failed."
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
if (record.status !== "finished") {
|
|
885
|
+
return {
|
|
886
|
+
requestId,
|
|
887
|
+
available: false,
|
|
888
|
+
reason: "Response body is unavailable until the request finishes loading."
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
const request = networkInspector.http.getNetworkRequestsRegistry().getEntry(requestId);
|
|
892
|
+
if (!request) {
|
|
893
|
+
return {
|
|
894
|
+
requestId,
|
|
895
|
+
available: false,
|
|
896
|
+
reason: "Response body is unavailable because the request object is no longer in the plugin registry."
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
const body = await bootRecording.getResponseBody(request);
|
|
900
|
+
if (body === null) {
|
|
901
|
+
return {
|
|
902
|
+
requestId,
|
|
903
|
+
available: false,
|
|
904
|
+
reason: "The plugin could not extract a text response body for this request."
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
return {
|
|
908
|
+
requestId,
|
|
909
|
+
available: true,
|
|
910
|
+
body,
|
|
911
|
+
base64Encoded: false,
|
|
912
|
+
decoded: false,
|
|
913
|
+
mimeType: record.response?.contentType
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
agentBridge.useRozenitePluginAgentTool({
|
|
918
|
+
pluginId,
|
|
919
|
+
tool: listRealtimeConnectionsTool,
|
|
920
|
+
handler: (input = {}) => state.listRealtimeConnections(input)
|
|
921
|
+
});
|
|
922
|
+
agentBridge.useRozenitePluginAgentTool({
|
|
923
|
+
pluginId,
|
|
924
|
+
tool: getRealtimeConnectionDetailsTool,
|
|
925
|
+
handler: ({ requestId }) => state.getRealtimeConnectionDetails(requestId)
|
|
926
|
+
});
|
|
927
|
+
};
|
|
6
928
|
const overridesRegistry = bootRecording.getOverridesRegistry();
|
|
7
929
|
const useHttpInspector = (client, httpInspector, isEnabled, isRecordingEnabled) => {
|
|
8
930
|
react.useEffect(() => {
|
|
@@ -96,6 +1018,15 @@ const useNetworkActivityDevTools = (config = bootRecording.DEFAULT_CONFIG) => {
|
|
|
96
1018
|
const isSSEInspectorEnabled = config.inspectors?.sse ?? true;
|
|
97
1019
|
const showUrlAsName = config.clientUISettings?.showUrlAsName;
|
|
98
1020
|
const { eventsListener, networkInspector } = inspectorsConfig;
|
|
1021
|
+
useNetworkActivityAgentTools({
|
|
1022
|
+
client,
|
|
1023
|
+
networkInspector,
|
|
1024
|
+
enabledInspectors: {
|
|
1025
|
+
http: isHttpInspectorEnabled,
|
|
1026
|
+
websocket: isWebSocketInspectorEnabled,
|
|
1027
|
+
sse: isSSEInspectorEnabled
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
99
1030
|
react.useEffect(() => {
|
|
100
1031
|
if (!client) {
|
|
101
1032
|
return;
|