@wutiange/log-listener-plugin 1.3.2 → 2.0.1-alpha.2
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 +130 -24
- package/dist/src/HTTPInterceptor.d.ts +1 -0
- package/dist/src/HTTPInterceptor.js +12 -9
- package/dist/src/HTTPInterceptor.js.map +1 -1
- package/dist/src/Server.d.ts +13 -6
- package/dist/src/Server.js +119 -41
- package/dist/src/Server.js.map +1 -1
- package/dist/src/__mocks__/react-native/Libraries/Blob/FileReader.js +0 -1
- package/dist/src/__mocks__/react-native/Libraries/Blob/FileReader.js.map +1 -1
- package/dist/src/__mocks__/react-native/Libraries/Network/XHRInterceptor.js +0 -1
- package/dist/src/__mocks__/react-native/Libraries/Network/XHRInterceptor.js.map +1 -1
- package/dist/src/__tests__/Server.test.js +76 -115
- package/dist/src/__tests__/Server.test.js.map +1 -1
- package/dist/src/common.d.ts +19 -10
- package/dist/src/common.js +63 -4
- package/dist/src/common.js.map +1 -1
- package/dist/src/logPlugin.d.ts +12 -9
- package/dist/src/logPlugin.js +87 -82
- package/dist/src/logPlugin.js.map +1 -1
- package/dist/src/logger.d.ts +6 -0
- package/dist/src/logger.js +16 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/utils.js +12 -7
- package/dist/src/utils.js.map +1 -1
- package/index.ts +3 -0
- package/package.json +18 -12
- package/src/HTTPInterceptor.ts +339 -0
- package/src/Server.ts +164 -0
- package/src/__mocks__/react-native/Libraries/Blob/FileReader.js +45 -0
- package/src/__mocks__/react-native/Libraries/Network/XHRInterceptor.js +39 -0
- package/src/__tests__/HTTPInterceptor.test.ts +322 -0
- package/src/__tests__/Server.test.ts +150 -0
- package/src/__tests__/utils.test.ts +113 -0
- package/src/common.ts +70 -0
- package/src/logPlugin.ts +224 -0
- package/src/logger.ts +15 -0
- package/src/utils.ts +112 -0
- package/tsconfig.json +27 -0
@@ -0,0 +1,150 @@
|
|
1
|
+
import Server from '../Server';
|
2
|
+
import { sleep } from '../utils';
|
3
|
+
|
4
|
+
// Mock dependencies
|
5
|
+
jest.mock('../utils', () => {
|
6
|
+
const actual = jest.requireActual('../utils');
|
7
|
+
return {
|
8
|
+
...actual, // 保留所有真实实现
|
9
|
+
sleep: jest.fn() // 只模拟 sleep 函数
|
10
|
+
};
|
11
|
+
});
|
12
|
+
|
13
|
+
|
14
|
+
// Mock require for react-native-zeroconf
|
15
|
+
jest.mock('react-native-zeroconf', () => undefined, { virtual: true });
|
16
|
+
|
17
|
+
// Mock fetch
|
18
|
+
global.fetch = jest.fn();
|
19
|
+
|
20
|
+
describe('Server', () => {
|
21
|
+
let server: Server;
|
22
|
+
|
23
|
+
beforeEach(() => {
|
24
|
+
// Clear all mocks before each test
|
25
|
+
jest.clearAllMocks();
|
26
|
+
(global.fetch as jest.Mock).mockReset();
|
27
|
+
});
|
28
|
+
|
29
|
+
describe('Constructor and URL Management', () => {
|
30
|
+
it('should initialize with default values', () => {
|
31
|
+
server = new Server();
|
32
|
+
expect(server.getUrls()).toEqual([]);
|
33
|
+
});
|
34
|
+
|
35
|
+
it('should initialize with custom URL', () => {
|
36
|
+
server = new Server('localhost:8080');
|
37
|
+
expect(server.getUrls()).toEqual(['http://localhost:8080']);
|
38
|
+
});
|
39
|
+
|
40
|
+
it('should handle URLs with and without http prefix', () => {
|
41
|
+
server = new Server();
|
42
|
+
server.updateUrl('localhost:8080');
|
43
|
+
expect(server.getUrls()).toEqual(['http://localhost:8080']);
|
44
|
+
|
45
|
+
server.updateUrl('http://localhost:8080');
|
46
|
+
expect(server.getUrls()).toEqual(['http://localhost:8080']);
|
47
|
+
});
|
48
|
+
});
|
49
|
+
|
50
|
+
describe('ZeroConf Handling', () => {
|
51
|
+
it('should handle case when zeroconf is not available', () => {
|
52
|
+
server = new Server();
|
53
|
+
const mockListener = jest.fn();
|
54
|
+
server.addUrlsListener(mockListener);
|
55
|
+
|
56
|
+
// Since Zeroconf is not available, the listener should not be called
|
57
|
+
expect(mockListener).not.toHaveBeenCalled();
|
58
|
+
});
|
59
|
+
|
60
|
+
// Test with mock Zeroconf implementation
|
61
|
+
it('should handle case when zeroconf is available', () => {
|
62
|
+
// Temporarily mock require to return a mock Zeroconf implementation
|
63
|
+
const mockZeroconfInstance = {
|
64
|
+
on: jest.fn(),
|
65
|
+
scan: jest.fn()
|
66
|
+
};
|
67
|
+
|
68
|
+
jest.doMock('react-native-zeroconf', () => ({
|
69
|
+
__esModule: true,
|
70
|
+
default: jest.fn(() => mockZeroconfInstance)
|
71
|
+
}), { virtual: true });
|
72
|
+
|
73
|
+
server = new Server();
|
74
|
+
const mockListener = jest.fn();
|
75
|
+
server.addUrlsListener(mockListener);
|
76
|
+
|
77
|
+
// Verify that Zeroconf methods were not called since module is mocked as undefined
|
78
|
+
expect(mockListener).not.toHaveBeenCalled();
|
79
|
+
});
|
80
|
+
});
|
81
|
+
|
82
|
+
describe('Data Sending', () => {
|
83
|
+
beforeEach(() => {
|
84
|
+
server = new Server('localhost:8080');
|
85
|
+
(global.fetch as jest.Mock).mockImplementation(() =>
|
86
|
+
Promise.resolve({ ok: true })
|
87
|
+
);
|
88
|
+
});
|
89
|
+
|
90
|
+
it('should send log data', async () => {
|
91
|
+
const testData = { message: 'test log' };
|
92
|
+
await server.log(testData);
|
93
|
+
|
94
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
95
|
+
'http://localhost:8080/log',
|
96
|
+
expect.objectContaining({
|
97
|
+
method: 'POST',
|
98
|
+
headers: {
|
99
|
+
'Content-Type': 'application/json;charset=utf-8'
|
100
|
+
},
|
101
|
+
body: expect.any(String)
|
102
|
+
})
|
103
|
+
);
|
104
|
+
});
|
105
|
+
|
106
|
+
it('should send network data', async () => {
|
107
|
+
const testData = { url: 'test.com' };
|
108
|
+
await server.network(testData);
|
109
|
+
|
110
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
111
|
+
'http://localhost:8080/network',
|
112
|
+
expect.objectContaining({
|
113
|
+
method: 'POST',
|
114
|
+
headers: {
|
115
|
+
'Content-Type': 'application/json;charset=utf-8'
|
116
|
+
},
|
117
|
+
body: expect.any(String)
|
118
|
+
})
|
119
|
+
);
|
120
|
+
});
|
121
|
+
|
122
|
+
it('should handle timeout', async () => {
|
123
|
+
server.updateTimeout(100);
|
124
|
+
(sleep as jest.Mock).mockImplementation(() => Promise.reject(new Error('Timeout')));
|
125
|
+
|
126
|
+
const testData = { message: 'test' };
|
127
|
+
await server.log(testData);
|
128
|
+
|
129
|
+
expect(global.fetch).toHaveBeenCalled();
|
130
|
+
expect(sleep).toHaveBeenCalledWith(100, true);
|
131
|
+
});
|
132
|
+
});
|
133
|
+
|
134
|
+
describe('Base Data Management', () => {
|
135
|
+
it('should update base data', async () => {
|
136
|
+
server = new Server('localhost:8080');
|
137
|
+
const baseData = { userId: '123' };
|
138
|
+
server.updateBaseData(baseData);
|
139
|
+
|
140
|
+
await server.log({ message: 'test' });
|
141
|
+
|
142
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
143
|
+
expect.any(String),
|
144
|
+
expect.objectContaining({
|
145
|
+
body: expect.stringContaining('"userId":"123"')
|
146
|
+
})
|
147
|
+
);
|
148
|
+
});
|
149
|
+
});
|
150
|
+
});
|
@@ -0,0 +1,113 @@
|
|
1
|
+
import { createClassWithErrorHandling, hasPort } from '../utils';
|
2
|
+
|
3
|
+
describe('createClassWithErrorHandling', () => {
|
4
|
+
class TestClass {
|
5
|
+
normalMethod(): string {
|
6
|
+
return 'normal';
|
7
|
+
}
|
8
|
+
|
9
|
+
errorMethod(): void {
|
10
|
+
throw new Error('Test error');
|
11
|
+
}
|
12
|
+
|
13
|
+
async asyncMethod(): Promise<string> {
|
14
|
+
return 'async';
|
15
|
+
}
|
16
|
+
|
17
|
+
async asyncErrorMethod(): Promise<void> {
|
18
|
+
throw new Error('Async test error');
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
let consoleErrorSpy: jest.SpyInstance;
|
23
|
+
|
24
|
+
beforeEach(() => {
|
25
|
+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
26
|
+
});
|
27
|
+
|
28
|
+
afterEach(() => {
|
29
|
+
consoleErrorSpy.mockRestore();
|
30
|
+
});
|
31
|
+
|
32
|
+
test('should not interfere with normal methods', () => {
|
33
|
+
const EnhancedClass = createClassWithErrorHandling(TestClass);
|
34
|
+
const instance = new EnhancedClass();
|
35
|
+
expect(instance.normalMethod()).toBe('normal');
|
36
|
+
});
|
37
|
+
|
38
|
+
test('should catch and log errors from methods', () => {
|
39
|
+
const EnhancedClass = createClassWithErrorHandling(TestClass);
|
40
|
+
const instance = new EnhancedClass();
|
41
|
+
expect(() => instance.errorMethod()).toThrow('Test error');
|
42
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Error in errorMethod:', expect.any(Error));
|
43
|
+
});
|
44
|
+
|
45
|
+
test('should not interfere with async methods that resolve', async () => {
|
46
|
+
const EnhancedClass = createClassWithErrorHandling(TestClass);
|
47
|
+
const instance = new EnhancedClass();
|
48
|
+
await expect(instance.asyncMethod()).resolves.toBe('async');
|
49
|
+
});
|
50
|
+
|
51
|
+
test('should catch and log errors from async methods that reject', async () => {
|
52
|
+
const EnhancedClass = createClassWithErrorHandling(TestClass);
|
53
|
+
const instance = new EnhancedClass();
|
54
|
+
await expect(instance.asyncErrorMethod()).rejects.toThrow('Async test error');
|
55
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Error in asyncErrorMethod:', expect.any(Error));
|
56
|
+
});
|
57
|
+
|
58
|
+
test('should handle methods added after instantiation', () => {
|
59
|
+
const EnhancedClass = createClassWithErrorHandling(TestClass);
|
60
|
+
const instance = new EnhancedClass();
|
61
|
+
(instance as any).dynamicMethod = function(): void {
|
62
|
+
throw new Error('Dynamic method error');
|
63
|
+
};
|
64
|
+
expect(() => (instance as any).dynamicMethod()).toThrow('Dynamic method error');
|
65
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Error in dynamicMethod:', expect.any(Error));
|
66
|
+
});
|
67
|
+
});
|
68
|
+
|
69
|
+
|
70
|
+
describe('hasPort function', () => {
|
71
|
+
test('should return true for URLs with explicit ports', () => {
|
72
|
+
expect(hasPort('http://example.com:8080')).toBe(true);
|
73
|
+
expect(hasPort('ftp://example.com:210')).toBe(true);
|
74
|
+
});
|
75
|
+
|
76
|
+
test('should return false for URLs without explicit ports', () => {
|
77
|
+
expect(hasPort('http://example.com')).toBe(false);
|
78
|
+
expect(hasPort('https://example.com')).toBe(false);
|
79
|
+
expect(hasPort('ftp://example.com')).toBe(false);
|
80
|
+
});
|
81
|
+
|
82
|
+
test('should return false for invalid URLs', () => {
|
83
|
+
expect(hasPort('not a url')).toBe(false);
|
84
|
+
expect(hasPort('http:/example.com')).toBe(false);
|
85
|
+
expect(hasPort('example.com:8080')).toBe(false);
|
86
|
+
});
|
87
|
+
|
88
|
+
test('should return false for empty input', () => {
|
89
|
+
expect(hasPort('')).toBe(false);
|
90
|
+
});
|
91
|
+
|
92
|
+
test('should return false for non-string input', () => {
|
93
|
+
expect(hasPort(null as any)).toBe(false);
|
94
|
+
expect(hasPort(undefined as any)).toBe(false);
|
95
|
+
expect(hasPort(123 as any)).toBe(false);
|
96
|
+
expect(hasPort({} as any)).toBe(false);
|
97
|
+
});
|
98
|
+
|
99
|
+
test('should handle URLs with default ports correctly', () => {
|
100
|
+
expect(hasPort('http://example.com:80')).toBe(false);
|
101
|
+
expect(hasPort('https://example.com:443')).toBe(false);
|
102
|
+
});
|
103
|
+
|
104
|
+
test('should handle URLs with IPv6 addresses', () => {
|
105
|
+
expect(hasPort('http://[2001:db8::1]:8080')).toBe(true);
|
106
|
+
expect(hasPort('https://[2001:db8::1]')).toBe(false);
|
107
|
+
});
|
108
|
+
|
109
|
+
test('should handle URLs with userinfo', () => {
|
110
|
+
expect(hasPort('http://user:pass@example.com:8080')).toBe(true);
|
111
|
+
expect(hasPort('http://user:pass@example.com')).toBe(false);
|
112
|
+
});
|
113
|
+
});
|
package/src/common.ts
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
import logger from "./logger";
|
2
|
+
|
3
|
+
export const URLS_KEY = 'log-listener-plugin-urls$$key'
|
4
|
+
export const DEFAULT_TIMEOUT = 3000
|
5
|
+
export const LOG_KEY = '[@wutiange/log-listener-plugin 日志]'
|
6
|
+
export enum Level {
|
7
|
+
LOG = 'log',
|
8
|
+
WARN = 'warn',
|
9
|
+
ERROR = 'error',
|
10
|
+
}
|
11
|
+
|
12
|
+
export enum Tag {
|
13
|
+
LOG_PLUGIN_INTERNAL_ERROR = 'log-plugin-internal-error',
|
14
|
+
DEFAULT = 'default',
|
15
|
+
}
|
16
|
+
|
17
|
+
const getDefaultDeviceINfo = () => {
|
18
|
+
try {
|
19
|
+
const {Platform} = require('react-native')
|
20
|
+
return {
|
21
|
+
SystemName: Platform.OS,
|
22
|
+
Version: Platform.Version,
|
23
|
+
...Platform.constants
|
24
|
+
}
|
25
|
+
} catch (error) {
|
26
|
+
logger.warn(LOG_KEY, '这个插件只能在 react-native 中使用')
|
27
|
+
return {}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
export const getBaseData = (): Record<string, string> => {
|
32
|
+
|
33
|
+
try {
|
34
|
+
const DeviceInfo = require("react-native-device-info")?.default;
|
35
|
+
return {
|
36
|
+
Brand: DeviceInfo.getBrand(),
|
37
|
+
Model: DeviceInfo.getModel(),
|
38
|
+
AppVersion: DeviceInfo.getVersion(),
|
39
|
+
Carrier: DeviceInfo.getCarrierSync(),
|
40
|
+
Manufacturer: DeviceInfo.getManufacturerSync(),
|
41
|
+
SystemName: DeviceInfo.getSystemName(),
|
42
|
+
...getDefaultDeviceINfo()
|
43
|
+
};
|
44
|
+
} catch (error) {
|
45
|
+
return getDefaultDeviceINfo()
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
export const getDefaultStorage = (): Storage => {
|
50
|
+
try {
|
51
|
+
const AsyncStorage = require("@react-native-async-storage/async-storage")?.default;
|
52
|
+
return AsyncStorage;
|
53
|
+
} catch (error) {
|
54
|
+
return null;
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
|
59
|
+
export const getErrMsg = (error: any) => {
|
60
|
+
return {
|
61
|
+
message: [
|
62
|
+
`${
|
63
|
+
error?.message ?? error
|
64
|
+
}--->这是@wutiange/log-listener-plugin内部错误,请提issue反馈,issue地址:https://github.com/wutiange/log-listener-plugin/issues`,
|
65
|
+
],
|
66
|
+
tag: Tag.LOG_PLUGIN_INTERNAL_ERROR,
|
67
|
+
level: Level.ERROR,
|
68
|
+
createTime: Date.now(),
|
69
|
+
}
|
70
|
+
}
|
package/src/logPlugin.ts
ADDED
@@ -0,0 +1,224 @@
|
|
1
|
+
import Server from './Server';
|
2
|
+
import { createClassWithErrorHandling } from './utils';
|
3
|
+
import { httpInterceptor } from './HTTPInterceptor';
|
4
|
+
import { DEFAULT_TIMEOUT, getDefaultStorage, Level, LOG_KEY, Tag, URLS_KEY } from './common';
|
5
|
+
import logger from './logger';
|
6
|
+
|
7
|
+
type Options = {
|
8
|
+
/**
|
9
|
+
* storage 用于存储已设置的日志系统的 url
|
10
|
+
* @default @react-native-async-storage/async-storage
|
11
|
+
*/
|
12
|
+
storage?: Storage
|
13
|
+
/**
|
14
|
+
* 设置上传日志的超时时间,单位为毫秒
|
15
|
+
* @default 3000
|
16
|
+
*/
|
17
|
+
timeout?: number
|
18
|
+
/**
|
19
|
+
* 日志系统的url
|
20
|
+
*/
|
21
|
+
testUrl?: string
|
22
|
+
/**
|
23
|
+
* 是否自动开启日志记录
|
24
|
+
* @default false
|
25
|
+
*/
|
26
|
+
isAuto?: boolean
|
27
|
+
/**
|
28
|
+
* 设置日志系统的基础数据,这些数据会自动添加到每条日志中
|
29
|
+
*/
|
30
|
+
baseData?: Record<string, any>
|
31
|
+
}
|
32
|
+
|
33
|
+
class LogPlugin {
|
34
|
+
private server: Server | null = null;
|
35
|
+
private timeout: number | null = null;
|
36
|
+
private isAuto = false
|
37
|
+
private storage: Storage | null = getDefaultStorage();
|
38
|
+
|
39
|
+
constructor() {
|
40
|
+
this.init()
|
41
|
+
}
|
42
|
+
|
43
|
+
private init = async () => {
|
44
|
+
this.server = new Server();
|
45
|
+
if (!this.storage) {
|
46
|
+
logger.warn(LOG_KEY, '你并没有设置 storage ,这会导致 App 杀死后可能需要重新加入日志系统才能收集日志数据,建议你设置 storage 。')
|
47
|
+
} else {
|
48
|
+
const urlsStr = await this.storage.getItem(URLS_KEY)
|
49
|
+
if (urlsStr) {
|
50
|
+
const urls = JSON.parse(urlsStr)
|
51
|
+
this.server.setBaseUrlObj(urls)
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
|
56
|
+
this.server.addUrlsListener((_, urlsObj) => {
|
57
|
+
if (this.storage) {
|
58
|
+
this.storage.setItem(URLS_KEY, JSON.stringify(urlsObj))
|
59
|
+
}
|
60
|
+
httpInterceptor.setIgnoredUrls(this.handleIgnoredUrls())
|
61
|
+
})
|
62
|
+
}
|
63
|
+
|
64
|
+
config = ({ storage, timeout, testUrl, isAuto, baseData = {} }: Options) => {
|
65
|
+
if (isAuto) {
|
66
|
+
this.auto()
|
67
|
+
} else {
|
68
|
+
this.unAuto()
|
69
|
+
}
|
70
|
+
this.storage = storage ?? getDefaultStorage();
|
71
|
+
this.setTimeout(timeout ?? DEFAULT_TIMEOUT)
|
72
|
+
this.setBaseUrl(testUrl)
|
73
|
+
this.setBaseData(baseData)
|
74
|
+
};
|
75
|
+
|
76
|
+
/**
|
77
|
+
* @deprecated 这个方法将在下一个主要版本中被移除。请使用 config({isAuto: true}) 替代。
|
78
|
+
*/
|
79
|
+
auto = () => {
|
80
|
+
this.startRecordNetwork();
|
81
|
+
this.startRecordLog();
|
82
|
+
this.isAuto = true
|
83
|
+
}
|
84
|
+
|
85
|
+
unAuto = () => {
|
86
|
+
this.stopRecordLog()
|
87
|
+
httpInterceptor.disable()
|
88
|
+
httpInterceptor.removeAllListener()
|
89
|
+
this.isAuto = false
|
90
|
+
}
|
91
|
+
|
92
|
+
startRecordLog = () => {
|
93
|
+
console.log = (...data: any[]) => {
|
94
|
+
logger.log(...data)
|
95
|
+
this.log(...data);
|
96
|
+
};
|
97
|
+
|
98
|
+
console.warn = (...data: any[]) => {
|
99
|
+
logger.warn(...data)
|
100
|
+
this.warn(...data);
|
101
|
+
};
|
102
|
+
|
103
|
+
console.error = (...data: any[]) => {
|
104
|
+
logger.error(...data)
|
105
|
+
this.error(...data);
|
106
|
+
};
|
107
|
+
}
|
108
|
+
|
109
|
+
stopRecordLog = () => {
|
110
|
+
console.log = logger.log
|
111
|
+
console.warn = logger.warn
|
112
|
+
console.error = logger.error
|
113
|
+
}
|
114
|
+
|
115
|
+
private handleIgnoredUrls = () => {
|
116
|
+
const urls = this.server?.getUrls?.()
|
117
|
+
let ignoredUrls: string[] = []
|
118
|
+
if (urls?.length) {
|
119
|
+
ignoredUrls = urls.reduce((acc, url) => {
|
120
|
+
acc.push(`${url}/log`, `${url}/network`, `${url}/join`)
|
121
|
+
return acc
|
122
|
+
}, [] as string[])
|
123
|
+
}
|
124
|
+
return ignoredUrls
|
125
|
+
}
|
126
|
+
|
127
|
+
startRecordNetwork = () => {
|
128
|
+
httpInterceptor.addListener("send", (data) => {
|
129
|
+
this.server?.network({
|
130
|
+
url: data.url,
|
131
|
+
id: data.id,
|
132
|
+
method: data.method,
|
133
|
+
headers: data.requestHeaders,
|
134
|
+
body: data.requestData,
|
135
|
+
createTime: data.startTime
|
136
|
+
})
|
137
|
+
})
|
138
|
+
httpInterceptor.addListener("response", (data) => {
|
139
|
+
this.server?.network({
|
140
|
+
headers: data.responseHeaders,
|
141
|
+
body: data.responseData,
|
142
|
+
requestId: data.id,
|
143
|
+
statusCode: data.status,
|
144
|
+
endTime: data.endTime
|
145
|
+
})
|
146
|
+
})
|
147
|
+
httpInterceptor.enable({ignoredUrls: this.handleIgnoredUrls()})
|
148
|
+
}
|
149
|
+
|
150
|
+
/**
|
151
|
+
* @deprecated 这个方法将在下一个主要版本中被移除。请使用 config({testUrl: ''}) 替代。
|
152
|
+
*/
|
153
|
+
setBaseUrl = (url: string) => {
|
154
|
+
const tempUrl = url?.trim()
|
155
|
+
if (this.server) {
|
156
|
+
this.server.updateUrl(tempUrl);
|
157
|
+
} else {
|
158
|
+
this.server = new Server(tempUrl);
|
159
|
+
}
|
160
|
+
httpInterceptor.setIgnoredUrls(this.handleIgnoredUrls())
|
161
|
+
if (this.isAuto) {
|
162
|
+
this.startRecordNetwork();
|
163
|
+
this.startRecordLog()
|
164
|
+
}
|
165
|
+
}
|
166
|
+
|
167
|
+
/**
|
168
|
+
* @deprecated 这个方法将在下一个主要版本中被移除。请使用 config({timeout: 3000}) 替代。
|
169
|
+
*/
|
170
|
+
setTimeout = (timeout: number) => {
|
171
|
+
if (typeof timeout === 'number') {
|
172
|
+
this.timeout = timeout;
|
173
|
+
this.server?.updateTimeout(this.timeout);
|
174
|
+
}
|
175
|
+
}
|
176
|
+
|
177
|
+
/**
|
178
|
+
* @deprecated 这个方法将在下一个主要版本中被移除。移除后将不再支持获取超时时间。
|
179
|
+
*/
|
180
|
+
getTimeout = () => {
|
181
|
+
if (typeof this.timeout === 'number') {
|
182
|
+
return this.timeout;
|
183
|
+
}
|
184
|
+
return null;
|
185
|
+
}
|
186
|
+
|
187
|
+
/**
|
188
|
+
* @deprecated 这个方法将在下一个主要版本中被移除。请使用 config({baseData: {}}) 替代。
|
189
|
+
*/
|
190
|
+
setBaseData = (data: Record<string, any> = {}) => {
|
191
|
+
this.server.updateBaseData(data)
|
192
|
+
}
|
193
|
+
|
194
|
+
private _log = (level: string, tag: string, ...data: any[]) => {
|
195
|
+
const sendData = {
|
196
|
+
message: data,
|
197
|
+
tag,
|
198
|
+
level: level ?? 'log',
|
199
|
+
createTime: Date.now(),
|
200
|
+
};
|
201
|
+
this.server?.log(sendData);
|
202
|
+
}
|
203
|
+
|
204
|
+
tag = (tag: string, ...data: any[]) => {
|
205
|
+
this._log(Level.LOG, tag, ...data);
|
206
|
+
}
|
207
|
+
|
208
|
+
log = (...data: any[]) => {
|
209
|
+
this._log(Level.LOG, Tag.DEFAULT, ...data);
|
210
|
+
}
|
211
|
+
|
212
|
+
warn = (...data: any[]) => {
|
213
|
+
this._log(Level.WARN, Tag.DEFAULT, ...data);
|
214
|
+
}
|
215
|
+
|
216
|
+
error = (...data: any[]) => {
|
217
|
+
this._log(Level.ERROR, Tag.DEFAULT, ...data);
|
218
|
+
}
|
219
|
+
|
220
|
+
}
|
221
|
+
const SafeLogPlugin = createClassWithErrorHandling(LogPlugin)
|
222
|
+
const logPlugin = new SafeLogPlugin();
|
223
|
+
export { SafeLogPlugin };
|
224
|
+
export default logPlugin;
|
package/src/logger.ts
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
const [log, warn, error] = [console.log, console.warn, console.error];
|
2
|
+
|
3
|
+
const logger = {
|
4
|
+
log: (...data: any[]) => {
|
5
|
+
log(...data)
|
6
|
+
},
|
7
|
+
warn: (...data: any[]) => {
|
8
|
+
warn(...data)
|
9
|
+
},
|
10
|
+
error: (...data: any[]) => {
|
11
|
+
error(...data)
|
12
|
+
},
|
13
|
+
}
|
14
|
+
|
15
|
+
export default logger
|
package/src/utils.ts
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
import URL from "url-parse";
|
2
|
+
import logger from "./logger";
|
3
|
+
|
4
|
+
export function sleep(ms: number, isReject: boolean = false) {
|
5
|
+
return new Promise((resolve, reject) => {
|
6
|
+
setTimeout(isReject ? () => reject({
|
7
|
+
code: 11001,
|
8
|
+
key: '@wutiange/log-listener-plugin%%timeout',
|
9
|
+
message: 'Timeout'
|
10
|
+
}) : resolve, ms)
|
11
|
+
})
|
12
|
+
}
|
13
|
+
|
14
|
+
// 检查 url 是否有端口号,不包含内置的端口号,比如 80 ,443 等
|
15
|
+
export function hasPort(url: string) {
|
16
|
+
// 如果 url 是空的或不是字符串,返回 false
|
17
|
+
if (!url || typeof url !== 'string') {
|
18
|
+
return false;
|
19
|
+
}
|
20
|
+
|
21
|
+
try {
|
22
|
+
// 使用 URL 构造函数解析 URL
|
23
|
+
const parsedUrl = new URL(url);
|
24
|
+
|
25
|
+
// 检查 port 属性是否为空
|
26
|
+
// 注意:如果使用默认端口(如 HTTP 的 80 或 HTTPS 的 443),port 会是空字符串
|
27
|
+
return parsedUrl.port !== '';
|
28
|
+
} catch (error) {
|
29
|
+
logger.error(error)
|
30
|
+
// 如果 URL 无效,捕获错误并返回 false
|
31
|
+
return false;
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
|
36
|
+
type Constructor<T = {}> = new (...args: any[]) => T;
|
37
|
+
|
38
|
+
export function createClassWithErrorHandling<T extends Constructor>(BaseClass: T): T {
|
39
|
+
return new Proxy(BaseClass, {
|
40
|
+
construct(target: T, args: any[]): object {
|
41
|
+
const instance = new target(...args);
|
42
|
+
return new Proxy(instance, {
|
43
|
+
get(target: any, prop: string | symbol): any {
|
44
|
+
const value = target[prop];
|
45
|
+
if (typeof value === 'function') {
|
46
|
+
return function(this: any, ...args: any[]): any {
|
47
|
+
try {
|
48
|
+
const result = value.apply(this, args);
|
49
|
+
if (result instanceof Promise) {
|
50
|
+
return result.catch((error: Error) => {
|
51
|
+
console.error(`Error in ${String(prop)}:`, error);
|
52
|
+
throw error; // 重新抛出错误,以便调用者可以捕获它
|
53
|
+
});
|
54
|
+
}
|
55
|
+
return result;
|
56
|
+
} catch (error) {
|
57
|
+
console.error(`Error in ${String(prop)}:`, error);
|
58
|
+
throw error; // 重新抛出错误,以便调用者可以捕获它
|
59
|
+
}
|
60
|
+
};
|
61
|
+
}
|
62
|
+
return value;
|
63
|
+
},
|
64
|
+
set(target: any, prop: string | symbol, value: any): boolean {
|
65
|
+
if (typeof value === 'function') {
|
66
|
+
target[prop] = function(this: any, ...args: any[]): any {
|
67
|
+
try {
|
68
|
+
const result = value.apply(this, args);
|
69
|
+
if (result instanceof Promise) {
|
70
|
+
return result.catch((error: Error) => {
|
71
|
+
console.error(`Error in ${String(prop)}:`, error);
|
72
|
+
throw error;
|
73
|
+
});
|
74
|
+
}
|
75
|
+
return result;
|
76
|
+
} catch (error) {
|
77
|
+
console.error(`Error in ${String(prop)}:`, error);
|
78
|
+
throw error;
|
79
|
+
}
|
80
|
+
};
|
81
|
+
} else {
|
82
|
+
target[prop] = value;
|
83
|
+
}
|
84
|
+
return true;
|
85
|
+
}
|
86
|
+
});
|
87
|
+
}
|
88
|
+
});
|
89
|
+
}
|
90
|
+
|
91
|
+
|
92
|
+
export function formDataToString(formData: FormData): string {
|
93
|
+
const boundary =
|
94
|
+
'----WebKitFormBoundary' + Math.random().toString(36).substr(2);
|
95
|
+
let result = '';
|
96
|
+
// 这是 react-native 中的实现,这里面是存在这个方法的
|
97
|
+
const parts = (formData as any).getParts();
|
98
|
+
for (const part of parts) {
|
99
|
+
result += `--${boundary}\r\n`;
|
100
|
+
result += `Content-Disposition: ${part.headers['content-disposition']}\r\n`;
|
101
|
+
if (part.headers['content-type']) {
|
102
|
+
result += `Content-Type: ${part.headers['content-type']}\r\n`;
|
103
|
+
}
|
104
|
+
const value = 'string' in part ? part.string : part.uri;
|
105
|
+
result += `Content-Length: ${value.length}\r\n\r\n`;
|
106
|
+
result += `${value}\r\n`;
|
107
|
+
}
|
108
|
+
result += `--${boundary}--\r\n`;
|
109
|
+
return result;
|
110
|
+
}
|
111
|
+
|
112
|
+
|