import type { BaseContext, Context, ReportItem, ScopeContext, ReportItemWithContext, UserInfo, LinkedData, } from './types'; import { Collector } from './Collector'; import { Transport } from './Transport'; import type { Adapter } from './Adapter'; import { adapters, AdapterType } from './adapters'; import { uuidV4 } from './utils'; export interface ReporterOptions, R extends Reporter> { /** 初始化用户信息 */ user?: UserInfo; /** 环境桥接器 */ adapter?: AdapterType; /** 收集器 */ collectors?: (Collector | undefined)[]; /** 上报器 */ transports?: (Transport | undefined)[]; /** 调试模式,是否输出内部调试日志 * @default false */ debug?: boolean; /** 构建版本,若未传入则缺省尝试从 BRANCH | VERSION 环境变量读入, * 若环境变量得到的结果为空,会在运行时尝试读取 globalThis.release 的值。 * 若显式传入了 false 则不会自动读取 */ release?: string | false; /** Merlin 内部错误处理 */ errorHandler?: (err: Error, detail?: Record) => void; /** 自定义用户唯一 Id 算法 * @desc 覆盖默认的 aid 算法 */ aIdGenerator?: () => string; } export class Reporter> { /** 局部上下文 */ scopeInfo: ScopeContext = { contextId: uuidV4(), }; protected userInfo: Partial = {}; /** 调试模式,是否输出内部调试日志 */ protected debug = false; /** 构建版本 */ protected release?: string; protected flushIntervalTimer?: ReturnType; /** 是否已经初始化过 */ protected initialized = false; /** 是否处于缓存态 * * 缓存态下,reporter 只会 report,但不 flush,在缓存态解除后会将缓存的 records flush 出去 */ protected buffering = false; protected buffer: ReportItemWithContext[] = []; protected adapter: Adapter = new adapters.web(); // TODO: 思考动态添加 Collector 和 Transport 的可行性(这样在代码中可以不必都想办法在一个统一的 init 方法中传入所有要用到的对象,比如请求服务的 Service,Vue router 和 app 实例等) protected collectors: Collector[] = []; protected transports: Transport[] = []; errorHandler: (err: Error, detail?: Record) => void = (err, detail) => { console.error('[Merlin]', { stack: err?.stack, msg: err?.message, detail, }); }; setOptions(options: ReporterOptions) { if (options.adapter) { this.adapter = new adapters[options.adapter](); } if (options.collectors) { this.collectors = options.collectors.filter((c) => Collector.isCollector(c)) as Collector[]; } if (options.transports) { this.transports = options.transports.filter((t) => Transport.isTransport(t)) as Transport[]; } if (options.debug !== undefined) { this.debug = options.debug; } if (options.errorHandler) { this.errorHandler = options.errorHandler; } if (options.aIdGenerator) { this.aIdGenerator = options.aIdGenerator; } if (options.user) { this.setUserInfo(options.user); } if (options.release !== false) { this.release = options.release || globalThis.release; } } init(options: ReporterOptions) { if (this.initialized) { this.errorHandler(new Error('reporter was already initialized')); return; } try { this.setOptions(options); this.adapter.init(this); const possiblyPromiseAid = this.adapter.getAid(); if (possiblyPromiseAid?.then) { // 若返回 Promise,异步初始化 aid 即可,无伤大雅 possiblyPromiseAid .then((aid) => { if (aid && typeof aid === 'string') { this.log('get aid from async storage', aid); this.userInfo.aid = aid; } else { const newAid = this.aIdGenerator?.(); this.userInfo.aid = newAid; this.adapter.saveAid(newAid); } }) .catch((_e) => { const newAid = this.aIdGenerator?.(); this.userInfo.aid = newAid; this.adapter.saveAid(newAid); }); } else if (possiblyPromiseAid && typeof possiblyPromiseAid === 'string') { this.log('get aid from storage', possiblyPromiseAid); this.userInfo.aid = possiblyPromiseAid; } else { const newAid = this.aIdGenerator?.(); this.userInfo.aid = newAid; this.adapter.saveAid(newAid); } this.transports.forEach((item) => { try { item.init(this); } catch (e) { console.error('[Merlin] transport init failed', e); this.errorHandler(new Error('transport init failed')); } }); this.collectors.forEach((item) => { try { item.init(this); } catch (e) { console.error('[Merlin] collector init failed', e); this.errorHandler(new Error('collector init failed')); } }); this.initialized = true; } catch (e) { this.errorHandler(e as Error); } } /** 调试用控制台 log */ log(...args: Parameters) { if (this.debug) { console.log('[Merlin]', ...args); } } get baseContext(): BaseContext { return { user: this.userInfo, release: this.release, }; } get scopeContext(): ScopeContext { return this.scopeInfo; } setUserInfo(info: UserInfo) { this.userInfo = this.userInfo.aid ? { ...info, aid: this.userInfo.aid, } : info; } // TODO: 看看是否可以把 buffering 的使用封装到 merlin 内部的配置项里,不用业务侧控制 /** 激活缓存态,会缓存接下来的所有上报,直到缓存态解除 */ enableBuffering() { this.buffering = true; } /** 解除缓存态 */ async settleBuffering() { if (!this.buffering) { return; } this.readLinkedData(); this.buffering = false; // 解除 buffering 时立刻触发一次上报 try { return await this.flush(); } catch (e) { this.errorHandler(e as Error); } } report(item: ReportItem) { if (!this.initialized) { console.warn('[Merlin] Reporter not initialized'); return; } // if (this.settled) { // console.warn('[Merlin] Reporter already settled'); // return; // } try { this.updateScopeInfo(); // report 的时候记下 context,虽然有点冗余,但是后面埋点发送的时候会更容易管理(每一个 item 都包含所有需要的数据) this.buffer.push({ ctime: Date.now(), type: item.type, data: item.data, context: this.getContext(), }); this.flush(); } catch (e) { this.errorHandler(e as Error, item); } } /** 读取环境相关 linkedData 并写入上下文 */ readLinkedData() { this.adapter.readLinkedData(); } get serializeLinkedData(): string { return this.adapter.serializeLinkedData() || ''; } get objectLinkedData(): LinkedData { return this.adapter.linkedData; } /** 当前 storage 是否支持同步读写 */ get supportSyncStorage() { return this.adapter.storage.supportSync; } getStorageItem(...args: Parameters['storage']['getItem']>) { return this.adapter.storage?.getItem(...args); } setStorageItem(...args: Parameters['storage']['setItem']>) { this.adapter.storage?.setItem(...args); } removeStorageItem(...args: Parameters['storage']['removeItem']>) { this.adapter.storage?.removeItem(...args); } httpPost(...args: Parameters['httpPost']>) { this.adapter.httpPost(...args); } /** 清算,回收所有埋点事件并立刻发送一次上报 */ async settle() { // 防止反复触发,这样可以用多层兜底来调用 settle try { this.collectors.forEach((collector) => collector.settle()); await this.flush(true); this.adapter.settle(); } catch (e) { this.errorHandler(e as Error); } } /** 将当前缓存的上报项全部发送给 Transports */ protected async flush(immediate = false) { if (this.buffering) return; const buffer = [...this.buffer]; this.buffer.length = 0; return this.sendToTransports(buffer, immediate); } protected getContext(): Context { return { base: this.baseContext, env: this.adapter.envInfo || {}, scope: this.scopeContext, }; } protected updateScopeInfo() { this.adapter.updateScopeInfo(); } protected aIdGenerator: () => string = () => uuidV4(); protected sendToTransports(records: ReportItemWithContext[], immediate = false) { return Promise.all(this.transports.map((transport) => transport.receiveFromReporter(records, immediate))); } }