/* eslint-disable @typescript-eslint/member-ordering */ /* eslint-disable @typescript-eslint/no-useless-constructor */ /* eslint-disable no-useless-constructor */ import { getDefaultConfig } from './constant'; import { InterfaceEventEmitter, EventEmitter, buildLogParam, stringifyPlus, } from './util'; import { Config, LOG_TYPE, SendOption, SendSuccess, SendFail, SpeedLog, ReportTimeLog, SEND_TYPE, EventLog, } from './interface'; import Plugin from './plugin'; import { createThrottlePipe, createPipeline, formatNormalLogPipe, createWhitelistPipe, createWriteReportPipe, createErrorLogLimitPipe, } from './pipes'; const assignEmptyFunctionToObject = function (obj: object, sourceObj?: object) { const properties = Object.getOwnPropertyNames(obj); properties.forEach((item) => { // @ts-ignore if (typeof obj[item] === 'function' && item !== 'constructor') { if (sourceObj) { // @ts-ignore sourceObj[item] = function () { }; } else { // @ts-ignore obj[item] = function () { }; } } }); }; const assignNullToObject = function (obj: object, sourceObj?: object) { const propertyDescriptors = Object.getOwnPropertyDescriptors(obj); Object.keys(propertyDescriptors).forEach((item) => { if (!propertyDescriptors[item].writable) return; if (sourceObj) { // @ts-ignore sourceObj[item] = null; } else { // @ts-ignore obj[item] = null; } }); }; export default class Core { public static instances: Core[] = []; public constructor(config: Config) { Core.instances.push(this); } // 所有的日志类型 public static LOG_TYPE = LOG_TYPE; // 配置项 public config: Config = getDefaultConfig(); // 首屏时间信息记录 public firstScreenInfo: { element: any; timing: number; [key: string]: any; }; public isWhiteList = false; // 生命周期 public lifeCycle: InterfaceEventEmitter = new EventEmitter(); // 继承 Core 时需要提供该方法,send 在发送请求时,必须将 bean 中的数据拼接到 url 中 public bean: { [key: string]: string | number | boolean } = {}; public init(config: Config) { this.setConfig(config); // 执行已经安装的插件 // ps:这里只能用for循环,因为插件里可能会安装其他插件 // 如果使用forEach,后续安装的插件将遍历不到 // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < Core.installedPlugins.length; i++) { try { Core.installedPlugins[i].patch(this); } catch (e) { this.sendSDKError(e); } } this.lifeCycle.emit('onInited'); } // 设置配置 public setConfig(config: Partial) { Object.assign(this.config, config); const { id, uin, version, ext1, ext2, ext3, aid } = this.config; const shouldSendWhiteList = this.bean.id !== id || this.bean.uin !== uin || this.bean.aid !== aid; this.bean.id = id || ''; this.bean.uin = uin || ''; this.bean.version = version || VERSION; this.bean.aid = aid || ''; ext1 && (this.bean.ext1 = ext1); ext2 && (this.bean.ext2 = ext2); ext3 && (this.bean.ext3 = ext3); shouldSendWhiteList && this.lifeCycle.emit('onConfigChange', this.config); return this.config; } // 插件 private static installedPlugins: Plugin[] = []; public static use(plugin: Plugin) { if (Core.installedPlugins.indexOf(plugin) === -1 && plugin.aegisPlugin) { Core.installedPlugins.push(plugin); } } public static unuse(plugin: Plugin) { const index = Core.installedPlugins.indexOf(plugin); if (index !== -1) { Core.installedPlugins.splice(index, 1); } } public info(...msg: any) { this.normalLogPipeline({ msg, level: LOG_TYPE.INFO, }); } public infoAll(...msg: any) { this.normalLogPipeline({ msg, level: LOG_TYPE.INFO_ALL, }); } public report(...msg: any) { this.normalLogPipeline({ msg, level: LOG_TYPE.REPORT, }); } public error(...msg: any) { this.normalLogPipeline({ msg, level: LOG_TYPE.ERROR, }); } // 基础上报 public normalLogPipeline = createPipeline([ // 节流,之后的logs都将是数组 createThrottlePipe(this, 5), // 格式化 formatNormalLogPipe, // 同一个错误只上报5次 createErrorLogLimitPipe(this.config), // 在日志过滤写入之前处理一些杂事 createWriteReportPipe(this.lifeCycle.emit, this.config), // 白名单过滤,如果用户不在白名单中,将会把API_RESPONSE和INFO两种等级的日志过滤掉。 createWhitelistPipe(this), // 钩子beforeReport,ps: 只有 config 中的 beforeReport 能阻止上报 (logs, resolve) => { const newLogs = JSON.parse(JSON.stringify(logs)); this.lifeCycle.emit('beforeReport', newLogs); const { beforeReport } = this.config; if (typeof beforeReport === 'function') { logs = logs.filter((log: any) => beforeReport(log) !== false); } if (logs.length) { return resolve(logs); } }, // 上报 (logs) => { this.send( { url: this.config.url || '', data: buildLogParam(logs), method: 'post', contentType: 'application/x-www-form-urlencoded', type: SEND_TYPE.LOG, }, () => { const { onReport } = this.config; if (typeof onReport === 'function') { logs.forEach((log: any) => { onReport(log); }); } }, ); }, ]); // TODO:测速管道,不应该放到这里,但为了修复ts类型问题,后续需要删掉 public speedLogPipeline(log?: SpeedLog) { throw new Error('You need to override "speedLogPipeline" method'); } // 自定义PV public reportPv(id: number) { if (!id) return; const baseQuery = `${Object.getOwnPropertyNames(this.bean) .filter(key => key !== 'id') .map(key => `${key}=${this.bean[key]}`) .join('&')}`; this.send({ url: `${this.config.url}/${id}?${baseQuery}`, // 不能拼接bean上去,否则bean里面的id会覆盖链接中的id addBean: false, type: SEND_TYPE.CUSTOME_PV, }); } public reportEvent(event: string | EventLog) { if (!event) return; // 参数兼容逻辑 const eventParams = typeof event === 'string' ? { name: event, ext1: this.config.ext1 || '', ext2: this.config.ext2 || '', ext3: this.config.ext3 || '', } : event; if (!eventParams.name) { console.warn('reportEvent params error'); return; } this.eventPipeline(eventParams); } private eventPipeline = createPipeline([ // 节流 createThrottlePipe(this, 5), (logs: EventLog[]) => { this.send({ url: `${this.config.eventUrl}?${logs .map((e: EventLog, i: number) => { const ext1 = encodeURIComponent(e.ext1 || this.config.ext1 || ''); const ext2 = encodeURIComponent(e.ext2 || this.config.ext2 || ''); const ext3 = encodeURIComponent(e.ext3 || this.config.ext3 || ''); return `event[${i}]=${encodeURIComponent(e.name)}&ext1[${i}]=${ext1}&ext2[${i}]=${ext2}&ext3[${i}]=${ext3}`; }) .join('&')}`, type: SEND_TYPE.EVENT, }); }, ]); // 自定义测速 private timeMap: { [k: string]: number; } = {}; // 上报自定义测速 public reportTime(key: string | ReportTimeLog, duration?: number) { if (typeof key === 'object') { return this.reportT(key); } if (typeof key !== 'string') { console.warn('reportTime: first param must be a string'); return; } if (typeof duration !== 'number') { console.warn('reportTime: second param must be number'); return; } if (duration < 0 || duration > 60000) { console.warn('reportTime: duration must between 0 and 60000'); return; } this.submitCustomTime(key, duration); } // 上报自定义测速 public reportT(obj: ReportTimeLog) { const { name, duration, ext1 = '', ext2 = '', ext3 = '', from = '' } = obj; if (typeof name !== 'string' || typeof duration !== 'number' || typeof ext1 !== 'string' || typeof ext2 !== 'string' || typeof ext3 !== 'string' ) { console.warn('reportTime: params error'); return; } if (duration < 0 || duration > 60000) { console.warn('reportTime: duration must between 0 and 60000'); return; } return this.submitCustomTime(name, duration, ext1, ext2, ext3, from); } public time(key: string) { if (typeof key !== 'string') { console.warn('time: first param must be a string'); return; } if (!this.timeMap[key]) { this.timeMap[key] = Date.now(); } else { console.warn(`Timer ${key} already exists`); } } public timeEnd(key: string) { if (typeof key !== 'string') { console.warn('timeEnd: first param must be a string'); return; } if (this.timeMap[key]) { this.submitCustomTime(key, Date.now() - this.timeMap[key]); delete this.timeMap[key]; } else { console.warn(`Timer ${key} does not exist`); } } private submitCustomTime(name: string, duration: number, ext1?: string, ext2?: string, ext3?: string, from?: string) { this.customTimePipeline({ name, duration, ext1: ext1 || this.config.ext1, ext2: ext2 || this.config.ext2, ext3: ext3 || this.config.ext3, from: from || undefined, }); } private customTimePipeline = createPipeline([ // 节流 自定义测速因为数据量比较少,可以汇聚多一点 createThrottlePipe(this, 10), (logs) => { this.send({ url: `${this.config.customTimeUrl}?payload=${encodeURIComponent(JSON.stringify({ custom: logs }))}`, type: SEND_TYPE.CUSTOM, }); }, ]); // 扩展Bean数据 public extendBean(key: string, value: any) { this.bean[key] = value; } // eslint-disable-next-line send(options?: SendOption, success?: SendSuccess, fail?: SendFail) { throw new Error('You need to override "send" method'); } // SDK 发生错误时上报方法 public sendSDKError(err: any) { this.send({ url: `https://aegis.qq.com/collect?id=1085&msg[0]=${encodeURIComponent(stringifyPlus(err))}&level[0]=2&from=${this.config.id}&count=1&version=${this.config.id}(${VERSION})`, addBean: false, method: 'get', type: SEND_TYPE.SDK_ERROR, }); } public destroy(force = false) { // 删除实例 const instanceIndex = Core.instances.indexOf(this); if (instanceIndex !== -1) { Core.instances.splice(instanceIndex, 1); } // 删除插件中的实例 // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < Core.installedPlugins.length; i++) { try { const plugin = Core.installedPlugins[i]; plugin.unpatch(this); if (plugin.countInstance() === 0) { plugin.uninstall(); Core.unuse(plugin); (plugin as unknown) = null; } } catch (e) { this.sendSDKError(e); } } this.lifeCycle.emit('destroy'); this.lifeCycle.clear(); if (force) { // 清空实例 assignNullToObject(this); Object.setPrototypeOf(this, null); } else { // 用空方法替换实例方法 & 原型链上的方法 // 如果直接替换原型链上的方法,会导致其它实例的方法也被影响 // eslint-disable-next-line let objectForLoop = this; do { const constructorType = objectForLoop.constructor; if (constructorType !== Object) { assignEmptyFunctionToObject(objectForLoop, this); } } while (objectForLoop = Object.getPrototypeOf(objectForLoop)); // 没有其它实例时,清空类静态方法 if (Core.instances.length === 0) { const classObj = Object.getPrototypeOf(this).constructor; assignEmptyFunctionToObject(classObj); assignEmptyFunctionToObject(Core); } } } }