import type { ErrorReporter } from '../ErrorReporter'; import { Transport, TransportOptions, ReportItemWithContext } from '@tencent/merlin-core'; import { mapHrefToPath, mapHrefToPathWithMultiSearchKey, mapHrefToPathWithOneSearchKey } from './utils'; import type { ErrorReportItemData, StackFrame } from '../types'; /** 将 stackFrames 转换为 cube 支持的标准格式 */ function parseFrameStacksToCubeStack(stackFrames: StackFrame[], msg?: string, file?: string): string { const lines = stackFrames.map((frame) => { const filename = frame.filename === file ? '__FILE__' : frame.filename; if (frame.func === '?') { return ` at ${filename}:${frame.lineno}:${frame.colno}`; } return ` at ${frame.func} (${filename}:${frame.lineno}:${frame.colno})`; }); return `${msg ? `${msg}\n` : ''}${lines.join('\n')}`; } function defaultUserTokenMapper(_href?: string) { return undefined; } export interface CubeErrorTransportOptions extends TransportOptions { /** 协议 Id(可缺省,由下游接口决定) */ bizId?: number; /** 群组名(可缺省,由下游接口决定) */ groupKey?: string; /** 模块名(可缺省,由下游接口决定) */ moduleName?: string; /** 协议接口 */ url: string; /** 原始 href 到 page_uri 字段的映射,用于区分页面 * @default 去除 search 和 hash * @example https://qq.com/?a=1#haha/a => https://qq.com/ * - `(href: string) => string` 自定义映射函数 * - `string` 在缺省映射基础上,保留指定 search 字段 * - `string[]` 在缺省映射基础上,按顺序保留指定 search 字段 */ uriMapper?: ((href?: string) => string) | string | string[]; /** 接口预设 * @default 'avalon' * - `avalon` 适用于 avalon monitor 错误上报接口 * - `cube` 适用于直打 cube reportbizdata 接口 * - `custom` 非预设接口,会以对象数组形式上报所有数据 */ preset?: 'avalon' | 'cube' | 'custom'; } /** 按照标准协议格式通过 HTTP 方式直接上报至 Cube */ export class CubeErrorTransport extends Transport { protected url: string; protected bizId?: number; protected groupKey?: string; protected moduleName?: string; protected preset: 'avalon' | 'cube' | 'custom' = 'avalon'; protected uriMapper = mapHrefToPath; protected userTokenMapper: (href?: string) => string | undefined = defaultUserTokenMapper; constructor(options: CubeErrorTransportOptions) { super({ bufferSize: 5, flushInterval: 500, frequencyLimit: { max: 25, perSeconds: 900, }, ...options, }); const { bizId, groupKey, moduleName, uriMapper, url } = options; this.url = url; this.bizId = bizId; this.groupKey = groupKey; this.moduleName = moduleName; if (options.preset !== undefined) { this.preset = options.preset; } if (typeof uriMapper === 'string') { this.uriMapper = (href?: string) => mapHrefToPathWithOneSearchKey(href, uriMapper); } else if (Array.isArray(uriMapper)) { this.uriMapper = (href?: string) => mapHrefToPathWithMultiSearchKey(href, uriMapper); } else if (typeof uriMapper === 'function') { this.uriMapper = uriMapper; } } send(records: ReportItemWithContext[]) { const data = records.map((record) => { const parsedRecord: Record = { fingerprint: `${record.data.file || ''}:${record.data.line || ''}:${record.data.col || ''}`, file: this.stringify(record.data.file), line: this.stringify(record.data.line), col: this.stringify(record.data.col), stack: record.data.stackFrames?.length ? parseFrameStacksToCubeStack(record.data.stackFrames, record.data.message, record.data.file) : record.data.stack, release: this.stringify(record.context.base.release), msg: this.stringify(record.data.message), refer: this.stringify(record.context.scope.href), page: this.uriMapper(record.context.scope.href), user_token: this.userTokenMapper(record.context.scope.href), aid: this.stringify(record.context.base.user.aid), type: this.stringify(record.data.name), // TODO: pandora idx1: this.stringify(record.data.idx1), idx2: this.stringify(record.data.idx2), idx3: this.stringify(record.data.idx3), log: this.stringify(record.data.log), e_idx1: this.stringify(record.data.sysIdx1), e_idx2: this.stringify(record.data.sysIdx2), e_idx3: this.stringify(record.data.sysIdx3), extra: `ts:${record.data.timestamp}`, }; if (this.preset !== 'avalon') { // 非 avalon 时,加上一些基本字段 parsedRecord.biz_id = this.bizId; parsedRecord.time = Math.round(record.ctime / 1000); parsedRecord.group_key = this.stringify(this.groupKey); parsedRecord.mid = this.stringify(this.moduleName); parsedRecord.module_name = this.stringify(this.moduleName); parsedRecord.ua = this.stringify(record.context.env.userAgent); } return parsedRecord; }); this.reporter?.log('CubeErrorTransport.send', data); if (this.preset !== 'cube') { this.reporter?.httpPost( this.url, JSON.stringify({ items: data, }), { contentType: 'application/json', }, ); } else { // cube 按要求序列化 this.reporter?.httpPost(this.url, `report_items=${JSON.stringify(data).replace(/&/g, '%26')}`, { contentType: 'text/plain', }); } } /** 字符串化,若 undefined 则直接返回 undefined */ private stringify(value: any): string | undefined { if (typeof value === 'string') return value; if (value === undefined) return undefined; return `${value}`; } }