@rozenite/network-activity-plugin 1.0.0 → 1.2.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/README.md +9 -0
- package/dist/App.html +1 -1
- package/dist/assets/{App-C6wCDVkW.js → App-o_iVtD-5.js} +50 -7
- package/dist/boot-recording.cjs +1092 -0
- package/dist/boot-recording.js +1091 -0
- package/dist/react-native.cjs +3 -0
- package/dist/react-native.d.ts +3 -0
- package/dist/react-native.js +5 -1
- package/dist/rozenite.json +1 -1
- package/dist/src/react-native/boot-recording.d.ts +41 -0
- package/dist/src/react-native/config.d.ts +7 -4
- package/dist/src/react-native/events-listener.d.ts +44 -0
- package/dist/src/react-native/http/http-inspector.d.ts +10 -0
- package/dist/src/react-native/http/http-utils.d.ts +15 -0
- package/dist/src/react-native/inspector.d.ts +7 -0
- package/dist/src/react-native/network-inspector.d.ts +16 -0
- package/dist/src/react-native/sse/sse-inspector.d.ts +4 -7
- package/dist/src/react-native/useHttpInspector.d.ts +3 -0
- package/dist/src/react-native/useSSEInspector.d.ts +3 -0
- package/dist/src/react-native/useWebSocketInspector.d.ts +3 -0
- package/dist/src/react-native/websocket/websocket-inspector.d.ts +4 -7
- package/dist/src/shared/client.d.ts +3 -98
- package/dist/src/shared/http-events.d.ts +106 -0
- package/dist/src/shared/sse-events.d.ts +1 -1
- package/dist/src/ui/state/hooks.d.ts +3 -3
- package/dist/src/ui/state/model.d.ts +10 -0
- package/dist/useNetworkActivityDevTools.cjs +112 -993
- package/dist/useNetworkActivityDevTools.js +110 -989
- package/package.json +4 -4
- package/react-native.ts +8 -0
- package/src/react-native/boot-recording.ts +90 -0
- package/src/react-native/config.ts +9 -4
- package/src/react-native/events-listener.ts +102 -0
- package/src/react-native/http/http-inspector.ts +174 -0
- package/src/react-native/http/http-utils.ts +217 -0
- package/src/react-native/inspector.ts +10 -0
- package/src/react-native/network-inspector.ts +78 -0
- package/src/react-native/sse/sse-inspector.ts +12 -10
- package/src/react-native/useHttpInspector.ts +59 -0
- package/src/react-native/useNetworkActivityDevTools.ts +60 -115
- package/src/react-native/useSSEInspector.ts +35 -0
- package/src/react-native/useWebSocketInspector.ts +35 -0
- package/src/react-native/websocket/websocket-inspector.ts +18 -10
- package/src/shared/client.ts +4 -132
- package/src/shared/http-events.ts +140 -0
- package/src/shared/sse-events.ts +1 -1
- package/src/ui/components/RequestList.tsx +18 -6
- package/src/ui/components/Toolbar.tsx +3 -2
- package/src/ui/state/derived.ts +9 -3
- package/src/ui/state/model.ts +10 -0
- package/src/ui/state/store.ts +34 -3
- package/dist/src/react-native/http/network-inspector.d.ts +0 -8
- package/src/react-native/http/network-inspector.ts +0 -388
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rozenite/network-activity-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Network Activity for Rozenite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/react-native.cjs",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"types": "./dist/react-native.d.ts",
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"nanoevents": "^9.1.0",
|
|
11
|
-
"@rozenite/plugin-bridge": "1.
|
|
11
|
+
"@rozenite/plugin-bridge": "1.2.0"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"@floating-ui/react": "^0.26.0",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"zustand": "^5.0.6",
|
|
37
37
|
"@types/react": "~18.3.23",
|
|
38
38
|
"@types/react-dom": "~18.3.1",
|
|
39
|
-
"
|
|
40
|
-
"rozenite": "1.
|
|
39
|
+
"rozenite": "1.2.0",
|
|
40
|
+
"@rozenite/vite-plugin": "1.2.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"react-native-sse": "*"
|
package/react-native.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export let useNetworkActivityDevTools: typeof import('./src/react-native/useNetworkActivityDevTools').useNetworkActivityDevTools;
|
|
2
|
+
export let withOnBootNetworkActivityRecording: typeof import('./src/react-native/boot-recording').withOnBootNetworkActivityRecording;
|
|
3
|
+
|
|
4
|
+
// Export types for user-facing configuration
|
|
5
|
+
export type { NetworkActivityDevToolsConfig } from './src/react-native/config';
|
|
6
|
+
export type { BootRecordingOptions } from './src/react-native/boot-recording';
|
|
2
7
|
|
|
3
8
|
const isWeb =
|
|
4
9
|
typeof window !== 'undefined' && window.navigator.product !== 'ReactNative';
|
|
@@ -8,6 +13,9 @@ const isServer = typeof window === 'undefined';
|
|
|
8
13
|
if (isDev && !isWeb && !isServer) {
|
|
9
14
|
useNetworkActivityDevTools =
|
|
10
15
|
require('./src/react-native/useNetworkActivityDevTools').useNetworkActivityDevTools;
|
|
16
|
+
withOnBootNetworkActivityRecording =
|
|
17
|
+
require('./src/react-native/boot-recording').withOnBootNetworkActivityRecording;
|
|
11
18
|
} else {
|
|
12
19
|
useNetworkActivityDevTools = () => null;
|
|
20
|
+
withOnBootNetworkActivityRecording = (options: any) => null;
|
|
13
21
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { NetworkActivityEventMap } from '../shared/client';
|
|
2
|
+
import { createEventsListener, EventsListenerOptions } from './events-listener';
|
|
3
|
+
import { getNetworkInspector, NetworkInspector } from './network-inspector';
|
|
4
|
+
import { validateConfig, type NetworkInspectorConfig } from './config';
|
|
5
|
+
|
|
6
|
+
type InspectorsConfiguration = {
|
|
7
|
+
eventsListener: ReturnType<
|
|
8
|
+
typeof createEventsListener<NetworkActivityEventMap>
|
|
9
|
+
>;
|
|
10
|
+
networkInspector: NetworkInspector;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let inspectorsConfig: InspectorsConfiguration;
|
|
14
|
+
|
|
15
|
+
let bootRecordingEnabled = false;
|
|
16
|
+
|
|
17
|
+
export type BootRecordingOptions = NetworkInspectorConfig &
|
|
18
|
+
EventsListenerOptions & {
|
|
19
|
+
/**
|
|
20
|
+
* Enable queuing of events during boot before DevTools connects.
|
|
21
|
+
* When true, network activity is captured and queued until DevTools is ready.
|
|
22
|
+
* When false, nothing is queued and inspectors are not even set up.
|
|
23
|
+
* @default true
|
|
24
|
+
*/
|
|
25
|
+
enableBootRecording?: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
*
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
export const createNetworkInspectorsConfiguration = (
|
|
33
|
+
options?: BootRecordingOptions,
|
|
34
|
+
) => {
|
|
35
|
+
if (inspectorsConfig) {
|
|
36
|
+
return inspectorsConfig;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
bootRecordingEnabled = options?.enableBootRecording ?? true;
|
|
40
|
+
const maxQueueSize = options?.maxQueueSize ?? 200;
|
|
41
|
+
const inspectors = {
|
|
42
|
+
http: true,
|
|
43
|
+
websocket: true,
|
|
44
|
+
sse: true,
|
|
45
|
+
...options?.inspectors,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
validateConfig({ inspectors });
|
|
49
|
+
|
|
50
|
+
const eventsListener = createEventsListener<NetworkActivityEventMap>();
|
|
51
|
+
eventsListener.setMaxQueueSize(maxQueueSize);
|
|
52
|
+
|
|
53
|
+
const networkInspector = getNetworkInspector();
|
|
54
|
+
networkInspector.setup(eventsListener);
|
|
55
|
+
|
|
56
|
+
if (bootRecordingEnabled) {
|
|
57
|
+
eventsListener.enableQueuing();
|
|
58
|
+
networkInspector.enable(inspectors);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
inspectorsConfig = {
|
|
62
|
+
eventsListener,
|
|
63
|
+
networkInspector,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return inspectorsConfig;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Enable network activity recording during app boot, before DevTools connects.
|
|
71
|
+
* Call this at the root of your app to capture early network requests.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```tsx
|
|
75
|
+
* import { withOnBootNetworkActivityRecording } from '@rozenite/network-activity-plugin';
|
|
76
|
+
*
|
|
77
|
+
* // At app entry point, before any network requests
|
|
78
|
+
* withOnBootNetworkActivityRecording();
|
|
79
|
+
*
|
|
80
|
+
* function App() {
|
|
81
|
+
* useNetworkActivityDevTools();
|
|
82
|
+
* // ...
|
|
83
|
+
* }
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export const withOnBootNetworkActivityRecording = (
|
|
87
|
+
options?: BootRecordingOptions,
|
|
88
|
+
) => {
|
|
89
|
+
createNetworkInspectorsConfiguration(options);
|
|
90
|
+
};
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
export type InspectorType = 'http' | 'websocket' | 'sse';
|
|
2
2
|
|
|
3
|
-
export type
|
|
3
|
+
export type InspectorsConfig = {
|
|
4
|
+
[key in InspectorType]?: boolean;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type NetworkInspectorConfig = {
|
|
4
8
|
/**
|
|
5
9
|
* Specifies which network inspectors are enabled.
|
|
6
10
|
* Set to `false` to disable monitoring for a specific type of network traffic.
|
|
7
11
|
* @default { http: true, websocket: true, sse: true }
|
|
8
12
|
*/
|
|
9
|
-
inspectors?:
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
inspectors?: InspectorsConfig;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type NetworkActivityDevToolsConfig = NetworkInspectorConfig & {
|
|
12
17
|
clientUISettings?: {
|
|
13
18
|
/**
|
|
14
19
|
* If true, display the entire relative URL as the request name in the UI instead of only the last path segment.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
type QueuedMessage<TEventMap extends Record<string, unknown>> = {
|
|
2
|
+
type: keyof TEventMap;
|
|
3
|
+
data: TEventMap[keyof TEventMap];
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
type SendFunction<TEventMap extends Record<string, unknown>> = <
|
|
7
|
+
K extends keyof TEventMap,
|
|
8
|
+
>(
|
|
9
|
+
type: K,
|
|
10
|
+
data: TEventMap[K],
|
|
11
|
+
) => void;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generic events listener that queues messages until a send function is registered.
|
|
15
|
+
* This allows capturing events before the DevTools client connects and so be boot compliant.
|
|
16
|
+
*/
|
|
17
|
+
export class EventsListener<TEventMap extends Record<string, unknown>> {
|
|
18
|
+
private messageQueue: QueuedMessage<TEventMap>[] = [];
|
|
19
|
+
private sendFunction: SendFunction<TEventMap> | null = null;
|
|
20
|
+
private maxQueueSize = 200;
|
|
21
|
+
private isQueuing = false;
|
|
22
|
+
|
|
23
|
+
public setMaxQueueSize(size: number): void {
|
|
24
|
+
this.maxQueueSize = size;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Enable queuing mode to capture events before client is connected
|
|
29
|
+
*/
|
|
30
|
+
public enableQueuing(): void {
|
|
31
|
+
this.isQueuing = true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Connect the actual send function and automatically flush queued messages
|
|
36
|
+
*/
|
|
37
|
+
public connect(
|
|
38
|
+
sendFn: SendFunction<TEventMap>,
|
|
39
|
+
filterFn?: (message: QueuedMessage<TEventMap>) => boolean
|
|
40
|
+
): void {
|
|
41
|
+
this.sendFunction = sendFn;
|
|
42
|
+
this.isQueuing = false;
|
|
43
|
+
this.flushQueue(filterFn);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public isInQueueMode(): boolean {
|
|
47
|
+
return this.isQueuing;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Send a message (queued if not connected, sent directly if connected)
|
|
52
|
+
*/
|
|
53
|
+
public send<K extends keyof TEventMap>(type: K, data: TEventMap[K]): void {
|
|
54
|
+
if (this.isQueuing) {
|
|
55
|
+
this.enqueueMessage({ type, data });
|
|
56
|
+
} else if (this.sendFunction) {
|
|
57
|
+
this.sendFunction(type, data);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private enqueueMessage(message: QueuedMessage<TEventMap>): void {
|
|
62
|
+
if (this.messageQueue.length >= this.maxQueueSize) {
|
|
63
|
+
this.messageQueue.shift();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.messageQueue.push(message);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private flushQueue(
|
|
70
|
+
filterFn?: (message: QueuedMessage<TEventMap>) => boolean
|
|
71
|
+
): void {
|
|
72
|
+
if (!this.sendFunction) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
while (this.messageQueue.length > 0) {
|
|
77
|
+
const message = this.messageQueue.shift();
|
|
78
|
+
if (message) {
|
|
79
|
+
if (filterFn && !filterFn(message)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
this.sendFunction(message.type, message.data);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type EventsListenerOptions = {
|
|
89
|
+
/**
|
|
90
|
+
* Maximum number of messages to queue before DevTools connects.
|
|
91
|
+
* @default 200
|
|
92
|
+
*/
|
|
93
|
+
maxQueueSize?: number;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create a new events listener instance for a specific event map.
|
|
98
|
+
* This factory can be used to create listeners for different protocols or plugins.
|
|
99
|
+
*/
|
|
100
|
+
export const createEventsListener = <TEventMap extends Record<string, unknown>>(): EventsListener<TEventMap> => {
|
|
101
|
+
return new EventsListener<TEventMap>();
|
|
102
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { createNanoEvents } from 'nanoevents';
|
|
2
|
+
import { getNetworkRequestsRegistry } from './network-requests-registry';
|
|
3
|
+
import { XHRInterceptor } from './xhr-interceptor';
|
|
4
|
+
import { getRequestBody, getResponseSize, getInitiatorFromStack, setupRequestOverride } from './http-utils';
|
|
5
|
+
import { applyReactNativeResponseHeadersLogic } from '../../utils/applyReactNativeResponseHeadersLogic';
|
|
6
|
+
import { getContentType } from '../utils';
|
|
7
|
+
import { getOverridesRegistry } from './overrides-registry';
|
|
8
|
+
import type { HttpEventMap, HttpMethod } from '../../shared/http-events';
|
|
9
|
+
import type { Inspector } from '../inspector';
|
|
10
|
+
|
|
11
|
+
// HTTP-specific event map for the inspector
|
|
12
|
+
export type { HttpEventMap };
|
|
13
|
+
|
|
14
|
+
export const HTTP_EVENTS: (keyof HttpEventMap)[] = [
|
|
15
|
+
'request-sent',
|
|
16
|
+
'response-received',
|
|
17
|
+
'request-completed',
|
|
18
|
+
'request-failed',
|
|
19
|
+
'request-progress',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export const isHttpEvent = (type: string): type is keyof HttpEventMap => {
|
|
23
|
+
return (HTTP_EVENTS as readonly string[]).includes(type);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type NanoEventsMap = {
|
|
27
|
+
[K in keyof HttpEventMap]: (data: HttpEventMap[K]) => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type HTTPInspector = Inspector<HttpEventMap> & {
|
|
31
|
+
getNetworkRequestsRegistry: () => ReturnType<typeof getNetworkRequestsRegistry>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const READY_STATE_HEADERS_RECEIVED = 2;
|
|
35
|
+
|
|
36
|
+
export const getHTTPInspector = (): HTTPInspector => {
|
|
37
|
+
const eventEmitter = createNanoEvents<NanoEventsMap>();
|
|
38
|
+
const networkRequestsRegistry = getNetworkRequestsRegistry();
|
|
39
|
+
|
|
40
|
+
const overridesRegistry = getOverridesRegistry();
|
|
41
|
+
XHRInterceptor.setOverrideCallback((request) =>
|
|
42
|
+
setupRequestOverride(overridesRegistry, request)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
enable: () => {
|
|
47
|
+
if (XHRInterceptor.isInterceptorEnabled()) return;
|
|
48
|
+
|
|
49
|
+
XHRInterceptor.disableInterception();
|
|
50
|
+
|
|
51
|
+
XHRInterceptor.setSendCallback((data, request) => {
|
|
52
|
+
const initiator = getInitiatorFromStack();
|
|
53
|
+
const sendTime = Date.now();
|
|
54
|
+
const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
55
|
+
|
|
56
|
+
request._rozeniteRequestId = requestId;
|
|
57
|
+
networkRequestsRegistry.addEntry(requestId, request);
|
|
58
|
+
|
|
59
|
+
let ttfb = 0;
|
|
60
|
+
|
|
61
|
+
eventEmitter.emit('request-sent', {
|
|
62
|
+
requestId: requestId,
|
|
63
|
+
timestamp: sendTime,
|
|
64
|
+
request: {
|
|
65
|
+
url: request._url as string,
|
|
66
|
+
method: request._method as HttpMethod,
|
|
67
|
+
headers: request._headers,
|
|
68
|
+
postData: getRequestBody(data),
|
|
69
|
+
},
|
|
70
|
+
type: 'XHR',
|
|
71
|
+
initiator,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
request.addEventListener('readystatechange', () => {
|
|
75
|
+
if (request.readyState === READY_STATE_HEADERS_RECEIVED) {
|
|
76
|
+
ttfb = Date.now() - sendTime;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
request.addEventListener('progress', (event) => {
|
|
81
|
+
eventEmitter.emit('request-progress', {
|
|
82
|
+
requestId: requestId,
|
|
83
|
+
timestamp: Date.now(),
|
|
84
|
+
loaded: event.loaded,
|
|
85
|
+
total: event.total,
|
|
86
|
+
lengthComputable: event.lengthComputable,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
request.addEventListener('load', () => {
|
|
91
|
+
eventEmitter.emit('response-received', {
|
|
92
|
+
requestId: requestId,
|
|
93
|
+
timestamp: Date.now(),
|
|
94
|
+
type: 'XHR',
|
|
95
|
+
response: {
|
|
96
|
+
url: request._url as string,
|
|
97
|
+
status: request.status,
|
|
98
|
+
statusText: request.statusText,
|
|
99
|
+
headers: applyReactNativeResponseHeadersLogic(
|
|
100
|
+
request.responseHeaders || {},
|
|
101
|
+
),
|
|
102
|
+
contentType: getContentType(request),
|
|
103
|
+
size: getResponseSize(request),
|
|
104
|
+
responseTime: Date.now(),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
request.addEventListener('loadend', () => {
|
|
110
|
+
eventEmitter.emit('request-completed', {
|
|
111
|
+
requestId: requestId,
|
|
112
|
+
timestamp: Date.now(),
|
|
113
|
+
duration: Date.now() - sendTime,
|
|
114
|
+
size: getResponseSize(request),
|
|
115
|
+
ttfb,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
request.addEventListener('error', () => {
|
|
120
|
+
eventEmitter.emit('request-failed', {
|
|
121
|
+
requestId: requestId,
|
|
122
|
+
timestamp: Date.now(),
|
|
123
|
+
type: 'XHR',
|
|
124
|
+
error: 'Failed',
|
|
125
|
+
canceled: false,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
request.addEventListener('abort', () => {
|
|
130
|
+
eventEmitter.emit('request-failed', {
|
|
131
|
+
requestId: requestId,
|
|
132
|
+
timestamp: Date.now(),
|
|
133
|
+
type: 'XHR',
|
|
134
|
+
error: 'Aborted',
|
|
135
|
+
canceled: true,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
request.addEventListener('timeout', () => {
|
|
140
|
+
eventEmitter.emit('request-failed', {
|
|
141
|
+
requestId: requestId,
|
|
142
|
+
timestamp: Date.now(),
|
|
143
|
+
type: 'XHR',
|
|
144
|
+
error: 'Timeout',
|
|
145
|
+
canceled: false,
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
XHRInterceptor.enableInterception();
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
disable: () => {
|
|
154
|
+
XHRInterceptor.disableInterception();
|
|
155
|
+
networkRequestsRegistry.clear();
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
isEnabled: () => {
|
|
159
|
+
return XHRInterceptor.isInterceptorEnabled();
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
dispose: () => {
|
|
163
|
+
XHRInterceptor.disableInterception();
|
|
164
|
+
networkRequestsRegistry.clear();
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
getNetworkRequestsRegistry: () => networkRequestsRegistry,
|
|
168
|
+
|
|
169
|
+
on: <TEventType extends keyof HttpEventMap>(
|
|
170
|
+
event: TEventType,
|
|
171
|
+
callback: (data: HttpEventMap[TEventType]) => void
|
|
172
|
+
) => eventEmitter.on(event, callback as NanoEventsMap[TEventType]),
|
|
173
|
+
};
|
|
174
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import {
|
|
2
|
+
XHRPostData,
|
|
3
|
+
RequestPostData,
|
|
4
|
+
RequestTextPostData,
|
|
5
|
+
RequestBinaryPostData,
|
|
6
|
+
RequestFormDataPostData,
|
|
7
|
+
} from '../../shared/client';
|
|
8
|
+
import { safeStringify } from '../../utils/safeStringify';
|
|
9
|
+
import { getStringSizeInBytes } from '../../utils/getStringSizeInBytes';
|
|
10
|
+
import {
|
|
11
|
+
isBlob,
|
|
12
|
+
isArrayBuffer,
|
|
13
|
+
isFormData,
|
|
14
|
+
isNullOrUndefined,
|
|
15
|
+
} from '../../utils/typeChecks';
|
|
16
|
+
import { getContentType } from '../utils';
|
|
17
|
+
import { getBlobName } from '../utils/getBlobName';
|
|
18
|
+
import { getFormDataEntries } from '../utils/getFormDataEntries';
|
|
19
|
+
import type { OverridesRegistry } from './overrides-registry';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Utility functions for tracking HTTP requests
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const getBinaryPostData = (body: Blob): RequestBinaryPostData => ({
|
|
26
|
+
type: 'binary',
|
|
27
|
+
value: {
|
|
28
|
+
size: body.size,
|
|
29
|
+
type: body.type,
|
|
30
|
+
name: getBlobName(body),
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const getArrayBufferPostData = (
|
|
35
|
+
body: ArrayBuffer | ArrayBufferView,
|
|
36
|
+
): RequestBinaryPostData => ({
|
|
37
|
+
type: 'binary',
|
|
38
|
+
value: {
|
|
39
|
+
size: body.byteLength,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const getTextPostData = (body: unknown): RequestTextPostData => ({
|
|
44
|
+
type: 'text',
|
|
45
|
+
value: safeStringify(body),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const getFormDataPostData = (body: FormData): RequestFormDataPostData => ({
|
|
49
|
+
type: 'form-data',
|
|
50
|
+
value: getFormDataEntries(body).reduce<RequestFormDataPostData['value']>(
|
|
51
|
+
(acc, [key, value]) => {
|
|
52
|
+
if (isBlob(value)) {
|
|
53
|
+
acc[key] = getBinaryPostData(value);
|
|
54
|
+
} else if (isArrayBuffer(value)) {
|
|
55
|
+
acc[key] = getArrayBufferPostData(value);
|
|
56
|
+
} else {
|
|
57
|
+
acc[key] = getTextPostData(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return acc;
|
|
61
|
+
},
|
|
62
|
+
{},
|
|
63
|
+
),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export const getRequestBody = (body: XHRPostData): RequestPostData => {
|
|
67
|
+
if (isNullOrUndefined(body)) {
|
|
68
|
+
return body;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (isBlob(body)) {
|
|
72
|
+
return getBinaryPostData(body);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (isArrayBuffer(body)) {
|
|
76
|
+
return getArrayBufferPostData(body);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isFormData(body)) {
|
|
80
|
+
return getFormDataPostData(body);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return getTextPostData(body);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const getResponseSize = (request: XMLHttpRequest): number | null => {
|
|
87
|
+
try {
|
|
88
|
+
const { responseType, response } = request;
|
|
89
|
+
|
|
90
|
+
// Handle a case of 204 where no-content was sent.
|
|
91
|
+
if (response === null) {
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (responseType === '' || responseType === 'text') {
|
|
96
|
+
return getStringSizeInBytes(request.responseText);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (responseType === 'json') {
|
|
100
|
+
return getStringSizeInBytes(safeStringify(response));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (responseType === 'blob') {
|
|
104
|
+
return response.size;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (responseType === 'arraybuffer') {
|
|
108
|
+
return response.byteLength;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return 0;
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const getResponseBody = async (
|
|
118
|
+
request: XMLHttpRequest,
|
|
119
|
+
): Promise<string | null> => {
|
|
120
|
+
const responseType = request.responseType;
|
|
121
|
+
|
|
122
|
+
// Response type is empty in certain cases, like when using axios.
|
|
123
|
+
if (responseType === '' || responseType === 'text') {
|
|
124
|
+
return request.responseText as string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (responseType === 'blob') {
|
|
128
|
+
// This may be a text blob.
|
|
129
|
+
const contentType = request.getResponseHeader('Content-Type') || '';
|
|
130
|
+
|
|
131
|
+
if (
|
|
132
|
+
contentType.startsWith('text/') ||
|
|
133
|
+
contentType.startsWith('application/json')
|
|
134
|
+
) {
|
|
135
|
+
// It looks like a text blob, let's read it and forward it to the client.
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
const reader = new FileReader();
|
|
138
|
+
reader.onload = () => {
|
|
139
|
+
resolve(reader.result as string);
|
|
140
|
+
};
|
|
141
|
+
reader.readAsText(request.response);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (responseType === 'json') {
|
|
147
|
+
return safeStringify(request.response);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export const getInitiatorFromStack = (): {
|
|
154
|
+
type: string;
|
|
155
|
+
url?: string;
|
|
156
|
+
lineNumber?: number;
|
|
157
|
+
columnNumber?: number;
|
|
158
|
+
} => {
|
|
159
|
+
try {
|
|
160
|
+
const stack = new Error().stack;
|
|
161
|
+
if (!stack) {
|
|
162
|
+
return { type: 'other' };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const line = stack.split('\n')[9];
|
|
166
|
+
const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/);
|
|
167
|
+
if (match) {
|
|
168
|
+
return {
|
|
169
|
+
type: 'script',
|
|
170
|
+
url: match[2],
|
|
171
|
+
lineNumber: parseInt(match[3]),
|
|
172
|
+
columnNumber: parseInt(match[4]),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
// Ignore stack parsing errors
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { type: 'other' };
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Applies override body and status to XMLHttpRequest objects.
|
|
184
|
+
*/
|
|
185
|
+
export const setupRequestOverride = (
|
|
186
|
+
overridesRegistry: OverridesRegistry,
|
|
187
|
+
request: XMLHttpRequest,
|
|
188
|
+
): void => {
|
|
189
|
+
const override = overridesRegistry.getOverrideForUrl(request._url as string);
|
|
190
|
+
if (!override) return;
|
|
191
|
+
|
|
192
|
+
request.addEventListener('readystatechange', () => {
|
|
193
|
+
if (override.body !== undefined) {
|
|
194
|
+
Object.defineProperty(request, 'responseType', { writable: true });
|
|
195
|
+
Object.defineProperty(request, 'response', { writable: true });
|
|
196
|
+
Object.defineProperty(request, 'responseText', { writable: true });
|
|
197
|
+
|
|
198
|
+
const contentType = getContentType(request);
|
|
199
|
+
if (contentType === 'application/json') {
|
|
200
|
+
request.responseType = 'json';
|
|
201
|
+
} else if (contentType === 'text/plain') {
|
|
202
|
+
request.responseType = 'text';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// @ts-expect-error - Mocking response
|
|
206
|
+
request.response = override.body;
|
|
207
|
+
// @ts-expect-error - Mocking responseText
|
|
208
|
+
request.responseText = override.body;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (override.status !== undefined) {
|
|
212
|
+
Object.defineProperty(request, 'status', { writable: true });
|
|
213
|
+
// @ts-expect-error - Mocking status
|
|
214
|
+
request.status = override.status;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface Inspector<TEventMap> {
|
|
2
|
+
enable: () => void;
|
|
3
|
+
disable: () => void;
|
|
4
|
+
isEnabled: () => boolean;
|
|
5
|
+
dispose: () => void;
|
|
6
|
+
on<TEventType extends keyof TEventMap>(
|
|
7
|
+
type: TEventType,
|
|
8
|
+
callback: (event: TEventMap[TEventType]) => void
|
|
9
|
+
): () => void;
|
|
10
|
+
}
|