@rozenite/network-activity-plugin 1.0.0-alpha.7 → 1.0.0-alpha.9
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/dist/App.html +2 -2
- package/dist/assets/{App-CIflVb88.js → App-CA1Fbh0I.js} +12009 -10809
- package/dist/assets/{App-Czu6Vt2P.css → App-DoHQsY5s.css} +43 -0
- package/dist/event-source.cjs +22 -0
- package/dist/event-source.js +23 -0
- package/dist/rozenite.json +1 -1
- package/dist/src/react-native/{network-inspector.d.ts → http/network-inspector.d.ts} +1 -1
- package/dist/src/react-native/sse/event-source.d.ts +2 -0
- package/dist/src/react-native/sse/sse-inspector.d.ts +9 -0
- package/dist/src/react-native/sse/sse-interceptor.d.ts +36 -0
- package/dist/src/react-native/sse/types.d.ts +6 -0
- package/dist/src/react-native/utils.d.ts +6 -0
- package/dist/src/react-native/websocket/websocket-inspector.d.ts +9 -0
- package/dist/src/react-native/websocket/websocket-interceptor.d.ts +74 -0
- package/dist/src/shared/client.d.ts +8 -4
- package/dist/src/shared/sse-events.d.ts +35 -0
- package/dist/src/shared/websocket-events.d.ts +60 -0
- package/dist/src/ui/components/Badge.d.ts +1 -1
- package/dist/src/ui/components/Button.d.ts +1 -1
- package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +7 -0
- package/dist/src/ui/components/RequestList.d.ts +6 -26
- package/dist/src/ui/components/SidePanel.d.ts +1 -0
- package/dist/src/ui/components/Toolbar.d.ts +1 -0
- package/dist/src/ui/hooks/useCopyToClipboard.d.ts +4 -0
- package/dist/src/ui/state/derived.d.ts +5 -0
- package/dist/src/ui/state/hooks.d.ts +17 -0
- package/dist/src/ui/state/model.d.ts +98 -0
- package/dist/src/ui/state/store.d.ts +24 -0
- package/dist/src/ui/tabs/CookiesTab.d.ts +3 -6
- package/dist/src/ui/tabs/HeadersTab.d.ts +3 -15
- package/dist/src/ui/tabs/MessagesTab.d.ts +5 -0
- package/dist/src/ui/tabs/RequestTab.d.ts +2 -7
- package/dist/src/ui/tabs/ResponseTab.d.ts +2 -8
- package/dist/src/ui/tabs/SSEMessagesTab.d.ts +5 -0
- package/dist/src/ui/tabs/TimingTab.d.ts +3 -5
- package/dist/src/ui/types.d.ts +6 -3
- package/dist/src/ui/utils/assert.d.ts +1 -0
- package/dist/src/ui/utils/copyToClipboard.d.ts +1 -0
- package/dist/src/ui/utils/getHttpHeaderValue.d.ts +2 -0
- package/dist/src/ui/utils/getId.d.ts +1 -0
- package/dist/src/ui/utils/getStatusColor.d.ts +1 -0
- package/dist/useNetworkActivityDevTools.cjs +433 -34
- package/dist/useNetworkActivityDevTools.js +431 -34
- package/package.json +19 -8
- package/src/react-native/{network-inspector.ts → http/network-inspector.ts} +14 -32
- package/src/react-native/{xml-request.d.ts → http/xml-request.d.ts} +1 -0
- package/src/react-native/sse/event-source.ts +25 -0
- package/src/react-native/sse/sse-inspector.ts +117 -0
- package/src/react-native/sse/sse-interceptor.ts +162 -0
- package/src/react-native/sse/types.ts +9 -0
- package/src/react-native/useNetworkActivityDevTools.ts +75 -1
- package/src/react-native/utils.ts +43 -0
- package/src/react-native/websocket/websocket-inspector.ts +180 -0
- package/src/react-native/websocket/websocket-interceptor.d.ts +4 -0
- package/src/react-native/websocket/websocket-interceptor.ts +166 -0
- package/src/shared/client.ts +10 -4
- package/src/shared/sse-events.ts +44 -0
- package/src/shared/websocket-events.ts +79 -0
- package/src/ui/components/Badge.tsx +1 -1
- package/src/ui/components/Button.tsx +1 -1
- package/src/ui/components/Input.tsx +1 -1
- package/src/ui/components/JsonTree.tsx +13 -0
- package/src/ui/components/JsonTreeCopyableItem.tsx +33 -0
- package/src/ui/components/RequestList.tsx +42 -123
- package/src/ui/components/ScrollArea.tsx +1 -1
- package/src/ui/components/Separator.tsx +1 -1
- package/src/ui/components/SidePanel.tsx +323 -0
- package/src/ui/components/Tabs.tsx +2 -2
- package/src/ui/components/Toolbar.tsx +45 -0
- package/src/ui/hooks/useCopyToClipboard.ts +28 -0
- package/src/ui/state/derived.ts +112 -0
- package/src/ui/state/hooks.ts +44 -0
- package/src/ui/state/model.ts +129 -0
- package/src/ui/state/store.ts +559 -0
- package/src/ui/tabs/CookiesTab.tsx +168 -179
- package/src/ui/tabs/HeadersTab.tsx +24 -31
- package/src/ui/tabs/MessagesTab.tsx +276 -0
- package/src/ui/tabs/RequestTab.tsx +28 -31
- package/src/ui/tabs/ResponseTab.tsx +10 -12
- package/src/ui/tabs/SSEMessagesTab.tsx +213 -0
- package/src/ui/tabs/TimingTab.tsx +33 -44
- package/src/ui/types.ts +6 -2
- package/src/ui/utils/assert.ts +5 -0
- package/src/ui/utils/copyToClipboard.ts +3 -0
- package/src/ui/utils/getHttpHeaderValue.ts +14 -0
- package/src/ui/utils/getId.ts +10 -0
- package/src/ui/utils/getStatusColor.ts +15 -0
- package/src/ui/views/InspectorView.tsx +24 -320
- package/tailwind.config.ts +3 -0
- package/vite.config.ts +12 -0
- /package/dist/src/react-native/{network-requests-registry.d.ts → http/network-requests-registry.d.ts} +0 -0
- /package/dist/src/react-native/{xhr-interceptor.d.ts → http/xhr-interceptor.d.ts} +0 -0
- /package/dist/src/ui/{utils.d.ts → utils/cn.d.ts} +0 -0
- /package/src/react-native/{network-requests-registry.ts → http/network-requests-registry.ts} +0 -0
- /package/src/react-native/{xhr-interceptor.ts → http/xhr-interceptor.ts} +0 -0
- /package/src/ui/{utils.ts → utils/cn.ts} +0 -0
|
@@ -1,31 +1,10 @@
|
|
|
1
|
-
import { NetworkActivityDevToolsClient } from '
|
|
1
|
+
import { HttpMethod, NetworkActivityDevToolsClient } from '../../shared/client';
|
|
2
|
+
import { getContentType } from '../utils';
|
|
2
3
|
import { getNetworkRequestsRegistry } from './network-requests-registry';
|
|
3
4
|
import { XHRInterceptor } from './xhr-interceptor';
|
|
4
5
|
|
|
5
6
|
const networkRequestsRegistry = getNetworkRequestsRegistry();
|
|
6
7
|
|
|
7
|
-
const getContentType = (request: XMLHttpRequest): string => {
|
|
8
|
-
const responseHeaders = request.responseHeaders;
|
|
9
|
-
const responseType = request.responseType;
|
|
10
|
-
|
|
11
|
-
if (responseHeaders?.['content-type']) {
|
|
12
|
-
return responseHeaders['content-type'].split(';')[0].trim();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
switch (responseType) {
|
|
16
|
-
case 'arraybuffer':
|
|
17
|
-
case 'blob':
|
|
18
|
-
return 'application/octet-stream';
|
|
19
|
-
case 'text':
|
|
20
|
-
case '':
|
|
21
|
-
return 'text/plain';
|
|
22
|
-
case 'json':
|
|
23
|
-
return 'application/json';
|
|
24
|
-
case 'document':
|
|
25
|
-
return 'text/html';
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
|
|
29
8
|
const getResponseSize = (request: XMLHttpRequest): number => {
|
|
30
9
|
if (typeof request.response === 'object') {
|
|
31
10
|
return request.response.size;
|
|
@@ -39,7 +18,8 @@ const getResponseBody = async (
|
|
|
39
18
|
): Promise<string | null> => {
|
|
40
19
|
const responseType = request.responseType;
|
|
41
20
|
|
|
42
|
-
|
|
21
|
+
// Response type is empty in certain cases, like when using axios.
|
|
22
|
+
if (responseType === '' || responseType === 'text') {
|
|
43
23
|
return request.responseText as string;
|
|
44
24
|
}
|
|
45
25
|
|
|
@@ -114,6 +94,8 @@ export const getNetworkInspector = (
|
|
|
114
94
|
const sendTime = Date.now();
|
|
115
95
|
|
|
116
96
|
const requestId = generateRequestId();
|
|
97
|
+
request._rozeniteRequestId = requestId;
|
|
98
|
+
|
|
117
99
|
const initiator = getInitiatorFromStack();
|
|
118
100
|
|
|
119
101
|
networkRequestsRegistry.addEntry(requestId, request);
|
|
@@ -122,10 +104,10 @@ export const getNetworkInspector = (
|
|
|
122
104
|
|
|
123
105
|
pluginClient.send('request-sent', {
|
|
124
106
|
requestId: requestId,
|
|
125
|
-
timestamp: sendTime
|
|
107
|
+
timestamp: sendTime,
|
|
126
108
|
request: {
|
|
127
109
|
url: request._url as string,
|
|
128
|
-
method: request._method as
|
|
110
|
+
method: request._method as HttpMethod,
|
|
129
111
|
headers: request._headers,
|
|
130
112
|
postData: data,
|
|
131
113
|
},
|
|
@@ -142,16 +124,16 @@ export const getNetworkInspector = (
|
|
|
142
124
|
request.addEventListener('load', () => {
|
|
143
125
|
pluginClient.send('response-received', {
|
|
144
126
|
requestId: requestId,
|
|
145
|
-
timestamp: Date.now()
|
|
127
|
+
timestamp: Date.now(),
|
|
146
128
|
type: 'XHR',
|
|
147
129
|
response: {
|
|
148
130
|
url: request._url as string,
|
|
149
131
|
status: request.status,
|
|
150
132
|
statusText: request.statusText,
|
|
151
|
-
headers: request.responseHeaders
|
|
133
|
+
headers: request.responseHeaders || {},
|
|
152
134
|
contentType: getContentType(request),
|
|
153
135
|
size: getResponseSize(request),
|
|
154
|
-
responseTime: Date.now()
|
|
136
|
+
responseTime: Date.now(),
|
|
155
137
|
},
|
|
156
138
|
});
|
|
157
139
|
});
|
|
@@ -159,7 +141,7 @@ export const getNetworkInspector = (
|
|
|
159
141
|
request.addEventListener('loadend', () => {
|
|
160
142
|
pluginClient.send('request-completed', {
|
|
161
143
|
requestId: requestId,
|
|
162
|
-
timestamp: Date.now()
|
|
144
|
+
timestamp: Date.now(),
|
|
163
145
|
duration: Date.now() - sendTime,
|
|
164
146
|
size: getResponseSize(request),
|
|
165
147
|
ttfb,
|
|
@@ -169,7 +151,7 @@ export const getNetworkInspector = (
|
|
|
169
151
|
request.addEventListener('error', () => {
|
|
170
152
|
pluginClient.send('request-failed', {
|
|
171
153
|
requestId: requestId,
|
|
172
|
-
timestamp: Date.now()
|
|
154
|
+
timestamp: Date.now(),
|
|
173
155
|
type: 'XHR',
|
|
174
156
|
error: 'Failed',
|
|
175
157
|
canceled: false,
|
|
@@ -179,7 +161,7 @@ export const getNetworkInspector = (
|
|
|
179
161
|
request.addEventListener('abort', () => {
|
|
180
162
|
pluginClient.send('request-failed', {
|
|
181
163
|
requestId: requestId,
|
|
182
|
-
timestamp: Date.now()
|
|
164
|
+
timestamp: Date.now(),
|
|
183
165
|
type: 'XHR',
|
|
184
166
|
error: 'Aborted',
|
|
185
167
|
canceled: true,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type EventSource from 'react-native-sse';
|
|
2
|
+
|
|
3
|
+
const NOOP = () => {
|
|
4
|
+
// noop
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const MOCK_EVENT_SOURCE = class {
|
|
8
|
+
open = NOOP;
|
|
9
|
+
close = NOOP;
|
|
10
|
+
addEventListener = NOOP;
|
|
11
|
+
removeEventListener = NOOP;
|
|
12
|
+
dispatch = NOOP;
|
|
13
|
+
removeAllEventListeners = NOOP;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const getEventSource = (): typeof EventSource => {
|
|
17
|
+
try {
|
|
18
|
+
const { default: EventSource } = require('react-native-sse');
|
|
19
|
+
return EventSource;
|
|
20
|
+
} catch {
|
|
21
|
+
// This is a workaround for the fact that Vite doesn't support require() calls for in-project dependencies.
|
|
22
|
+
// We are going to return a mock object, so the code will work fine, but it will not be able to intercept SSE requests.
|
|
23
|
+
return MOCK_EVENT_SOURCE;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createNanoEvents } from 'nanoevents';
|
|
2
|
+
import { SSEInterceptor } from './sse-interceptor';
|
|
3
|
+
import { EventSourceWithInternals } from './types';
|
|
4
|
+
import { SSEEvent, SSEEventMap } from '../../shared/sse-events';
|
|
5
|
+
import { getContentType } from '../utils';
|
|
6
|
+
|
|
7
|
+
type NanoEventsMap = {
|
|
8
|
+
[K in keyof SSEEventMap]: (data: SSEEventMap[K]) => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type SSEInspector = {
|
|
12
|
+
enable: () => void;
|
|
13
|
+
disable: () => void;
|
|
14
|
+
isEnabled: () => boolean;
|
|
15
|
+
dispose: () => void;
|
|
16
|
+
on: <TEventType extends keyof SSEEventMap>(
|
|
17
|
+
event: TEventType,
|
|
18
|
+
callback: (data: SSEEventMap[TEventType]) => void
|
|
19
|
+
) => () => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const getSSEInspector = (): SSEInspector => {
|
|
23
|
+
const eventEmitter = createNanoEvents<NanoEventsMap>();
|
|
24
|
+
|
|
25
|
+
const getRequestId = (eventSource: EventSourceWithInternals): string => {
|
|
26
|
+
const requestId = eventSource._xhr?._rozeniteRequestId;
|
|
27
|
+
|
|
28
|
+
if (!requestId) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
'No request ID found for EventSource. This should never happen!'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return requestId;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
enable: () => {
|
|
39
|
+
SSEInterceptor.setOpenEventCallback((_, eventSource) => {
|
|
40
|
+
const sseEventSource = eventSource as EventSourceWithInternals;
|
|
41
|
+
const requestId = getRequestId(sseEventSource);
|
|
42
|
+
const sseXhr = sseEventSource._xhr as XMLHttpRequest;
|
|
43
|
+
|
|
44
|
+
const event: SSEEvent = {
|
|
45
|
+
type: 'sse-open',
|
|
46
|
+
requestId,
|
|
47
|
+
timestamp: Date.now(),
|
|
48
|
+
response: {
|
|
49
|
+
url: sseXhr._url as string,
|
|
50
|
+
status: sseXhr.status,
|
|
51
|
+
statusText: sseXhr.statusText,
|
|
52
|
+
headers: sseXhr.responseHeaders || {},
|
|
53
|
+
contentType: getContentType(sseXhr),
|
|
54
|
+
size: 0,
|
|
55
|
+
responseTime: Date.now(),
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
eventEmitter.emit('sse-open', event);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
SSEInterceptor.setMessageCallback((messageEvent, eventSource) => {
|
|
62
|
+
const sseEventSource = eventSource as EventSourceWithInternals;
|
|
63
|
+
const requestId = getRequestId(sseEventSource);
|
|
64
|
+
|
|
65
|
+
const event: SSEEvent = {
|
|
66
|
+
type: 'sse-message',
|
|
67
|
+
requestId,
|
|
68
|
+
timestamp: Date.now(),
|
|
69
|
+
data: messageEvent.data || '',
|
|
70
|
+
};
|
|
71
|
+
eventEmitter.emit('sse-message', event);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
SSEInterceptor.setErrorCallback((errorEvent, eventSource) => {
|
|
75
|
+
const sseEventSource = eventSource as EventSourceWithInternals;
|
|
76
|
+
const requestId = getRequestId(sseEventSource);
|
|
77
|
+
|
|
78
|
+
const event: SSEEvent = {
|
|
79
|
+
type: 'sse-error',
|
|
80
|
+
requestId,
|
|
81
|
+
timestamp: Date.now(),
|
|
82
|
+
error: {
|
|
83
|
+
type: errorEvent.type,
|
|
84
|
+
message:
|
|
85
|
+
errorEvent.type === 'timeout' ? 'Timeout' : errorEvent.message,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
eventEmitter.emit('sse-error', event);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
SSEInterceptor.setCloseCallback((_, eventSource) => {
|
|
92
|
+
const sseEventSource = eventSource as EventSourceWithInternals;
|
|
93
|
+
const requestId = getRequestId(sseEventSource);
|
|
94
|
+
|
|
95
|
+
const event: SSEEvent = {
|
|
96
|
+
type: 'sse-close',
|
|
97
|
+
requestId,
|
|
98
|
+
timestamp: Date.now(),
|
|
99
|
+
};
|
|
100
|
+
eventEmitter.emit('sse-close', event);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
SSEInterceptor.enableInterception();
|
|
104
|
+
},
|
|
105
|
+
disable: () => {
|
|
106
|
+
SSEInterceptor.disableInterception();
|
|
107
|
+
},
|
|
108
|
+
isEnabled: () => SSEInterceptor.isInterceptorEnabled(),
|
|
109
|
+
dispose: () => {
|
|
110
|
+
eventEmitter.events = {};
|
|
111
|
+
},
|
|
112
|
+
on: <TEventType extends keyof SSEEventMap>(
|
|
113
|
+
event: TEventType,
|
|
114
|
+
callback: (data: SSEEventMap[TEventType]) => void
|
|
115
|
+
) => eventEmitter.on(event, callback as NanoEventsMap[TEventType]),
|
|
116
|
+
};
|
|
117
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type EventSource from 'react-native-sse';
|
|
2
|
+
import type {
|
|
3
|
+
MessageEvent,
|
|
4
|
+
ErrorEvent,
|
|
5
|
+
OpenEvent,
|
|
6
|
+
CloseEvent,
|
|
7
|
+
TimeoutEvent,
|
|
8
|
+
ExceptionEvent,
|
|
9
|
+
} from 'react-native-sse';
|
|
10
|
+
import { EventSourceWithInternals } from './types';
|
|
11
|
+
import { getEventSource } from './event-source';
|
|
12
|
+
|
|
13
|
+
export type SSEInterceptorConnectCallback = (
|
|
14
|
+
url: string,
|
|
15
|
+
request: EventSource
|
|
16
|
+
) => void;
|
|
17
|
+
|
|
18
|
+
export type SSEInterceptorMessageCallback = (
|
|
19
|
+
event: MessageEvent,
|
|
20
|
+
request: EventSource
|
|
21
|
+
) => void;
|
|
22
|
+
|
|
23
|
+
export type SSEInterceptorErrorCallback = (
|
|
24
|
+
error: ErrorEvent | TimeoutEvent | ExceptionEvent,
|
|
25
|
+
request: EventSource
|
|
26
|
+
) => void;
|
|
27
|
+
|
|
28
|
+
export type SSEInterceptorOpenEventCallback = (
|
|
29
|
+
event: OpenEvent,
|
|
30
|
+
request: EventSource
|
|
31
|
+
) => void;
|
|
32
|
+
|
|
33
|
+
export type SSEInterceptorCloseCallback = (
|
|
34
|
+
event: CloseEvent,
|
|
35
|
+
request: EventSource
|
|
36
|
+
) => void;
|
|
37
|
+
|
|
38
|
+
let connectCallback: SSEInterceptorConnectCallback | null;
|
|
39
|
+
let messageCallback: SSEInterceptorMessageCallback | null;
|
|
40
|
+
let errorCallback: SSEInterceptorErrorCallback | null;
|
|
41
|
+
let openEventCallback: SSEInterceptorOpenEventCallback | null;
|
|
42
|
+
let closeCallback: SSEInterceptorCloseCallback | null;
|
|
43
|
+
|
|
44
|
+
let isInterceptorEnabled = false;
|
|
45
|
+
|
|
46
|
+
const eventSourceClass = getEventSource();
|
|
47
|
+
|
|
48
|
+
// Store original EventSource open method
|
|
49
|
+
const originalOpen = eventSourceClass.prototype.open;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A network interceptor which monkey-patches EventSource open method
|
|
53
|
+
* to gather all SSE connections and events, in order to show their
|
|
54
|
+
* information in the Network Activity panel.
|
|
55
|
+
*/
|
|
56
|
+
export const SSEInterceptor = {
|
|
57
|
+
/**
|
|
58
|
+
* Invoked when EventSource.open() is called (connection attempt starting).
|
|
59
|
+
*/
|
|
60
|
+
setConnectCallback(callback: SSEInterceptorConnectCallback) {
|
|
61
|
+
connectCallback = callback;
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Invoked when a message event is received.
|
|
66
|
+
*/
|
|
67
|
+
setMessageCallback(callback: SSEInterceptorMessageCallback) {
|
|
68
|
+
messageCallback = callback;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Invoked when an error event occurs.
|
|
73
|
+
*/
|
|
74
|
+
setErrorCallback(callback: SSEInterceptorErrorCallback) {
|
|
75
|
+
errorCallback = callback;
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Invoked when the connection is successfully opened (open event fired).
|
|
80
|
+
*/
|
|
81
|
+
setOpenEventCallback(callback: SSEInterceptorOpenEventCallback) {
|
|
82
|
+
openEventCallback = callback;
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Invoked when the connection is closed.
|
|
87
|
+
*/
|
|
88
|
+
setCloseCallback(callback: SSEInterceptorCloseCallback) {
|
|
89
|
+
closeCallback = callback;
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
isInterceptorEnabled(): boolean {
|
|
93
|
+
return isInterceptorEnabled;
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
enableInterception() {
|
|
97
|
+
if (isInterceptorEnabled) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Override EventSource open method to intercept SSE connections
|
|
102
|
+
eventSourceClass.prototype.open = function (
|
|
103
|
+
this: EventSourceWithInternals
|
|
104
|
+
) {
|
|
105
|
+
// Invoke connect callback
|
|
106
|
+
if (connectCallback) {
|
|
107
|
+
connectCallback(this.url, this);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Add event listeners to intercept all events
|
|
111
|
+
this.addEventListener('open', (event: OpenEvent) => {
|
|
112
|
+
if (openEventCallback) {
|
|
113
|
+
openEventCallback(event, this);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
this.addEventListener('message', (event: MessageEvent) => {
|
|
118
|
+
if (messageCallback) {
|
|
119
|
+
messageCallback(event, this);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
this.addEventListener(
|
|
124
|
+
'error',
|
|
125
|
+
(event: ErrorEvent | TimeoutEvent | ExceptionEvent) => {
|
|
126
|
+
if (errorCallback) {
|
|
127
|
+
errorCallback(event, this);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
this.addEventListener('close', (event: CloseEvent) => {
|
|
133
|
+
if (closeCallback) {
|
|
134
|
+
closeCallback(event, this);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Call original open method
|
|
139
|
+
return originalOpen.call(this);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
isInterceptorEnabled = true;
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
// Unpatch EventSource open method and remove the callbacks.
|
|
146
|
+
disableInterception() {
|
|
147
|
+
if (!isInterceptorEnabled) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
isInterceptorEnabled = false;
|
|
151
|
+
|
|
152
|
+
// Restore original open method
|
|
153
|
+
eventSourceClass.prototype.open = originalOpen;
|
|
154
|
+
|
|
155
|
+
// Clear callbacks
|
|
156
|
+
connectCallback = null;
|
|
157
|
+
messageCallback = null;
|
|
158
|
+
errorCallback = null;
|
|
159
|
+
openEventCallback = null;
|
|
160
|
+
closeCallback = null;
|
|
161
|
+
},
|
|
162
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type EventSource from 'react-native-sse';
|
|
2
|
+
|
|
3
|
+
export interface EventSourceWithInternals<E extends string = never>
|
|
4
|
+
extends EventSource<E> {
|
|
5
|
+
url: string;
|
|
6
|
+
|
|
7
|
+
/** Used internally to mark the underlying XHR to skip it in XHR interceptor. */
|
|
8
|
+
_xhr?: XMLHttpRequest;
|
|
9
|
+
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
2
|
import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge';
|
|
3
|
-
import { getNetworkInspector } from './network-inspector';
|
|
3
|
+
import { getNetworkInspector } from './http/network-inspector';
|
|
4
4
|
import { NetworkActivityEventMap } from '../shared/client';
|
|
5
|
+
import { getWebSocketInspector } from './websocket/websocket-inspector';
|
|
6
|
+
import { WebSocketEventMap } from '../shared/websocket-events';
|
|
7
|
+
import { UnionToTuple } from './utils';
|
|
8
|
+
import { getSSEInspector } from './sse/sse-inspector';
|
|
9
|
+
import { SSEEventMap } from '../shared/sse-events';
|
|
5
10
|
|
|
6
11
|
export const useNetworkActivityDevTools = () => {
|
|
7
12
|
const client = useRozeniteDevToolsClient<NetworkActivityEventMap>({
|
|
@@ -20,5 +25,74 @@ export const useNetworkActivityDevTools = () => {
|
|
|
20
25
|
};
|
|
21
26
|
}, [client]);
|
|
22
27
|
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!client) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const eventsToForward: UnionToTuple<keyof WebSocketEventMap> = [
|
|
34
|
+
'websocket-connect',
|
|
35
|
+
'websocket-open',
|
|
36
|
+
'websocket-close',
|
|
37
|
+
'websocket-message-sent',
|
|
38
|
+
'websocket-message-received',
|
|
39
|
+
'websocket-error',
|
|
40
|
+
'websocket-connection-status-changed',
|
|
41
|
+
];
|
|
42
|
+
const websocketInspector = getWebSocketInspector();
|
|
43
|
+
|
|
44
|
+
eventsToForward.forEach((event) => {
|
|
45
|
+
websocketInspector.on(event, (event) => {
|
|
46
|
+
client.send(event.type, event);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
client.onMessage('network-enable', () => {
|
|
51
|
+
websocketInspector.enable();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
client.onMessage('network-disable', () => {
|
|
55
|
+
websocketInspector.disable();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return () => {
|
|
59
|
+
// Subscriptions will be disposed by the inspector
|
|
60
|
+
websocketInspector.dispose();
|
|
61
|
+
};
|
|
62
|
+
}, [client]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!client) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const eventsToForward: UnionToTuple<keyof SSEEventMap> = [
|
|
70
|
+
'sse-open',
|
|
71
|
+
'sse-message',
|
|
72
|
+
'sse-error',
|
|
73
|
+
'sse-close',
|
|
74
|
+
];
|
|
75
|
+
const sseInspector = getSSEInspector();
|
|
76
|
+
|
|
77
|
+
eventsToForward.forEach((event) => {
|
|
78
|
+
sseInspector.on(event, (event) => {
|
|
79
|
+
client.send(event.type, event);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
client.onMessage('network-enable', () => {
|
|
84
|
+
sseInspector.enable();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
client.onMessage('network-disable', () => {
|
|
88
|
+
sseInspector.disable();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return () => {
|
|
92
|
+
// Subscriptions will be disposed by the inspector
|
|
93
|
+
sseInspector.dispose();
|
|
94
|
+
};
|
|
95
|
+
}, [client]);
|
|
96
|
+
|
|
23
97
|
return client;
|
|
24
98
|
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getHttpHeaderValue } from '../ui/utils/getHttpHeaderValue';
|
|
2
|
+
|
|
3
|
+
type UnionToIntersection<U> = (
|
|
4
|
+
U extends unknown ? (k: U) => void : never
|
|
5
|
+
) extends (k: infer I) => void
|
|
6
|
+
? I
|
|
7
|
+
: never;
|
|
8
|
+
|
|
9
|
+
type LastOf<T> = UnionToIntersection<
|
|
10
|
+
T extends unknown ? () => T : never
|
|
11
|
+
> extends () => infer R
|
|
12
|
+
? R
|
|
13
|
+
: never;
|
|
14
|
+
|
|
15
|
+
type Push<T extends unknown[], V> = [...T, V];
|
|
16
|
+
|
|
17
|
+
export type UnionToTuple<T, L = LastOf<T>> = [T] extends [never]
|
|
18
|
+
? []
|
|
19
|
+
: Push<UnionToTuple<Exclude<T, L>>, L>;
|
|
20
|
+
|
|
21
|
+
export const getContentType = (request: XMLHttpRequest): string => {
|
|
22
|
+
const responseHeaders = request.responseHeaders;
|
|
23
|
+
const responseType = request.responseType;
|
|
24
|
+
|
|
25
|
+
const contentType = getHttpHeaderValue(responseHeaders || {}, 'content-type');
|
|
26
|
+
|
|
27
|
+
if (contentType) {
|
|
28
|
+
return contentType.split(';')[0].trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
switch (responseType) {
|
|
32
|
+
case 'arraybuffer':
|
|
33
|
+
case 'blob':
|
|
34
|
+
return 'application/octet-stream';
|
|
35
|
+
case 'text':
|
|
36
|
+
case '':
|
|
37
|
+
return 'text/plain';
|
|
38
|
+
case 'json':
|
|
39
|
+
return 'application/json';
|
|
40
|
+
case 'document':
|
|
41
|
+
return 'text/html';
|
|
42
|
+
}
|
|
43
|
+
};
|