react-native-network-lens 0.0.0-stage → 0.1.1
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/LICENSE +20 -0
- package/README.md +208 -2
- package/lib/module/backHandler.js +31 -0
- package/lib/module/backHandler.js.map +1 -0
- package/lib/module/components/BodyViewer.js +213 -0
- package/lib/module/components/BodyViewer.js.map +1 -0
- package/lib/module/components/CodeList.js +104 -0
- package/lib/module/components/CodeList.js.map +1 -0
- package/lib/module/components/JsonTree/JsonTreeView.js +206 -0
- package/lib/module/components/JsonTree/JsonTreeView.js.map +1 -0
- package/lib/module/components/JsonTree/model.js +196 -0
- package/lib/module/components/JsonTree/model.js.map +1 -0
- package/lib/module/components/NetworkLens.js +110 -0
- package/lib/module/components/NetworkLens.js.map +1 -0
- package/lib/module/components/NetworkLensOverlay.js +158 -0
- package/lib/module/components/NetworkLensOverlay.js.map +1 -0
- package/lib/module/components/RawTextView.js +119 -0
- package/lib/module/components/RawTextView.js.map +1 -0
- package/lib/module/components/RequestDetails/RequestDetails.js +256 -0
- package/lib/module/components/RequestDetails/RequestDetails.js.map +1 -0
- package/lib/module/components/RequestList/FilterBar.js +138 -0
- package/lib/module/components/RequestList/FilterBar.js.map +1 -0
- package/lib/module/components/RequestList/RequestList.js +163 -0
- package/lib/module/components/RequestList/RequestList.js.map +1 -0
- package/lib/module/components/RequestList/RequestRow.js +161 -0
- package/lib/module/components/RequestList/RequestRow.js.map +1 -0
- package/lib/module/components/RequestList/filters.js +57 -0
- package/lib/module/components/RequestList/filters.js.map +1 -0
- package/lib/module/components/ui/ActionSheet.js +94 -0
- package/lib/module/components/ui/ActionSheet.js.map +1 -0
- package/lib/module/components/ui/Button.js +104 -0
- package/lib/module/components/ui/Button.js.map +1 -0
- package/lib/module/components/ui/Pill.js +53 -0
- package/lib/module/components/ui/Pill.js.map +1 -0
- package/lib/module/components/ui/SearchInput.js +77 -0
- package/lib/module/components/ui/SearchInput.js.map +1 -0
- package/lib/module/components/ui/Section.js +147 -0
- package/lib/module/components/ui/Section.js.map +1 -0
- package/lib/module/components/ui/SegmentedControl.js +65 -0
- package/lib/module/components/ui/SegmentedControl.js.map +1 -0
- package/lib/module/components/ui/index.js +9 -0
- package/lib/module/components/ui/index.js.map +1 -0
- package/lib/module/core/Logger.js +230 -0
- package/lib/module/core/Logger.js.map +1 -0
- package/lib/module/core/NetworkRequest.js +155 -0
- package/lib/module/core/NetworkRequest.js.map +1 -0
- package/lib/module/core/XHRInterceptor.js +104 -0
- package/lib/module/core/XHRInterceptor.js.map +1 -0
- package/lib/module/core/har.js +72 -0
- package/lib/module/core/har.js.map +1 -0
- package/lib/module/core/loggerSingleton.js +12 -0
- package/lib/module/core/loggerSingleton.js.map +1 -0
- package/lib/module/hooks.js +42 -0
- package/lib/module/hooks.js.map +1 -0
- package/lib/module/index.js +34 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/theme.js +136 -0
- package/lib/module/theme.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/body.js +179 -0
- package/lib/module/utils/body.js.map +1 -0
- package/lib/module/utils/debounce.js +18 -0
- package/lib/module/utils/debounce.js.map +1 -0
- package/lib/module/utils/format.js +45 -0
- package/lib/module/utils/format.js.map +1 -0
- package/lib/module/utils/logger.js +6 -0
- package/lib/module/utils/logger.js.map +1 -0
- package/lib/module/utils/search.js +70 -0
- package/lib/module/utils/search.js.map +1 -0
- package/lib/module/utils/url.js +50 -0
- package/lib/module/utils/url.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/backHandler.d.ts +15 -0
- package/lib/typescript/src/backHandler.d.ts.map +1 -0
- package/lib/typescript/src/components/BodyViewer.d.ts +16 -0
- package/lib/typescript/src/components/BodyViewer.d.ts.map +1 -0
- package/lib/typescript/src/components/CodeList.d.ts +21 -0
- package/lib/typescript/src/components/CodeList.d.ts.map +1 -0
- package/lib/typescript/src/components/JsonTree/JsonTreeView.d.ts +21 -0
- package/lib/typescript/src/components/JsonTree/JsonTreeView.d.ts.map +1 -0
- package/lib/typescript/src/components/JsonTree/model.d.ts +53 -0
- package/lib/typescript/src/components/JsonTree/model.d.ts.map +1 -0
- package/lib/typescript/src/components/NetworkLens.d.ts +31 -0
- package/lib/typescript/src/components/NetworkLens.d.ts.map +1 -0
- package/lib/typescript/src/components/NetworkLensOverlay.d.ts +32 -0
- package/lib/typescript/src/components/NetworkLensOverlay.d.ts.map +1 -0
- package/lib/typescript/src/components/RawTextView.d.ts +14 -0
- package/lib/typescript/src/components/RawTextView.d.ts.map +1 -0
- package/lib/typescript/src/components/RequestDetails/RequestDetails.d.ts +9 -0
- package/lib/typescript/src/components/RequestDetails/RequestDetails.d.ts.map +1 -0
- package/lib/typescript/src/components/RequestList/FilterBar.d.ts +12 -0
- package/lib/typescript/src/components/RequestList/FilterBar.d.ts.map +1 -0
- package/lib/typescript/src/components/RequestList/RequestList.d.ts +18 -0
- package/lib/typescript/src/components/RequestList/RequestList.d.ts.map +1 -0
- package/lib/typescript/src/components/RequestList/RequestRow.d.ts +11 -0
- package/lib/typescript/src/components/RequestList/RequestRow.d.ts.map +1 -0
- package/lib/typescript/src/components/RequestList/filters.d.ts +17 -0
- package/lib/typescript/src/components/RequestList/filters.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/ActionSheet.d.ts +15 -0
- package/lib/typescript/src/components/ui/ActionSheet.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Button.d.ts +21 -0
- package/lib/typescript/src/components/ui/Button.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Pill.d.ts +11 -0
- package/lib/typescript/src/components/ui/Pill.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/SearchInput.d.ts +17 -0
- package/lib/typescript/src/components/ui/SearchInput.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/Section.d.ts +16 -0
- package/lib/typescript/src/components/ui/Section.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/SegmentedControl.d.ts +14 -0
- package/lib/typescript/src/components/ui/SegmentedControl.d.ts.map +1 -0
- package/lib/typescript/src/components/ui/index.d.ts +7 -0
- package/lib/typescript/src/components/ui/index.d.ts.map +1 -0
- package/lib/typescript/src/core/Logger.d.ts +48 -0
- package/lib/typescript/src/core/Logger.d.ts.map +1 -0
- package/lib/typescript/src/core/NetworkRequest.d.ts +48 -0
- package/lib/typescript/src/core/NetworkRequest.d.ts.map +1 -0
- package/lib/typescript/src/core/XHRInterceptor.d.ts +29 -0
- package/lib/typescript/src/core/XHRInterceptor.d.ts.map +1 -0
- package/lib/typescript/src/core/har.d.ts +65 -0
- package/lib/typescript/src/core/har.d.ts.map +1 -0
- package/lib/typescript/src/core/loggerSingleton.d.ts +9 -0
- package/lib/typescript/src/core/loggerSingleton.d.ts.map +1 -0
- package/lib/typescript/src/hooks.d.ts +11 -0
- package/lib/typescript/src/hooks.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +33 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/theme.d.ts +49 -0
- package/lib/typescript/src/theme.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +80 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utils/body.d.ts +19 -0
- package/lib/typescript/src/utils/body.d.ts.map +1 -0
- package/lib/typescript/src/utils/debounce.d.ts +4 -0
- package/lib/typescript/src/utils/debounce.d.ts.map +1 -0
- package/lib/typescript/src/utils/format.d.ts +7 -0
- package/lib/typescript/src/utils/format.d.ts.map +1 -0
- package/lib/typescript/src/utils/logger.d.ts +2 -0
- package/lib/typescript/src/utils/logger.d.ts.map +1 -0
- package/lib/typescript/src/utils/search.d.ts +17 -0
- package/lib/typescript/src/utils/search.d.ts.map +1 -0
- package/lib/typescript/src/utils/url.d.ts +15 -0
- package/lib/typescript/src/utils/url.d.ts.map +1 -0
- package/package.json +186 -4
- package/src/backHandler.ts +29 -0
- package/src/components/BodyViewer.tsx +250 -0
- package/src/components/CodeList.tsx +120 -0
- package/src/components/JsonTree/JsonTreeView.tsx +304 -0
- package/src/components/JsonTree/model.ts +266 -0
- package/src/components/NetworkLens.tsx +176 -0
- package/src/components/NetworkLensOverlay.tsx +187 -0
- package/src/components/RawTextView.tsx +163 -0
- package/src/components/RequestDetails/RequestDetails.tsx +315 -0
- package/src/components/RequestList/FilterBar.tsx +168 -0
- package/src/components/RequestList/RequestList.tsx +226 -0
- package/src/components/RequestList/RequestRow.tsx +150 -0
- package/src/components/RequestList/filters.ts +98 -0
- package/src/components/ui/ActionSheet.tsx +109 -0
- package/src/components/ui/Button.tsx +124 -0
- package/src/components/ui/Pill.tsx +53 -0
- package/src/components/ui/SearchInput.tsx +97 -0
- package/src/components/ui/Section.tsx +133 -0
- package/src/components/ui/SegmentedControl.tsx +79 -0
- package/src/components/ui/index.ts +6 -0
- package/src/core/Logger.ts +293 -0
- package/src/core/NetworkRequest.ts +216 -0
- package/src/core/XHRInterceptor.ts +179 -0
- package/src/core/har.ts +71 -0
- package/src/core/loggerSingleton.ts +14 -0
- package/src/hooks.ts +51 -0
- package/src/index.tsx +56 -0
- package/src/theme.ts +174 -0
- package/src/types.ts +83 -0
- package/src/utils/body.ts +194 -0
- package/src/utils/debounce.ts +18 -0
- package/src/utils/format.ts +50 -0
- package/src/utils/logger.ts +3 -0
- package/src/utils/search.ts +72 -0
- 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
|
+
};
|
package/src/core/har.ts
ADDED
|
@@ -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);
|