react-native-network-lens 0.0.0-stage → 0.1.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 (180) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +207 -2
  3. package/lib/module/backHandler.js +31 -0
  4. package/lib/module/backHandler.js.map +1 -0
  5. package/lib/module/components/BodyViewer.js +207 -0
  6. package/lib/module/components/BodyViewer.js.map +1 -0
  7. package/lib/module/components/CodeList.js +104 -0
  8. package/lib/module/components/CodeList.js.map +1 -0
  9. package/lib/module/components/JsonTree/JsonTreeView.js +206 -0
  10. package/lib/module/components/JsonTree/JsonTreeView.js.map +1 -0
  11. package/lib/module/components/JsonTree/model.js +194 -0
  12. package/lib/module/components/JsonTree/model.js.map +1 -0
  13. package/lib/module/components/NetworkLens.js +110 -0
  14. package/lib/module/components/NetworkLens.js.map +1 -0
  15. package/lib/module/components/NetworkLensOverlay.js +158 -0
  16. package/lib/module/components/NetworkLensOverlay.js.map +1 -0
  17. package/lib/module/components/RawTextView.js +119 -0
  18. package/lib/module/components/RawTextView.js.map +1 -0
  19. package/lib/module/components/RequestDetails/RequestDetails.js +251 -0
  20. package/lib/module/components/RequestDetails/RequestDetails.js.map +1 -0
  21. package/lib/module/components/RequestList/FilterBar.js +138 -0
  22. package/lib/module/components/RequestList/FilterBar.js.map +1 -0
  23. package/lib/module/components/RequestList/RequestList.js +163 -0
  24. package/lib/module/components/RequestList/RequestList.js.map +1 -0
  25. package/lib/module/components/RequestList/RequestRow.js +161 -0
  26. package/lib/module/components/RequestList/RequestRow.js.map +1 -0
  27. package/lib/module/components/RequestList/filters.js +57 -0
  28. package/lib/module/components/RequestList/filters.js.map +1 -0
  29. package/lib/module/components/ui/ActionSheet.js +94 -0
  30. package/lib/module/components/ui/ActionSheet.js.map +1 -0
  31. package/lib/module/components/ui/Button.js +90 -0
  32. package/lib/module/components/ui/Button.js.map +1 -0
  33. package/lib/module/components/ui/Pill.js +53 -0
  34. package/lib/module/components/ui/Pill.js.map +1 -0
  35. package/lib/module/components/ui/SearchInput.js +77 -0
  36. package/lib/module/components/ui/SearchInput.js.map +1 -0
  37. package/lib/module/components/ui/Section.js +147 -0
  38. package/lib/module/components/ui/Section.js.map +1 -0
  39. package/lib/module/components/ui/SegmentedControl.js +65 -0
  40. package/lib/module/components/ui/SegmentedControl.js.map +1 -0
  41. package/lib/module/components/ui/index.js +9 -0
  42. package/lib/module/components/ui/index.js.map +1 -0
  43. package/lib/module/core/Logger.js +230 -0
  44. package/lib/module/core/Logger.js.map +1 -0
  45. package/lib/module/core/NetworkRequest.js +155 -0
  46. package/lib/module/core/NetworkRequest.js.map +1 -0
  47. package/lib/module/core/XHRInterceptor.js +104 -0
  48. package/lib/module/core/XHRInterceptor.js.map +1 -0
  49. package/lib/module/core/har.js +72 -0
  50. package/lib/module/core/har.js.map +1 -0
  51. package/lib/module/core/loggerSingleton.js +12 -0
  52. package/lib/module/core/loggerSingleton.js.map +1 -0
  53. package/lib/module/hooks.js +42 -0
  54. package/lib/module/hooks.js.map +1 -0
  55. package/lib/module/index.js +34 -0
  56. package/lib/module/index.js.map +1 -0
  57. package/lib/module/package.json +1 -0
  58. package/lib/module/theme.js +136 -0
  59. package/lib/module/theme.js.map +1 -0
  60. package/lib/module/types.js +2 -0
  61. package/lib/module/types.js.map +1 -0
  62. package/lib/module/utils/body.js +179 -0
  63. package/lib/module/utils/body.js.map +1 -0
  64. package/lib/module/utils/debounce.js +18 -0
  65. package/lib/module/utils/debounce.js.map +1 -0
  66. package/lib/module/utils/format.js +45 -0
  67. package/lib/module/utils/format.js.map +1 -0
  68. package/lib/module/utils/logger.js +6 -0
  69. package/lib/module/utils/logger.js.map +1 -0
  70. package/lib/module/utils/search.js +70 -0
  71. package/lib/module/utils/search.js.map +1 -0
  72. package/lib/module/utils/url.js +50 -0
  73. package/lib/module/utils/url.js.map +1 -0
  74. package/lib/typescript/package.json +1 -0
  75. package/lib/typescript/src/backHandler.d.ts +15 -0
  76. package/lib/typescript/src/backHandler.d.ts.map +1 -0
  77. package/lib/typescript/src/components/BodyViewer.d.ts +16 -0
  78. package/lib/typescript/src/components/BodyViewer.d.ts.map +1 -0
  79. package/lib/typescript/src/components/CodeList.d.ts +21 -0
  80. package/lib/typescript/src/components/CodeList.d.ts.map +1 -0
  81. package/lib/typescript/src/components/JsonTree/JsonTreeView.d.ts +21 -0
  82. package/lib/typescript/src/components/JsonTree/JsonTreeView.d.ts.map +1 -0
  83. package/lib/typescript/src/components/JsonTree/model.d.ts +53 -0
  84. package/lib/typescript/src/components/JsonTree/model.d.ts.map +1 -0
  85. package/lib/typescript/src/components/NetworkLens.d.ts +31 -0
  86. package/lib/typescript/src/components/NetworkLens.d.ts.map +1 -0
  87. package/lib/typescript/src/components/NetworkLensOverlay.d.ts +32 -0
  88. package/lib/typescript/src/components/NetworkLensOverlay.d.ts.map +1 -0
  89. package/lib/typescript/src/components/RawTextView.d.ts +14 -0
  90. package/lib/typescript/src/components/RawTextView.d.ts.map +1 -0
  91. package/lib/typescript/src/components/RequestDetails/RequestDetails.d.ts +9 -0
  92. package/lib/typescript/src/components/RequestDetails/RequestDetails.d.ts.map +1 -0
  93. package/lib/typescript/src/components/RequestList/FilterBar.d.ts +12 -0
  94. package/lib/typescript/src/components/RequestList/FilterBar.d.ts.map +1 -0
  95. package/lib/typescript/src/components/RequestList/RequestList.d.ts +18 -0
  96. package/lib/typescript/src/components/RequestList/RequestList.d.ts.map +1 -0
  97. package/lib/typescript/src/components/RequestList/RequestRow.d.ts +11 -0
  98. package/lib/typescript/src/components/RequestList/RequestRow.d.ts.map +1 -0
  99. package/lib/typescript/src/components/RequestList/filters.d.ts +17 -0
  100. package/lib/typescript/src/components/RequestList/filters.d.ts.map +1 -0
  101. package/lib/typescript/src/components/ui/ActionSheet.d.ts +15 -0
  102. package/lib/typescript/src/components/ui/ActionSheet.d.ts.map +1 -0
  103. package/lib/typescript/src/components/ui/Button.d.ts +21 -0
  104. package/lib/typescript/src/components/ui/Button.d.ts.map +1 -0
  105. package/lib/typescript/src/components/ui/Pill.d.ts +11 -0
  106. package/lib/typescript/src/components/ui/Pill.d.ts.map +1 -0
  107. package/lib/typescript/src/components/ui/SearchInput.d.ts +17 -0
  108. package/lib/typescript/src/components/ui/SearchInput.d.ts.map +1 -0
  109. package/lib/typescript/src/components/ui/Section.d.ts +16 -0
  110. package/lib/typescript/src/components/ui/Section.d.ts.map +1 -0
  111. package/lib/typescript/src/components/ui/SegmentedControl.d.ts +14 -0
  112. package/lib/typescript/src/components/ui/SegmentedControl.d.ts.map +1 -0
  113. package/lib/typescript/src/components/ui/index.d.ts +7 -0
  114. package/lib/typescript/src/components/ui/index.d.ts.map +1 -0
  115. package/lib/typescript/src/core/Logger.d.ts +48 -0
  116. package/lib/typescript/src/core/Logger.d.ts.map +1 -0
  117. package/lib/typescript/src/core/NetworkRequest.d.ts +48 -0
  118. package/lib/typescript/src/core/NetworkRequest.d.ts.map +1 -0
  119. package/lib/typescript/src/core/XHRInterceptor.d.ts +29 -0
  120. package/lib/typescript/src/core/XHRInterceptor.d.ts.map +1 -0
  121. package/lib/typescript/src/core/har.d.ts +65 -0
  122. package/lib/typescript/src/core/har.d.ts.map +1 -0
  123. package/lib/typescript/src/core/loggerSingleton.d.ts +9 -0
  124. package/lib/typescript/src/core/loggerSingleton.d.ts.map +1 -0
  125. package/lib/typescript/src/hooks.d.ts +11 -0
  126. package/lib/typescript/src/hooks.d.ts.map +1 -0
  127. package/lib/typescript/src/index.d.ts +33 -0
  128. package/lib/typescript/src/index.d.ts.map +1 -0
  129. package/lib/typescript/src/theme.d.ts +49 -0
  130. package/lib/typescript/src/theme.d.ts.map +1 -0
  131. package/lib/typescript/src/types.d.ts +80 -0
  132. package/lib/typescript/src/types.d.ts.map +1 -0
  133. package/lib/typescript/src/utils/body.d.ts +19 -0
  134. package/lib/typescript/src/utils/body.d.ts.map +1 -0
  135. package/lib/typescript/src/utils/debounce.d.ts +4 -0
  136. package/lib/typescript/src/utils/debounce.d.ts.map +1 -0
  137. package/lib/typescript/src/utils/format.d.ts +7 -0
  138. package/lib/typescript/src/utils/format.d.ts.map +1 -0
  139. package/lib/typescript/src/utils/logger.d.ts +2 -0
  140. package/lib/typescript/src/utils/logger.d.ts.map +1 -0
  141. package/lib/typescript/src/utils/search.d.ts +17 -0
  142. package/lib/typescript/src/utils/search.d.ts.map +1 -0
  143. package/lib/typescript/src/utils/url.d.ts +15 -0
  144. package/lib/typescript/src/utils/url.d.ts.map +1 -0
  145. package/package.json +186 -4
  146. package/src/backHandler.ts +29 -0
  147. package/src/components/BodyViewer.tsx +240 -0
  148. package/src/components/CodeList.tsx +120 -0
  149. package/src/components/JsonTree/JsonTreeView.tsx +304 -0
  150. package/src/components/JsonTree/model.ts +264 -0
  151. package/src/components/NetworkLens.tsx +176 -0
  152. package/src/components/NetworkLensOverlay.tsx +187 -0
  153. package/src/components/RawTextView.tsx +163 -0
  154. package/src/components/RequestDetails/RequestDetails.tsx +313 -0
  155. package/src/components/RequestList/FilterBar.tsx +168 -0
  156. package/src/components/RequestList/RequestList.tsx +226 -0
  157. package/src/components/RequestList/RequestRow.tsx +150 -0
  158. package/src/components/RequestList/filters.ts +98 -0
  159. package/src/components/ui/ActionSheet.tsx +109 -0
  160. package/src/components/ui/Button.tsx +120 -0
  161. package/src/components/ui/Pill.tsx +53 -0
  162. package/src/components/ui/SearchInput.tsx +97 -0
  163. package/src/components/ui/Section.tsx +133 -0
  164. package/src/components/ui/SegmentedControl.tsx +79 -0
  165. package/src/components/ui/index.ts +6 -0
  166. package/src/core/Logger.ts +293 -0
  167. package/src/core/NetworkRequest.ts +216 -0
  168. package/src/core/XHRInterceptor.ts +179 -0
  169. package/src/core/har.ts +71 -0
  170. package/src/core/loggerSingleton.ts +14 -0
  171. package/src/hooks.ts +51 -0
  172. package/src/index.tsx +56 -0
  173. package/src/theme.ts +174 -0
  174. package/src/types.ts +83 -0
  175. package/src/utils/body.ts +194 -0
  176. package/src/utils/debounce.ts +18 -0
  177. package/src/utils/format.ts +50 -0
  178. package/src/utils/logger.ts +3 -0
  179. package/src/utils/search.ts +72 -0
  180. package/src/utils/url.ts +59 -0
@@ -0,0 +1,216 @@
1
+ import type {
2
+ FailureReason,
3
+ Headers,
4
+ NetworkRequestRow,
5
+ ParsedBody,
6
+ RequestMethod,
7
+ RequestState,
8
+ StatusGroup,
9
+ } from '../types';
10
+ import { parseRequestBody, parseResponseBody } from '../utils/body';
11
+ import { parseUrl, type UrlParts } from '../utils/url';
12
+
13
+ const getHeader = (headers: Headers, name: string): string | undefined => {
14
+ const lower = name.toLowerCase();
15
+ for (const key of Object.keys(headers)) {
16
+ if (key.toLowerCase() === lower) return headers[key];
17
+ }
18
+ return undefined;
19
+ };
20
+
21
+ export const statusGroupOf = (
22
+ status: number,
23
+ state: RequestState
24
+ ): StatusGroup => {
25
+ if (state === 'failed') return 'failed';
26
+ if (state === 'pending' || status <= 0) return 'pending';
27
+ if (status < 200) return '1xx';
28
+ if (status < 300) return '2xx';
29
+ if (status < 400) return '3xx';
30
+ if (status < 500) return '4xx';
31
+ return '5xx';
32
+ };
33
+
34
+ export default class NetworkRequest {
35
+ readonly id: string;
36
+ readonly type = 'XMLHttpRequest';
37
+ readonly method: RequestMethod;
38
+ readonly url: string;
39
+ status = -1;
40
+ state: RequestState = 'pending';
41
+ failureReason?: FailureReason;
42
+ requestHeaders: Headers = {};
43
+ responseHeaders: Headers = {};
44
+ dataSent: unknown = null;
45
+ response: unknown = null;
46
+ responseType = '';
47
+ responseURL = '';
48
+ responseContentType = '';
49
+ responseSize = 0;
50
+ timeout = 0;
51
+ startTime = 0;
52
+ endTime = 0;
53
+ gqlOperation?: string;
54
+ updatedAt: number;
55
+
56
+ private urlParts?: UrlParts;
57
+ private requestBodyCache?: ParsedBody;
58
+ private responseBodyCache?: Promise<ParsedBody>;
59
+ private responseBodyVersion = -1;
60
+
61
+ constructor(id: string, method: RequestMethod, url: string) {
62
+ this.id = id;
63
+ this.method = (method || 'GET').toUpperCase();
64
+ this.url = url;
65
+ this.updatedAt = Date.now();
66
+ }
67
+
68
+ get duration(): number {
69
+ if (!this.startTime) return -1;
70
+ if (!this.endTime) return -1;
71
+ return this.endTime - this.startTime;
72
+ }
73
+
74
+ get parsedUrl(): UrlParts {
75
+ if (!this.urlParts) this.urlParts = parseUrl(this.url);
76
+ return this.urlParts;
77
+ }
78
+
79
+ get host(): string {
80
+ return this.parsedUrl.host;
81
+ }
82
+
83
+ get path(): string {
84
+ return this.parsedUrl.path;
85
+ }
86
+
87
+ get statusGroup(): StatusGroup {
88
+ return statusGroupOf(this.status, this.state);
89
+ }
90
+
91
+ get requestContentType(): string | undefined {
92
+ return getHeader(this.requestHeaders, 'content-type');
93
+ }
94
+
95
+ update(values: Partial<NetworkRequest>): void {
96
+ Object.assign(this, values);
97
+ if ('dataSent' in values) {
98
+ this.requestBodyCache = undefined;
99
+ const body = this.getRequestBody();
100
+ if (
101
+ body.kind === 'json' &&
102
+ body.value &&
103
+ typeof body.value === 'object'
104
+ ) {
105
+ const op = (body.value as { operationName?: unknown }).operationName;
106
+ if (typeof op === 'string') this.gqlOperation = op;
107
+ }
108
+ }
109
+ this.updatedAt = Date.now();
110
+ }
111
+
112
+ getRequestBody(): ParsedBody {
113
+ if (!this.requestBodyCache) {
114
+ this.requestBodyCache = parseRequestBody(
115
+ this.dataSent,
116
+ this.requestContentType
117
+ );
118
+ }
119
+ return this.requestBodyCache;
120
+ }
121
+
122
+ /**
123
+ * Resolves the response body. Blob responses are read asynchronously, hence the promise.
124
+ * Result is cached until the response changes.
125
+ */
126
+ getResponseBody(): Promise<ParsedBody> {
127
+ if (
128
+ !this.responseBodyCache ||
129
+ this.responseBodyVersion !== this.updatedAt
130
+ ) {
131
+ this.responseBodyVersion = this.updatedAt;
132
+ this.responseBodyCache = parseResponseBody(
133
+ this.response,
134
+ this.responseType,
135
+ this.responseContentType
136
+ ).catch(
137
+ () =>
138
+ ({
139
+ kind: 'text',
140
+ text: '[Unable to read response body]',
141
+ }) as ParsedBody
142
+ );
143
+ }
144
+ return this.responseBodyCache;
145
+ }
146
+
147
+ get curl(): string {
148
+ const parts = ['curl'];
149
+ if (this.method !== 'GET') parts.push(`-X ${this.method}`);
150
+ for (const [key, value] of Object.entries(this.requestHeaders)) {
151
+ parts.push(`-H ${shellQuote(`${key}: ${value}`)}`);
152
+ }
153
+ const body = this.getRequestBody();
154
+ if (body.kind === 'json' || body.kind === 'text') {
155
+ parts.push(
156
+ `--data-raw ${shellQuote(body.kind === 'json' ? JSON.stringify(body.value) : body.text)}`
157
+ );
158
+ } else if (body.kind === 'form') {
159
+ for (const [key, value] of Object.entries(body.value)) {
160
+ parts.push(`-F ${shellQuote(`${key}=${value}`)}`);
161
+ }
162
+ }
163
+ parts.push(shellQuote(this.url));
164
+ return parts.join(' \\\n ');
165
+ }
166
+
167
+ toRow(): NetworkRequestRow {
168
+ return {
169
+ id: this.id,
170
+ method: this.method,
171
+ url: this.url,
172
+ host: this.host,
173
+ path: this.path,
174
+ status: this.status,
175
+ state: this.state,
176
+ statusGroup: this.statusGroup,
177
+ failureReason: this.failureReason,
178
+ duration: this.duration,
179
+ startTime: this.startTime,
180
+ responseSize: this.responseSize,
181
+ gqlOperation: this.gqlOperation,
182
+ updatedAt: this.updatedAt,
183
+ };
184
+ }
185
+
186
+ async toJSON(): Promise<Record<string, unknown>> {
187
+ const response = await this.getResponseBody();
188
+ const request = this.getRequestBody();
189
+ return {
190
+ id: this.id,
191
+ method: this.method,
192
+ url: this.url,
193
+ status: this.status,
194
+ state: this.state,
195
+ failureReason: this.failureReason,
196
+ startTime: this.startTime,
197
+ endTime: this.endTime,
198
+ duration: this.duration,
199
+ requestHeaders: this.requestHeaders,
200
+ requestBody:
201
+ request.kind === 'json' || request.kind === 'form'
202
+ ? request.value
203
+ : request.text,
204
+ responseHeaders: this.responseHeaders,
205
+ responseBody:
206
+ response.kind === 'json' || response.kind === 'form'
207
+ ? response.value
208
+ : response.text,
209
+ responseSize: this.responseSize,
210
+ responseContentType: this.responseContentType,
211
+ };
212
+ }
213
+ }
214
+
215
+ export const shellQuote = (value: string): string =>
216
+ `'${value.replace(/'/g, `'\\''`)}'`;
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Patches the global XMLHttpRequest so every request made from JavaScript can
3
+ * be observed. `fetch` in React Native is built on XMLHttpRequest, so this also
4
+ * covers fetch, axios, Apollo and friends.
5
+ *
6
+ * Every callback is wrapped in try/catch: the inspector must never break the
7
+ * app's networking.
8
+ */
9
+
10
+ export interface InterceptedXHR {
11
+ __lensId?: number;
12
+ __lensSeenHeaders?: boolean;
13
+ __lensSeenDone?: boolean;
14
+ }
15
+
16
+ type XHRInstance = XMLHttpRequest & InterceptedXHR;
17
+
18
+ export interface InterceptorCallbacks {
19
+ onOpen: (method: string, url: string, xhr: XHRInstance) => void;
20
+ onRequestHeader: (header: string, value: string, xhr: XHRInstance) => void;
21
+ onSend: (body: unknown, xhr: XHRInstance) => void;
22
+ onHeadersReceived: (
23
+ status: number,
24
+ contentType: string,
25
+ contentLength: number,
26
+ headers: Record<string, string>,
27
+ xhr: XHRInstance
28
+ ) => void;
29
+ onResponse: (
30
+ status: number,
31
+ timeout: number,
32
+ response: unknown,
33
+ responseURL: string,
34
+ responseType: string,
35
+ xhr: XHRInstance
36
+ ) => void;
37
+ onFailure: (reason: 'error' | 'timeout' | 'abort', xhr: XHRInstance) => void;
38
+ }
39
+
40
+ // XMLHttpRequest readyState values (fixed by the spec).
41
+ const HEADERS_RECEIVED = 2;
42
+ const DONE = 4;
43
+
44
+ type Originals = Pick<XMLHttpRequest, 'open' | 'send' | 'setRequestHeader'>;
45
+
46
+ const noop = () => {};
47
+ const noopCallbacks: InterceptorCallbacks = {
48
+ onOpen: noop,
49
+ onRequestHeader: noop,
50
+ onSend: noop,
51
+ onHeadersReceived: noop,
52
+ onResponse: noop,
53
+ onFailure: noop,
54
+ };
55
+
56
+ let callbacks = noopCallbacks;
57
+ let originals: Originals | undefined;
58
+
59
+ const safely = (fn: () => void) => {
60
+ try {
61
+ fn();
62
+ } catch {
63
+ // never let inspector code break a request
64
+ }
65
+ };
66
+
67
+ export const parseResponseHeaders = (raw: string): Record<string, string> => {
68
+ const headers: Record<string, string> = {};
69
+ for (const line of raw.split(/[\r\n]+/)) {
70
+ const index = line.indexOf(':');
71
+ if (index <= 0) continue;
72
+ const key = line.slice(0, index).trim();
73
+ const value = line.slice(index + 1).trim();
74
+ headers[key] = key in headers ? `${headers[key]}, ${value}` : value;
75
+ }
76
+ return headers;
77
+ };
78
+
79
+ export const isInterceptorEnabled = (): boolean => originals !== undefined;
80
+
81
+ export const setCallbacks = (next: InterceptorCallbacks) => {
82
+ callbacks = next;
83
+ };
84
+
85
+ const readResponse = (xhr: XMLHttpRequest): unknown => {
86
+ if (xhr.responseType === '' || xhr.responseType === 'text') {
87
+ try {
88
+ return xhr.responseText;
89
+ } catch {
90
+ // responseText throws for some response types on some platforms
91
+ }
92
+ }
93
+ return xhr.response;
94
+ };
95
+
96
+ export const enableInterception = (): void => {
97
+ if (originals || typeof XMLHttpRequest === 'undefined') return;
98
+
99
+ const proto = XMLHttpRequest.prototype;
100
+ const saved: Originals = {
101
+ open: proto.open,
102
+ send: proto.send,
103
+ setRequestHeader: proto.setRequestHeader,
104
+ };
105
+ originals = saved;
106
+
107
+ proto.open = function open(
108
+ this: XHRInstance,
109
+ method: string,
110
+ url: string | URL,
111
+ ...rest: unknown[]
112
+ ) {
113
+ safely(() => callbacks.onOpen(method, String(url), this));
114
+ return (saved.open as (...args: unknown[]) => void).call(
115
+ this,
116
+ method,
117
+ url,
118
+ ...rest
119
+ );
120
+ } as XMLHttpRequest['open'];
121
+
122
+ proto.setRequestHeader = function setRequestHeader(
123
+ this: XHRInstance,
124
+ header: string,
125
+ value: string
126
+ ) {
127
+ safely(() => callbacks.onRequestHeader(header, value, this));
128
+ return saved.setRequestHeader.call(this, header, value);
129
+ };
130
+
131
+ proto.send = function send(this: XHRInstance, body?: unknown) {
132
+ // eslint-disable-next-line consistent-this -- the listeners below outlive `this`
133
+ const xhr = this;
134
+ safely(() => callbacks.onSend(body, xhr));
135
+
136
+ xhr.addEventListener('readystatechange', () => {
137
+ if (xhr.readyState === HEADERS_RECEIVED && !xhr.__lensSeenHeaders) {
138
+ xhr.__lensSeenHeaders = true;
139
+ safely(() => {
140
+ const length = Number(xhr.getResponseHeader('content-length'));
141
+ callbacks.onHeadersReceived(
142
+ xhr.status,
143
+ xhr.getResponseHeader('content-type') ?? '',
144
+ Number.isFinite(length) ? length : 0,
145
+ parseResponseHeaders(xhr.getAllResponseHeaders()),
146
+ xhr
147
+ );
148
+ });
149
+ }
150
+ if (xhr.readyState === DONE && !xhr.__lensSeenDone) {
151
+ xhr.__lensSeenDone = true;
152
+ safely(() =>
153
+ callbacks.onResponse(
154
+ xhr.status,
155
+ xhr.timeout,
156
+ readResponse(xhr),
157
+ xhr.responseURL,
158
+ xhr.responseType,
159
+ xhr
160
+ )
161
+ );
162
+ }
163
+ });
164
+ for (const reason of ['error', 'timeout', 'abort'] as const) {
165
+ xhr.addEventListener(reason, () =>
166
+ safely(() => callbacks.onFailure(reason, xhr))
167
+ );
168
+ }
169
+
170
+ return saved.send.call(this, body as Parameters<XMLHttpRequest['send']>[0]);
171
+ };
172
+ };
173
+
174
+ export const disableInterception = (): void => {
175
+ if (!originals) return;
176
+ Object.assign(XMLHttpRequest.prototype, originals);
177
+ originals = undefined;
178
+ callbacks = noopCallbacks;
179
+ };
@@ -0,0 +1,71 @@
1
+ import type NetworkRequest from './NetworkRequest';
2
+
3
+ const headersToArray = (headers: Record<string, string>) =>
4
+ Object.entries(headers).map(([name, value]) => ({ name, value }));
5
+
6
+ const createHarEntry = async (request: NetworkRequest) => {
7
+ const requestBody = request.getRequestBody();
8
+ const responseBody = await request.getResponseBody();
9
+ return {
10
+ startedDateTime: new Date(request.startTime || Date.now()).toISOString(),
11
+ time: Math.max(request.duration, 0),
12
+ request: {
13
+ method: request.method,
14
+ url: request.url.replace(/\s/g, '%20'),
15
+ httpVersion: 'HTTP/1.1',
16
+ headers: headersToArray(request.requestHeaders),
17
+ queryString: Object.entries(request.parsedUrl.query).map(
18
+ ([name, value]) => ({
19
+ name,
20
+ value,
21
+ })
22
+ ),
23
+ cookies: [],
24
+ headersSize: -1,
25
+ bodySize: -1,
26
+ ...(requestBody.kind !== 'empty'
27
+ ? {
28
+ postData: {
29
+ mimeType: request.requestContentType ?? 'text/plain',
30
+ text: requestBody.text,
31
+ },
32
+ }
33
+ : {}),
34
+ },
35
+ response: {
36
+ status: Math.max(request.status, 0),
37
+ statusText: '',
38
+ httpVersion: 'HTTP/1.1',
39
+ headers: headersToArray(request.responseHeaders),
40
+ cookies: [],
41
+ content: {
42
+ size: request.responseSize || -1,
43
+ mimeType: request.responseContentType || 'text/plain',
44
+ text: responseBody.text,
45
+ },
46
+ redirectURL: '',
47
+ headersSize: -1,
48
+ bodySize: -1,
49
+ },
50
+ cache: {},
51
+ timings: {
52
+ blocked: -1,
53
+ dns: -1,
54
+ ssl: -1,
55
+ connect: -1,
56
+ send: 0,
57
+ wait: Math.max(request.duration, 0),
58
+ receive: 0,
59
+ },
60
+ };
61
+ };
62
+
63
+ /** Minimal HAR 1.2 document that Chrome/Charles/Proxyman can import. */
64
+ export const createHar = async (requests: NetworkRequest[]) => ({
65
+ log: {
66
+ version: '1.2',
67
+ creator: { name: 'react-native-network-lens', version: '1.0' },
68
+ pages: [],
69
+ entries: await Promise.all(requests.map(createHarEntry)),
70
+ },
71
+ });
@@ -0,0 +1,14 @@
1
+ import Logger from './Logger';
2
+
3
+ const KEY = '__reactNativeNetworkLens__';
4
+
5
+ type GlobalWithLogger = typeof globalThis & { [KEY]?: Logger };
6
+
7
+ /**
8
+ * One logger per JS runtime. It lives on `globalThis` so that Fast Refresh
9
+ * (which re-evaluates modules) keeps the same instance and the same patched
10
+ * XMLHttpRequest instead of silently creating a fresh, disabled one.
11
+ */
12
+ const logger: Logger = ((globalThis as GlobalWithLogger)[KEY] ??= new Logger());
13
+
14
+ export default logger;
package/src/hooks.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { useEffect, useState, useSyncExternalStore } from 'react';
2
+ import logger from './core/loggerSingleton';
3
+ import type NetworkRequest from './core/NetworkRequest';
4
+
5
+ /** Live list of captured requests, newest first. */
6
+ export const useNetworkRequests = (): NetworkRequest[] =>
7
+ useSyncExternalStore(
8
+ logger.subscribe,
9
+ logger.getSnapshot,
10
+ logger.getSnapshot
11
+ );
12
+
13
+ const getEnabled = () => logger.enabled;
14
+ const getPaused = () => logger.isPaused();
15
+
16
+ export const useLoggerEnabled = (): boolean =>
17
+ useSyncExternalStore(logger.subscribe, getEnabled, getEnabled);
18
+
19
+ export const useLoggerPaused = (): boolean =>
20
+ useSyncExternalStore(logger.subscribe, getPaused, getPaused);
21
+
22
+ /** Resolves a promise factory, re-running whenever `deps` change. */
23
+ export const useAsyncValue = <T>(
24
+ factory: () => Promise<T>,
25
+ deps: readonly unknown[]
26
+ ): { value: T | undefined; loading: boolean } => {
27
+ const [state, setState] = useState<{
28
+ value: T | undefined;
29
+ loading: boolean;
30
+ }>({
31
+ value: undefined,
32
+ loading: true,
33
+ });
34
+ useEffect(() => {
35
+ let cancelled = false;
36
+ setState((prev) => (prev.loading ? prev : { ...prev, loading: true }));
37
+ factory().then(
38
+ (value) => {
39
+ if (!cancelled) setState({ value, loading: false });
40
+ },
41
+ () => {
42
+ if (!cancelled) setState({ value: undefined, loading: false });
43
+ }
44
+ );
45
+ return () => {
46
+ cancelled = true;
47
+ };
48
+ // eslint-disable-next-line react-hooks/exhaustive-deps
49
+ }, deps);
50
+ return state;
51
+ };
package/src/index.tsx ADDED
@@ -0,0 +1,56 @@
1
+ import logger from './core/loggerSingleton';
2
+ import type { NetworkLensOptions } from './types';
3
+
4
+ export { default as NetworkLens, default } from './components/NetworkLens';
5
+ export type {
6
+ NetworkLensProps,
7
+ NetworkLensHandle,
8
+ } from './components/NetworkLens';
9
+ export { default as NetworkLensOverlay } from './components/NetworkLensOverlay';
10
+ export type { NetworkLensOverlayProps } from './components/NetworkLensOverlay';
11
+ export { default as JsonTreeView } from './components/JsonTree/JsonTreeView';
12
+ export type { JsonTreeHandle } from './components/JsonTree/JsonTreeView';
13
+
14
+ export { default as NetworkRequest } from './core/NetworkRequest';
15
+ export { createHar } from './core/har';
16
+ export { getBackHandler } from './backHandler';
17
+ export { useNetworkRequests, useLoggerEnabled, useLoggerPaused } from './hooks';
18
+ export { lightTheme, darkTheme } from './theme';
19
+ export type { Theme, ThemeName, ThemeInput } from './theme';
20
+ export type {
21
+ NetworkLensOptions,
22
+ StartNetworkLoggingOptions,
23
+ NetworkRequestRow,
24
+ ParsedBody,
25
+ RequestMethod,
26
+ RequestState,
27
+ FailureReason,
28
+ StatusGroup,
29
+ Headers,
30
+ } from './types';
31
+
32
+ /**
33
+ * Start capturing requests. Call it once, as early as possible (e.g. in your
34
+ * `index.js`). Pass `enabled: false` to turn the whole thing into a no-op.
35
+ *
36
+ * ```ts
37
+ * startNetworkLogging({ enabled: __DEV__, maxRequests: 500 });
38
+ * ```
39
+ */
40
+ export const startNetworkLogging = (options?: NetworkLensOptions): void =>
41
+ logger.start(options);
42
+
43
+ /** Stop capturing, restore XMLHttpRequest and drop every captured request. */
44
+ export const stopNetworkLogging = (): void => logger.stop();
45
+
46
+ /** Whether requests are currently being captured. */
47
+ export const isNetworkLoggingEnabled = (): boolean => logger.enabled;
48
+
49
+ /** Snapshot of captured requests, newest first. */
50
+ export const getRequests = () => logger.getRequests();
51
+
52
+ export const clearRequests = (): void => logger.clear();
53
+
54
+ /** Pause or resume publishing new requests to the UI. Capturing continues while paused. */
55
+ export const setNetworkLoggingPaused = (paused: boolean): void =>
56
+ logger.setPaused(paused);