@wutiange/log-listener-plugin 1.3.1 → 2.0.1-alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. package/README.md +155 -2
  2. package/dist/src/HTTPInterceptor.d.ts +1 -0
  3. package/dist/src/HTTPInterceptor.js +12 -9
  4. package/dist/src/HTTPInterceptor.js.map +1 -1
  5. package/dist/src/Server.d.ts +13 -6
  6. package/dist/src/Server.js +119 -41
  7. package/dist/src/Server.js.map +1 -1
  8. package/dist/src/__mocks__/react-native/Libraries/Blob/FileReader.js +0 -1
  9. package/dist/src/__mocks__/react-native/Libraries/Blob/FileReader.js.map +1 -1
  10. package/dist/src/__mocks__/react-native/Libraries/Network/XHRInterceptor.js +0 -1
  11. package/dist/src/__mocks__/react-native/Libraries/Network/XHRInterceptor.js.map +1 -1
  12. package/dist/src/__tests__/Server.test.js +89 -114
  13. package/dist/src/__tests__/Server.test.js.map +1 -1
  14. package/dist/src/common.d.ts +19 -10
  15. package/dist/src/common.js +63 -4
  16. package/dist/src/common.js.map +1 -1
  17. package/dist/src/logPlugin.d.ts +12 -9
  18. package/dist/src/logPlugin.js +87 -82
  19. package/dist/src/logPlugin.js.map +1 -1
  20. package/dist/src/logger.d.ts +6 -0
  21. package/dist/src/logger.js +16 -0
  22. package/dist/src/logger.js.map +1 -0
  23. package/dist/src/utils.js +12 -7
  24. package/dist/src/utils.js.map +1 -1
  25. package/index.ts +3 -0
  26. package/package.json +18 -12
  27. package/src/HTTPInterceptor.ts +339 -0
  28. package/src/Server.ts +166 -0
  29. package/src/__mocks__/react-native/Libraries/Blob/FileReader.js +45 -0
  30. package/src/__mocks__/react-native/Libraries/Network/XHRInterceptor.js +39 -0
  31. package/src/__tests__/HTTPInterceptor.test.ts +322 -0
  32. package/src/__tests__/Server.test.ts +175 -0
  33. package/src/__tests__/utils.test.ts +113 -0
  34. package/src/common.ts +70 -0
  35. package/src/logPlugin.ts +224 -0
  36. package/src/logger.ts +15 -0
  37. package/src/utils.ts +112 -0
  38. package/tsconfig.json +27 -0
@@ -0,0 +1,339 @@
1
+ import XHRInterceptor from 'react-native/Libraries/Network/XHRInterceptor';
2
+ import BlobFileReader from 'react-native/Libraries/Blob/FileReader';
3
+ import {Blob} from 'buffer';
4
+ import { createClassWithErrorHandling, formDataToString } from './utils';
5
+
6
+ type StartNetworkLoggingOptions = {
7
+ /** List of hosts to ignore, e.g. `services.test.com` */
8
+ ignoredHosts?: string[];
9
+ /** List of urls to ignore, e.g. `https://services.test.com/test` */
10
+ ignoredUrls?: string[];
11
+ /**
12
+ * List of url patterns to ignore, e.g. `/^GET https://test.com\/pages\/.*$/`
13
+ *
14
+ * Url to match with is in the format: `${method} ${url}`, e.g. `GET https://test.com/pages/123`
15
+ */
16
+ ignoredPatterns?: RegExp[];
17
+ /**
18
+ * Force the network logger to start even if another program is using the network interceptor
19
+ * e.g. a dev/debuging program
20
+ */
21
+ forceEnable?: boolean;
22
+ };
23
+
24
+ interface HttpRequestInfo {
25
+ id: string;
26
+ method: RequestMethod;
27
+ url: string;
28
+ timeout: number;
29
+ requestHeaders: Record<string, string>;
30
+ requestData: any | null;
31
+ startTime: number;
32
+ endTime: number;
33
+ responseHeaders: Headers;
34
+ responseData: any | null;
35
+ status: number;
36
+ duration: number;
37
+ responseContentType: string;
38
+ responseSize: number;
39
+ responseURL: string;
40
+ responseType: string;
41
+ }
42
+
43
+ type RequestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
44
+
45
+ type XHR = {
46
+ uniqueId: string;
47
+ responseHeaders?: Headers;
48
+ };
49
+
50
+ type EventName =
51
+ | 'open'
52
+ | 'requestHeader'
53
+ | 'headerReceived'
54
+ | 'send'
55
+ | 'response';
56
+
57
+ const extractHost = (url: string) => {
58
+ const host = url.split('//')[1]?.split(':')[0]?.split('/')[0] || undefined;
59
+
60
+ return host;
61
+ };
62
+
63
+ const generateUniqueId = () => {
64
+ return Date.now().toString(36) + Math.random().toString(36).substr(2);
65
+ };
66
+
67
+ const parseResponseBlob = async (response: Blob) => {
68
+ const blobReader = new BlobFileReader();
69
+ blobReader.readAsText(response);
70
+
71
+ return await new Promise<string>((resolve, reject) => {
72
+ const handleError = () => reject(blobReader.error);
73
+
74
+ blobReader.addEventListener('load', () => {
75
+ resolve(blobReader.result);
76
+ });
77
+ blobReader.addEventListener('error', handleError);
78
+ blobReader.addEventListener('abort', handleError);
79
+ });
80
+ };
81
+
82
+ const getResponseBody = async (responseType: string, response: any) => {
83
+ try {
84
+ if (responseType === 'blob' && response) {
85
+ return await parseResponseBlob(response as unknown as Blob);
86
+ }
87
+ return response ?? null;
88
+ } catch (error) {
89
+ console.warn("getResponseBody---error---", error)
90
+ return null;
91
+ }
92
+ };
93
+
94
+ class HTTPInterceptor {
95
+ private static _index = 0;
96
+ private ignoredHosts: Set<string> | undefined;
97
+ private ignoredUrls: Set<string> | undefined;
98
+ private ignoredPatterns: RegExp[] | undefined;
99
+ // 只保存正在请求中的
100
+ private allRequests = new Map<string, Partial<HttpRequestInfo>>();
101
+
102
+ private userListeners: [
103
+ EventName,
104
+ (data: Partial<HttpRequestInfo>) => Promise<void> | void,
105
+ ][] = [];
106
+
107
+ private enabled = false;
108
+
109
+ addListener = (
110
+ eventName: EventName,
111
+ listener: (data: Partial<HttpRequestInfo>) => Promise<void> | void,
112
+ ) => {
113
+ // 如果之前已经订阅过了就过滤掉
114
+ if (
115
+ this.userListeners.find(
116
+ ([name, tempListener]) =>
117
+ name === eventName && tempListener === listener,
118
+ )
119
+ ) {
120
+ return;
121
+ }
122
+ this.userListeners.push([eventName, listener]);
123
+
124
+ return () => {
125
+ this.userListeners = this.userListeners.filter(
126
+ ([name, tempListener]) =>
127
+ name !== eventName || tempListener !== listener,
128
+ );
129
+ };
130
+ };
131
+
132
+ removeListener = (
133
+ eventName: EventName,
134
+ listener: (data: Partial<HttpRequestInfo>) => Promise<void> | void,
135
+ ) => {
136
+ this.userListeners = this.userListeners.filter(
137
+ ([name, tempListener]) => name !== eventName || tempListener !== listener,
138
+ );
139
+ };
140
+
141
+ removeAllListener() {
142
+ this.userListeners = [];
143
+ }
144
+
145
+ private listenerHandle = (
146
+ eventName: EventName,
147
+ data: Partial<HttpRequestInfo>,
148
+ ) => {
149
+ this.userListeners.forEach(async ([name, listener]) => {
150
+ try {
151
+ if (name === eventName) {
152
+ await listener(data);
153
+ }
154
+ } catch (error: any) {
155
+ console.warn(`eventName=${eventName}, error=${error?.message}`)
156
+ }
157
+ });
158
+ };
159
+
160
+ private openHandle = (method: RequestMethod, url: string, xhr: XHR) => {
161
+ if (this.ignoredHosts) {
162
+ const host = extractHost(url);
163
+ if (host && this.ignoredHosts.has(host)) {
164
+ return;
165
+ }
166
+ }
167
+ if (this.ignoredUrls && this.ignoredUrls.has(url)) {
168
+ return;
169
+ }
170
+
171
+ if (this.ignoredPatterns) {
172
+ if (
173
+ this.ignoredPatterns.some(pattern => pattern.test(`${method} ${url}`))
174
+ ) {
175
+ return;
176
+ }
177
+ }
178
+ xhr.uniqueId = HTTPInterceptor._index + generateUniqueId();
179
+ const newRequest = {
180
+ id: xhr.uniqueId,
181
+ method,
182
+ url,
183
+ };
184
+ this.allRequests.set(xhr.uniqueId, newRequest);
185
+ this.listenerHandle('open', newRequest);
186
+ };
187
+
188
+ private requestHeaderHandle = (header: string, value: string, xhr: XHR) => {
189
+ const currentRequest = this.allRequests.get(xhr.uniqueId);
190
+ if (!currentRequest) {
191
+ return;
192
+ }
193
+ if (!currentRequest.requestHeaders) {
194
+ currentRequest.requestHeaders = {};
195
+ }
196
+ currentRequest.requestHeaders[header] = value;
197
+ this.listenerHandle('requestHeader', currentRequest);
198
+ };
199
+
200
+ private headerReceivedHandle = (
201
+ responseContentType: string,
202
+ responseSize: number,
203
+ responseHeaders: Headers,
204
+ xhr: XHR,
205
+ ) => {
206
+ const currentRequest = this.allRequests.get(xhr.uniqueId);
207
+ if (!currentRequest) {
208
+ return;
209
+ }
210
+ currentRequest.responseContentType = responseContentType;
211
+ currentRequest.responseSize = responseSize;
212
+ currentRequest.responseHeaders = xhr.responseHeaders;
213
+ this.listenerHandle('headerReceived', currentRequest);
214
+ };
215
+
216
+ private responseHandle = async (
217
+ status: number,
218
+ timeout: number,
219
+ response: any,
220
+ responseURL: string,
221
+ responseType: string,
222
+ xhr: XHR,
223
+ ) => {
224
+ const currentRequest = this.allRequests.get(xhr.uniqueId);
225
+ if (!currentRequest) {
226
+ return;
227
+ }
228
+ currentRequest.endTime = Date.now();
229
+ currentRequest.status = status;
230
+ currentRequest.timeout = timeout;
231
+ currentRequest.responseData = await getResponseBody(responseType, response);
232
+ currentRequest.responseURL = responseURL;
233
+ currentRequest.responseType = responseType;
234
+ currentRequest.duration =
235
+ currentRequest.endTime - (currentRequest.startTime ?? 0);
236
+ this.listenerHandle('response', currentRequest);
237
+ this.allRequests.delete(xhr.uniqueId);
238
+ };
239
+
240
+ private sendHandle = (data: any, xhr: XHR) => {
241
+ const currentRequest = this.allRequests.get(xhr.uniqueId);
242
+ if (!currentRequest) {
243
+ return;
244
+ }
245
+ try {
246
+ if (data && typeof data === 'object' && data instanceof FormData) {
247
+ currentRequest.requestData = formDataToString(data);
248
+ } else {
249
+ currentRequest.requestData = JSON.parse(data);
250
+ }
251
+ } catch (error) {
252
+ currentRequest.requestData = null;
253
+ }
254
+ currentRequest.startTime = Date.now();
255
+ this.listenerHandle('send', currentRequest);
256
+ };
257
+
258
+ setIgnoredUrls = (ignoredUrls: string[]) => {
259
+ if (ignoredUrls?.length) {
260
+ if (
261
+ !Array.isArray(ignoredUrls) ||
262
+ typeof ignoredUrls[0] !== 'string'
263
+ ) {
264
+ console.warn(
265
+ 'ignoredUrls must be an array of strings. The logger has not been started.',
266
+ );
267
+ return;
268
+ }
269
+ this.ignoredUrls = new Set(ignoredUrls);
270
+ }
271
+ }
272
+
273
+ enable = (options?: StartNetworkLoggingOptions) => {
274
+ try {
275
+ if (
276
+ this.enabled ||
277
+ (XHRInterceptor.isInterceptorEnabled() && !options?.forceEnable)
278
+ ) {
279
+ if (!this.enabled) {
280
+ console.warn(
281
+ 'network interceptor has not been enabled as another interceptor is already running (e.g. another debugging program). Use option `forceEnable: true` to override this behaviour.',
282
+ );
283
+ }
284
+ return;
285
+ }
286
+
287
+ if (options?.ignoredHosts) {
288
+ if (
289
+ !Array.isArray(options.ignoredHosts) ||
290
+ typeof options.ignoredHosts[0] !== 'string'
291
+ ) {
292
+ console.warn(
293
+ 'ignoredHosts must be an array of strings. The logger has not been started.',
294
+ );
295
+ return;
296
+ }
297
+ this.ignoredHosts = new Set(options.ignoredHosts);
298
+ }
299
+
300
+ if (options?.ignoredPatterns) {
301
+ this.ignoredPatterns = options.ignoredPatterns;
302
+ }
303
+ this.setIgnoredUrls(options?.ignoredUrls)
304
+ XHRInterceptor.setOpenCallback(this.openHandle);
305
+ XHRInterceptor.setRequestHeaderCallback(this.requestHeaderHandle);
306
+ XHRInterceptor.setHeaderReceivedCallback(this.headerReceivedHandle);
307
+ XHRInterceptor.setSendCallback(this.sendHandle);
308
+ XHRInterceptor.setResponseCallback(this.responseHandle);
309
+ XHRInterceptor.enableInterception();
310
+ this.enabled = true;
311
+ } catch (error) {}
312
+ };
313
+
314
+ disable = () => {
315
+ if (!this.enabled) {
316
+ return;
317
+ }
318
+ XHRInterceptor.disableInterception();
319
+ this.enabled = false;
320
+ };
321
+
322
+ reset = () => {
323
+ this.disable();
324
+ this.removeAllListener();
325
+ this.ignoredHosts = undefined;
326
+ this.ignoredUrls = undefined;
327
+ this.ignoredPatterns = undefined;
328
+ this.allRequests.clear();
329
+ };
330
+ }
331
+
332
+ const SafeHTTPInterceptor = createClassWithErrorHandling(HTTPInterceptor)
333
+ const httpInterceptor = new SafeHTTPInterceptor();
334
+ export {
335
+ type StartNetworkLoggingOptions,
336
+ httpInterceptor,
337
+ type EventName,
338
+ type RequestMethod,
339
+ };
package/src/Server.ts ADDED
@@ -0,0 +1,166 @@
1
+ import { hasPort, sleep } from "./utils";
2
+ import Zeroconf from "react-native-zeroconf";
3
+ import { getBaseData, getErrMsg, LOG_KEY } from "./common";
4
+ import logger from "./logger";
5
+ import md5 from 'crypto-js/md5';
6
+
7
+
8
+ const DEFAULT_PORT = 27751;
9
+ class Server {
10
+ private baseUrlObj: Record<string, string> = {};
11
+ private timeout: number;
12
+ private baseData: Record<string, any> = {};
13
+ private urlsListener: (
14
+ urls: string[],
15
+ urlsObj: Record<string, string>
16
+ ) => void;
17
+ private innerBaseData: Record<string, string> = {};
18
+
19
+ constructor(url?: string | Record<string, string>, timeout: number = 30000) {
20
+ if (typeof url === "string") {
21
+ this.updateUrl(url);
22
+ } else {
23
+ this.setBaseUrlObj(url ?? {});
24
+ }
25
+ this.timeout = timeout;
26
+ this.innerBaseData = getBaseData();
27
+ this.handleZeroConf();
28
+ }
29
+
30
+ addUrlsListener = (
31
+ onNewUrlCallback: (urls: string[], urlsObj: Record<string, string>) => void
32
+ ) => {
33
+ this.urlsListener = onNewUrlCallback;
34
+ };
35
+
36
+ private requestJoin = async (url: string, token: string) => {
37
+ const response = await fetch(url, {
38
+ method: "POST",
39
+ headers: {
40
+ "Content-Type": "application/json;charset=utf-8",
41
+ },
42
+ body: JSON.stringify({
43
+ token,
44
+ model: this.innerBaseData.model,
45
+ id: md5(JSON.stringify(this.innerBaseData)).toString(),
46
+ }),
47
+ });
48
+ if (response.status !== 200) {
49
+ return false;
50
+ }
51
+ const json = await response.json();
52
+
53
+ if (json.code !== 0) {
54
+ return false;
55
+ }
56
+ return true
57
+ }
58
+
59
+ private async handleZeroConf() {
60
+ try {
61
+ const Zeroconf = require("react-native-zeroconf")?.default;
62
+ if (!Zeroconf) {
63
+ return;
64
+ }
65
+ // @ts-ignore
66
+ const zeroconf: Zeroconf = new Zeroconf();
67
+ zeroconf.on("resolved", async (service) => {
68
+ try {
69
+ const { path, token } = service.txt ?? {};
70
+ if (!(path && token) || this.baseUrlObj[token]) {
71
+ return;
72
+ }
73
+ const url = `http://${service.host}:${service.port}`;
74
+ if (!(await this.requestJoin(`${url}${path}`, token))) {
75
+ return;
76
+ }
77
+ this.baseUrlObj[token] = url;
78
+ if (this.urlsListener) {
79
+ this.urlsListener(this.getUrls(), this.baseUrlObj);
80
+ }
81
+ } catch (error) {
82
+ logger.warn(LOG_KEY, "加入日志系统失败---", error);
83
+ }
84
+ });
85
+ zeroconf.scan("http", "tcp");
86
+ } catch (error: any) {
87
+ logger.warn(LOG_KEY, "zeroconf扫描或处理相关逻辑失败或者您根本就没有安装 react-native-zeroconf ,如果您没有安装,那么您将无法使用发现功能", error);
88
+ }
89
+ }
90
+
91
+ updateTimeout(timeout = 3000) {
92
+ this.timeout = timeout;
93
+ }
94
+
95
+ getUrls() {
96
+ return Object.values(this.baseUrlObj).map((e) => {
97
+ if (hasPort(e)) {
98
+ return e;
99
+ }
100
+ return `${e}:${DEFAULT_PORT}`;
101
+ });
102
+ }
103
+
104
+ private send = async (
105
+ path: string,
106
+ data: Record<string, any>
107
+ ): Promise<void> => {
108
+ const request = async (url: string, _data: Record<string, any>) => {
109
+ await Promise.race([
110
+ fetch(`${url}/${path}`, {
111
+ method: "POST",
112
+ headers: {
113
+ "Content-Type": "application/json;charset=utf-8",
114
+ },
115
+ body: JSON.stringify(
116
+ { ...this.innerBaseData, ...this.baseData, ..._data },
117
+ (_, val) => {
118
+ if (val instanceof Error) {
119
+ return val.toString();
120
+ }
121
+ return val;
122
+ }
123
+ ),
124
+ }),
125
+ sleep(this.timeout, true),
126
+ ]);
127
+ };
128
+ try {
129
+ if (Object.keys(this.baseUrlObj).length === 0) {
130
+ return;
131
+ }
132
+ await Promise.all(this.getUrls().map(async (e) => request(e, data)));
133
+ } catch (error: any) {
134
+ Object.values(this.baseUrlObj).map(async (e) =>
135
+ request(e, getErrMsg(error)).catch((_) => {})
136
+ );
137
+ }
138
+ };
139
+
140
+ updateUrl(url: string) {
141
+ const tempUrl = url.includes("http") ? url : `http://${url}`;
142
+ if (!url) {
143
+ delete this.baseUrlObj["Default"];
144
+ } else {
145
+ this.baseUrlObj["Default"] = tempUrl;
146
+ }
147
+ }
148
+
149
+ setBaseUrlObj(urlObj: Record<string, string>) {
150
+ this.baseUrlObj = urlObj;
151
+ }
152
+
153
+ updateBaseData(data: Record<string, any>) {
154
+ this.baseData = data;
155
+ }
156
+
157
+ log = async (data: Record<string, any>) => {
158
+ return this.send("log", data);
159
+ };
160
+
161
+ network = async (data: Record<string, any>) => {
162
+ return this.send("network", data);
163
+ };
164
+ }
165
+
166
+ export default Server;
@@ -0,0 +1,45 @@
1
+ class FileReader {
2
+ constructor() {
3
+ this.result = null;
4
+ this.error = null;
5
+ this.readyState = FileReader.EMPTY;
6
+ }
7
+
8
+ static EMPTY = 0;
9
+ static LOADING = 1;
10
+ static DONE = 2;
11
+
12
+ addEventListener(event, callback) {
13
+ this[`on${event}`] = callback;
14
+ }
15
+
16
+ removeEventListener(event, callback) {
17
+ if (this[`on${event}`] === callback) {
18
+ this[`on${event}`] = null;
19
+ }
20
+ }
21
+
22
+ readAsText(blob) {
23
+ this._read(blob, 'text');
24
+ }
25
+
26
+ readAsArrayBuffer(blob) {
27
+ this._read(blob, 'arraybuffer');
28
+ }
29
+
30
+ _read(blob, resultType) {
31
+ this.readyState = FileReader.LOADING;
32
+ setTimeout(() => {
33
+ this.readyState = FileReader.DONE;
34
+ if (resultType === 'text') {
35
+ this.result = blob.text();
36
+ } else if (resultType === 'arraybuffer') {
37
+ // 这里我们简单地返回一个空的 ArrayBuffer
38
+ this.result = new ArrayBuffer(0);
39
+ }
40
+ if (this.onload) this.onload({target: this});
41
+ }, 0);
42
+ }
43
+ }
44
+
45
+ module.exports = FileReader;
@@ -0,0 +1,39 @@
1
+ // __mocks__/react-native/Libraries/XHRInterceptor.js
2
+
3
+ class XHRInterceptor {
4
+ static _isInterceptorEnabled = false
5
+ static openCallback = null
6
+ static requestHeaderCallback = null
7
+ static headerReceivedCallback = null
8
+ static sendCallback = null
9
+ static responseCallback = null
10
+
11
+ static setOpenCallback = jest.fn((callback) => {
12
+ XHRInterceptor.openCallback = callback;
13
+ })
14
+ static setRequestHeaderCallback = jest.fn((callback) => {
15
+ XHRInterceptor.requestHeaderCallback = callback;
16
+ })
17
+ static setHeaderReceivedCallback = jest.fn((callback) => {
18
+ XHRInterceptor.headerReceivedCallback = callback;
19
+ })
20
+ static setSendCallback = jest.fn((callback) => {
21
+ XHRInterceptor.sendCallback = callback;
22
+ })
23
+ static setResponseCallback = jest.fn((callback) => {
24
+ XHRInterceptor.responseCallback = callback;
25
+ })
26
+
27
+ static enableInterception = jest.fn(() => {
28
+ XHRInterceptor._isInterceptorEnabled = true;
29
+ })
30
+
31
+ static disableInterception = jest.fn(() => {
32
+ XHRInterceptor._isInterceptorEnabled = false;
33
+ })
34
+
35
+ static isInterceptorEnabled = jest.fn(() => XHRInterceptor._isInterceptorEnabled)
36
+ };
37
+
38
+ module.exports = XHRInterceptor;
39
+