import { NormalLog } from '../interface'; export { EventEmitter, InterfaceEventEmitter } from './event-emitter'; // 将logs转为字符串形式,例如:msg[0]=Error:%20error\n%20%20%20%20at%20http://127.0.0.1:8080/index.html:20:14&level[0]=4&msg[1]=info&level[1]=2&count=2 export const buildLogParam = function (logs: NormalLog | NormalLog[]) { logs = Array.isArray(logs) ? logs : [logs]; return ( `${logs .map((log: NormalLog, index) => Object.getOwnPropertyNames(log) .map((key: string) => `${encodeOnce(key)}[${index}]=${log[key] === undefined ? '' : encodeOnce(log[key])}`) .join('&')) .join('&')}${logs.length ? `&count=${logs.length}` : ''}` ); }; // 调用该方法保证所有的 string 只会进行一层 encode export const encodeOnce = function (str: string): string { try { return encodeURIComponent(decodeURIComponent(str)); } catch (e) { // TODO stringify 出错,调用sdk出错上报 return str; } }; // 上报默认值 export enum ReportDefaultVal { number = -1, string = '', } // 获取上报值 export const getReportVal = function (rawVal?: T, isDefaultByString?: boolean): T | ReportDefaultVal { if (typeof rawVal === 'number') { return rawVal; } if (typeof rawVal === 'string') { return rawVal; } return isDefaultByString ? ReportDefaultVal.string : ReportDefaultVal.number; }; // 去掉/获取url中的query export const formatUrl = function (url: string, isGetQuery?: boolean) { if (typeof url === 'string') { return url.split('?')[isGetQuery ? 1 : 0] || ''; } return url; }; // 判断url是否https export const urlIsHttps = function (url: string): boolean { return /^https/.test(url); }; // 判断是否是原生方法 export const isNative = function (Ctor: any): boolean { return typeof Ctor === 'function' && /native code/.test(Ctor.toString()); }; // 判断是否能使用performance export const canUseResourceTiming = function (): boolean { return ( typeof window.performance !== 'undefined' // && isNative(window.Performance) && typeof performance.clearResourceTimings === 'function' && typeof performance.getEntriesByType === 'function' && typeof performance.now === 'function' ); }; // 判断请求是请求接口还是请求静态资源 // 根据content-type判断xhr、fetch的是请求接口还是请求静态资源 // 使用枚举静态资源content-type的方法,不在枚举中的类型都将视为api接口。 const assetContentType: string[] = [ 'application/octet-stream', 'application/xhtml+xml', 'application/xml', 'application/pdf', 'application/pkcs12', 'application/javascript', 'application/ecmascript', 'application/vnd.mspowerpoint', 'application/ogg', 'text/html', 'text/css', 'text/javascript', 'image', 'audio', 'video', ]; export const isRequestAsset = function (contentType: string): boolean { return assetContentType.some(type => contentType.indexOf(type) !== -1); }; let possibleRetCode: string[] = ['ret', 'retcode', 'code']; export const tryToGetRetCode = (obj: any, api?: Record): string => { try { if (typeof obj === 'string') { obj = JSON.parse(obj); } // 是否是数组 if (typeof api?.ret?.join === 'function') { possibleRetCode = [].concat(api?.ret?.map((e: string) => e.toLowerCase())); } const keys = Object.getOwnPropertyNames(obj); const intersection = keys.filter(key => possibleRetCode.indexOf(key.toLowerCase()) !== -1); if (intersection.length) { return `${obj[intersection[0]]}`; } return 'unknown'; } catch (e) { return 'unknown'; } }; type Replacer = (key: string, value: any) => any; const stringifyHandler = function (): Replacer { const cache: any[] = []; const keyCache: string[] = []; return function (key, value) { if (value instanceof Error) { // 处理Error对象 return `Error.message: ${value.message} \n Error.stack: ${value.stack}`; } if (typeof value === 'object' && value !== null) { // 处理循环引用 const index = cache.indexOf(value); if (index !== -1) { return `[Circular ${keyCache[index]}]`; } cache.push(value); keyCache.push(key || 'root'); } return value; }; }; // 处理对象中含有Error对象 // 处理循环引用 export const stringifyPlus = function (target: any): string { // 如果target是字符串,则直接返回,避免二次加工,导致JSON标准字符串改坏 if (typeof target === 'string') { return target; } try { return ( JSON.stringify(target, stringifyHandler(), 4) || 'undefined' ).replace(/"/gim, ''); // 这里之所以要去掉字符串中的所有 “ " ” ,是因为传进来的是 Error 对象时会 stringify 两次 } catch (e) { return `error happen when aegis stringify: \n ${e.message} \n ${ e.stack }`; } }; /** * 获取xhr相关资源 * 此类存在意义:是fetch请求和XMLHttpRequest请求的payload数据格式相同 * @param {Any} data XMLHttpRequest对象 */ export class PayloadXHR { // 此对象所属类型 public type = 'xhr' // 原始数据, 这里表示XMLHttpRequest public data: any | {} public constructor(data: any) { this.data = data; } // 获取完整的url public sourceURL(): string { return this.data.responseURL; } // 获取请求状态码 public status(): number { return Number(this.data.status); } // 获取所有的headers,并且value都为string public headers(): object { const headersStr = this.data.getAllResponseHeaders(); const headersArr = headersStr.split('\n'); const headers: any = {}; headersArr.forEach((item: string) => { if (!!item) { const tempArr = item.split(': '); // eslint-disable-next-line prefer-destructuring const key = tempArr[0]; const value = tempArr[1].trim(); headers[key] = value; } }); return headers; } } /** * 获取fetch相关资源 * 此类存在意义:是fetch请求和XMLHttpRequest请求的payload数据格式相同 * @param {Any} data fetch请求的response对象 */ export class PayloadFetch { // 此对象所属类型 public type = 'fetch' // 原始数据, 这里表示fetch请求的response对象 public data: any | {} public response: any | {} public constructor(data: any, response: any) { this.data = data || {}; this.data.response = response; } // 获取完整的url public sourceURL(): string { return this.data.url; } // 获取请求状态码 public status(): number { return Number(this.data.status); } // 获取所有的headers,并且value都为string public headers(): object { const headers: any = {}; this.data.headers.forEach((value: any, key: any) => { headers[key] = value; }); return headers; } }