|
@@ -0,0 +1,393 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * BUG数据提交工具层
|
|
|
|
|
+ *
|
|
|
|
|
+ * Copyright © 2025 imengyu.top imengyu-bugreport-server
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+export interface BugReporterConfig {
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 设置抽象层以 BugReporter 正常工作
|
|
|
|
|
+ */
|
|
|
|
|
+ abstractionLayer: BugReporterAbstractionLayer,
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 错误收集服务器URL
|
|
|
|
|
+ */
|
|
|
|
|
+ serverUrl: string,
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 应用ID
|
|
|
|
|
+ */
|
|
|
|
|
+ appId: number,
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 应用KEY
|
|
|
|
|
+ */
|
|
|
|
|
+ appKey: string,
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 是否立即提交错误(通常用于调试),否则为定时收集错误发送。默认:false
|
|
|
|
|
+ */
|
|
|
|
|
+ reportImmediately?: boolean,
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 定时收集错误模式时错误数量超过此数值,进行提交。默认:8
|
|
|
|
|
+ */
|
|
|
|
|
+ delayMaxCount?: number,
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 定时收集错误模式时, 超过指定时间(天),则进行提交。默认:2
|
|
|
|
|
+ */
|
|
|
|
|
+ delayTime?: number,
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 在应用一次运行中,如果相同数量的错误出现次数超出此阈值,则强制提交一次错误。默认:4
|
|
|
|
|
+ */
|
|
|
|
|
+ errorSameRunTimeLimit?: number,
|
|
|
|
|
+}
|
|
|
|
|
+export interface BugReporterAbstractionLayer {
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取设备信息钩子
|
|
|
|
|
+ */
|
|
|
|
|
+ getDeviceInfo: () => Promise<BugDetailDeviceInfo>;
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 输出调试日志的钩子函数,这个函数返回false,将不启用错误提交功能
|
|
|
|
|
+ * 通常用于在开发时禁止提交错误到服务器
|
|
|
|
|
+ */
|
|
|
|
|
+ log: (str: string) => void;
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 是否启用
|
|
|
|
|
+ */
|
|
|
|
|
+ enable: () => Promise<boolean>;
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 自定义HTTP请求钩子
|
|
|
|
|
+ */
|
|
|
|
|
+ doPost: (url: string, body: unknown) => Promise<void>;
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 自定义获取存储钩子
|
|
|
|
|
+ */
|
|
|
|
|
+ getStorage: (key: string) => Promise<unknown>;
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 自定义设置存储钩子
|
|
|
|
|
+ */
|
|
|
|
|
+ setStorage: (key: string, value: unknown) => Promise<void>;
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 自定义删除存储钩子
|
|
|
|
|
+ */
|
|
|
|
|
+ reomveStorage: (key: string) => Promise<void>;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+//========================================================
|
|
|
|
|
+
|
|
|
|
|
+export function stringHash(str: string) : string {
|
|
|
|
|
+ let hash = 0, i, chr;
|
|
|
|
|
+ if (str.length === 0) return hash.toString();
|
|
|
|
|
+ for (i = 0; i < str.length; i++) {
|
|
|
|
|
+ chr = str.charCodeAt(i);
|
|
|
|
|
+ hash = ((hash << 5) - hash) + chr;
|
|
|
|
|
+ hash |= 0; // Convert to 32bit integer
|
|
|
|
|
+ }
|
|
|
|
|
+ return hash.toString();
|
|
|
|
|
+}
|
|
|
|
|
+function pad(num: number|string, n: number) : string {
|
|
|
|
|
+ let str = num.toString();
|
|
|
|
|
+ let len = str.length;
|
|
|
|
|
+ while (len < n) {
|
|
|
|
|
+ str = "0" + num;
|
|
|
|
|
+ len++;
|
|
|
|
|
+ }
|
|
|
|
|
+ return str;
|
|
|
|
|
+}
|
|
|
|
|
+function formatDate(date: Date, formatStr?: string) {
|
|
|
|
|
+ let str = formatStr ? formatStr : "YYYY-MM-dd HH:ii:ss";
|
|
|
|
|
+ str = str.replace(/yyyy|YYYY/, date.getFullYear().toString());
|
|
|
|
|
+ str = str.replace(/MM/, pad(date.getMonth() + 1, 2));
|
|
|
|
|
+ str = str.replace(/M/, (date.getMonth() + 1).toString());
|
|
|
|
|
+ str = str.replace(/dd|DD/, pad(date.getDate(), 2));
|
|
|
|
|
+ str = str.replace(/d/, date.getDate().toString());
|
|
|
|
|
+ str = str.replace(/HH/, pad(date.getHours(), 2));
|
|
|
|
|
+ str = str.replace(/hh/, pad(date.getHours() > 12 ? date.getHours() - 12 : date.getHours(), 2));
|
|
|
|
|
+ str = str.replace(/mm/, pad(date.getMinutes(), 2));
|
|
|
|
|
+ str = str.replace(/ii/, pad(date.getMinutes(), 2));
|
|
|
|
|
+ str = str.replace(/ss/, pad(date.getSeconds(), 2));
|
|
|
|
|
+ return str;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+//========================================================
|
|
|
|
|
+
|
|
|
|
|
+const errStorageKey = 'BugReporterStorage';
|
|
|
|
|
+const errStorageCountKey = 'BugReporterStorageCount';
|
|
|
|
|
+const errStorageTimeKey = 'BugReporterStorageLastSubmitTime';
|
|
|
|
|
+const errStorageCustomKey = 'BugReporterStorageData';
|
|
|
|
|
+
|
|
|
|
|
+//========================================================
|
|
|
|
|
+
|
|
|
|
|
+const customDataArr = new Map<string, string>();
|
|
|
|
|
+const onceTimeHashCheck = new Map<string, number>();
|
|
|
|
|
+const defaultConfig : BugReporterConfig = {
|
|
|
|
|
+ abstractionLayer: {} as BugReporterAbstractionLayer,
|
|
|
|
|
+ serverUrl: '',
|
|
|
|
|
+ appId: 0,
|
|
|
|
|
+ appKey: '',
|
|
|
|
|
+ reportImmediately: false,
|
|
|
|
|
+ delayMaxCount: 8,
|
|
|
|
|
+ delayTime: 2,
|
|
|
|
|
+ errorSameRunTimeLimit: 4,
|
|
|
|
|
+};
|
|
|
|
|
+let abstractionLayer : BugReporterAbstractionLayer = {} as BugReporterAbstractionLayer;
|
|
|
|
|
+let config : BugReporterConfig = {} as BugReporterConfig;
|
|
|
|
|
+
|
|
|
|
|
+//========================================================
|
|
|
|
|
+
|
|
|
|
|
+const BugReporter = {
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 配置
|
|
|
|
|
+ */
|
|
|
|
|
+ config(configData: BugReporterConfig) : void {
|
|
|
|
|
+ abstractionLayer = configData.abstractionLayer;
|
|
|
|
|
+ config = {
|
|
|
|
|
+ ...defaultConfig,
|
|
|
|
|
+ ...configData,
|
|
|
|
|
+ };
|
|
|
|
|
+ if (!abstractionLayer)
|
|
|
|
|
+ throw new Error('[BugReporter] You have not configured abstractionLayer, so you cannot use BugReporter.');
|
|
|
|
|
+ if (!configData.serverUrl)
|
|
|
|
|
+ abstractionLayer.log('serverUrl is not configured!');
|
|
|
|
|
+ if (!configData.appId)
|
|
|
|
|
+ abstractionLayer.log('appId is not configured!');
|
|
|
|
|
+ if (!configData.appKey)
|
|
|
|
|
+ abstractionLayer.log('appKey is empty!');
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 添加与错误一并提交的自定义数据。注意,自定义数据不是持久保存的,在应用关闭后会丢失
|
|
|
|
|
+ * @param key
|
|
|
|
|
+ * @param value
|
|
|
|
|
+ */
|
|
|
|
|
+ addCustomData(key: string, value: string) : void {
|
|
|
|
|
+ customDataArr.set(key, value);
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取之前设置的自定义数据
|
|
|
|
|
+ * @param key
|
|
|
|
|
+ */
|
|
|
|
|
+ getCustomData(key: string) : string|null {
|
|
|
|
|
+ return customDataArr.get(key) || null;
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 删除与错误一并提交的自定义数据
|
|
|
|
|
+ * @param key
|
|
|
|
|
+ */
|
|
|
|
|
+ deleteCustomData(key: string) : void {
|
|
|
|
|
+ customDataArr.delete(key);
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 设置与错误一并提交的用户ID数据
|
|
|
|
|
+ * @param userId
|
|
|
|
|
+ */
|
|
|
|
|
+ async setUserId(userId: string) : Promise<void> {
|
|
|
|
|
+ await abstractionLayer.setStorage(errStorageCustomKey + 'UserId', userId);
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取之前设置的用户ID
|
|
|
|
|
+ * @returns
|
|
|
|
|
+ */
|
|
|
|
|
+ async getUserId(): Promise<string | null> {
|
|
|
|
|
+ return await abstractionLayer.getStorage(errStorageCustomKey + 'UserId') as string|null;
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 程序启动时收集错误信息并提交
|
|
|
|
|
+ */
|
|
|
|
|
+ async checkAndReportBug() : Promise<boolean> {
|
|
|
|
|
+ if(!await abstractionLayer.enable())
|
|
|
|
|
+ return false;
|
|
|
|
|
+
|
|
|
|
|
+ const errorCount = await abstractionLayer.getStorage(errStorageCountKey) as number || 0;
|
|
|
|
|
+ const submitLastTime = new Date(await abstractionLayer.getStorage(errStorageTimeKey) as string);
|
|
|
|
|
+
|
|
|
|
|
+ if (
|
|
|
|
|
+ errorCount > 0 &&
|
|
|
|
|
+ (
|
|
|
|
|
+ errorCount > (config.delayMaxCount as number)
|
|
|
|
|
+ || new Date().getTime() - submitLastTime.getTime() > (config.delayTime as number) * 1000 * 60 * 60 * 24
|
|
|
|
|
+ )
|
|
|
|
|
+ ) {
|
|
|
|
|
+ abstractionLayer.log('The number or time exceeds the threshold, now submit bug ');
|
|
|
|
|
+ abstractionLayer.log('Last submit time ' + formatDate(submitLastTime));
|
|
|
|
|
+ abstractionLayer.log('Error count ' + errorCount + '/' + config.delayMaxCount);
|
|
|
|
|
+
|
|
|
|
|
+ this.reportBugsInStorage();
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ abstractionLayer.log('No need to submit bug now');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return false;
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 提交在暂存区中的错误信息
|
|
|
|
|
+ */
|
|
|
|
|
+ async reportBugsInStorage() : Promise<void> {
|
|
|
|
|
+ if(!await abstractionLayer.enable())
|
|
|
|
|
+ return ;
|
|
|
|
|
+
|
|
|
|
|
+ const errorCount = await abstractionLayer.getStorage(errStorageCountKey) as number || 0;
|
|
|
|
|
+ abstractionLayer.log('Error count ' + errorCount + '/' + config.delayMaxCount);
|
|
|
|
|
+
|
|
|
|
|
+ //开始提交
|
|
|
|
|
+ const bugArr = [] as unknown[];
|
|
|
|
|
+ for(let i = 0; i < errorCount; i++) {
|
|
|
|
|
+ const obj = await abstractionLayer.getStorage(errStorageKey + i);
|
|
|
|
|
+ if (obj && typeof obj === 'object' && typeof (obj as Record<string, unknown>).errorType === 'number')
|
|
|
|
|
+ bugArr.push(obj);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ abstractionLayer.log('Submit error now, count ' + bugArr.length + ' storage count: ' + errorCount);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ //立即提交错误
|
|
|
|
|
+ abstractionLayer.doPost(config.serverUrl, {
|
|
|
|
|
+ appId: config.appId,
|
|
|
|
|
+ app_key: config.appKey,
|
|
|
|
|
+ data: {
|
|
|
|
|
+ errorList: bugArr
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ abstractionLayer.log('Failed to report bug, will submit next time error: ' + e);
|
|
|
|
|
+
|
|
|
|
|
+ //太多错误堆积,需要删除一些
|
|
|
|
|
+ if (errorCount > 256) {
|
|
|
|
|
+ abstractionLayer.log('Too many errors, clear some errors ' + (errorCount / 3));
|
|
|
|
|
+ for(let i = 0; i < errorCount / 3; i++)
|
|
|
|
|
+ abstractionLayer.reomveStorage(errStorageKey + i);
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ //清除之前存储的数据
|
|
|
|
|
+ for(let i = 0; i < errorCount; i++)
|
|
|
|
|
+ abstractionLayer.reomveStorage(errStorageKey + i);
|
|
|
|
|
+ abstractionLayer.setStorage(errStorageCountKey, 0);
|
|
|
|
|
+ abstractionLayer.log('Clear bug storage count: ' + errorCount);
|
|
|
|
|
+ abstractionLayer.setStorage(errStorageTimeKey, new Date().toISOString()); //设置当前时间
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 提交Bug
|
|
|
|
|
+ * @param type 0 普通Bug 1 请求Bug 2 崩溃Bug 3 其他
|
|
|
|
|
+ * @param hash 对这个错误信息进行归类的hash,当相同hash错误发生时,将在后台系统中归类显示,有助于您查找Bug
|
|
|
|
|
+ * @param summary 对这个错误进行简要描述的文案,有助于您查找Bug
|
|
|
|
|
+ * @param data 主要错误信息
|
|
|
|
|
+ * @param extendData 附加错误信息
|
|
|
|
|
+ */
|
|
|
|
|
+ async reportBug(type: number, hash: string, summary: string, data: unknown, extendData?: Record<string, string>) : Promise<void> {
|
|
|
|
|
+ if(!await abstractionLayer.enable())
|
|
|
|
|
+ return;
|
|
|
|
|
+
|
|
|
|
|
+ //获取设备信息
|
|
|
|
|
+ const deviceInfo = await abstractionLayer.getDeviceInfo();
|
|
|
|
|
+
|
|
|
|
|
+ //追加自定义数据
|
|
|
|
|
+ const finalExtendData : Record<string, string> = extendData || {};
|
|
|
|
|
+ customDataArr.forEach((v, k) => finalExtendData[k] = v);
|
|
|
|
|
+
|
|
|
|
|
+ //检查本次运行错误数量
|
|
|
|
|
+ let forceReportThisTime = false;
|
|
|
|
|
+ const onceTimeCount = onceTimeHashCheck.get(hash);
|
|
|
|
|
+ if (onceTimeCount) {
|
|
|
|
|
+ onceTimeHashCheck.set(hash, onceTimeCount + 1);
|
|
|
|
|
+ if (onceTimeCount > (config.errorSameRunTimeLimit as number))
|
|
|
|
|
+ forceReportThisTime = true;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ onceTimeHashCheck.set(hash, 1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ //错误对象
|
|
|
|
|
+ const errorObj = {
|
|
|
|
|
+ errorHash: hash,
|
|
|
|
|
+ errorSummary: summary,
|
|
|
|
|
+ errorData: JSON.stringify(data),
|
|
|
|
|
+ errorDeviceName: deviceInfo.deviceName,
|
|
|
|
|
+ errorDeviceVersion: deviceInfo.deviceVersion,
|
|
|
|
|
+ errorAppVersion: deviceInfo.appVersion,
|
|
|
|
|
+ errorUserId: await this.getUserId(),
|
|
|
|
|
+ errorAdditionData: JSON.stringify(finalExtendData),
|
|
|
|
|
+ errorTime: formatDate(new Date()),
|
|
|
|
|
+ errorType: type
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (config.reportImmediately) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ //立即提交错误
|
|
|
|
|
+ abstractionLayer.doPost(config.serverUrl, {
|
|
|
|
|
+ appId: config.appId,
|
|
|
|
|
+ appKey: config.appKey,
|
|
|
|
|
+ data: {
|
|
|
|
|
+ errorList: [ errorObj ]
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ abstractionLayer.log('Failed to report bug. error: ' + e);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ //存储当前条目至存储中
|
|
|
|
|
+ const errorCount = await abstractionLayer.getStorage(errStorageCountKey) as number || 0;
|
|
|
|
|
+ abstractionLayer.setStorage(errStorageKey + errorCount, errorObj);//设置存储
|
|
|
|
|
+ abstractionLayer.setStorage(errStorageCountKey, errorCount + 1);//数量+1
|
|
|
|
|
+
|
|
|
|
|
+ //检查相关阈值,并提交错误
|
|
|
|
|
+ if (forceReportThisTime)
|
|
|
|
|
+ this.reportBugsInStorage();
|
|
|
|
|
+ else
|
|
|
|
|
+ await this.checkAndReportBug();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 提交请求类错误
|
|
|
|
|
+ * @param data 主要错误信息
|
|
|
|
|
+ * @param extendData 附加错误信息
|
|
|
|
|
+ */
|
|
|
|
|
+ async reportRequestBug(data: BugDetailErrorRequestData, extendData?: Record<string, string>) : Promise<void> {
|
|
|
|
|
+ await this.reportBug(1, stringHash(data.errorMessage), `请求错误:${data.errorMessage}`, data, extendData);
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 提交崩溃类错误
|
|
|
|
|
+ * @param data 主要错误信息
|
|
|
|
|
+ * @param extendData 附加错误信息
|
|
|
|
|
+ */
|
|
|
|
|
+ async reportCrashBug(data: BugDetailErrorCrashData, extendData?: Record<string, string>) : Promise<void> {
|
|
|
|
|
+ await this.reportBug(2, stringHash(data.errorMessage), `崩溃:${data.errorMessage}`, data, extendData);
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 提交普通错误
|
|
|
|
|
+ * @param e 主要错误信息
|
|
|
|
|
+ * @param extendData 附加错误信息
|
|
|
|
|
+ */
|
|
|
|
|
+ async reportError(e: Error|string, extendData?: Record<string, string>) : Promise<void> {
|
|
|
|
|
+ await this.reportBug(2,
|
|
|
|
|
+ stringHash(typeof e === 'string' ? e : (e.message || ('' + e))),
|
|
|
|
|
+ '' + e,
|
|
|
|
|
+ (e instanceof Error ? e.stack : ''),
|
|
|
|
|
+ extendData
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export interface BugDetailDeviceInfo {
|
|
|
|
|
+ deviceName: string;
|
|
|
|
|
+ deviceVersion: string;
|
|
|
|
|
+ appVersion: string;
|
|
|
|
|
+}
|
|
|
|
|
+export interface BugDetailErrorRequestData {
|
|
|
|
|
+ apiName: string;
|
|
|
|
|
+ apiUrl: string;
|
|
|
|
|
+ code: string;
|
|
|
|
|
+ errorType: string;
|
|
|
|
|
+ errorMessage: string;
|
|
|
|
|
+ errorCodeMessage: string;
|
|
|
|
|
+ data: unknown;
|
|
|
|
|
+ rawData: unknown;
|
|
|
|
|
+ rawRequest: unknown;
|
|
|
|
|
+}
|
|
|
|
|
+export interface BugDetailErrorCrashData {
|
|
|
|
|
+ deviceName: string;
|
|
|
|
|
+ deviceRom: string;
|
|
|
|
|
+ deviceSystemVersion: string;
|
|
|
|
|
+ errorCode: string;
|
|
|
|
|
+ errorMessage: string;
|
|
|
|
|
+ errorStack: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export default BugReporter;
|