react-native-vconsole 0.3.0 → 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.
@@ -2,7 +2,8 @@ import type { NetworkEntry } from '../types';
2
2
 
3
3
  type NetworkListener = (entries: NetworkEntry[]) => void;
4
4
  type InstallXhrProxyOptions = {
5
- filterHosts?: string[];
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 (!host) {
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
- return ignoredHosts.has(host);
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: string): Record<string, string> {
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('\r\n')
272
+ .split(/\r?\n/)
177
273
  .filter(Boolean)
178
274
  .forEach((line) => {
179
275
  const splitIndex = line.indexOf(':');
@@ -187,7 +283,8 @@ function parseHeaders(rawHeaders: string): Record<string, string> {
187
283
  }
188
284
 
189
285
  export function installXhrProxy(options?: InstallXhrProxyOptions) {
190
- ignoredHosts = normalizeHosts(options?.filterHosts);
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 = finishedAt - current.startedAt;
264
- current.responseHeaders = parseHeaders(this.getAllResponseHeaders());
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
- return super.send(body as never);
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
 
package/src/index.tsx CHANGED
@@ -20,10 +20,6 @@ const Vconsole = NativeModules.Vconsole
20
20
  }
21
21
  );
22
22
 
23
- export function multiply(a: number, b: number): Promise<number> {
24
- return Vconsole.multiply(a, b);
25
- }
26
-
27
23
  export function getSystemInfo(): Promise<SystemInfo> {
28
24
  return Vconsole.getSystemInfo();
29
25
  }
package/src/types.ts CHANGED
@@ -17,6 +17,8 @@ export interface NetworkEntry {
17
17
  method: string;
18
18
  url: string;
19
19
  status?: number;
20
+ isError?: boolean;
21
+ errorReason?: string;
20
22
  startedAt: number;
21
23
  finishedAt?: number;
22
24
  durationMs?: number;
@@ -31,7 +33,7 @@ export interface SystemInfo {
31
33
  model: string;
32
34
  osVersion: string;
33
35
  networkType: string;
34
- isNetworkReachable: boolean;
36
+ isNetworkReachable: string;
35
37
  totalMemory: number;
36
38
  availableMemory: number;
37
39
  }