react-native-vconsole 0.3.1 → 0.4.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 +16 -1
- package/lib/module/VConsole.js +283 -46
- package/lib/module/VConsole.js.map +1 -1
- package/lib/module/core/xhrProxy.js +145 -7
- package/lib/module/core/xhrProxy.js.map +1 -1
- package/lib/typescript/src/VConsole.d.ts +6 -1
- package/lib/typescript/src/VConsole.d.ts.map +1 -1
- package/lib/typescript/src/core/xhrProxy.d.ts +1 -0
- package/lib/typescript/src/core/xhrProxy.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +2 -0
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/VConsole.tsx +356 -53
- package/src/core/xhrProxy.ts +177 -8
- package/src/types.ts +2 -0
package/src/core/xhrProxy.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { NetworkEntry } from '../types';
|
|
|
3
3
|
type NetworkListener = (entries: NetworkEntry[]) => void;
|
|
4
4
|
type InstallXhrProxyOptions = {
|
|
5
5
|
excludeHosts?: string[];
|
|
6
|
+
excludeIp?: boolean;
|
|
6
7
|
};
|
|
7
8
|
|
|
8
9
|
const MAX_NETWORK_COUNT = 500;
|
|
@@ -14,6 +15,7 @@ let isInstalled = false;
|
|
|
14
15
|
let networkId = 1;
|
|
15
16
|
let OriginalXHR: typeof XMLHttpRequest | undefined;
|
|
16
17
|
let ignoredHosts = new Set<string>();
|
|
18
|
+
let ignoredIpHost = false;
|
|
17
19
|
|
|
18
20
|
function normalizeHosts(hosts?: string[]): Set<string> {
|
|
19
21
|
return new Set(
|
|
@@ -42,12 +44,102 @@ function getHostFromUrl(rawUrl: string): string | undefined {
|
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
function getHostnameFromUrl(rawUrl: string): string | undefined {
|
|
48
|
+
if (!rawUrl) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const isAbsoluteHttp = /^https?:\/\//i.test(rawUrl);
|
|
53
|
+
const isProtocolRelative = /^\/\//.test(rawUrl);
|
|
54
|
+
if (!isAbsoluteHttp && !isProtocolRelative) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const normalizedUrl = isProtocolRelative ? `https:${rawUrl}` : rawUrl;
|
|
59
|
+
try {
|
|
60
|
+
return new URL(normalizedUrl).hostname.toLowerCase();
|
|
61
|
+
} catch {
|
|
62
|
+
const authority = normalizedUrl.match(/^https?:\/\/([^/?#]+)/i)?.[1];
|
|
63
|
+
if (!authority) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
if (authority.startsWith('[')) {
|
|
67
|
+
const endIndex = authority.indexOf(']');
|
|
68
|
+
return endIndex > 1
|
|
69
|
+
? authority.slice(1, endIndex).toLowerCase()
|
|
70
|
+
: undefined;
|
|
71
|
+
}
|
|
72
|
+
return authority.split(':')[0]?.toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isIpv4Address(hostname: string): boolean {
|
|
77
|
+
const segments = hostname.split('.');
|
|
78
|
+
if (segments.length !== 4) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return segments.every((part) => {
|
|
82
|
+
if (!/^\d{1,3}$/.test(part)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const value = Number(part);
|
|
86
|
+
return value >= 0 && value <= 255;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isIpv6Address(hostname: string): boolean {
|
|
91
|
+
return hostname.includes(':') && /^[0-9a-f:.]+$/i.test(hostname);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isIpAddressHostname(hostname: string): boolean {
|
|
95
|
+
return isIpv4Address(hostname) || isIpv6Address(hostname);
|
|
96
|
+
}
|
|
97
|
+
|
|
45
98
|
function shouldSkipNetworkCapture(rawUrl: string): boolean {
|
|
46
99
|
const host = getHostFromUrl(rawUrl);
|
|
47
|
-
if (
|
|
100
|
+
if (host && ignoredHosts.has(host)) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
if (!ignoredIpHost) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
const hostname = getHostnameFromUrl(rawUrl);
|
|
107
|
+
if (!hostname) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return isIpAddressHostname(hostname);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getErrorMessage(error: unknown): string | undefined {
|
|
114
|
+
if (error instanceof Error) {
|
|
115
|
+
return error.message || error.name;
|
|
116
|
+
}
|
|
117
|
+
if (typeof error === 'string') {
|
|
118
|
+
return error;
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isInvalidHttpUrl(rawUrl: string): boolean {
|
|
124
|
+
if (!rawUrl) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
if (/^https?:\/\//i.test(rawUrl) || /^\/\//.test(rawUrl)) {
|
|
48
128
|
return false;
|
|
49
129
|
}
|
|
50
|
-
|
|
130
|
+
if (rawUrl.startsWith('/')) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
const hasScheme = /^[a-z][a-z0-9+.-]*:/i.test(rawUrl);
|
|
134
|
+
return hasScheme || !rawUrl.includes('/');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function markNetworkError(entry: NetworkEntry, reason: string) {
|
|
138
|
+
entry.status = entry.status ?? 0;
|
|
139
|
+
entry.isError = true;
|
|
140
|
+
entry.errorReason = reason;
|
|
141
|
+
entry.responseHeaders = {};
|
|
142
|
+
entry.responseData = undefined;
|
|
51
143
|
}
|
|
52
144
|
|
|
53
145
|
function notify() {
|
|
@@ -170,10 +262,14 @@ function parseResponseData(
|
|
|
170
262
|
return xhr.response ?? '';
|
|
171
263
|
}
|
|
172
264
|
|
|
173
|
-
function parseHeaders(rawHeaders
|
|
265
|
+
function parseHeaders(rawHeaders?: string | null): Record<string, string> {
|
|
174
266
|
const result: Record<string, string> = {};
|
|
267
|
+
if (!rawHeaders) {
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
|
|
175
271
|
rawHeaders
|
|
176
|
-
.split(
|
|
272
|
+
.split(/\r?\n/)
|
|
177
273
|
.filter(Boolean)
|
|
178
274
|
.forEach((line) => {
|
|
179
275
|
const splitIndex = line.indexOf(':');
|
|
@@ -188,6 +284,7 @@ function parseHeaders(rawHeaders: string): Record<string, string> {
|
|
|
188
284
|
|
|
189
285
|
export function installXhrProxy(options?: InstallXhrProxyOptions) {
|
|
190
286
|
ignoredHosts = normalizeHosts(options?.excludeHosts);
|
|
287
|
+
ignoredIpHost = options?.excludeIp === true;
|
|
191
288
|
|
|
192
289
|
if (isInstalled || typeof XMLHttpRequest === 'undefined') {
|
|
193
290
|
return;
|
|
@@ -259,9 +356,26 @@ export function installXhrProxy(options?: InstallXhrProxyOptions) {
|
|
|
259
356
|
|
|
260
357
|
const finishedAt = Date.now();
|
|
261
358
|
current.status = this.status;
|
|
262
|
-
current.finishedAt = finishedAt;
|
|
263
|
-
current.durationMs =
|
|
264
|
-
|
|
359
|
+
current.finishedAt = current.finishedAt ?? finishedAt;
|
|
360
|
+
current.durationMs =
|
|
361
|
+
current.durationMs ?? finishedAt - current.startedAt;
|
|
362
|
+
if (current.isError) {
|
|
363
|
+
notify();
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
current.responseHeaders = parseHeaders(this.getAllResponseHeaders());
|
|
368
|
+
} catch {
|
|
369
|
+
current.responseHeaders = {};
|
|
370
|
+
}
|
|
371
|
+
if (this.status === 0) {
|
|
372
|
+
const reason = isInvalidHttpUrl(this._url)
|
|
373
|
+
? `Invalid URL: ${this._url}`
|
|
374
|
+
: 'Network request failed';
|
|
375
|
+
markNetworkError(current, reason);
|
|
376
|
+
notify();
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
265
379
|
try {
|
|
266
380
|
const parsedData = parseResponseData(this, current.responseHeaders);
|
|
267
381
|
if (isPromiseLike(parsedData)) {
|
|
@@ -285,7 +399,62 @@ export function installXhrProxy(options?: InstallXhrProxyOptions) {
|
|
|
285
399
|
notify();
|
|
286
400
|
});
|
|
287
401
|
|
|
288
|
-
|
|
402
|
+
this.addEventListener('timeout', () => {
|
|
403
|
+
const current = entries.find((item) => item.id === this._entryId);
|
|
404
|
+
if (!current) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
current.finishedAt = current.finishedAt ?? Date.now();
|
|
408
|
+
current.durationMs =
|
|
409
|
+
current.durationMs ?? current.finishedAt - current.startedAt;
|
|
410
|
+
markNetworkError(
|
|
411
|
+
current,
|
|
412
|
+
this.timeout > 0
|
|
413
|
+
? `Request timeout (${this.timeout}ms)`
|
|
414
|
+
: 'Request timeout'
|
|
415
|
+
);
|
|
416
|
+
notify();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
this.addEventListener('error', () => {
|
|
420
|
+
const current = entries.find((item) => item.id === this._entryId);
|
|
421
|
+
if (!current) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
current.finishedAt = current.finishedAt ?? Date.now();
|
|
425
|
+
current.durationMs =
|
|
426
|
+
current.durationMs ?? current.finishedAt - current.startedAt;
|
|
427
|
+
const reason = isInvalidHttpUrl(this._url)
|
|
428
|
+
? `Invalid URL: ${this._url}`
|
|
429
|
+
: 'Network request failed';
|
|
430
|
+
markNetworkError(current, reason);
|
|
431
|
+
notify();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
this.addEventListener('abort', () => {
|
|
435
|
+
const current = entries.find((item) => item.id === this._entryId);
|
|
436
|
+
if (!current) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
current.finishedAt = current.finishedAt ?? Date.now();
|
|
440
|
+
current.durationMs =
|
|
441
|
+
current.durationMs ?? current.finishedAt - current.startedAt;
|
|
442
|
+
markNetworkError(current, 'Request aborted');
|
|
443
|
+
notify();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
return super.send(body as never);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
entry.finishedAt = Date.now();
|
|
450
|
+
entry.durationMs = entry.finishedAt - entry.startedAt;
|
|
451
|
+
const fallbackReason = isInvalidHttpUrl(this._url)
|
|
452
|
+
? `Invalid URL: ${this._url}`
|
|
453
|
+
: 'Network request failed';
|
|
454
|
+
markNetworkError(entry, getErrorMessage(error) ?? fallbackReason);
|
|
455
|
+
notify();
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
289
458
|
}
|
|
290
459
|
}
|
|
291
460
|
|