@rozenite/network-activity-plugin 1.0.0-alpha.9 → 1.0.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 (111) hide show
  1. package/README.md +2 -0
  2. package/dist/App.html +2 -2
  3. package/dist/assets/{App-DoHQsY5s.css → App-BrSkOkws.css} +223 -2
  4. package/dist/assets/{App-CA1Fbh0I.js → App-C6wCDVkW.js} +8157 -2677
  5. package/dist/react-native.cjs +4 -1
  6. package/dist/react-native.js +4 -1
  7. package/dist/rozenite.json +1 -1
  8. package/dist/src/react-native/config.d.ts +20 -0
  9. package/dist/src/react-native/http/overrides-registry.d.ts +6 -0
  10. package/dist/src/react-native/http/xhr-interceptor.d.ts +7 -1
  11. package/dist/src/react-native/sse/sse-interceptor.d.ts +2 -2
  12. package/dist/src/react-native/useNetworkActivityDevTools.d.ts +2 -1
  13. package/dist/src/react-native/utils/getBlobName.d.ts +35 -0
  14. package/dist/src/react-native/utils/getFormDataEntries.d.ts +18 -0
  15. package/dist/src/shared/client.d.ts +48 -4
  16. package/dist/src/shared/sse-events.d.ts +4 -1
  17. package/dist/src/ui/components/Button.d.ts +2 -2
  18. package/dist/src/ui/components/CodeBlock.d.ts +3 -0
  19. package/dist/src/ui/components/CodeEditor.d.ts +5 -0
  20. package/dist/src/ui/components/CookieCard.d.ts +7 -0
  21. package/dist/src/ui/components/CopyRequestDropdown.d.ts +7 -0
  22. package/dist/src/ui/components/DropdownMenu.d.ts +27 -0
  23. package/dist/src/ui/components/FilterBar.d.ts +10 -0
  24. package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +1 -1
  25. package/dist/src/ui/components/KeyValueGrid.d.ts +13 -0
  26. package/dist/src/ui/components/OverrideResponse.d.ts +8 -0
  27. package/dist/src/ui/components/RequestBody.d.ts +6 -0
  28. package/dist/src/ui/components/RequestList.d.ts +9 -4
  29. package/dist/src/ui/components/ScrollArea.d.ts +3 -2
  30. package/dist/src/ui/components/Section.d.ts +8 -0
  31. package/dist/src/ui/components/Separator.d.ts +2 -1
  32. package/dist/src/ui/components/Tabs.d.ts +7 -0
  33. package/dist/src/ui/state/hooks.d.ts +4 -0
  34. package/dist/src/ui/state/model.d.ts +12 -7
  35. package/dist/src/ui/state/store.d.ts +27 -3
  36. package/dist/src/ui/utils/checkRequestBodyBinary.d.ts +2 -0
  37. package/dist/src/ui/utils/escapeShellArg.d.ts +1 -0
  38. package/dist/src/ui/utils/generateCurlCommand.d.ts +2 -0
  39. package/dist/src/ui/utils/generateFetchCall.d.ts +2 -0
  40. package/dist/src/ui/utils/generateMultipartBody.d.ts +4 -0
  41. package/dist/src/utils/applyReactNativeRequestHeadersLogic.d.ts +7 -0
  42. package/dist/src/utils/applyReactNativeResponseHeadersLogic.d.ts +9 -0
  43. package/dist/src/utils/cookieParser.d.ts +6 -0
  44. package/dist/src/utils/getContentTypeMimeType.d.ts +2 -0
  45. package/dist/src/utils/getHttpHeader.d.ts +5 -0
  46. package/dist/src/utils/getHttpHeaderValueAsString.d.ts +11 -0
  47. package/dist/src/utils/getStringSizeInBytes.d.ts +1 -0
  48. package/dist/src/utils/inferContentTypeFromPostData.d.ts +2 -0
  49. package/dist/src/utils/safeStringify.d.ts +1 -0
  50. package/dist/src/utils/typeChecks.d.ts +9 -0
  51. package/dist/useNetworkActivityDevTools.cjs +319 -24
  52. package/dist/useNetworkActivityDevTools.js +320 -25
  53. package/package.json +7 -4
  54. package/react-native.ts +6 -1
  55. package/src/react-native/config.ts +43 -0
  56. package/src/react-native/http/network-inspector.ts +170 -8
  57. package/src/react-native/http/overrides-registry.ts +32 -0
  58. package/src/react-native/http/xhr-interceptor.ts +19 -2
  59. package/src/react-native/sse/sse-inspector.ts +27 -5
  60. package/src/react-native/sse/sse-interceptor.ts +26 -8
  61. package/src/react-native/useNetworkActivityDevTools.ts +86 -8
  62. package/src/react-native/utils/getBlobName.ts +45 -0
  63. package/src/react-native/utils/getFormDataEntries.ts +32 -0
  64. package/src/react-native/utils.ts +3 -3
  65. package/src/shared/client.ts +73 -4
  66. package/src/shared/sse-events.ts +4 -1
  67. package/src/ui/components/Button.tsx +1 -0
  68. package/src/ui/components/CodeBlock.tsx +19 -0
  69. package/src/ui/components/CodeEditor.tsx +26 -0
  70. package/src/ui/components/CookieCard.tsx +64 -0
  71. package/src/ui/components/CopyRequestDropdown.tsx +95 -0
  72. package/src/ui/components/DropdownMenu.tsx +206 -0
  73. package/src/ui/components/FilterBar.tsx +117 -0
  74. package/src/ui/components/Input.tsx +1 -1
  75. package/src/ui/components/JsonTree.tsx +10 -3
  76. package/src/ui/components/JsonTreeCopyableItem.tsx +14 -10
  77. package/src/ui/components/KeyValueGrid.tsx +51 -0
  78. package/src/ui/components/OverrideResponse.tsx +132 -0
  79. package/src/ui/components/RequestBody.tsx +86 -0
  80. package/src/ui/components/RequestList.tsx +65 -13
  81. package/src/ui/components/ScrollArea.tsx +1 -0
  82. package/src/ui/components/Section.tsx +46 -0
  83. package/src/ui/components/SidePanel.tsx +15 -5
  84. package/src/ui/globals.css +4 -0
  85. package/src/ui/hooks/useCopyToClipboard.ts +2 -2
  86. package/src/ui/state/hooks.ts +8 -0
  87. package/src/ui/state/model.ts +18 -7
  88. package/src/ui/state/store.ts +610 -500
  89. package/src/ui/tabs/CookiesTab.tsx +60 -263
  90. package/src/ui/tabs/HeadersTab.tsx +78 -89
  91. package/src/ui/tabs/RequestTab.tsx +58 -46
  92. package/src/ui/tabs/ResponseTab.tsx +98 -67
  93. package/src/ui/tabs/SSEMessagesTab.tsx +50 -39
  94. package/src/ui/utils/checkRequestBodyBinary.ts +7 -0
  95. package/src/ui/utils/escapeShellArg.ts +12 -0
  96. package/src/ui/utils/generateCurlCommand.ts +83 -0
  97. package/src/ui/utils/generateFetchCall.ts +64 -0
  98. package/src/ui/utils/generateMultipartBody.ts +19 -0
  99. package/src/ui/views/InspectorView.tsx +15 -3
  100. package/src/utils/applyReactNativeRequestHeadersLogic.ts +30 -0
  101. package/src/utils/applyReactNativeResponseHeadersLogic.ts +28 -0
  102. package/src/utils/cookieParser.ts +126 -0
  103. package/src/utils/getContentTypeMimeType.ts +17 -0
  104. package/src/utils/getHttpHeader.ts +17 -0
  105. package/src/utils/getHttpHeaderValueAsString.ts +13 -0
  106. package/src/utils/getStringSizeInBytes.ts +3 -0
  107. package/src/utils/inferContentTypeFromPostData.ts +9 -0
  108. package/src/utils/safeStringify.ts +7 -0
  109. package/src/utils/typeChecks.ts +27 -0
  110. package/dist/src/ui/utils/getHttpHeaderValue.d.ts +0 -2
  111. package/src/ui/utils/getHttpHeaderValue.ts +0 -14
@@ -1,16 +1,121 @@
1
- import { HttpMethod, NetworkActivityDevToolsClient } from '../../shared/client';
1
+ import { safeStringify } from '../../utils/safeStringify';
2
+ import {
3
+ HttpMethod,
4
+ NetworkActivityDevToolsClient,
5
+ RequestPostData,
6
+ RequestTextPostData,
7
+ RequestBinaryPostData,
8
+ RequestFormDataPostData,
9
+ XHRPostData,
10
+ } from '../../shared/client';
2
11
  import { getContentType } from '../utils';
3
12
  import { getNetworkRequestsRegistry } from './network-requests-registry';
13
+ import { getBlobName } from '../utils/getBlobName';
14
+ import { getFormDataEntries } from '../utils/getFormDataEntries';
4
15
  import { XHRInterceptor } from './xhr-interceptor';
16
+ import { getStringSizeInBytes } from '../../utils/getStringSizeInBytes';
17
+ import { applyReactNativeResponseHeadersLogic } from '../../utils/applyReactNativeResponseHeadersLogic';
18
+ import {
19
+ isBlob,
20
+ isArrayBuffer,
21
+ isFormData,
22
+ isNullOrUndefined,
23
+ } from '../../utils/typeChecks';
24
+ import { getOverridesRegistry } from './overrides-registry';
5
25
 
6
26
  const networkRequestsRegistry = getNetworkRequestsRegistry();
27
+ const overridesRegistry = getOverridesRegistry();
28
+
29
+ const getBinaryPostData = (body: Blob): RequestBinaryPostData => ({
30
+ type: 'binary',
31
+ value: {
32
+ size: body.size,
33
+ type: body.type,
34
+ name: getBlobName(body),
35
+ },
36
+ });
37
+
38
+ const getArrayBufferPostData = (
39
+ body: ArrayBuffer | ArrayBufferView
40
+ ): RequestBinaryPostData => ({
41
+ type: 'binary',
42
+ value: {
43
+ size: body.byteLength,
44
+ },
45
+ });
46
+
47
+ const getTextPostData = (body: unknown): RequestTextPostData => ({
48
+ type: 'text',
49
+ value: safeStringify(body),
50
+ });
51
+
52
+ const getFormDataPostData = (body: FormData): RequestFormDataPostData => ({
53
+ type: 'form-data',
54
+ value: getFormDataEntries(body).reduce<RequestFormDataPostData['value']>(
55
+ (acc, [key, value]) => {
56
+ if (isBlob(value)) {
57
+ acc[key] = getBinaryPostData(value);
58
+ } else if (isArrayBuffer(value)) {
59
+ acc[key] = getArrayBufferPostData(value);
60
+ } else {
61
+ acc[key] = getTextPostData(value);
62
+ }
63
+
64
+ return acc;
65
+ },
66
+ {}
67
+ ),
68
+ });
69
+
70
+ const getRequestBody = (body: XHRPostData): RequestPostData => {
71
+ if (isNullOrUndefined(body)) {
72
+ return body;
73
+ }
74
+
75
+ if (isBlob(body)) {
76
+ return getBinaryPostData(body);
77
+ }
78
+
79
+ if (isArrayBuffer(body)) {
80
+ return getArrayBufferPostData(body);
81
+ }
7
82
 
8
- const getResponseSize = (request: XMLHttpRequest): number => {
9
- if (typeof request.response === 'object') {
10
- return request.response.size;
83
+ if (isFormData(body)) {
84
+ return getFormDataPostData(body);
11
85
  }
12
86
 
13
- return request.response.length || 0;
87
+ return getTextPostData(body);
88
+ };
89
+
90
+ const getResponseSize = (request: XMLHttpRequest): number | null => {
91
+ try {
92
+ const { responseType, response } = request;
93
+
94
+ // Handle a case of 204 where no-content was sent.
95
+ if (response === null) {
96
+ return 0;
97
+ }
98
+
99
+ if (responseType === '' || responseType === 'text') {
100
+ return getStringSizeInBytes(request.responseText);
101
+ }
102
+
103
+ if (responseType === 'json') {
104
+ return getStringSizeInBytes(safeStringify(response));
105
+ }
106
+
107
+ if (responseType === 'blob') {
108
+ return response.size;
109
+ }
110
+
111
+ if (responseType === 'arraybuffer') {
112
+ return response.byteLength;
113
+ }
114
+
115
+ return 0;
116
+ } catch {
117
+ return null;
118
+ }
14
119
  };
15
120
 
16
121
  const getResponseBody = async (
@@ -42,6 +147,10 @@ const getResponseBody = async (
42
147
  }
43
148
  }
44
149
 
150
+ if (responseType === 'json') {
151
+ return safeStringify(request.response);
152
+ }
153
+
45
154
  return null;
46
155
  };
47
156
 
@@ -90,7 +199,10 @@ export const getNetworkInspector = (
90
199
  return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
91
200
  };
92
201
 
93
- const handleRequestSend = (data: string, request: XMLHttpRequest): void => {
202
+ const handleRequestSend = (
203
+ data: XHRPostData,
204
+ request: XMLHttpRequest
205
+ ): void => {
94
206
  const sendTime = Date.now();
95
207
 
96
208
  const requestId = generateRequestId();
@@ -109,7 +221,7 @@ export const getNetworkInspector = (
109
221
  url: request._url as string,
110
222
  method: request._method as HttpMethod,
111
223
  headers: request._headers,
112
- postData: data,
224
+ postData: getRequestBody(data),
113
225
  },
114
226
  type: 'XHR',
115
227
  initiator,
@@ -130,7 +242,9 @@ export const getNetworkInspector = (
130
242
  url: request._url as string,
131
243
  status: request.status,
132
244
  statusText: request.statusText,
133
- headers: request.responseHeaders || {},
245
+ headers: applyReactNativeResponseHeadersLogic(
246
+ request.responseHeaders || {}
247
+ ),
134
248
  contentType: getContentType(request),
135
249
  size: getResponseSize(request),
136
250
  responseTime: Date.now(),
@@ -169,9 +283,57 @@ export const getNetworkInspector = (
169
283
  });
170
284
  };
171
285
 
286
+ const handleRequestOverride = (request: XMLHttpRequest): void => {
287
+ const override = overridesRegistry.getOverrideForUrl(
288
+ request._url as string
289
+ );
290
+
291
+ if (!override) {
292
+ return;
293
+ }
294
+
295
+ request.addEventListener('readystatechange', () => {
296
+ if (override.body !== undefined) {
297
+ Object.defineProperty(request, 'responseType', {
298
+ writable: true,
299
+ });
300
+
301
+ Object.defineProperty(request, 'response', {
302
+ writable: true,
303
+ });
304
+ Object.defineProperty(request, 'responseText', {
305
+ writable: true,
306
+ });
307
+
308
+ const contentType = getContentType(request);
309
+
310
+ if (contentType === 'application/json') {
311
+ request.responseType = 'json';
312
+ } else if (contentType === 'text/plain') {
313
+ request.responseType = 'text';
314
+ }
315
+
316
+ // @ts-expect-error - Mocking response
317
+ request.response = override.body;
318
+ // @ts-expect-error - Mocking responseText
319
+ request.responseText = override.body;
320
+ }
321
+
322
+ if (override.status !== undefined) {
323
+ Object.defineProperty(request, 'status', {
324
+ writable: true,
325
+ });
326
+
327
+ // @ts-expect-error - Mocking status
328
+ request.status = override.status;
329
+ }
330
+ });
331
+ };
332
+
172
333
  const enable = () => {
173
334
  XHRInterceptor.disableInterception();
174
335
  XHRInterceptor.setSendCallback(handleRequestSend);
336
+ XHRInterceptor.setOverrideCallback(handleRequestOverride);
175
337
  XHRInterceptor.enableInterception();
176
338
  };
177
339
 
@@ -0,0 +1,32 @@
1
+ import { RequestOverride } from '../../shared/client';
2
+
3
+ export type OverridesRegistry = {
4
+ setOverrides: (newOverrides: [string, RequestOverride][]) => void;
5
+ getOverrideForUrl: (url: string) => RequestOverride | undefined;
6
+ };
7
+
8
+ const createOverridesRegistry = (): OverridesRegistry => {
9
+ let overrides = new Map<string, RequestOverride>();
10
+
11
+ const setOverrides = (newOverrides: [string, RequestOverride][]) => {
12
+ overrides = new Map(newOverrides);
13
+ };
14
+
15
+ const getOverrideForUrl = (url: string) => {
16
+ return overrides.get(url);
17
+ };
18
+
19
+ return {
20
+ setOverrides,
21
+ getOverrideForUrl,
22
+ };
23
+ };
24
+
25
+ let registryInstance: OverridesRegistry | null = null;
26
+
27
+ export const getOverridesRegistry = (): OverridesRegistry => {
28
+ if (!registryInstance) {
29
+ registryInstance = createOverridesRegistry();
30
+ }
31
+ return registryInstance;
32
+ };
@@ -1,5 +1,7 @@
1
1
  /* eslint-disable prefer-rest-params */
2
2
 
3
+ import { XHRPostData } from '../../shared/client';
4
+
3
5
  /**
4
6
  * Copyright (c) Meta Platforms, Inc. and affiliates.
5
7
  *
@@ -9,6 +11,7 @@
9
11
  * Source: https://github.com/facebook/react-native/blob/2c683c5787dd03ac15d2aad45dcc53650529ee7f/packages/react-native/src/private/devsupport/devmenu/elementinspector/XHRInterceptor.js
10
12
  */
11
13
 
14
+ const XMLHttpRequest = global.XMLHttpRequest || window.XMLHttpRequest;
12
15
  const originalXHROpen = XMLHttpRequest.prototype.open;
13
16
  const originalXHRSend = XMLHttpRequest.prototype.send;
14
17
  const originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
@@ -20,7 +23,7 @@ type XHRInterceptorOpenCallback = (
20
23
  ) => void;
21
24
 
22
25
  type XHRInterceptorSendCallback = (
23
- data: string,
26
+ data: XHRPostData,
24
27
  request: XMLHttpRequest
25
28
  ) => void;
26
29
 
@@ -46,11 +49,14 @@ type XHRInterceptorResponseCallback = (
46
49
  request: XMLHttpRequest
47
50
  ) => void;
48
51
 
52
+ type XHRInterceptorOverrideCallback = (request: XMLHttpRequest) => void;
53
+
49
54
  let openCallback: XHRInterceptorOpenCallback | null;
50
55
  let sendCallback: XHRInterceptorSendCallback | null;
51
56
  let requestHeaderCallback: XHRInterceptorRequestHeaderCallback | null;
52
57
  let headerReceivedCallback: XHRInterceptorHeaderReceivedCallback | null;
53
58
  let responseCallback: XHRInterceptorResponseCallback | null;
59
+ let overrideCallback: XHRInterceptorOverrideCallback | null;
54
60
 
55
61
  let isInterceptorEnabled = false;
56
62
 
@@ -97,6 +103,13 @@ export const XHRInterceptor = {
97
103
  requestHeaderCallback = callback;
98
104
  },
99
105
 
106
+ /**
107
+ * Invoked before XMLHttpRequest.send(...) is called.
108
+ */
109
+ setOverrideCallback(callback: XHRInterceptorOverrideCallback) {
110
+ overrideCallback = callback;
111
+ },
112
+
100
113
  isInterceptorEnabled(): boolean {
101
114
  return isInterceptorEnabled;
102
115
  },
@@ -136,10 +149,13 @@ export const XHRInterceptor = {
136
149
  // register listeners to intercept the response, and invoke the callbacks.
137
150
  // $FlowFixMe[cannot-write]
138
151
  // $FlowFixMe[missing-this-annot]
139
- XMLHttpRequest.prototype.send = function (data: string) {
152
+ XMLHttpRequest.prototype.send = function (data: XHRPostData) {
140
153
  if (sendCallback) {
141
154
  sendCallback(data, this);
142
155
  }
156
+ if (overrideCallback) {
157
+ overrideCallback(this);
158
+ }
143
159
  if (this.addEventListener) {
144
160
  this.addEventListener(
145
161
  'readystatechange',
@@ -207,5 +223,6 @@ export const XHRInterceptor = {
207
223
  sendCallback = null;
208
224
  headerReceivedCallback = null;
209
225
  requestHeaderCallback = null;
226
+ overrideCallback = null;
210
227
  },
211
228
  };
@@ -22,13 +22,14 @@ export type SSEInspector = {
22
22
  export const getSSEInspector = (): SSEInspector => {
23
23
  const eventEmitter = createNanoEvents<NanoEventsMap>();
24
24
 
25
- const getRequestId = (eventSource: EventSourceWithInternals): string => {
25
+ const getRequestId = (
26
+ eventSource: EventSourceWithInternals
27
+ ): string | null => {
26
28
  const requestId = eventSource._xhr?._rozeniteRequestId;
27
29
 
28
30
  if (!requestId) {
29
- throw new Error(
30
- 'No request ID found for EventSource. This should never happen!'
31
- );
31
+ // It means that the EventSource was created before the inspector was enabled.
32
+ return null;
32
33
  }
33
34
 
34
35
  return requestId;
@@ -39,6 +40,11 @@ export const getSSEInspector = (): SSEInspector => {
39
40
  SSEInterceptor.setOpenEventCallback((_, eventSource) => {
40
41
  const sseEventSource = eventSource as EventSourceWithInternals;
41
42
  const requestId = getRequestId(sseEventSource);
43
+
44
+ if (!requestId) {
45
+ return;
46
+ }
47
+
42
48
  const sseXhr = sseEventSource._xhr as XMLHttpRequest;
43
49
 
44
50
  const event: SSEEvent = {
@@ -62,11 +68,18 @@ export const getSSEInspector = (): SSEInspector => {
62
68
  const sseEventSource = eventSource as EventSourceWithInternals;
63
69
  const requestId = getRequestId(sseEventSource);
64
70
 
71
+ if (!requestId) {
72
+ return;
73
+ }
74
+
65
75
  const event: SSEEvent = {
66
76
  type: 'sse-message',
67
77
  requestId,
68
78
  timestamp: Date.now(),
69
- data: messageEvent.data || '',
79
+ payload: {
80
+ type: messageEvent.type,
81
+ data: messageEvent.data || '',
82
+ },
70
83
  };
71
84
  eventEmitter.emit('sse-message', event);
72
85
  });
@@ -75,6 +88,10 @@ export const getSSEInspector = (): SSEInspector => {
75
88
  const sseEventSource = eventSource as EventSourceWithInternals;
76
89
  const requestId = getRequestId(sseEventSource);
77
90
 
91
+ if (!requestId) {
92
+ return;
93
+ }
94
+
78
95
  const event: SSEEvent = {
79
96
  type: 'sse-error',
80
97
  requestId,
@@ -92,6 +109,10 @@ export const getSSEInspector = (): SSEInspector => {
92
109
  const sseEventSource = eventSource as EventSourceWithInternals;
93
110
  const requestId = getRequestId(sseEventSource);
94
111
 
112
+ if (!requestId) {
113
+ return;
114
+ }
115
+
95
116
  const event: SSEEvent = {
96
117
  type: 'sse-close',
97
118
  requestId,
@@ -107,6 +128,7 @@ export const getSSEInspector = (): SSEInspector => {
107
128
  },
108
129
  isEnabled: () => SSEInterceptor.isInterceptorEnabled(),
109
130
  dispose: () => {
131
+ SSEInterceptor.disableInterception();
110
132
  eventEmitter.events = {};
111
133
  },
112
134
  on: <TEventType extends keyof SSEEventMap>(
@@ -6,6 +6,8 @@ import type {
6
6
  CloseEvent,
7
7
  TimeoutEvent,
8
8
  ExceptionEvent,
9
+ EventSourceEvent,
10
+ CustomEvent,
9
11
  } from 'react-native-sse';
10
12
  import { EventSourceWithInternals } from './types';
11
13
  import { getEventSource } from './event-source';
@@ -16,7 +18,7 @@ export type SSEInterceptorConnectCallback = (
16
18
  ) => void;
17
19
 
18
20
  export type SSEInterceptorMessageCallback = (
19
- event: MessageEvent,
21
+ event: MessageEvent | CustomEvent<string>,
20
22
  request: EventSource
21
23
  ) => void;
22
24
 
@@ -45,8 +47,12 @@ let isInterceptorEnabled = false;
45
47
 
46
48
  const eventSourceClass = getEventSource();
47
49
 
48
- // Store original EventSource open method
50
+ // Store original EventSource methods
49
51
  const originalOpen = eventSourceClass.prototype.open;
52
+ const originalDispatch = eventSourceClass.prototype.dispatch;
53
+
54
+ // Built-in SSE event types that we don't want to capture as messages
55
+ const BUILT_IN_EVENT_TYPES = new Set(['open', 'error', 'close', 'done']);
50
56
 
51
57
  /**
52
58
  * A network interceptor which monkey-patches EventSource open method
@@ -114,12 +120,6 @@ export const SSEInterceptor = {
114
120
  }
115
121
  });
116
122
 
117
- this.addEventListener('message', (event: MessageEvent) => {
118
- if (messageCallback) {
119
- messageCallback(event, this);
120
- }
121
- });
122
-
123
123
  this.addEventListener(
124
124
  'error',
125
125
  (event: ErrorEvent | TimeoutEvent | ExceptionEvent) => {
@@ -139,6 +139,21 @@ export const SSEInterceptor = {
139
139
  return originalOpen.call(this);
140
140
  };
141
141
 
142
+ eventSourceClass.prototype.dispatch = function (
143
+ this: EventSourceWithInternals,
144
+ eventType: string,
145
+ data: EventSourceEvent<string>
146
+ ) {
147
+ if (!BUILT_IN_EVENT_TYPES.has(eventType)) {
148
+ if (messageCallback) {
149
+ messageCallback(data, this);
150
+ }
151
+ }
152
+
153
+ // Call original open method
154
+ return originalDispatch.call(this, eventType, data);
155
+ };
156
+
142
157
  isInterceptorEnabled = true;
143
158
  },
144
159
 
@@ -152,6 +167,9 @@ export const SSEInterceptor = {
152
167
  // Restore original open method
153
168
  eventSourceClass.prototype.open = originalOpen;
154
169
 
170
+ // Restore original dispatch method
171
+ eventSourceClass.prototype.dispatch = originalDispatch;
172
+
155
173
  // Clear callbacks
156
174
  connectCallback = null;
157
175
  messageCallback = null;
@@ -1,32 +1,100 @@
1
- import { useEffect } from 'react';
1
+ import { useEffect, useRef } from 'react';
2
2
  import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge';
3
3
  import { getNetworkInspector } from './http/network-inspector';
4
+ import { getOverridesRegistry } from './http/overrides-registry';
4
5
  import { NetworkActivityEventMap } from '../shared/client';
5
6
  import { getWebSocketInspector } from './websocket/websocket-inspector';
6
7
  import { WebSocketEventMap } from '../shared/websocket-events';
7
8
  import { UnionToTuple } from './utils';
8
9
  import { getSSEInspector } from './sse/sse-inspector';
9
10
  import { SSEEventMap } from '../shared/sse-events';
10
-
11
- export const useNetworkActivityDevTools = () => {
11
+ import {
12
+ DEFAULT_CONFIG,
13
+ NetworkActivityDevToolsConfig,
14
+ validateConfig,
15
+ } from './config';
16
+
17
+ const overridesRegistry = getOverridesRegistry();
18
+
19
+ export const useNetworkActivityDevTools = (
20
+ config: NetworkActivityDevToolsConfig = DEFAULT_CONFIG
21
+ ) => {
22
+ const isRecordingEnabledRef = useRef(false);
12
23
  const client = useRozeniteDevToolsClient<NetworkActivityEventMap>({
13
24
  pluginId: '@rozenite/network-activity-plugin',
14
25
  });
15
26
 
27
+ const isHttpInspectorEnabled = config.inspectors?.http ?? true;
28
+ const isWebSocketInspectorEnabled = config.inspectors?.websocket ?? true;
29
+ const isSSEInspectorEnabled = config.inspectors?.sse ?? true;
30
+ const showUrlAsName = config.clientUISettings?.showUrlAsName;
31
+
32
+ useEffect(() => {
33
+ if (!client) {
34
+ return;
35
+ }
36
+
37
+ validateConfig(config);
38
+ }, [config]);
39
+
40
+ /** Persist the recording state across hot reloads */
16
41
  useEffect(() => {
17
42
  if (!client) {
18
43
  return;
19
44
  }
20
45
 
46
+
47
+ const sendClientUISettings = () => {
48
+ client.send('client-ui-settings', {
49
+ settings: {
50
+ showUrlAsName: showUrlAsName ?? DEFAULT_CONFIG.clientUISettings?.showUrlAsName,
51
+ },
52
+ });
53
+ }
54
+
55
+ const subscriptions = [
56
+ client.onMessage('network-enable', () => {
57
+ isRecordingEnabledRef.current = true;
58
+ }),
59
+ client.onMessage('network-disable', () => {
60
+ isRecordingEnabledRef.current = false;
61
+ }),
62
+ client.onMessage('set-overrides', (data) => {
63
+ overridesRegistry.setOverrides(data.overrides);
64
+ }),
65
+
66
+ client.onMessage('get-client-ui-settings', () => {
67
+ sendClientUISettings();
68
+ }),
69
+ ];
70
+
71
+ // Send initial or changed values live
72
+ sendClientUISettings();
73
+
74
+ return () => {
75
+ subscriptions.forEach((subscription) => subscription.remove());
76
+ };
77
+ }, [client, showUrlAsName]);
78
+
79
+ useEffect(() => {
80
+ if (!client || !isHttpInspectorEnabled) {
81
+ return;
82
+ }
83
+
21
84
  const networkInspector = getNetworkInspector(client);
22
85
 
86
+ // If recording was previously enabled, enable the inspector (hot reload)
87
+ if (isRecordingEnabledRef.current) {
88
+ networkInspector.enable();
89
+ }
90
+
23
91
  return () => {
24
92
  networkInspector.dispose();
25
93
  };
26
- }, [client]);
94
+ }, [client, isHttpInspectorEnabled]);
27
95
 
28
96
  useEffect(() => {
29
- if (!client) {
97
+ if (!client || !isWebSocketInspectorEnabled) {
30
98
  return;
31
99
  }
32
100
 
@@ -55,14 +123,19 @@ export const useNetworkActivityDevTools = () => {
55
123
  websocketInspector.disable();
56
124
  });
57
125
 
126
+ // If recording was previously enabled, enable the inspector (hot reload)
127
+ if (isRecordingEnabledRef.current) {
128
+ websocketInspector.enable();
129
+ }
130
+
58
131
  return () => {
59
132
  // Subscriptions will be disposed by the inspector
60
133
  websocketInspector.dispose();
61
134
  };
62
- }, [client]);
135
+ }, [client, isWebSocketInspectorEnabled]);
63
136
 
64
137
  useEffect(() => {
65
- if (!client) {
138
+ if (!client || !isSSEInspectorEnabled) {
66
139
  return;
67
140
  }
68
141
 
@@ -88,11 +161,16 @@ export const useNetworkActivityDevTools = () => {
88
161
  sseInspector.disable();
89
162
  });
90
163
 
164
+ // If recording was previously enabled, enable the inspector (hot reload)
165
+ if (isRecordingEnabledRef.current) {
166
+ sseInspector.enable();
167
+ }
168
+
91
169
  return () => {
92
170
  // Subscriptions will be disposed by the inspector
93
171
  sseInspector.dispose();
94
172
  };
95
- }, [client]);
173
+ }, [client, isSSEInspectorEnabled]);
96
174
 
97
175
  return client;
98
176
  };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Utility function to get the name of a blob. Handles both the direct name property and the data object.
3
+ *
4
+ * ```
5
+ * // node_modules/react-native/Libraries/Blob/Blob.js
6
+ *
7
+ * export type BlobData = {
8
+ * blobId: string,
9
+ * offset: number,
10
+ * size: number,
11
+ * name?: string,
12
+ * type?: string,
13
+ * lastModified?: number,
14
+ * __collector?: ?BlobCollector,
15
+ * ...
16
+ * };
17
+ *
18
+ * get data(): BlobData {
19
+ * if (!this._data) {
20
+ * throw new Error('Blob has been closed and is no longer available');
21
+ * }
22
+ *
23
+ * return this._data;
24
+ * }
25
+ *
26
+ * get size(): number {
27
+ * return this.data.size;
28
+ * }
29
+ *
30
+ * get type(): string {
31
+ * return this.data.type || '';
32
+ * }
33
+ * ```
34
+ */
35
+ export function getBlobName(blob: any): string | undefined {
36
+ if (typeof blob?.name === 'string') {
37
+ return blob.name;
38
+ }
39
+
40
+ if (blob?.data && typeof blob.data.name === 'string') {
41
+ return blob.data.name;
42
+ }
43
+
44
+ return undefined;
45
+ }