@rozenite/network-activity-plugin 1.5.1 → 1.7.0-rc.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.
Files changed (113) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +2 -0
  3. package/dist/{App.html → devtools/App.html} +2 -2
  4. package/dist/{boot-recording.cjs → react-native/chunks/boot-recording.cjs} +1 -1
  5. package/dist/{boot-recording.js → react-native/chunks/boot-recording.js} +3 -3
  6. package/dist/react-native/chunks/boot-recording.require.cjs +5 -0
  7. package/dist/react-native/chunks/boot-recording.require.js +5 -0
  8. package/dist/react-native/chunks/useNetworkActivityDevTools.require.cjs +1102 -0
  9. package/dist/react-native/chunks/useNetworkActivityDevTools.require.js +1102 -0
  10. package/dist/{react-native.cjs → react-native/index.cjs} +2 -2
  11. package/dist/react-native/index.d.ts +305 -0
  12. package/dist/{react-native.js → react-native/index.js} +2 -2
  13. package/dist/rozenite.json +1 -1
  14. package/package.json +27 -7
  15. package/src/react-native/agent/__tests__/network-activity-agent-state.test.ts +250 -0
  16. package/src/react-native/agent/state.ts +869 -0
  17. package/src/react-native/agent/tools.ts +146 -0
  18. package/src/react-native/agent/use-network-activity-agent-tools.ts +244 -0
  19. package/src/react-native/http/http-inspector.ts +0 -1
  20. package/src/react-native/useNetworkActivityDevTools.ts +11 -0
  21. package/tsconfig.json +3 -0
  22. package/dist/react-native.d.ts +0 -4
  23. package/dist/rozenite.config.d.ts +0 -7
  24. package/dist/src/react-native/boot-recording.d.ts +0 -41
  25. package/dist/src/react-native/config.d.ts +0 -23
  26. package/dist/src/react-native/events-listener.d.ts +0 -44
  27. package/dist/src/react-native/http/http-inspector.d.ts +0 -10
  28. package/dist/src/react-native/http/http-utils.d.ts +0 -15
  29. package/dist/src/react-native/http/network-requests-registry.d.ts +0 -6
  30. package/dist/src/react-native/http/overrides-registry.d.ts +0 -6
  31. package/dist/src/react-native/http/xhr-interceptor.d.ts +0 -44
  32. package/dist/src/react-native/inspector.d.ts +0 -7
  33. package/dist/src/react-native/network-inspector.d.ts +0 -16
  34. package/dist/src/react-native/sse/event-source.d.ts +0 -2
  35. package/dist/src/react-native/sse/sse-inspector.d.ts +0 -6
  36. package/dist/src/react-native/sse/sse-interceptor.d.ts +0 -36
  37. package/dist/src/react-native/sse/types.d.ts +0 -6
  38. package/dist/src/react-native/useHttpInspector.d.ts +0 -3
  39. package/dist/src/react-native/useNetworkActivityDevTools.d.ts +0 -3
  40. package/dist/src/react-native/useSSEInspector.d.ts +0 -3
  41. package/dist/src/react-native/useWebSocketInspector.d.ts +0 -3
  42. package/dist/src/react-native/utils/getBlobName.d.ts +0 -35
  43. package/dist/src/react-native/utils/getFormDataEntries.d.ts +0 -18
  44. package/dist/src/react-native/utils.d.ts +0 -6
  45. package/dist/src/react-native/websocket/websocket-inspector.d.ts +0 -6
  46. package/dist/src/react-native/websocket/websocket-interceptor.d.ts +0 -73
  47. package/dist/src/shared/client.d.ts +0 -17
  48. package/dist/src/shared/http-events.d.ts +0 -106
  49. package/dist/src/shared/sse-events.d.ts +0 -38
  50. package/dist/src/shared/websocket-events.d.ts +0 -60
  51. package/dist/src/ui/App.d.ts +0 -1
  52. package/dist/src/ui/components/Badge.d.ts +0 -9
  53. package/dist/src/ui/components/Button.d.ts +0 -11
  54. package/dist/src/ui/components/CodeBlock.d.ts +0 -3
  55. package/dist/src/ui/components/CodeEditor.d.ts +0 -5
  56. package/dist/src/ui/components/CookieCard.d.ts +0 -7
  57. package/dist/src/ui/components/CopyRequestDropdown.d.ts +0 -7
  58. package/dist/src/ui/components/DropdownMenu.d.ts +0 -27
  59. package/dist/src/ui/components/FilterBar.d.ts +0 -10
  60. package/dist/src/ui/components/Input.d.ts +0 -3
  61. package/dist/src/ui/components/JsonTree.d.ts +0 -5
  62. package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +0 -7
  63. package/dist/src/ui/components/KeyValueGrid.d.ts +0 -13
  64. package/dist/src/ui/components/OverrideResponse.d.ts +0 -8
  65. package/dist/src/ui/components/RequestBody.d.ts +0 -6
  66. package/dist/src/ui/components/RequestList.d.ts +0 -30
  67. package/dist/src/ui/components/ScrollArea.d.ts +0 -5
  68. package/dist/src/ui/components/Section.d.ts +0 -8
  69. package/dist/src/ui/components/Separator.d.ts +0 -4
  70. package/dist/src/ui/components/SidePanel.d.ts +0 -1
  71. package/dist/src/ui/components/Tabs.d.ts +0 -7
  72. package/dist/src/ui/components/Toolbar.d.ts +0 -1
  73. package/dist/src/ui/hooks/useCopyToClipboard.d.ts +0 -4
  74. package/dist/src/ui/state/derived.d.ts +0 -5
  75. package/dist/src/ui/state/hooks.d.ts +0 -21
  76. package/dist/src/ui/state/model.d.ts +0 -113
  77. package/dist/src/ui/state/store.d.ts +0 -48
  78. package/dist/src/ui/tabs/CookiesTab.d.ts +0 -5
  79. package/dist/src/ui/tabs/HeadersTab.d.ts +0 -5
  80. package/dist/src/ui/tabs/MessagesTab.d.ts +0 -5
  81. package/dist/src/ui/tabs/RequestTab.d.ts +0 -5
  82. package/dist/src/ui/tabs/ResponseTab.d.ts +0 -6
  83. package/dist/src/ui/tabs/SSEMessagesTab.d.ts +0 -5
  84. package/dist/src/ui/tabs/TimingTab.d.ts +0 -5
  85. package/dist/src/ui/types.d.ts +0 -26
  86. package/dist/src/ui/utils/assert.d.ts +0 -1
  87. package/dist/src/ui/utils/checkRequestBodyBinary.d.ts +0 -2
  88. package/dist/src/ui/utils/cn.d.ts +0 -2
  89. package/dist/src/ui/utils/copyToClipboard.d.ts +0 -1
  90. package/dist/src/ui/utils/escapeShellArg.d.ts +0 -1
  91. package/dist/src/ui/utils/generateCurlCommand.d.ts +0 -2
  92. package/dist/src/ui/utils/generateFetchCall.d.ts +0 -2
  93. package/dist/src/ui/utils/generateMultipartBody.d.ts +0 -4
  94. package/dist/src/ui/utils/getId.d.ts +0 -1
  95. package/dist/src/ui/utils/getStatusColor.d.ts +0 -1
  96. package/dist/src/ui/views/InspectorView.d.ts +0 -5
  97. package/dist/src/ui/views/LoadingView.d.ts +0 -1
  98. package/dist/src/utils/applyReactNativeRequestHeadersLogic.d.ts +0 -7
  99. package/dist/src/utils/applyReactNativeResponseHeadersLogic.d.ts +0 -9
  100. package/dist/src/utils/cookieParser.d.ts +0 -6
  101. package/dist/src/utils/getContentTypeMimeType.d.ts +0 -2
  102. package/dist/src/utils/getHttpHeader.d.ts +0 -5
  103. package/dist/src/utils/getHttpHeaderValueAsString.d.ts +0 -11
  104. package/dist/src/utils/getStringSizeInBytes.d.ts +0 -1
  105. package/dist/src/utils/inferContentTypeFromPostData.d.ts +0 -2
  106. package/dist/src/utils/safeStringify.d.ts +0 -1
  107. package/dist/src/utils/typeChecks.d.ts +0 -9
  108. package/dist/useNetworkActivityDevTools.cjs +0 -171
  109. package/dist/useNetworkActivityDevTools.js +0 -171
  110. /package/dist/{assets → devtools/assets}/App-BrSkOkws.css +0 -0
  111. /package/dist/{assets/App-CGt4qucR.js → devtools/assets/App-pokLiGYV.js} +0 -0
  112. /package/dist/{event-source.cjs → react-native/chunks/event-source.cjs} +0 -0
  113. /package/dist/{event-source.js → react-native/chunks/event-source.js} +0 -0
@@ -0,0 +1,869 @@
1
+ import type {
2
+ HttpEventMap,
3
+ Request,
4
+ RequestId,
5
+ RequestPostData,
6
+ Response,
7
+ ResourceType,
8
+ Initiator,
9
+ } from '../../shared/client';
10
+ import type { WebSocketEventMap } from '../../shared/websocket-events';
11
+ import type { SSEEventMap } from '../../shared/sse-events';
12
+ import { safeStringify } from '../../utils/safeStringify';
13
+
14
+ const DEFAULT_PAGE_LIMIT = 20;
15
+ const MAX_PAGE_LIMIT = 100;
16
+ const HTTP_BUFFER_CAPACITY = 500;
17
+ const REALTIME_BUFFER_CAPACITY = 200;
18
+ const MAX_WEBSOCKET_MESSAGES_PER_CONNECTION = 32;
19
+ const MAX_SSE_MESSAGES_PER_CONNECTION = 32;
20
+
21
+ type HttpAgentRecord = {
22
+ requestId: RequestId;
23
+ request: Request;
24
+ resourceType: ResourceType;
25
+ initiator: Initiator;
26
+ startTimeMs: number;
27
+ status: 'pending' | 'loading' | 'finished' | 'failed';
28
+ progress?: HttpEventMap['request-progress'];
29
+ response?: Response;
30
+ endTimeMs?: number;
31
+ durationMs?: number;
32
+ size?: number | null;
33
+ ttfb?: number;
34
+ error?: string;
35
+ canceled?: boolean;
36
+ };
37
+
38
+ type WebSocketAgentMessage = {
39
+ id: string;
40
+ direction: 'sent' | 'received';
41
+ data: string;
42
+ messageType: 'text' | 'binary';
43
+ timestamp: number;
44
+ };
45
+
46
+ type WebSocketAgentRecord = {
47
+ requestId: string;
48
+ kind: 'websocket';
49
+ url: string;
50
+ socketId: number;
51
+ status: 'connecting' | 'open' | 'closing' | 'closed' | 'error';
52
+ startedAt: number;
53
+ endedAt?: number;
54
+ durationMs?: number;
55
+ protocols?: string[] | null;
56
+ options?: string[];
57
+ error?: string;
58
+ closeCode?: number;
59
+ closeReason?: string;
60
+ messages: WebSocketAgentMessage[];
61
+ };
62
+
63
+ type SSEAgentMessage = {
64
+ id: string;
65
+ type: string;
66
+ data: string;
67
+ timestamp: number;
68
+ };
69
+
70
+ type SSEAgentRecord = {
71
+ requestId: string;
72
+ kind: 'sse';
73
+ status: 'connecting' | 'open' | 'closed' | 'error';
74
+ startedAt: number;
75
+ endedAt?: number;
76
+ durationMs?: number;
77
+ request?: Request;
78
+ response?: Response;
79
+ initiator?: Initiator;
80
+ resourceType?: ResourceType;
81
+ error?: string;
82
+ messages: SSEAgentMessage[];
83
+ };
84
+
85
+ type RealtimeAgentRecord = WebSocketAgentRecord | SSEAgentRecord;
86
+
87
+ export type NetworkActivityAgentBodyResult = {
88
+ requestId: string;
89
+ available: boolean;
90
+ body?: string;
91
+ base64Encoded?: boolean;
92
+ decoded?: boolean;
93
+ mimeType?: string;
94
+ reason?: string;
95
+ };
96
+
97
+ type RecordingMetadata = {
98
+ enabledInspectors: {
99
+ http: boolean;
100
+ websocket: boolean;
101
+ sse: boolean;
102
+ };
103
+ };
104
+
105
+ type NetworkActivityAgentStateInternal = {
106
+ isRecording: boolean;
107
+ startedAt?: number;
108
+ stoppedAt?: number;
109
+ generation: number;
110
+ httpOrder: string[];
111
+ httpRecords: Map<string, HttpAgentRecord>;
112
+ httpTotalRecorded: number;
113
+ httpEvictedCount: number;
114
+ httpTruncated: boolean;
115
+ realtimeOrder: string[];
116
+ realtimeRecords: Map<string, RealtimeAgentRecord>;
117
+ realtimeTotalRecorded: number;
118
+ realtimeEvictedCount: number;
119
+ realtimeTruncated: boolean;
120
+ nextMessageId: number;
121
+ recordingMetadata: RecordingMetadata;
122
+ };
123
+
124
+ type Page<T> = {
125
+ items: T[];
126
+ page: {
127
+ limit: number;
128
+ hasMore: boolean;
129
+ nextCursor?: string;
130
+ };
131
+ };
132
+
133
+ const createInitialState = (): NetworkActivityAgentStateInternal => ({
134
+ isRecording: false,
135
+ generation: 0,
136
+ httpOrder: [],
137
+ httpRecords: new Map(),
138
+ httpTotalRecorded: 0,
139
+ httpEvictedCount: 0,
140
+ httpTruncated: false,
141
+ realtimeOrder: [],
142
+ realtimeRecords: new Map(),
143
+ realtimeTotalRecorded: 0,
144
+ realtimeEvictedCount: 0,
145
+ realtimeTruncated: false,
146
+ nextMessageId: 1,
147
+ recordingMetadata: {
148
+ enabledInspectors: {
149
+ http: true,
150
+ websocket: true,
151
+ sse: true,
152
+ },
153
+ },
154
+ });
155
+
156
+ const getLimit = (value: unknown): number => {
157
+ if (
158
+ typeof value !== 'number' ||
159
+ !Number.isInteger(value) ||
160
+ !Number.isFinite(value) ||
161
+ value < 1
162
+ ) {
163
+ return DEFAULT_PAGE_LIMIT;
164
+ }
165
+
166
+ return Math.min(value, MAX_PAGE_LIMIT);
167
+ };
168
+
169
+ const encodeCursor = (scope: string, index: number): string => {
170
+ return `${scope}:${index}`;
171
+ };
172
+
173
+ const decodeCursor = (cursor: string, scope: string): number => {
174
+ const [cursorScope, rawIndex] = cursor.split(':', 2);
175
+ if (cursorScope !== scope || !rawIndex) {
176
+ throw new Error(
177
+ 'Cursor does not match the requested listing. Run the command again.'
178
+ );
179
+ }
180
+
181
+ const index = Number(rawIndex);
182
+ if (!Number.isInteger(index) || index < 0) {
183
+ throw new Error('Cursor is invalid. Run the command again.');
184
+ }
185
+
186
+ return index;
187
+ };
188
+
189
+ const paginate = <T>(
190
+ rows: T[],
191
+ scope: string,
192
+ limit: number,
193
+ cursor?: string
194
+ ): Page<T> => {
195
+ const startIndex = cursor ? decodeCursor(cursor, scope) : 0;
196
+ const endIndex = Math.min(startIndex + limit, rows.length);
197
+ const hasMore = endIndex < rows.length;
198
+
199
+ return {
200
+ items: rows.slice(startIndex, endIndex),
201
+ page: {
202
+ limit,
203
+ hasMore,
204
+ ...(hasMore ? { nextCursor: encodeCursor(scope, endIndex) } : {}),
205
+ },
206
+ };
207
+ };
208
+
209
+ const serializeRequestBody = (
210
+ requestId: string,
211
+ postData?: RequestPostData
212
+ ): NetworkActivityAgentBodyResult => {
213
+ if (!postData) {
214
+ return {
215
+ requestId,
216
+ available: false,
217
+ reason: 'No request body is available for this request.',
218
+ };
219
+ }
220
+
221
+ if (postData.type === 'text') {
222
+ return {
223
+ requestId,
224
+ available: true,
225
+ body: postData.value,
226
+ base64Encoded: false,
227
+ };
228
+ }
229
+
230
+ return {
231
+ requestId,
232
+ available: true,
233
+ body: safeStringify(postData),
234
+ base64Encoded: false,
235
+ };
236
+ };
237
+
238
+ const createHttpSummary = (record: HttpAgentRecord) => ({
239
+ requestId: record.requestId,
240
+ method: record.request.method,
241
+ url: record.request.url,
242
+ status: record.response?.status ?? null,
243
+ type: record.resourceType,
244
+ startTimeMs: record.startTimeMs,
245
+ endTimeMs: record.endTimeMs ?? null,
246
+ durationMs: record.durationMs ?? null,
247
+ transferSize: record.size ?? null,
248
+ encodedDataLength: record.response?.size ?? null,
249
+ outcome:
250
+ record.status === 'failed'
251
+ ? 'failed'
252
+ : record.status === 'finished'
253
+ ? 'success'
254
+ : 'in-flight',
255
+ });
256
+
257
+ const getRealtimeSummary = (record: RealtimeAgentRecord) => {
258
+ if (record.kind === 'websocket') {
259
+ return {
260
+ requestId: record.requestId,
261
+ kind: record.kind,
262
+ url: record.url,
263
+ status: record.status,
264
+ startedAt: record.startedAt,
265
+ endedAt: record.endedAt ?? null,
266
+ durationMs: record.durationMs ?? null,
267
+ messageCount: record.messages.length,
268
+ error: record.error ?? null,
269
+ closeCode: record.closeCode ?? null,
270
+ };
271
+ }
272
+
273
+ return {
274
+ requestId: record.requestId,
275
+ kind: record.kind,
276
+ url: record.request?.url ?? record.response?.url ?? null,
277
+ status: record.status,
278
+ startedAt: record.startedAt,
279
+ endedAt: record.endedAt ?? null,
280
+ durationMs: record.durationMs ?? null,
281
+ messageCount: record.messages.length,
282
+ error: record.error ?? null,
283
+ httpStatus: record.response?.status ?? null,
284
+ };
285
+ };
286
+
287
+ const trimMap = <T>(
288
+ order: string[],
289
+ records: Map<string, T>,
290
+ capacity: number
291
+ ): number => {
292
+ let evicted = 0;
293
+ while (order.length > capacity) {
294
+ const oldestId = order.shift();
295
+ if (!oldestId) {
296
+ break;
297
+ }
298
+ records.delete(oldestId);
299
+ evicted += 1;
300
+ }
301
+ return evicted;
302
+ };
303
+
304
+ export const createNetworkActivityAgentState = () => {
305
+ const state = createInitialState();
306
+
307
+ const getStatus = () => ({
308
+ recording: {
309
+ isRecording: state.isRecording,
310
+ startedAt: state.startedAt ?? null,
311
+ stoppedAt: state.stoppedAt ?? null,
312
+ httpRequestCount: state.httpOrder.length,
313
+ realtimeConnectionCount: state.realtimeOrder.length,
314
+ http: {
315
+ totalRecorded: state.httpTotalRecorded,
316
+ evictedCount: state.httpEvictedCount,
317
+ truncated: state.httpTruncated,
318
+ capacity: HTTP_BUFFER_CAPACITY,
319
+ },
320
+ realtime: {
321
+ totalRecorded: state.realtimeTotalRecorded,
322
+ evictedCount: state.realtimeEvictedCount,
323
+ truncated: state.realtimeTruncated,
324
+ capacity: REALTIME_BUFFER_CAPACITY,
325
+ },
326
+ generation: state.generation,
327
+ enabledInspectors: state.recordingMetadata.enabledInspectors,
328
+ },
329
+ });
330
+
331
+ const ensureHttpRecord = (
332
+ requestId: string,
333
+ fallback?: Partial<HttpAgentRecord>
334
+ ): HttpAgentRecord => {
335
+ const existing = state.httpRecords.get(requestId);
336
+ if (existing) {
337
+ return existing;
338
+ }
339
+
340
+ const record: HttpAgentRecord = {
341
+ requestId,
342
+ request:
343
+ fallback?.request ||
344
+ ({
345
+ url: '',
346
+ method: 'GET',
347
+ headers: {},
348
+ } as Request),
349
+ resourceType: fallback?.resourceType || 'Other',
350
+ initiator: fallback?.initiator || { type: 'other' },
351
+ startTimeMs: fallback?.startTimeMs ?? Date.now(),
352
+ status: fallback?.status || 'pending',
353
+ };
354
+ state.httpRecords.set(requestId, record);
355
+ state.httpOrder.push(requestId);
356
+ state.httpTotalRecorded += 1;
357
+ const evicted = trimMap(
358
+ state.httpOrder,
359
+ state.httpRecords,
360
+ HTTP_BUFFER_CAPACITY
361
+ );
362
+ if (evicted > 0) {
363
+ state.httpEvictedCount += evicted;
364
+ state.httpTruncated = true;
365
+ }
366
+ return record;
367
+ };
368
+
369
+ const ensureRealtimeRecord = (
370
+ requestId: string,
371
+ createRecord: () => RealtimeAgentRecord
372
+ ): RealtimeAgentRecord => {
373
+ const existing = state.realtimeRecords.get(requestId);
374
+ if (existing) {
375
+ return existing;
376
+ }
377
+
378
+ const record = createRecord();
379
+ state.realtimeRecords.set(requestId, record);
380
+ state.realtimeOrder.push(requestId);
381
+ state.realtimeTotalRecorded += 1;
382
+ const evicted = trimMap(
383
+ state.realtimeOrder,
384
+ state.realtimeRecords,
385
+ REALTIME_BUFFER_CAPACITY
386
+ );
387
+ if (evicted > 0) {
388
+ state.realtimeEvictedCount += evicted;
389
+ state.realtimeTruncated = true;
390
+ }
391
+ return record;
392
+ };
393
+
394
+ const nextMessageId = (prefix: string) => {
395
+ const id = `${prefix}-${state.nextMessageId}`;
396
+ state.nextMessageId += 1;
397
+ return id;
398
+ };
399
+
400
+ return {
401
+ startRecording(metadata?: Partial<RecordingMetadata>) {
402
+ state.isRecording = true;
403
+ state.startedAt = Date.now();
404
+ state.stoppedAt = undefined;
405
+ state.generation += 1;
406
+ state.httpOrder = [];
407
+ state.httpRecords.clear();
408
+ state.httpTotalRecorded = 0;
409
+ state.httpEvictedCount = 0;
410
+ state.httpTruncated = false;
411
+ state.realtimeOrder = [];
412
+ state.realtimeRecords.clear();
413
+ state.realtimeTotalRecorded = 0;
414
+ state.realtimeEvictedCount = 0;
415
+ state.realtimeTruncated = false;
416
+ state.recordingMetadata = {
417
+ enabledInspectors: {
418
+ ...state.recordingMetadata.enabledInspectors,
419
+ ...metadata?.enabledInspectors,
420
+ },
421
+ };
422
+ return getStatus();
423
+ },
424
+
425
+ stopRecording() {
426
+ if (!state.isRecording) {
427
+ throw new Error('No active network recording for this plugin session');
428
+ }
429
+
430
+ state.isRecording = false;
431
+ state.stoppedAt = Date.now();
432
+ return getStatus();
433
+ },
434
+
435
+ getStatus,
436
+
437
+ getHttpRecord(requestId: string) {
438
+ return state.httpRecords.get(requestId) || null;
439
+ },
440
+
441
+ getRealtimeRecord(requestId: string) {
442
+ return state.realtimeRecords.get(requestId) || null;
443
+ },
444
+
445
+ listRequests(input: { limit?: number; cursor?: string }) {
446
+ const limit = getLimit(input.limit);
447
+ const rows = state.httpOrder
448
+ .map((requestId) => state.httpRecords.get(requestId))
449
+ .filter((record): record is HttpAgentRecord => !!record)
450
+ .reverse()
451
+ .map(createHttpSummary);
452
+ return {
453
+ ...getStatus(),
454
+ ...paginate(rows, `http-${state.generation}`, limit, input.cursor),
455
+ };
456
+ },
457
+
458
+ listRealtimeConnections(input: { limit?: number; cursor?: string }) {
459
+ const limit = getLimit(input.limit);
460
+ const rows = state.realtimeOrder
461
+ .map((requestId) => state.realtimeRecords.get(requestId))
462
+ .filter((record): record is RealtimeAgentRecord => !!record)
463
+ .reverse()
464
+ .map(getRealtimeSummary);
465
+ return {
466
+ ...getStatus(),
467
+ ...paginate(rows, `realtime-${state.generation}`, limit, input.cursor),
468
+ };
469
+ },
470
+
471
+ getRequestDetails(requestId: string) {
472
+ const record = state.httpRecords.get(requestId);
473
+ if (!record) {
474
+ throw new Error(`Unknown request "${requestId}"`);
475
+ }
476
+
477
+ return {
478
+ ...getStatus(),
479
+ request: {
480
+ requestId: record.requestId,
481
+ method: record.request.method,
482
+ url: record.request.url,
483
+ type: record.resourceType,
484
+ initiator: record.initiator,
485
+ startTimeMs: record.startTimeMs,
486
+ endTimeMs: record.endTimeMs ?? null,
487
+ durationMs: record.durationMs ?? null,
488
+ request: record.request,
489
+ response: record.response ?? null,
490
+ loadingFinished: record.status === 'finished',
491
+ loadingFailed: record.status === 'failed',
492
+ failureText: record.error ?? null,
493
+ canceled: record.canceled ?? false,
494
+ progress: record.progress
495
+ ? {
496
+ loaded: record.progress.loaded,
497
+ total: record.progress.total,
498
+ lengthComputable: record.progress.lengthComputable,
499
+ }
500
+ : null,
501
+ ttfb: record.ttfb ?? null,
502
+ size: record.size ?? null,
503
+ },
504
+ };
505
+ },
506
+
507
+ getRealtimeConnectionDetails(requestId: string) {
508
+ const record = state.realtimeRecords.get(requestId);
509
+ if (!record) {
510
+ throw new Error(`Unknown realtime connection "${requestId}"`);
511
+ }
512
+
513
+ return {
514
+ ...getStatus(),
515
+ connection:
516
+ record.kind === 'websocket'
517
+ ? {
518
+ requestId: record.requestId,
519
+ kind: record.kind,
520
+ url: record.url,
521
+ socketId: record.socketId,
522
+ status: record.status,
523
+ startedAt: record.startedAt,
524
+ endedAt: record.endedAt ?? null,
525
+ durationMs: record.durationMs ?? null,
526
+ protocols: record.protocols ?? null,
527
+ options: record.options ?? [],
528
+ error: record.error ?? null,
529
+ closeCode: record.closeCode ?? null,
530
+ closeReason: record.closeReason ?? null,
531
+ messages: record.messages,
532
+ }
533
+ : {
534
+ requestId: record.requestId,
535
+ kind: record.kind,
536
+ status: record.status,
537
+ startedAt: record.startedAt,
538
+ endedAt: record.endedAt ?? null,
539
+ durationMs: record.durationMs ?? null,
540
+ request: record.request ?? null,
541
+ response: record.response ?? null,
542
+ initiator: record.initiator ?? null,
543
+ resourceType: record.resourceType ?? null,
544
+ error: record.error ?? null,
545
+ messages: record.messages,
546
+ },
547
+ };
548
+ },
549
+
550
+ getRequestBody(requestId: string): NetworkActivityAgentBodyResult {
551
+ const record = state.httpRecords.get(requestId);
552
+ if (!record) {
553
+ throw new Error(`Unknown request "${requestId}"`);
554
+ }
555
+
556
+ return serializeRequestBody(requestId, record.request.postData);
557
+ },
558
+
559
+ onRequestSent(event: HttpEventMap['request-sent']) {
560
+ if (!state.isRecording) {
561
+ return;
562
+ }
563
+
564
+ const record = ensureHttpRecord(event.requestId, {
565
+ request: event.request,
566
+ resourceType: event.type,
567
+ initiator: event.initiator,
568
+ startTimeMs: event.timestamp,
569
+ status: 'pending',
570
+ });
571
+ record.request = event.request;
572
+ record.resourceType = event.type;
573
+ record.initiator = event.initiator;
574
+ record.startTimeMs = event.timestamp;
575
+ record.status = 'pending';
576
+ record.response = undefined;
577
+ record.endTimeMs = undefined;
578
+ record.durationMs = undefined;
579
+ record.size = undefined;
580
+ record.ttfb = undefined;
581
+ record.error = undefined;
582
+ record.canceled = undefined;
583
+ record.progress = undefined;
584
+ },
585
+
586
+ onRequestProgress(event: HttpEventMap['request-progress']) {
587
+ if (!state.isRecording) {
588
+ return;
589
+ }
590
+
591
+ const record = ensureHttpRecord(event.requestId);
592
+ record.progress = event;
593
+ record.status = 'loading';
594
+ },
595
+
596
+ onResponseReceived(event: HttpEventMap['response-received']) {
597
+ if (!state.isRecording) {
598
+ return;
599
+ }
600
+
601
+ const record = ensureHttpRecord(event.requestId);
602
+ record.response = event.response;
603
+ record.status = 'loading';
604
+ },
605
+
606
+ onRequestCompleted(event: HttpEventMap['request-completed']) {
607
+ if (!state.isRecording) {
608
+ return;
609
+ }
610
+
611
+ const record = ensureHttpRecord(event.requestId);
612
+ record.status = 'finished';
613
+ record.endTimeMs = event.timestamp;
614
+ record.durationMs = event.duration;
615
+ record.size = event.size;
616
+ record.ttfb = event.ttfb;
617
+ },
618
+
619
+ onRequestFailed(event: HttpEventMap['request-failed']) {
620
+ if (!state.isRecording) {
621
+ return;
622
+ }
623
+
624
+ const record = ensureHttpRecord(event.requestId);
625
+ record.status = 'failed';
626
+ record.endTimeMs = event.timestamp;
627
+ record.error = event.error;
628
+ record.canceled = event.canceled;
629
+ },
630
+
631
+ onWebSocketConnect(event: WebSocketEventMap['websocket-connect']) {
632
+ if (!state.isRecording) {
633
+ return;
634
+ }
635
+
636
+ ensureRealtimeRecord(`ws-${event.socketId}`, () => ({
637
+ requestId: `ws-${event.socketId}`,
638
+ kind: 'websocket',
639
+ url: event.url,
640
+ socketId: event.socketId,
641
+ status: 'connecting',
642
+ startedAt: event.timestamp,
643
+ protocols: event.protocols,
644
+ options: event.options,
645
+ messages: [],
646
+ }));
647
+ },
648
+
649
+ onWebSocketOpen(event: WebSocketEventMap['websocket-open']) {
650
+ if (!state.isRecording) {
651
+ return;
652
+ }
653
+
654
+ const record = ensureRealtimeRecord(`ws-${event.socketId}`, () => ({
655
+ requestId: `ws-${event.socketId}`,
656
+ kind: 'websocket',
657
+ url: event.url,
658
+ socketId: event.socketId,
659
+ status: 'connecting',
660
+ startedAt: event.timestamp,
661
+ messages: [],
662
+ })) as WebSocketAgentRecord;
663
+ record.status = 'open';
664
+ },
665
+
666
+ onWebSocketClose(event: WebSocketEventMap['websocket-close']) {
667
+ if (!state.isRecording) {
668
+ return;
669
+ }
670
+
671
+ const record = ensureRealtimeRecord(`ws-${event.socketId}`, () => ({
672
+ requestId: `ws-${event.socketId}`,
673
+ kind: 'websocket',
674
+ url: event.url,
675
+ socketId: event.socketId,
676
+ status: 'connecting',
677
+ startedAt: event.timestamp,
678
+ messages: [],
679
+ })) as WebSocketAgentRecord;
680
+ record.status = 'closed';
681
+ record.endedAt = event.timestamp;
682
+ record.durationMs = event.timestamp - record.startedAt;
683
+ record.closeCode = event.code;
684
+ record.closeReason = event.reason;
685
+ },
686
+
687
+ onWebSocketMessageSent(event: WebSocketEventMap['websocket-message-sent']) {
688
+ if (!state.isRecording) {
689
+ return;
690
+ }
691
+
692
+ const record = ensureRealtimeRecord(`ws-${event.socketId}`, () => ({
693
+ requestId: `ws-${event.socketId}`,
694
+ kind: 'websocket',
695
+ url: event.url,
696
+ socketId: event.socketId,
697
+ status: 'connecting',
698
+ startedAt: event.timestamp,
699
+ messages: [],
700
+ })) as WebSocketAgentRecord;
701
+ const message: WebSocketAgentMessage = {
702
+ id: nextMessageId(record.requestId),
703
+ direction: 'sent',
704
+ data: event.data,
705
+ messageType: event.messageType,
706
+ timestamp: event.timestamp,
707
+ };
708
+ record.messages = [...record.messages, message].slice(
709
+ -MAX_WEBSOCKET_MESSAGES_PER_CONNECTION
710
+ );
711
+ },
712
+
713
+ onWebSocketMessageReceived(
714
+ event: WebSocketEventMap['websocket-message-received']
715
+ ) {
716
+ if (!state.isRecording) {
717
+ return;
718
+ }
719
+
720
+ const record = ensureRealtimeRecord(`ws-${event.socketId}`, () => ({
721
+ requestId: `ws-${event.socketId}`,
722
+ kind: 'websocket',
723
+ url: event.url,
724
+ socketId: event.socketId,
725
+ status: 'connecting',
726
+ startedAt: event.timestamp,
727
+ messages: [],
728
+ })) as WebSocketAgentRecord;
729
+ const message: WebSocketAgentMessage = {
730
+ id: nextMessageId(record.requestId),
731
+ direction: 'received',
732
+ data: event.data,
733
+ messageType: event.messageType,
734
+ timestamp: event.timestamp,
735
+ };
736
+ record.messages = [...record.messages, message].slice(
737
+ -MAX_WEBSOCKET_MESSAGES_PER_CONNECTION
738
+ );
739
+ },
740
+
741
+ onWebSocketError(event: WebSocketEventMap['websocket-error']) {
742
+ if (!state.isRecording) {
743
+ return;
744
+ }
745
+
746
+ const record = ensureRealtimeRecord(`ws-${event.socketId}`, () => ({
747
+ requestId: `ws-${event.socketId}`,
748
+ kind: 'websocket',
749
+ url: event.url,
750
+ socketId: event.socketId,
751
+ status: 'connecting',
752
+ startedAt: event.timestamp,
753
+ messages: [],
754
+ })) as WebSocketAgentRecord;
755
+ record.status = 'error';
756
+ record.error = event.error;
757
+ },
758
+
759
+ onWebSocketConnectionStatusChanged(
760
+ event: WebSocketEventMap['websocket-connection-status-changed']
761
+ ) {
762
+ if (!state.isRecording) {
763
+ return;
764
+ }
765
+
766
+ const record = ensureRealtimeRecord(`ws-${event.socketId}`, () => ({
767
+ requestId: `ws-${event.socketId}`,
768
+ kind: 'websocket',
769
+ url: event.url,
770
+ socketId: event.socketId,
771
+ status: 'connecting',
772
+ startedAt: event.timestamp,
773
+ messages: [],
774
+ })) as WebSocketAgentRecord;
775
+ record.status = event.status;
776
+ },
777
+
778
+ onSSEOpen(event: SSEEventMap['sse-open']) {
779
+ if (!state.isRecording) {
780
+ return;
781
+ }
782
+
783
+ const httpRecord = state.httpRecords.get(event.requestId);
784
+ ensureRealtimeRecord(event.requestId, () => ({
785
+ requestId: event.requestId,
786
+ kind: 'sse',
787
+ status: 'open',
788
+ startedAt: httpRecord?.startTimeMs ?? event.timestamp,
789
+ request: httpRecord?.request,
790
+ response: event.response,
791
+ initiator: httpRecord?.initiator,
792
+ resourceType: httpRecord?.resourceType,
793
+ messages: [],
794
+ }));
795
+ },
796
+
797
+ onSSEMessage(event: SSEEventMap['sse-message']) {
798
+ if (!state.isRecording) {
799
+ return;
800
+ }
801
+
802
+ const record = ensureRealtimeRecord(event.requestId, () => ({
803
+ requestId: event.requestId,
804
+ kind: 'sse',
805
+ status: 'connecting',
806
+ startedAt: event.timestamp,
807
+ messages: [],
808
+ })) as SSEAgentRecord;
809
+ record.messages = [
810
+ ...record.messages,
811
+ {
812
+ id: nextMessageId(record.requestId),
813
+ type: event.payload.type,
814
+ data: event.payload.data,
815
+ timestamp: event.timestamp,
816
+ },
817
+ ].slice(-MAX_SSE_MESSAGES_PER_CONNECTION);
818
+ },
819
+
820
+ onSSEError(event: SSEEventMap['sse-error']) {
821
+ if (!state.isRecording) {
822
+ return;
823
+ }
824
+
825
+ const record = ensureRealtimeRecord(event.requestId, () => ({
826
+ requestId: event.requestId,
827
+ kind: 'sse',
828
+ status: 'connecting',
829
+ startedAt: event.timestamp,
830
+ messages: [],
831
+ })) as SSEAgentRecord;
832
+ record.status = 'error';
833
+ record.error = event.error.message;
834
+ },
835
+
836
+ onSSEClose(event: SSEEventMap['sse-close']) {
837
+ if (!state.isRecording) {
838
+ return;
839
+ }
840
+
841
+ const record = ensureRealtimeRecord(event.requestId, () => ({
842
+ requestId: event.requestId,
843
+ kind: 'sse',
844
+ status: 'connecting',
845
+ startedAt: event.timestamp,
846
+ messages: [],
847
+ })) as SSEAgentRecord;
848
+ record.status = 'closed';
849
+ record.endedAt = event.timestamp;
850
+ record.durationMs = event.timestamp - record.startedAt;
851
+ },
852
+ };
853
+ };
854
+
855
+ export type NetworkActivityAgentState = ReturnType<
856
+ typeof createNetworkActivityAgentState
857
+ >;
858
+
859
+ export const getNetworkActivityAgentState = (() => {
860
+ let instance: NetworkActivityAgentState | null = null;
861
+
862
+ return () => {
863
+ if (!instance) {
864
+ instance = createNetworkActivityAgentState();
865
+ }
866
+
867
+ return instance;
868
+ };
869
+ })();