@rozenite/network-activity-plugin 1.7.0-rc.2 → 1.8.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 +31 -0
- package/README.md +24 -0
- package/dist/devtools/App.html +2 -2
- package/dist/devtools/assets/{App-pokLiGYV.js → App-B3xlUjs6.js} +163 -115
- package/dist/devtools/assets/{App-BrSkOkws.css → App-m6xge0az.css} +13 -0
- package/dist/react-native/chunks/boot-recording.cjs +307 -28
- package/dist/react-native/chunks/boot-recording.js +310 -31
- package/dist/react-native/chunks/useNetworkActivityDevTools.require.cjs +192 -141
- package/dist/react-native/chunks/useNetworkActivityDevTools.require.js +193 -142
- package/dist/react-native/index.d.ts +24 -7
- package/dist/rozenite.json +1 -1
- package/dist/sdk/index.cjs +127 -0
- package/dist/sdk/index.d.ts +1243 -0
- package/dist/sdk/index.js +127 -0
- package/package.json +17 -6
- package/sdk.ts +59 -0
- package/src/react-native/__tests__/events-listener.test.ts +35 -0
- package/src/react-native/agent/__tests__/network-activity-agent-state.test.ts +21 -9
- package/src/react-native/agent/state.ts +13 -13
- package/src/react-native/agent/tools.ts +20 -146
- package/src/react-native/agent/use-network-activity-agent-tools.ts +73 -64
- package/src/react-native/events-listener.ts +12 -3
- package/src/react-native/http/http-inspector.ts +19 -5
- package/src/react-native/network-inspector.ts +46 -8
- package/src/react-native/nitro-fetch/__tests__/nitro-network-inspector.test.ts +198 -0
- package/src/react-native/nitro-fetch/nitro-network-inspector.ts +403 -0
- package/src/react-native/useHttpInspector.ts +9 -19
- package/src/react-native/useNetworkActivityDevTools.ts +13 -1
- package/src/react-native/websocket/__tests__/websocket-inspector.test.ts +69 -0
- package/src/react-native/websocket/websocket-inspector.ts +32 -17
- package/src/shared/agent-tools.ts +230 -0
- package/src/shared/client.ts +3 -0
- package/src/shared/http-events.ts +6 -0
- package/src/shared/websocket-events.ts +16 -7
- package/src/ui/components/RequestList.tsx +21 -0
- package/src/ui/components/SidePanel.tsx +12 -9
- package/src/ui/state/derived.ts +4 -0
- package/src/ui/state/model.ts +6 -1
- package/src/ui/state/store.ts +52 -36
- package/src/ui/tabs/HeadersTab.tsx +18 -4
- package/src/ui/tabs/ResponseTab.tsx +5 -3
- package/tsconfig.json +4 -1
- package/vite.config.ts +7 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createNitroNetworkInspector } from '../nitro-network-inspector';
|
|
3
|
+
|
|
4
|
+
describe('nitro network inspector', () => {
|
|
5
|
+
it('translates nitro websocket updates with direct string ids and no duplicate messages', () => {
|
|
6
|
+
const listeners = new Set<(entry: any) => void>();
|
|
7
|
+
const inspector = createNitroNetworkInspector(() => ({
|
|
8
|
+
NetworkInspector: {
|
|
9
|
+
enable() {},
|
|
10
|
+
disable() {},
|
|
11
|
+
isEnabled() {
|
|
12
|
+
return true;
|
|
13
|
+
},
|
|
14
|
+
onEntry(callback) {
|
|
15
|
+
listeners.add(callback);
|
|
16
|
+
return () => listeners.delete(callback);
|
|
17
|
+
},
|
|
18
|
+
getEntries() {
|
|
19
|
+
return [];
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const events: Array<{ type: string; socketId?: string; data?: string }> =
|
|
25
|
+
[];
|
|
26
|
+
inspector.on('websocket-connect', (event) => {
|
|
27
|
+
events.push({ type: event.type, socketId: event.socketId });
|
|
28
|
+
});
|
|
29
|
+
inspector.on('websocket-open', (event) => {
|
|
30
|
+
events.push({ type: event.type, socketId: event.socketId });
|
|
31
|
+
});
|
|
32
|
+
inspector.on('websocket-message-sent', (event) => {
|
|
33
|
+
events.push({
|
|
34
|
+
type: event.type,
|
|
35
|
+
socketId: event.socketId,
|
|
36
|
+
data: event.data,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
inspector.on('websocket-message-received', (event) => {
|
|
40
|
+
events.push({
|
|
41
|
+
type: event.type,
|
|
42
|
+
socketId: event.socketId,
|
|
43
|
+
data: event.data,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
inspector.on('websocket-close', (event) => {
|
|
47
|
+
events.push({ type: event.type, socketId: event.socketId });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
inspector.enable();
|
|
51
|
+
|
|
52
|
+
const emit = (entry: any) => {
|
|
53
|
+
for (const listener of listeners) {
|
|
54
|
+
listener(entry);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
emit({
|
|
59
|
+
id: 'nitro-ws-1',
|
|
60
|
+
type: 'websocket',
|
|
61
|
+
url: 'wss://example.com/socket',
|
|
62
|
+
protocols: ['chat'],
|
|
63
|
+
requestHeaders: [],
|
|
64
|
+
startTime: 10,
|
|
65
|
+
endTime: 0,
|
|
66
|
+
duration: 0,
|
|
67
|
+
readyState: 'OPEN',
|
|
68
|
+
messages: [
|
|
69
|
+
{
|
|
70
|
+
direction: 'sent',
|
|
71
|
+
data: 'ping',
|
|
72
|
+
size: 4,
|
|
73
|
+
isBinary: false,
|
|
74
|
+
timestamp: 11,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
messagesSent: 1,
|
|
78
|
+
messagesReceived: 0,
|
|
79
|
+
bytesSent: 4,
|
|
80
|
+
bytesReceived: 0,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
emit({
|
|
84
|
+
id: 'nitro-ws-1',
|
|
85
|
+
type: 'websocket',
|
|
86
|
+
url: 'wss://example.com/socket',
|
|
87
|
+
protocols: ['chat'],
|
|
88
|
+
requestHeaders: [],
|
|
89
|
+
startTime: 10,
|
|
90
|
+
endTime: 0,
|
|
91
|
+
duration: 0,
|
|
92
|
+
readyState: 'OPEN',
|
|
93
|
+
messages: [
|
|
94
|
+
{
|
|
95
|
+
direction: 'sent',
|
|
96
|
+
data: 'ping',
|
|
97
|
+
size: 4,
|
|
98
|
+
isBinary: false,
|
|
99
|
+
timestamp: 11,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
messagesSent: 1,
|
|
103
|
+
messagesReceived: 0,
|
|
104
|
+
bytesSent: 4,
|
|
105
|
+
bytesReceived: 0,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
emit({
|
|
109
|
+
id: 'nitro-ws-1',
|
|
110
|
+
type: 'websocket',
|
|
111
|
+
url: 'wss://example.com/socket',
|
|
112
|
+
protocols: ['chat'],
|
|
113
|
+
requestHeaders: [],
|
|
114
|
+
startTime: 10,
|
|
115
|
+
endTime: 15,
|
|
116
|
+
duration: 5,
|
|
117
|
+
readyState: 'CLOSED',
|
|
118
|
+
messages: [
|
|
119
|
+
{
|
|
120
|
+
direction: 'sent',
|
|
121
|
+
data: 'ping',
|
|
122
|
+
size: 4,
|
|
123
|
+
isBinary: false,
|
|
124
|
+
timestamp: 11,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
direction: 'received',
|
|
128
|
+
data: 'pong',
|
|
129
|
+
size: 4,
|
|
130
|
+
isBinary: false,
|
|
131
|
+
timestamp: 12,
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
messagesSent: 1,
|
|
135
|
+
messagesReceived: 1,
|
|
136
|
+
bytesSent: 4,
|
|
137
|
+
bytesReceived: 4,
|
|
138
|
+
closeCode: 1000,
|
|
139
|
+
closeReason: 'done',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(events).toEqual([
|
|
143
|
+
{ type: 'websocket-connect', socketId: 'nitro-ws-1' },
|
|
144
|
+
{ type: 'websocket-open', socketId: 'nitro-ws-1' },
|
|
145
|
+
{ type: 'websocket-message-sent', socketId: 'nitro-ws-1', data: 'ping' },
|
|
146
|
+
{
|
|
147
|
+
type: 'websocket-message-received',
|
|
148
|
+
socketId: 'nitro-ws-1',
|
|
149
|
+
data: 'pong',
|
|
150
|
+
},
|
|
151
|
+
{ type: 'websocket-close', socketId: 'nitro-ws-1' },
|
|
152
|
+
]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('caches nitro http response bodies for later lookup', () => {
|
|
156
|
+
const listeners = new Set<(entry: any) => void>();
|
|
157
|
+
const inspector = createNitroNetworkInspector(() => ({
|
|
158
|
+
NetworkInspector: {
|
|
159
|
+
enable() {},
|
|
160
|
+
disable() {},
|
|
161
|
+
isEnabled() {
|
|
162
|
+
return true;
|
|
163
|
+
},
|
|
164
|
+
onEntry(callback) {
|
|
165
|
+
listeners.add(callback);
|
|
166
|
+
return () => listeners.delete(callback);
|
|
167
|
+
},
|
|
168
|
+
getEntries() {
|
|
169
|
+
return [];
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
}));
|
|
173
|
+
|
|
174
|
+
inspector.enable();
|
|
175
|
+
|
|
176
|
+
for (const listener of listeners) {
|
|
177
|
+
listener({
|
|
178
|
+
id: 'nitro-http-1',
|
|
179
|
+
type: 'http',
|
|
180
|
+
url: 'https://example.com/api',
|
|
181
|
+
method: 'GET',
|
|
182
|
+
requestHeaders: [],
|
|
183
|
+
requestBody: undefined,
|
|
184
|
+
requestBodySize: 0,
|
|
185
|
+
status: 200,
|
|
186
|
+
statusText: 'OK',
|
|
187
|
+
responseHeaders: [{ key: 'content-type', value: 'application/json' }],
|
|
188
|
+
responseBody: '{"ok":true}',
|
|
189
|
+
responseBodySize: 11,
|
|
190
|
+
startTime: 10,
|
|
191
|
+
endTime: 20,
|
|
192
|
+
duration: 10,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
expect(inspector.getResponseBody('nitro-http-1')).toBe('{"ok":true}');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { createNanoEvents } from 'nanoevents';
|
|
2
|
+
import type {
|
|
3
|
+
HttpEventMap,
|
|
4
|
+
HttpHeaders,
|
|
5
|
+
HttpMethod,
|
|
6
|
+
RequestPostData,
|
|
7
|
+
} from '../../shared/client';
|
|
8
|
+
import type { WebSocketEventMap } from '../../shared/websocket-events';
|
|
9
|
+
import type { Inspector } from '../inspector';
|
|
10
|
+
|
|
11
|
+
type NitroHttpHeader = {
|
|
12
|
+
key: string;
|
|
13
|
+
value: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type NitroHttpEntry = {
|
|
17
|
+
id: string;
|
|
18
|
+
type: 'http';
|
|
19
|
+
url: string;
|
|
20
|
+
method: string;
|
|
21
|
+
requestHeaders: NitroHttpHeader[];
|
|
22
|
+
requestBody?: string;
|
|
23
|
+
requestBodySize: number;
|
|
24
|
+
status: number;
|
|
25
|
+
statusText: string;
|
|
26
|
+
responseHeaders: NitroHttpHeader[];
|
|
27
|
+
responseBody?: string;
|
|
28
|
+
responseBodySize: number;
|
|
29
|
+
startTime: number;
|
|
30
|
+
endTime: number;
|
|
31
|
+
duration: number;
|
|
32
|
+
error?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type NitroWebSocketMessage = {
|
|
36
|
+
direction: 'sent' | 'received';
|
|
37
|
+
data: string;
|
|
38
|
+
size: number;
|
|
39
|
+
isBinary: boolean;
|
|
40
|
+
timestamp: number;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type NitroWebSocketEntry = {
|
|
44
|
+
id: string;
|
|
45
|
+
type: 'websocket';
|
|
46
|
+
url: string;
|
|
47
|
+
protocols: string[];
|
|
48
|
+
requestHeaders: NitroHttpHeader[];
|
|
49
|
+
startTime: number;
|
|
50
|
+
endTime: number;
|
|
51
|
+
duration: number;
|
|
52
|
+
readyState: string;
|
|
53
|
+
messages: NitroWebSocketMessage[];
|
|
54
|
+
messagesSent: number;
|
|
55
|
+
messagesReceived: number;
|
|
56
|
+
bytesSent: number;
|
|
57
|
+
bytesReceived: number;
|
|
58
|
+
closeCode?: number;
|
|
59
|
+
closeReason?: string;
|
|
60
|
+
error?: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type NitroInspectorEntry = NitroHttpEntry | NitroWebSocketEntry;
|
|
64
|
+
|
|
65
|
+
type NitroModule = {
|
|
66
|
+
NetworkInspector: {
|
|
67
|
+
enable: () => void;
|
|
68
|
+
disable: () => void;
|
|
69
|
+
isEnabled: () => boolean;
|
|
70
|
+
onEntry: (callback: (entry: NitroInspectorEntry) => void) => () => void;
|
|
71
|
+
getEntries: () => ReadonlyArray<NitroInspectorEntry>;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type NitroNetworkEventMap = Pick<
|
|
76
|
+
HttpEventMap & WebSocketEventMap,
|
|
77
|
+
| 'request-sent'
|
|
78
|
+
| 'response-received'
|
|
79
|
+
| 'request-completed'
|
|
80
|
+
| 'request-failed'
|
|
81
|
+
| 'websocket-connect'
|
|
82
|
+
| 'websocket-open'
|
|
83
|
+
| 'websocket-close'
|
|
84
|
+
| 'websocket-message-sent'
|
|
85
|
+
| 'websocket-message-received'
|
|
86
|
+
| 'websocket-error'
|
|
87
|
+
>;
|
|
88
|
+
|
|
89
|
+
type NanoEventsMap = {
|
|
90
|
+
[K in keyof NitroNetworkEventMap]: (data: NitroNetworkEventMap[K]) => void;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type NitroNetworkInspector = Inspector<NitroNetworkEventMap> & {
|
|
94
|
+
getResponseBody: (requestId: string) => string | null;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const NITRO_NETWORK_EVENTS: (keyof NitroNetworkEventMap)[] = [
|
|
98
|
+
'request-sent',
|
|
99
|
+
'response-received',
|
|
100
|
+
'request-completed',
|
|
101
|
+
'request-failed',
|
|
102
|
+
'websocket-connect',
|
|
103
|
+
'websocket-open',
|
|
104
|
+
'websocket-close',
|
|
105
|
+
'websocket-message-sent',
|
|
106
|
+
'websocket-message-received',
|
|
107
|
+
'websocket-error',
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const loadNitroModule = (): NitroModule | null => {
|
|
111
|
+
try {
|
|
112
|
+
return require('react-native-nitro-fetch') as NitroModule;
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const timestampOrigin =
|
|
119
|
+
typeof performance !== 'undefined' &&
|
|
120
|
+
typeof performance.timeOrigin === 'number'
|
|
121
|
+
? performance.timeOrigin
|
|
122
|
+
: Date.now() - performance.now();
|
|
123
|
+
|
|
124
|
+
const toEpochTime = (timestamp: number) =>
|
|
125
|
+
Math.round(timestampOrigin + timestamp);
|
|
126
|
+
|
|
127
|
+
const toHeaders = (headers: NitroHttpHeader[]): HttpHeaders => {
|
|
128
|
+
return headers.reduce<HttpHeaders>((acc, { key, value }) => {
|
|
129
|
+
const existing = acc[key];
|
|
130
|
+
if (existing === undefined) {
|
|
131
|
+
acc[key] = value;
|
|
132
|
+
return acc;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
acc[key] = Array.isArray(existing)
|
|
136
|
+
? [...existing, value]
|
|
137
|
+
: [existing, value];
|
|
138
|
+
return acc;
|
|
139
|
+
}, {});
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const toPostData = (body?: string): RequestPostData => {
|
|
143
|
+
if (body == null) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
type: 'text',
|
|
149
|
+
value: body,
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const cloneEntry = <TEntry extends NitroInspectorEntry>(
|
|
154
|
+
entry: TEntry,
|
|
155
|
+
): TEntry => {
|
|
156
|
+
return JSON.parse(JSON.stringify(entry)) as TEntry;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const getContentType = (headers: NitroHttpHeader[]) => {
|
|
160
|
+
return (
|
|
161
|
+
headers.find((header) => header.key.toLowerCase() === 'content-type')
|
|
162
|
+
?.value ?? 'text/plain'
|
|
163
|
+
);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const normalizeReadyState = (readyState: string) => readyState.toUpperCase();
|
|
167
|
+
|
|
168
|
+
export const createNitroNetworkInspector = (
|
|
169
|
+
getNitroModule: () => NitroModule | null = loadNitroModule,
|
|
170
|
+
): NitroNetworkInspector => {
|
|
171
|
+
const eventEmitter = createNanoEvents<NanoEventsMap>();
|
|
172
|
+
const previousEntries = new Map<string, NitroInspectorEntry>();
|
|
173
|
+
const responseBodies = new Map<string, string | null>();
|
|
174
|
+
let nitroModule: NitroModule | null = null;
|
|
175
|
+
let unsubscribe: (() => void) | null = null;
|
|
176
|
+
|
|
177
|
+
const emitHttpEvents = (entry: NitroHttpEntry, previous?: NitroHttpEntry) => {
|
|
178
|
+
if (!previous) {
|
|
179
|
+
eventEmitter.emit('request-sent', {
|
|
180
|
+
requestId: entry.id,
|
|
181
|
+
timestamp: toEpochTime(entry.startTime),
|
|
182
|
+
request: {
|
|
183
|
+
url: entry.url,
|
|
184
|
+
method: entry.method as HttpMethod,
|
|
185
|
+
headers: toHeaders(entry.requestHeaders),
|
|
186
|
+
postData: toPostData(entry.requestBody),
|
|
187
|
+
},
|
|
188
|
+
initiator: { type: 'other' },
|
|
189
|
+
type: 'Fetch',
|
|
190
|
+
source: 'nitro',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (entry.error) {
|
|
195
|
+
if (!previous || previous.error !== entry.error) {
|
|
196
|
+
eventEmitter.emit('request-failed', {
|
|
197
|
+
requestId: entry.id,
|
|
198
|
+
timestamp: toEpochTime(entry.endTime || entry.startTime),
|
|
199
|
+
type: 'Fetch',
|
|
200
|
+
error: entry.error,
|
|
201
|
+
canceled: entry.error === 'Request canceled',
|
|
202
|
+
source: 'nitro',
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const didResponseChange =
|
|
209
|
+
!previous ||
|
|
210
|
+
previous.status !== entry.status ||
|
|
211
|
+
previous.statusText !== entry.statusText ||
|
|
212
|
+
previous.responseBodySize !== entry.responseBodySize ||
|
|
213
|
+
previous.endTime !== entry.endTime;
|
|
214
|
+
|
|
215
|
+
if (!didResponseChange) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const responseTimestamp = toEpochTime(entry.endTime || entry.startTime);
|
|
220
|
+
|
|
221
|
+
eventEmitter.emit('response-received', {
|
|
222
|
+
requestId: entry.id,
|
|
223
|
+
timestamp: responseTimestamp,
|
|
224
|
+
type: 'Fetch',
|
|
225
|
+
response: {
|
|
226
|
+
url: entry.url,
|
|
227
|
+
status: entry.status,
|
|
228
|
+
statusText: entry.statusText,
|
|
229
|
+
headers: toHeaders(entry.responseHeaders),
|
|
230
|
+
contentType: getContentType(entry.responseHeaders),
|
|
231
|
+
size: entry.responseBodySize,
|
|
232
|
+
responseTime: responseTimestamp,
|
|
233
|
+
},
|
|
234
|
+
source: 'nitro',
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
eventEmitter.emit('request-completed', {
|
|
238
|
+
requestId: entry.id,
|
|
239
|
+
timestamp: responseTimestamp,
|
|
240
|
+
duration: entry.duration,
|
|
241
|
+
size: entry.responseBodySize,
|
|
242
|
+
ttfb: entry.duration,
|
|
243
|
+
source: 'nitro',
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const emitWebSocketEvents = (
|
|
248
|
+
entry: NitroWebSocketEntry,
|
|
249
|
+
previous?: NitroWebSocketEntry,
|
|
250
|
+
) => {
|
|
251
|
+
const socketId = entry.id;
|
|
252
|
+
const readyState = normalizeReadyState(entry.readyState);
|
|
253
|
+
const previousReadyState = previous
|
|
254
|
+
? normalizeReadyState(previous.readyState)
|
|
255
|
+
: null;
|
|
256
|
+
|
|
257
|
+
if (!previous) {
|
|
258
|
+
eventEmitter.emit('websocket-connect', {
|
|
259
|
+
type: 'websocket-connect',
|
|
260
|
+
url: entry.url,
|
|
261
|
+
socketId,
|
|
262
|
+
timestamp: toEpochTime(entry.startTime),
|
|
263
|
+
protocols: entry.protocols,
|
|
264
|
+
options: [],
|
|
265
|
+
source: 'nitro',
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (readyState === 'OPEN' && previousReadyState !== 'OPEN') {
|
|
270
|
+
eventEmitter.emit('websocket-open', {
|
|
271
|
+
type: 'websocket-open',
|
|
272
|
+
url: entry.url,
|
|
273
|
+
socketId,
|
|
274
|
+
timestamp: toEpochTime(entry.startTime),
|
|
275
|
+
source: 'nitro',
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const previousMessageCount = previous?.messages.length ?? 0;
|
|
280
|
+
for (const message of entry.messages.slice(previousMessageCount)) {
|
|
281
|
+
const event = {
|
|
282
|
+
url: entry.url,
|
|
283
|
+
socketId,
|
|
284
|
+
timestamp: toEpochTime(message.timestamp),
|
|
285
|
+
data: message.data,
|
|
286
|
+
messageType: message.isBinary ? ('binary' as const) : ('text' as const),
|
|
287
|
+
source: 'nitro' as const,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
if (message.direction === 'sent') {
|
|
291
|
+
eventEmitter.emit('websocket-message-sent', {
|
|
292
|
+
type: 'websocket-message-sent',
|
|
293
|
+
...event,
|
|
294
|
+
});
|
|
295
|
+
} else {
|
|
296
|
+
eventEmitter.emit('websocket-message-received', {
|
|
297
|
+
type: 'websocket-message-received',
|
|
298
|
+
...event,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (entry.error && (!previous || previous.error !== entry.error)) {
|
|
304
|
+
eventEmitter.emit('websocket-error', {
|
|
305
|
+
type: 'websocket-error',
|
|
306
|
+
url: entry.url,
|
|
307
|
+
socketId,
|
|
308
|
+
timestamp: toEpochTime(entry.endTime || entry.startTime),
|
|
309
|
+
error: entry.error,
|
|
310
|
+
source: 'nitro',
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (readyState === 'CLOSED' && previousReadyState !== 'CLOSED') {
|
|
315
|
+
eventEmitter.emit('websocket-close', {
|
|
316
|
+
type: 'websocket-close',
|
|
317
|
+
url: entry.url,
|
|
318
|
+
socketId,
|
|
319
|
+
timestamp: toEpochTime(entry.endTime || entry.startTime),
|
|
320
|
+
code: entry.closeCode ?? 0,
|
|
321
|
+
reason: entry.closeReason,
|
|
322
|
+
source: 'nitro',
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const handleEntry = (entry: NitroInspectorEntry) => {
|
|
328
|
+
const previous = previousEntries.get(entry.id);
|
|
329
|
+
|
|
330
|
+
if (entry.type === 'http') {
|
|
331
|
+
responseBodies.set(entry.id, entry.responseBody ?? null);
|
|
332
|
+
emitHttpEvents(entry, previous as NitroHttpEntry | undefined);
|
|
333
|
+
} else {
|
|
334
|
+
emitWebSocketEvents(entry, previous as NitroWebSocketEntry | undefined);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
previousEntries.set(entry.id, cloneEntry(entry));
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
enable() {
|
|
342
|
+
if (unsubscribe) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
nitroModule = getNitroModule();
|
|
347
|
+
if (!nitroModule) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
nitroModule.NetworkInspector.enable();
|
|
352
|
+
for (const entry of nitroModule.NetworkInspector.getEntries()) {
|
|
353
|
+
previousEntries.set(entry.id, cloneEntry(entry));
|
|
354
|
+
if (entry.type === 'http') {
|
|
355
|
+
responseBodies.set(entry.id, entry.responseBody ?? null);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
unsubscribe = nitroModule.NetworkInspector.onEntry(handleEntry);
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
disable() {
|
|
362
|
+
unsubscribe?.();
|
|
363
|
+
unsubscribe = null;
|
|
364
|
+
nitroModule?.NetworkInspector.disable();
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
isEnabled() {
|
|
368
|
+
return nitroModule?.NetworkInspector.isEnabled() ?? false;
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
dispose() {
|
|
372
|
+
unsubscribe?.();
|
|
373
|
+
unsubscribe = null;
|
|
374
|
+
previousEntries.clear();
|
|
375
|
+
responseBodies.clear();
|
|
376
|
+
nitroModule?.NetworkInspector.disable();
|
|
377
|
+
nitroModule = null;
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
getResponseBody(requestId: string) {
|
|
381
|
+
return responseBodies.get(requestId) ?? null;
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
on<TEventType extends keyof NitroNetworkEventMap>(
|
|
385
|
+
event: TEventType,
|
|
386
|
+
callback: (data: NitroNetworkEventMap[TEventType]) => void,
|
|
387
|
+
) {
|
|
388
|
+
return eventEmitter.on(event, callback as NanoEventsMap[TEventType]);
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
export const getNitroNetworkInspector = (() => {
|
|
394
|
+
let instance: NitroNetworkInspector | null = null;
|
|
395
|
+
|
|
396
|
+
return (): NitroNetworkInspector => {
|
|
397
|
+
if (!instance) {
|
|
398
|
+
instance = createNitroNetworkInspector();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return instance;
|
|
402
|
+
};
|
|
403
|
+
})();
|
|
@@ -1,43 +1,33 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type { NetworkInspector } from './network-inspector';
|
|
3
3
|
import type { NetworkActivityDevToolsClient } from '../shared/client';
|
|
4
|
-
import { getResponseBody } from './http/http-utils';
|
|
5
4
|
import { getOverridesRegistry } from './http/overrides-registry';
|
|
6
5
|
|
|
7
6
|
const overridesRegistry = getOverridesRegistry();
|
|
8
7
|
|
|
9
8
|
export const useHttpInspector = (
|
|
10
9
|
client: NetworkActivityDevToolsClient | null,
|
|
11
|
-
|
|
10
|
+
networkInspector: NetworkInspector,
|
|
12
11
|
isEnabled: boolean,
|
|
13
|
-
isRecordingEnabled: boolean
|
|
12
|
+
isRecordingEnabled: boolean,
|
|
14
13
|
) => {
|
|
15
14
|
useEffect(() => {
|
|
16
15
|
if (!client || !isEnabled) {
|
|
17
16
|
return;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
const networkRequestsRegistry =
|
|
21
|
-
httpInspector.getNetworkRequestsRegistry();
|
|
22
|
-
|
|
23
19
|
const subscriptions = [
|
|
24
20
|
client.onMessage('network-enable', () => {
|
|
25
|
-
|
|
21
|
+
networkInspector.http.enable();
|
|
26
22
|
}),
|
|
27
23
|
client.onMessage('network-disable', () => {
|
|
28
|
-
|
|
24
|
+
networkInspector.http.disable();
|
|
29
25
|
}),
|
|
30
26
|
client.onMessage('set-overrides', (data) => {
|
|
31
27
|
overridesRegistry.setOverrides(data.overrides);
|
|
32
28
|
}),
|
|
33
29
|
client.onMessage('get-response-body', async ({ requestId }) => {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
if (!request) {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const body = await getResponseBody(request);
|
|
30
|
+
const body = await networkInspector.getResponseBody(requestId);
|
|
41
31
|
|
|
42
32
|
client.send('response-body', {
|
|
43
33
|
requestId,
|
|
@@ -48,12 +38,12 @@ export const useHttpInspector = (
|
|
|
48
38
|
|
|
49
39
|
// If recording was previously enabled, enable the inspector (hot reload)
|
|
50
40
|
if (isRecordingEnabled) {
|
|
51
|
-
|
|
41
|
+
networkInspector.http.enable();
|
|
52
42
|
}
|
|
53
43
|
|
|
54
44
|
return () => {
|
|
55
45
|
subscriptions.forEach((subscription) => subscription.remove());
|
|
56
|
-
|
|
46
|
+
networkInspector.http.dispose();
|
|
57
47
|
};
|
|
58
|
-
}, [client,
|
|
48
|
+
}, [client, networkInspector, isEnabled, isRecordingEnabled]);
|
|
59
49
|
};
|
|
@@ -67,6 +67,11 @@ export const useNetworkActivityDevTools = (
|
|
|
67
67
|
const subscriptions = [
|
|
68
68
|
client.onMessage('network-enable', () => {
|
|
69
69
|
isRecordingEnabledRef.current = true;
|
|
70
|
+
networkInspector.enable({
|
|
71
|
+
http: isHttpInspectorEnabled,
|
|
72
|
+
websocket: isWebSocketInspectorEnabled,
|
|
73
|
+
sse: isSSEInspectorEnabled,
|
|
74
|
+
});
|
|
70
75
|
|
|
71
76
|
// Connect the events listener to send events through the DevTools client
|
|
72
77
|
// This also automatically flushes any queued messages
|
|
@@ -87,12 +92,19 @@ export const useNetworkActivityDevTools = (
|
|
|
87
92
|
}),
|
|
88
93
|
client.onMessage('network-disable', () => {
|
|
89
94
|
isRecordingEnabledRef.current = false;
|
|
95
|
+
networkInspector.disable();
|
|
90
96
|
}),
|
|
91
97
|
client.onMessage('get-client-ui-settings', () => {
|
|
92
98
|
sendClientUISettings();
|
|
93
99
|
}),
|
|
94
100
|
];
|
|
95
101
|
|
|
102
|
+
// Inform the DevTools UI of the current recording state so it can detect
|
|
103
|
+
// and resolve desynchronization (e.g. after an app reload)
|
|
104
|
+
client.send('recording-state', {
|
|
105
|
+
isRecording: isRecordingEnabledRef.current,
|
|
106
|
+
});
|
|
107
|
+
|
|
96
108
|
// Send initial or changed values live
|
|
97
109
|
sendClientUISettings();
|
|
98
110
|
|
|
@@ -109,7 +121,7 @@ export const useNetworkActivityDevTools = (
|
|
|
109
121
|
|
|
110
122
|
useHttpInspector(
|
|
111
123
|
client,
|
|
112
|
-
networkInspector
|
|
124
|
+
networkInspector,
|
|
113
125
|
isHttpInspectorEnabled,
|
|
114
126
|
isRecordingEnabledRef.current,
|
|
115
127
|
);
|