@rozenite/network-activity-plugin 1.9.0 → 1.11.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 (84) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/devtools/App.html +2 -2
  3. package/dist/devtools/assets/{App-hSoryVpJ.js → App-CEESZAW_.js} +7520 -937
  4. package/dist/devtools/assets/{App-m6xge0az.css → App-xppYUJvX.css} +246 -2
  5. package/dist/react-native/chunks/boot-recording.cjs +138 -14
  6. package/dist/react-native/chunks/boot-recording.js +138 -14
  7. package/dist/react-native/chunks/get-nitro-module.cjs +4 -1
  8. package/dist/react-native/chunks/get-nitro-module.js +4 -1
  9. package/dist/react-native/chunks/useNetworkActivityDevTools.require.cjs +20 -1
  10. package/dist/react-native/chunks/useNetworkActivityDevTools.require.js +20 -1
  11. package/dist/react-native/index.d.ts +37 -1
  12. package/dist/rozenite.json +1 -1
  13. package/dist/sdk/index.d.ts +37 -1
  14. package/package.json +12 -7
  15. package/src/react-native/agent/use-network-activity-agent-tools.ts +22 -4
  16. package/src/react-native/http/__tests__/http-utils.test.ts +228 -0
  17. package/src/react-native/http/http-utils.ts +208 -25
  18. package/src/react-native/network-inspector.ts +2 -2
  19. package/src/react-native/nitro-fetch/get-nitro-module.ts +5 -1
  20. package/src/react-native/nitro-fetch/nitro-network-inspector.ts +8 -2
  21. package/src/shared/http-events.ts +40 -1
  22. package/src/ui/components/CodeBlock.tsx +45 -1
  23. package/src/ui/components/FilterBar.tsx +337 -61
  24. package/src/ui/components/HexView.tsx +54 -0
  25. package/src/ui/components/MetadataCard.tsx +95 -0
  26. package/src/ui/components/NetworkTimeline.tsx +422 -0
  27. package/src/ui/components/RequestList.tsx +19 -40
  28. package/src/ui/components/SidePanel.tsx +42 -1
  29. package/src/ui/components/Toolbar.tsx +13 -1
  30. package/src/ui/components/ViewToggle.tsx +44 -0
  31. package/src/ui/components/XmlTree.tsx +160 -0
  32. package/src/ui/components/__tests__/CodeBlock.test.tsx +89 -0
  33. package/src/ui/components/__tests__/HexView.test.tsx +41 -0
  34. package/src/ui/components/__tests__/MetadataCard.test.tsx +107 -0
  35. package/src/ui/components/__tests__/ViewToggle.test.tsx +80 -0
  36. package/src/ui/components/__tests__/XmlTree.test.tsx +149 -0
  37. package/src/ui/hooks/useNetworkActivitySessionExport.ts +39 -0
  38. package/src/ui/response-renderers/__tests__/binary-too-large.test.tsx +56 -0
  39. package/src/ui/response-renderers/__tests__/binary.test.tsx +96 -0
  40. package/src/ui/response-renderers/__tests__/dispatch.test.ts +124 -0
  41. package/src/ui/response-renderers/__tests__/html.test.tsx +101 -0
  42. package/src/ui/response-renderers/__tests__/image.test.tsx +73 -0
  43. package/src/ui/response-renderers/__tests__/json.test.tsx +95 -0
  44. package/src/ui/response-renderers/__tests__/svg.test.tsx +46 -0
  45. package/src/ui/response-renderers/__tests__/xml.test.tsx +100 -0
  46. package/src/ui/response-renderers/binary-too-large.tsx +36 -0
  47. package/src/ui/response-renderers/binary.tsx +31 -0
  48. package/src/ui/response-renderers/empty.tsx +14 -0
  49. package/src/ui/response-renderers/html.tsx +36 -0
  50. package/src/ui/response-renderers/image.tsx +37 -0
  51. package/src/ui/response-renderers/index.ts +55 -0
  52. package/src/ui/response-renderers/json.tsx +40 -0
  53. package/src/ui/response-renderers/svg.tsx +27 -0
  54. package/src/ui/response-renderers/text-fallback.tsx +14 -0
  55. package/src/ui/response-renderers/types.ts +38 -0
  56. package/src/ui/response-renderers/unknown.tsx +18 -0
  57. package/src/ui/response-renderers/xml.tsx +46 -0
  58. package/src/ui/state/__tests__/store.test.ts +77 -0
  59. package/src/ui/state/derived.ts +14 -0
  60. package/src/ui/state/filter.ts +49 -0
  61. package/src/ui/state/hooks.ts +2 -2
  62. package/src/ui/state/model.ts +7 -1
  63. package/src/ui/state/store.ts +63 -4
  64. package/src/ui/tabs/InitiatorTab.tsx +230 -0
  65. package/src/ui/tabs/ResponseTab.tsx +80 -97
  66. package/src/ui/tabs/__tests__/ResponseTab.test.tsx +102 -0
  67. package/src/ui/utils/__tests__/download.test.ts +115 -0
  68. package/src/ui/utils/__tests__/hex.test.ts +84 -0
  69. package/src/ui/utils/__tests__/requestFilters.test.ts +32 -0
  70. package/src/ui/utils/__tests__/sessionExport.test.ts +174 -0
  71. package/src/ui/utils/__tests__/symbolication.test.ts +207 -0
  72. package/src/ui/utils/__tests__/timelineModel.test.ts +170 -0
  73. package/src/ui/utils/download.ts +161 -0
  74. package/src/ui/utils/hex.ts +59 -0
  75. package/src/ui/utils/initiator.ts +136 -0
  76. package/src/ui/utils/requestFilters.ts +183 -0
  77. package/src/ui/utils/sessionExport.ts +185 -0
  78. package/src/ui/utils/symbolication.ts +248 -0
  79. package/src/ui/utils/timelineModel.ts +352 -0
  80. package/src/ui/views/InspectorView.tsx +43 -8
  81. package/src/utils/__tests__/getContentTypeMimeType.test.ts +34 -0
  82. package/src/utils/getContentTypeMimeType.ts +14 -0
  83. package/vite.config.ts +5 -1
  84. package/vitest.setup.ts +31 -0
@@ -0,0 +1,49 @@
1
+ import type { HttpMethod, NetworkEventSource } from '../../shared/client';
2
+
3
+ export type RequestTypeFilter = 'http' | 'websocket' | 'sse';
4
+
5
+ export type AdvancedFilterState = {
6
+ methods: Set<HttpMethod>;
7
+ sources: Set<NetworkEventSource>;
8
+ status: string;
9
+ domain: string;
10
+ contentType: string;
11
+ failedOnly: boolean;
12
+ inFlightOnly: boolean;
13
+ overriddenOnly: boolean;
14
+ minSize: string;
15
+ maxSize: string;
16
+ minDuration: string;
17
+ maxDuration: string;
18
+ };
19
+
20
+ export type FilterState = {
21
+ text: string;
22
+ types: Set<RequestTypeFilter>;
23
+ advanced: AdvancedFilterState;
24
+ };
25
+
26
+ export const DEFAULT_REQUEST_TYPES: RequestTypeFilter[] = [
27
+ 'http',
28
+ 'websocket',
29
+ 'sse',
30
+ ];
31
+
32
+ export const createDefaultFilter = (): FilterState => ({
33
+ text: '',
34
+ types: new Set(DEFAULT_REQUEST_TYPES),
35
+ advanced: {
36
+ methods: new Set(),
37
+ sources: new Set(),
38
+ status: '',
39
+ domain: '',
40
+ contentType: '',
41
+ failedOnly: false,
42
+ inFlightOnly: false,
43
+ overriddenOnly: false,
44
+ minSize: '',
45
+ maxSize: '',
46
+ minDuration: '',
47
+ maxDuration: '',
48
+ },
49
+ });
@@ -4,7 +4,7 @@ import type { NetworkActivityState } from './store';
4
4
  import { getProcessedRequests, getSelectedRequest } from './derived';
5
5
 
6
6
  export const useNetworkActivityStore = <T>(
7
- selector: (state: NetworkActivityState) => T
7
+ selector: (state: NetworkActivityState) => T,
8
8
  ): T => {
9
9
  return useStore(store, selector);
10
10
  };
@@ -39,7 +39,7 @@ export const useNetworkActivityClientManagement = () => {
39
39
 
40
40
  export const useWebSocketMessages = (requestId: string) => {
41
41
  return useNetworkActivityStore(
42
- (state) => state.websocketMessages.get(requestId) || []
42
+ (state) => state.websocketMessages.get(requestId) || [],
43
43
  );
44
44
  };
45
45
 
@@ -4,6 +4,7 @@ import {
4
4
  HttpHeaders,
5
5
  RequestPostData,
6
6
  NetworkEventSource,
7
+ ResponseBody,
7
8
  } from '../../shared/client';
8
9
 
9
10
  export type RequestId = string;
@@ -21,7 +22,9 @@ export type HttpRequestData = {
21
22
 
22
23
  export type HttpResponseData = {
23
24
  type: string;
24
- data: string;
25
+ // Mirrors the bridge `ResponseBody` minus null — the store only assigns
26
+ // `data` when the wire body is non-null (null body → undefined response.body).
27
+ data: NonNullable<ResponseBody>;
25
28
  };
26
29
 
27
30
  export type HttpRequest = {
@@ -140,6 +143,7 @@ export type ProcessedRequest = {
140
143
  id: RequestId;
141
144
  type: NetworkEntryType;
142
145
  source?: NetworkEventSource;
146
+ initiator?: Initiator;
143
147
  name: string;
144
148
  status: HttpStatus | WebSocketStatus | SSEStatus;
145
149
  timestamp: Timestamp;
@@ -147,6 +151,8 @@ export type ProcessedRequest = {
147
151
  size: number | null;
148
152
  method: HttpMethod | 'WS' | 'SSE';
149
153
  httpStatus?: number;
154
+ contentType?: string;
155
+ ttfb?: number;
150
156
  progress?: {
151
157
  loaded: number;
152
158
  total: number;
@@ -19,12 +19,17 @@ import { getId } from '../utils/getId';
19
19
  import { assert } from '../utils/assert';
20
20
  import { getContentTypeMime } from '../../utils/getContentTypeMimeType';
21
21
  import { applyReactNativeRequestHeadersLogic } from '../../utils/applyReactNativeRequestHeadersLogic';
22
+ import { symbolicateInitiator } from '../utils/symbolication';
22
23
 
23
24
  const MAX_WEBSOCKET_MESSAGES_PER_CONNECTION = 32;
24
25
  const MAX_SSE_MESSAGES_PER_CONNECTION = 32;
25
26
 
26
27
  const STORE_VERSION = 1;
27
28
 
29
+ const getElapsedDuration = (endTimestamp: number, startTimestamp: number) => {
30
+ return Math.max(endTimestamp - startTimestamp, 0);
31
+ };
32
+
28
33
  export interface NetworkActivityState {
29
34
  // State
30
35
  isRecording: boolean;
@@ -128,7 +133,10 @@ export const createNetworkActivityStore = () =>
128
133
  data as NetworkActivityEventMap['recording-state'];
129
134
  const { isRecording, _client } = get();
130
135
  if (_client && isRecording !== eventData.isRecording) {
131
- _client.send(isRecording ? 'network-enable' : 'network-disable', {});
136
+ _client.send(
137
+ isRecording ? 'network-enable' : 'network-disable',
138
+ {},
139
+ );
132
140
  }
133
141
  break;
134
142
  }
@@ -177,6 +185,39 @@ export const createNetworkActivityStore = () =>
177
185
  newEntries.set(eventData.requestId, entry);
178
186
  return { networkEntries: newEntries };
179
187
  });
188
+
189
+ if (eventData.initiator.symbolicationStatus === 'pending') {
190
+ void symbolicateInitiator(eventData.initiator).then(
191
+ (symbolicatedInitiator) => {
192
+ if (!symbolicatedInitiator) {
193
+ return;
194
+ }
195
+
196
+ set((state) => {
197
+ const entry = state.networkEntries.get(
198
+ eventData.requestId,
199
+ );
200
+
201
+ if (
202
+ !entry ||
203
+ (entry.type !== 'http' && entry.type !== 'sse') ||
204
+ entry.initiator?.symbolicationStatus !== 'pending'
205
+ ) {
206
+ return {};
207
+ }
208
+
209
+ const updatedEntry = {
210
+ ...entry,
211
+ initiator: symbolicatedInitiator,
212
+ };
213
+
214
+ const newEntries = new Map(state.networkEntries);
215
+ newEntries.set(eventData.requestId, updatedEntry);
216
+ return { networkEntries: newEntries };
217
+ });
218
+ },
219
+ );
220
+ }
180
221
  break;
181
222
  }
182
223
 
@@ -265,6 +306,10 @@ export const createNetworkActivityStore = () =>
265
306
  const updatedEntry: HttpNetworkEntry = {
266
307
  ...httpEntry,
267
308
  status: 'failed',
309
+ duration: getElapsedDuration(
310
+ eventData.timestamp,
311
+ httpEntry.timestamp,
312
+ ),
268
313
  error: eventData.error,
269
314
  };
270
315
 
@@ -377,7 +422,10 @@ export const createNetworkActivityStore = () =>
377
422
  status: 'closed',
378
423
  closeCode: eventData.code,
379
424
  closeReason: eventData.reason,
380
- duration: eventData.timestamp - wsEntry.timestamp,
425
+ duration: getElapsedDuration(
426
+ eventData.timestamp,
427
+ wsEntry.timestamp,
428
+ ),
381
429
  };
382
430
 
383
431
  const newEntries = new Map(state.networkEntries);
@@ -458,6 +506,10 @@ export const createNetworkActivityStore = () =>
458
506
  const updatedEntry: WebSocketNetworkEntry = {
459
507
  ...wsEntry,
460
508
  status: 'error',
509
+ duration: getElapsedDuration(
510
+ eventData.timestamp,
511
+ wsEntry.timestamp,
512
+ ),
461
513
  error: eventData.error,
462
514
  };
463
515
 
@@ -554,6 +606,10 @@ export const createNetworkActivityStore = () =>
554
606
  const updatedEntry: SSENetworkEntry = {
555
607
  ...sseEntry,
556
608
  status: 'error',
609
+ duration: getElapsedDuration(
610
+ eventData.timestamp,
611
+ sseEntry.timestamp,
612
+ ),
557
613
  error: eventData.error.message,
558
614
  };
559
615
 
@@ -574,7 +630,10 @@ export const createNetworkActivityStore = () =>
574
630
  const updatedEntry: SSENetworkEntry = {
575
631
  ...sseEntry,
576
632
  status: 'closed',
577
- duration: eventData.timestamp - sseEntry.timestamp,
633
+ duration: getElapsedDuration(
634
+ eventData.timestamp,
635
+ sseEntry.timestamp,
636
+ ),
578
637
  };
579
638
 
580
639
  const newEntries = new Map(state.networkEntries);
@@ -594,7 +653,7 @@ export const createNetworkActivityStore = () =>
594
653
  // Subscribe to all events using the unified handler
595
654
  const unsubscribeFunctions = [
596
655
  client.onMessage('recording-state', (data) =>
597
- handleEvent('recording-state', data)
656
+ handleEvent('recording-state', data),
598
657
  ),
599
658
  client.onMessage('client-ui-settings', (data) =>
600
659
  handleEvent('client-ui-settings', data),
@@ -0,0 +1,230 @@
1
+ import { ScrollArea } from '../components/ScrollArea';
2
+ import { Section } from '../components/Section';
3
+ import { KeyValueGrid, type KeyValueItem } from '../components/KeyValueGrid';
4
+ import type { HttpNetworkEntry, SSENetworkEntry } from '../state/model';
5
+ import type { Initiator, InitiatorStackFrame } from '../../shared/client';
6
+ import {
7
+ formatFrameLocation,
8
+ formatSourcePath,
9
+ getBestInitiatorFrame,
10
+ getGeneratedFrameLocation,
11
+ getInitiatorLabel,
12
+ getInitiatorLocationLabel,
13
+ getSourceFrameLocation,
14
+ } from '../utils/initiator';
15
+
16
+ export type InitiatorTabProps = {
17
+ selectedRequest: HttpNetworkEntry | SSENetworkEntry;
18
+ };
19
+
20
+ const formatInitiatorType = (type: string) => {
21
+ switch (type) {
22
+ case 'script':
23
+ return 'Script';
24
+ case 'other':
25
+ return 'Other';
26
+ default:
27
+ return type;
28
+ }
29
+ };
30
+
31
+ const formatSymbolicationStatus = (initiator?: Initiator) => {
32
+ switch (initiator?.symbolicationStatus) {
33
+ case 'pending':
34
+ return 'Resolving source...';
35
+ case 'complete':
36
+ return 'Resolved';
37
+ case 'failed':
38
+ return 'Failed';
39
+ case 'unavailable':
40
+ return 'Unavailable';
41
+ default:
42
+ return null;
43
+ }
44
+ };
45
+
46
+ const getInitiatorItems = (initiator?: Initiator): KeyValueItem[] => {
47
+ if (!initiator) {
48
+ return [];
49
+ }
50
+
51
+ const sourceFrame = getBestInitiatorFrame(initiator);
52
+ const sourceLocation = getInitiatorLocationLabel(initiator);
53
+ const generatedLocation = formatFrameLocation(
54
+ getGeneratedFrameLocation(initiator),
55
+ );
56
+ const status = formatSymbolicationStatus(initiator);
57
+
58
+ return [
59
+ {
60
+ key: 'Type',
61
+ value: formatInitiatorType(initiator.type),
62
+ },
63
+ ...(status
64
+ ? [
65
+ {
66
+ key: 'Source map',
67
+ value: status,
68
+ valueClassName:
69
+ initiator.symbolicationStatus === 'failed'
70
+ ? 'text-red-300'
71
+ : 'text-gray-300',
72
+ },
73
+ ]
74
+ : []),
75
+ ...(sourceFrame?.functionName
76
+ ? [
77
+ {
78
+ key: 'Function',
79
+ value: sourceFrame.functionName,
80
+ valueClassName: 'font-mono text-blue-300',
81
+ },
82
+ ]
83
+ : []),
84
+ ...(sourceFrame?.url
85
+ ? [
86
+ {
87
+ key: 'Source',
88
+ value: formatSourcePath(sourceFrame.url),
89
+ valueClassName: 'font-mono text-blue-300',
90
+ },
91
+ ]
92
+ : []),
93
+ ...(sourceFrame?.lineNumber !== undefined
94
+ ? [
95
+ {
96
+ key: 'Line',
97
+ value: sourceFrame.lineNumber,
98
+ },
99
+ ]
100
+ : []),
101
+ ...(sourceFrame?.columnNumber !== undefined
102
+ ? [
103
+ {
104
+ key: 'Column',
105
+ value: sourceFrame.columnNumber,
106
+ },
107
+ ]
108
+ : []),
109
+ ...(sourceLocation
110
+ ? [
111
+ {
112
+ key: 'Location',
113
+ value: sourceLocation,
114
+ valueClassName: 'font-mono text-blue-300',
115
+ },
116
+ ]
117
+ : []),
118
+ ...(generatedLocation && generatedLocation !== sourceLocation
119
+ ? [
120
+ {
121
+ key: 'Generated',
122
+ value: generatedLocation,
123
+ valueClassName: 'font-mono text-gray-500',
124
+ },
125
+ ]
126
+ : []),
127
+ ];
128
+ };
129
+
130
+ const getStackFrameLocation = (frame: InitiatorStackFrame) => {
131
+ const sourceLocation = formatFrameLocation(getSourceFrameLocation(frame));
132
+ const generatedLocation = formatFrameLocation(
133
+ getGeneratedFrameLocation(frame),
134
+ );
135
+
136
+ if (
137
+ sourceLocation &&
138
+ generatedLocation &&
139
+ sourceLocation !== generatedLocation
140
+ ) {
141
+ return (
142
+ <span>
143
+ {sourceLocation}
144
+ <span className="ml-2 text-gray-500">
145
+ generated {generatedLocation}
146
+ </span>
147
+ </span>
148
+ );
149
+ }
150
+
151
+ return sourceLocation ?? generatedLocation ?? 'Unknown location';
152
+ };
153
+
154
+ const getStackItems = (initiator?: Initiator): KeyValueItem[] => {
155
+ return (
156
+ initiator?.stack?.map((frame, index) => {
157
+ return {
158
+ key: frame.functionName || `Frame ${index + 1}`,
159
+ value: getStackFrameLocation(frame),
160
+ keyClassName: 'font-mono',
161
+ valueClassName: 'font-mono text-blue-300',
162
+ };
163
+ }) ?? []
164
+ );
165
+ };
166
+
167
+ export const InitiatorTab = ({ selectedRequest }: InitiatorTabProps) => {
168
+ const initiator = selectedRequest.initiator;
169
+ const initiatorItems = getInitiatorItems(initiator);
170
+ const stackItems = getStackItems(initiator);
171
+ const initiatorLabel = getInitiatorLabel(initiator);
172
+ const initiatorLocation = getInitiatorLocationLabel(initiator);
173
+ const hasSourceMappedFrame = Boolean(
174
+ getSourceFrameLocation(initiator) ||
175
+ initiator?.stack?.some((frame) => getSourceFrameLocation(frame)),
176
+ );
177
+
178
+ return (
179
+ <ScrollArea className="h-full w-full">
180
+ <div className="p-4 space-y-4">
181
+ <div className="rounded-md border border-gray-700 bg-gray-800/60 p-3">
182
+ <div className="text-xs uppercase text-gray-500">Triggered by</div>
183
+ <div className="mt-1 font-mono text-sm text-blue-300">
184
+ {initiatorLabel ?? 'Unknown initiator'}
185
+ </div>
186
+ {initiatorLocation && initiatorLocation !== initiatorLabel && (
187
+ <div className="mt-1 font-mono text-xs text-gray-400 wrap-anywhere">
188
+ {initiatorLocation}
189
+ </div>
190
+ )}
191
+ </div>
192
+
193
+ <Section title="Initiator">
194
+ <KeyValueGrid
195
+ items={initiatorItems}
196
+ emptyMessage="No initiator metadata available"
197
+ />
198
+ </Section>
199
+
200
+ {!hasSourceMappedFrame &&
201
+ initiator?.symbolicationStatus !== 'pending' && (
202
+ <div className="rounded-md border border-gray-700 bg-gray-800/60 p-3 text-sm text-gray-400">
203
+ This request only includes generated bundle location data. Metro
204
+ source maps were not available for this entry.
205
+ </div>
206
+ )}
207
+
208
+ {initiator?.symbolicationError && (
209
+ <div className="rounded-md border border-red-900/70 bg-red-950/30 p-3 text-sm text-red-200">
210
+ {initiator.symbolicationError}
211
+ </div>
212
+ )}
213
+
214
+ {initiator?.codeFrame && (
215
+ <Section title="Code Frame">
216
+ <pre className="overflow-auto rounded-md bg-gray-950 p-3 text-xs text-gray-300">
217
+ <code>{initiator.codeFrame.content}</code>
218
+ </pre>
219
+ </Section>
220
+ )}
221
+
222
+ {stackItems.length > 0 && (
223
+ <Section title="Stack Preview">
224
+ <KeyValueGrid items={stackItems} />
225
+ </Section>
226
+ )}
227
+ </div>
228
+ </ScrollArea>
229
+ );
230
+ };