文件结构
Core 部分是主要的代码逻辑
代码
拦截错误信息
1. Web错误信息
通过重写原生js事件拿到错误信息,重写 XMLHttpRequest 的 open,send 以及 fetch 事件来截取借口信息。
/**
*
* 重写对象上面的某个属性
* ../param source 需要被重写的对象
* ../param name 需要被重写对象的key
* ../param replacement 以原有的函数作为参数,执行并重写原有函数
* ../param isForced 是否强制重写(可能原先没有该属性)
* ../returns void
*/
export function replaceOld(source: IAnyObject, name: string, replacement: (...args: any[]) => any, isForced = false): void {
if (source === undefined) return
if (name in source || isForced) {
const original = source[name]
const wrapped = replacement(original)
if (typeof wrapped === 'function') {
source[name] = wrapped
}
}
}
open
const originalXhrProto = XMLHttpRequest.prototype
replaceOld(originalXhrProto, 'open', (originalOpen: voidFun): voidFun => {
return function (this: MITOXMLHttpRequest, ...args: any[]): void {
this.mito_xhr = {
method: variableTypeDetection.isString(args[0]) ? args[0].toUpperCase() : args[0],
url: args[1],
sTime: getTimestamp(),
type: HTTPTYPE.XHR
}
originalOpen.apply(this, args)
}
})
send
replaceOld(originalXhrProto, 'send', (originalSend: voidFun): voidFun => {
return function (this: MITOXMLHttpRequest, ...args: any[]): void {
const { method, url } = this.mito_xhr
setTraceId(url, (headerFieldName: string, traceId: string) => {
this.mito_xhr.traceId = traceId
this.setRequestHeader(headerFieldName, traceId)
})
options.beforeAppAjaxSend && options.beforeAppAjaxSend({ method, url }, this)
on(this, 'loadend', function (this: MITOXMLHttpRequest) {
if ((method === EMethods.Post && transportData.isSdkTransportUrl(url)) || isFilterHttpUrl(url)) return
const { responseType, response, status } = this
this.mito_xhr.reqData = args[0]
const eTime = getTimestamp()
this.mito_xhr.time = this.mito_xhr.sTime
this.mito_xhr.status = status
if (['', 'json', 'text'].indexOf(responseType) !== -1) {
this.mito_xhr.responseText = typeof response === 'object' ? JSON.stringify(response) : response
}
this.mito_xhr.elapsedTime = eTime - this.mito_xhr.sTime
triggerHandlers(EVENTTYPES.XHR, this.mito_xhr)
})
originalSend.apply(this, args)
}
})
fetch
replaceOld(_global, EVENTTYPES.FETCH, (originalFetch: voidFun) => {
return function (url: string, config: Partial<Request> = {}): void {
const sTime = getTimestamp()
const method = (config && config.method) || 'GET'
let handlerData: MITOHttp = {
type: HTTPTYPE.FETCH,
method,
reqData: config && config.body,
url
}
const headers = new Headers(config.headers || {})
Object.assign(headers, {
setRequestHeader: headers.set
})
setTraceId(url, (headerFieldName: string, traceId: string) => {
handlerData.traceId = traceId
headers.set(headerFieldName, traceId)
})
options.beforeAppAjaxSend && options.beforeAppAjaxSend({ method, url }, headers)
config = {
...config,
headers
}
return originalFetch.apply(_global, [url, config]).then(
(res: Response) => {
const tempRes = res.clone()
const eTime = getTimestamp()
handlerData = {
...handlerData,
elapsedTime: eTime - sTime,
status: tempRes.status,
// statusText: tempRes.statusText,
time: sTime
}
tempRes.text().then((data) => {
if (method === EMethods.Post && transportData.isSdkTransportUrl(url)) return
if (isFilterHttpUrl(url)) return
handlerData.responseText = tempRes.status > HTTP_CODE.UNAUTHORIZED && data
triggerHandlers(EVENTTYPES.FETCH, handlerData)
})
return res
},
(err: Error) => {
const eTime = getTimestamp()
if (method === EMethods.Post && transportData.isSdkTransportUrl(url)) return
if (isFilterHttpUrl(url)) return
handlerData = {
...handlerData,
elapsedTime: eTime - sTime,
status: 0,
// statusText: err.name + err.message,
time: sTime
}
triggerHandlers(EVENTTYPES.FETCH, handlerData)
throw err
}
)
}
})
2. js代码错误 && 资源错误
当Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件
/**
* 处理window的error的监听回到
*/
handleError(errorEvent: ErrorEvent) {
const target = errorEvent.target as ResourceErrorTarget
if (target.localName) {
// 资源加载错误 提取有用数据
const data = resourceTransform(errorEvent.target as ResourceErrorTarget)
breadcrumb.push({
type: BREADCRUMBTYPES.RESOURCE,
category: breadcrumb.getCategory(BREADCRUMBTYPES.RESOURCE),
data,
level: Severity.Error
})
return transportData.send(data)
}
// code error
const { message, filename, lineno, colno, error } = errorEvent
let result: ReportDataType
if (error && isError(error)) {
result = extractErrorStack(error, Severity.Normal)
}
// 处理SyntaxError,stack没有lineno、colno
result || (result = HandleEvents.handleNotErrorInstance(message, filename, lineno, colno))
result.type = ERRORTYPES.JAVASCRIPT_ERROR
breadcrumb.push({
type: BREADCRUMBTYPES.CODE_ERROR,
category: breadcrumb.getCategory(BREADCRUMBTYPES.CODE_ERROR),
data: { ...result },
level: Severity.Error
})
transportData.send(result)
},
handleNotErrorInstance(message: string, filename: string, lineno: number, colno: number) {
let name: string | ERRORTYPES = ERRORTYPES.UNKNOWN
const url = filename || getLocationHref()
let msg = message
const matches = message.match(ERROR_TYPE_RE)
if (matches[1]) {
name = matches[1]
msg = matches[2]
}
const element = {
url,
func: ERRORTYPES.UNKNOWN_FUNCTION,
args: ERRORTYPES.UNKNOWN,
line: lineno,
col: colno
}
return {
url,
name,
message: msg,
level: Severity.Normal,
time: getTimestamp(),
stack: [element]
}
},
handleHistory(data: Replace.IRouter): void {
const { from, to } = data
const { relative: parsedFrom } = parseUrlToObj(from)
const { relative: parsedTo } = parseUrlToObj(to)
breadcrumb.push({
type: BREADCRUMBTYPES.ROUTE,
category: breadcrumb.getCategory(BREADCRUMBTYPES.ROUTE),
data: {
from: parsedFrom ? parsedFrom : '/',
to: parsedTo ? parsedTo : '/'
},
level: Severity.Info
})
const { onRouteChange } = options;
if (onRouteChange) {
onRouteChange(from, to)
}
},
handleHashchange(data: HashChangeEvent): void {
const { oldURL, newURL } = data
const { relative: from } = parseUrlToObj(oldURL)
const { relative: to } = parseUrlToObj(newURL)
breadcrumb.push({
type: BREADCRUMBTYPES.ROUTE,
category: breadcrumb.getCategory(BREADCRUMBTYPES.ROUTE),
data: {
from,
to
},
level: Severity.Info
})
const { onRouteChange } = options;
if (onRouteChange) {
onRouteChange(from, to)
}
},
handleUnhandleRejection(ev: PromiseRejectionEvent): void {
let data: ReportDataType = {
type: ERRORTYPES.PROMISE_ERROR,
message: unknownToString(ev.reason),
url: getLocationHref(),
name: ev.type,
time: getTimestamp(),
level: Severity.Low
}
if (isError(ev.reason)) {
data = {
...data,
...extractErrorStack(ev.reason, Severity.Low)
}
}
breadcrumb.push({
type: BREADCRUMBTYPES.UNHANDLEDREJECTION,
category: breadcrumb.getCategory(BREADCRUMBTYPES.UNHANDLEDREJECTION),
data: { ...data },
level: Severity.Error
})
transportData.send(data)
}
4. 监听 Promise 错误
function unhandledrejectionReplace(): void {
on(_global, EVENTTYPES.UNHANDLEDREJECTION, function (ev: PromiseRejectionEvent) {
// ev.preventDefault() 阻止默认行为后,控制台就不会再报红色错误
triggerHandlers(EVENTTYPES.UNHANDLEDREJECTION, ev)
})
}
triggerHandlers
export function triggerHandlers(type: EVENTTYPES | WxEvents, data: any): void {
if (!type || !handlers[type]) return
handlers[type].forEach((callback) => {
nativeTryCatch(
() => {
callback(data)
},
(e: Error) => {
logger.error(`重写事件triggerHandlers的回调函数发生错误\nType:${type}\nName: ${getFunctionName(callback)}\nError: ${e}`)
}
)
})
}
5. vue错误信息
通过Vue 提供的Vue.config.errorHandler和Vue.config.warnHandler两个api来获取。
const MitoVue = {
install(Vue: VueInstance): void {
if (getFlag(EVENTTYPES.VUE) || !Vue || !Vue.config) return
setFlag(EVENTTYPES.VUE, true)
// vue 提供 warnHandler errorHandler报错信息
Vue.config.errorHandler = function (err: Error, vm: ViewModel, info: string): void {
handleVueError.apply(null, [err, vm, info, Severity.Normal, Severity.Error, Vue])
if (hasConsole && !Vue.config.silent) {
slientConsoleScope(() => {
console.error('Error in ' + info + ': "' + err.toString() + '"', vm)
console.error(err)
})
}
}
}
}
6. react错误信息
暴露一个全局函数 errorBoundaryReport, 用户需要 在 componentDidCatch内获取。
/**
* 收集react ErrorBoundary中的错误对象
* 需要用户手动在componentDidCatch中设置
* @param ex ErrorBoundary中的componentDidCatch的一个参数error
*/
export function errorBoundaryReport(ex: any): void {
if (!isError(ex)) {
console.warn('传入的react error不是一个object Error')
return
}
const error = extractErrorStack(ex, Severity.Normal) as ReportDataType
error.type = ERRORTYPES.REACT_ERROR
breadcrumb.push({
type: BREADCRUMBTYPES.REACT,
category: breadcrumb.getCategory(BREADCRUMBTYPES.REACT),
data: `${error.name}: ${error.message}`,
level: Severity.fromString(error.level)
})
transportData.send(error)
}
收集用户行为
收集到错误信息后,添加breadcrumb.push(data)把数据传递到breadcrumb里保存用户的轨迹。
export class Breadcrumb {
maxBreadcrumbs = 10
beforePushBreadcrumb: unknown = null
stack: BreadcrumbPushData[] = []
constructor() {}
/**
* 添加用户行为栈
*
* ../param {BreadcrumbPushData} data
* ../memberof Breadcrumb
*/
push(data: BreadcrumbPushData): void {
if (typeof this.beforePushBreadcrumb === 'function') {
let result: BreadcrumbPushData = null
// 如果用户输入console,并且logger是打开的会造成无限递归,
// 应该加入一个开关,执行这个函数前,把监听console的行为关掉
const beforePushBreadcrumb = this.beforePushBreadcrumb
slientConsoleScope(() => {
result = beforePushBreadcrumb(this, data)
})
if (!result) return
this.immediatePush(result)
return
}
this.immediatePush(data)
}
immediatePush(data: BreadcrumbPushData): void {
data.time || (data.time = getTimestamp())
if (this.stack.length >= this.maxBreadcrumbs) {
this.shift()
}
this.stack.push(data)
// make sure xhr fetch is behind button click
this.stack.sort((a, b) => a.time - b.time)
logger.log(this.stack)
}
shift(): boolean {
return this.stack.shift() !== undefined
}
clear(): void {
this.stack = []
}
getStack(): BreadcrumbPushData[] {
return this.stack
}
getCategory(type: BREADCRUMBTYPES) {
switch (type) {
case BREADCRUMBTYPES.XHR: //
case BREADCRUMBTYPES.FETCH:
return BREADCRUMBCATEGORYS.HTTP
case BREADCRUMBTYPES.CLICK:
case BREADCRUMBTYPES.ROUTE:
case BREADCRUMBTYPES.TAP:
case BREADCRUMBTYPES.TOUCHMOVE:
return BREADCRUMBCATEGORYS.USER
case BREADCRUMBTYPES.CUSTOMER:
case BREADCRUMBTYPES.CONSOLE:
return BREADCRUMBCATEGORYS.DEBUG
case BREADCRUMBTYPES.APP_ON_LAUNCH:
case BREADCRUMBTYPES.APP_ON_SHOW:
case BREADCRUMBTYPES.APP_ON_HIDE:
case BREADCRUMBTYPES.PAGE_ON_SHOW:
case BREADCRUMBTYPES.PAGE_ON_HIDE:
case BREADCRUMBTYPES.PAGE_ON_SHARE_APP_MESSAGE:
case BREADCRUMBTYPES.PAGE_ON_SHARE_TIMELINE:
case BREADCRUMBTYPES.PAGE_ON_TAB_ITEM_TAP:
return BREADCRUMBCATEGORYS.LIFECYCLE
case BREADCRUMBTYPES.UNHANDLEDREJECTION:
case BREADCRUMBTYPES.CODE_ERROR:
case BREADCRUMBTYPES.RESOURCE:
case BREADCRUMBTYPES.VUE:
case BREADCRUMBTYPES.REACT:
default:
return BREADCRUMBCATEGORYS.EXCEPTION
}
}
bindOptions(options: InitOptions = {}): void {
const { maxBreadcrumbs, beforePushBreadcrumb } = options
validateOption(maxBreadcrumbs, 'maxBreadcrumbs', 'number') && (this.maxBreadcrumbs = maxBreadcrumbs)
validateOption(beforePushBreadcrumb, 'beforePushBreadcrumb', 'function') && (this.beforePushBreadcrumb = beforePushBreadcrumb)
}
}
const breadcrumb = _support.breadcrumb || (_support.breadcrumb = new Breadcrumb())
生成错误id的规则
const allErrorNumber: unknown = {}
/**
* generate error unique Id
* @param data
*/
export function createErrorId(data: ReportDataType, apikey: string): number | null {
let id: any
switch (data.type) {
case ERRORTYPES.FETCH_ERROR:
id = data.type + data.request.method + data.response.status + getRealPath(data.request.url) + apikey
break
case ERRORTYPES.JAVASCRIPT_ERROR:
case ERRORTYPES.VUE_ERROR:
case ERRORTYPES.REACT_ERROR:
id = data.type + data.name + data.message + apikey
break
case ERRORTYPES.LOG_ERROR:
id = data.customTag + data.type + data.name + apikey
break
case ERRORTYPES.PROMISE_ERROR:
id = generatePromiseErrorId(data, apikey)
break
default:
id = data.type + data.message + apikey
break
}
id = hashCode(id)
if (allErrorNumber[id] >= options.maxDuplicateCount) {
return null
}
if (typeof allErrorNumber[id] === 'number') {
allErrorNumber[id]++
} else {
allErrorNumber[id] = 1
}
return id
}
/**
* 如果是UNHANDLEDREJECTION,则按照项目主域名来生成
* 如果是其他的,按照当前页面来生成
* @param data
* @param originUrl
*/
function generatePromiseErrorId(data: ReportDataType, apikey: string) {
const locationUrl = getRealPath(data.url)
if (data.name === EVENTTYPES.UNHANDLEDREJECTION) {
return data.type + objectOrder(data.message) + apikey
}
return data.type + data.name + objectOrder(data.message) + locationUrl
}
/**
* sort object keys
* ../param reason promise.reject
*/
function objectOrder(reason: any) {
const sortFn = (obj: any) => {
return Object.keys(obj)
.sort()
.reduce((total, key) => {
if (variableTypeDetection.isObject(obj[key])) {
total[key] = sortFn(obj[key])
} else {
total[key] = obj[key]
}
return total
}, {})
}
try {
if (/\{.*\}/.test(reason)) {
let obj = JSON.parse(reason)
obj = sortFn(obj)
return JSON.stringify(obj)
}
} catch (error) {
return reason
}
}
/**
* http://.../project?id=1#a => http://.../project
* http://.../id/123=> http://.../id/{param}
*
* @param url
*/
export function getRealPath(url: string): string {
return url.replace(/[\?#].*$/, '').replace(/\/\d+([\/]*$)/, '{param}$1')
}
/**
*
* @param url
*/
export function getFlutterRealOrigin(url: string): string {
// for apple
return removeHashPath(getFlutterRealPath(url))
}
/**
* 获取flutter的原始地址:每个用户的文件夹hash不同
* @param url
*/
export function getFlutterRealPath(url: string): string {
// for apple
return url.replace(/(\S+)(\/Documents\/)(\S*)/, `$3`)
}
/**
* http://a.b.com/#/project?id=1 => a.b.com
* wx => appId
* @param url
*/
export function getRealPageOrigin(url: string): string {
const fileStartReg = /^file:\/\//
if (fileStartReg.test(url)) {
return getFlutterRealOrigin(url)
}
if (isWxMiniEnv) {
return getAppId()
}
return getRealPath(removeHashPath(url).replace(/(\S*)(\/\/)(\S+)/, '$3'))
}
export function removeHashPath(url: string): string {
return url.replace(/(\S+)(\/#\/)(\S*)/, `$1`)
}
export function hashCode(str: string): number {
let hash = 0
if (str.length == 0) return hash
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i)
hash = (hash << 5) - hash + char
hash = hash & hash
}
return hash
}
发布订阅策略
export interface ReplaceHandler {
type: EVENTTYPES | WxEvents
callback: ReplaceCallback
}
type ReplaceCallback = (data: any) => void
const handlers: { [key in EVENTTYPES]?: ReplaceCallback[] } = {}
export function subscribeEvent(handler: ReplaceHandler): boolean {
if (!handler || getFlag(handler.type)) return false
setFlag(handler.type, true)
handlers[handler.type] = handlers[handler.type] || []
handlers[handler.type].push(handler.callback)
return true
}
export function triggerHandlers(type: EVENTTYPES | WxEvents, data: any): void {
if (!type || !handlers[type]) return
handlers[type].forEach((callback) => {
nativeTryCatch(
() => {
callback(data)
},
(e: Error) => {
logger.error(`重写事件triggerHandlers的回调函数发生错误\nType:${type}\nName: ${getFunctionName(callback)}\nError: ${e}`)
}
)
})
}
配置项与钩子函数的绑定
export class Options {
beforeAppAjaxSend: Function = () => {}
enableTraceId: Boolean
filterXhrUrlRegExp: RegExp
includeHttpUrlTraceIdRegExp: RegExp
traceIdFieldName = 'Trace-Id'
throttleDelayTime = 0
maxDuplicateCount = 2
// wx-mini
appOnLaunch: Function = () => {}
appOnShow: Function = () => {}
onPageNotFound: Function = () => {}
appOnHide: Function = () => {}
pageOnUnload: Function = () => {}
pageOnShow: Function = () => {}
pageOnHide: Function = () => {}
onShareAppMessage: Function = () => {}
onShareTimeline: Function = () => {}
onTabItemTap: Function = () => {}
// need return opitons,so defaul value is undefined
wxNavigateToMiniProgram: Function
triggerWxEvent: Function = () => {}
onRouteChange?: Function
constructor() {
this.enableTraceId = false
}
bindOptions(options: InitOptions = {}): void {
const {
beforeAppAjaxSend,
enableTraceId,
filterXhrUrlRegExp,
traceIdFieldName,
throttleDelayTime,
includeHttpUrlTraceIdRegExp,
appOnLaunch,
appOnShow,
appOnHide,
pageOnUnload,
pageOnShow,
pageOnHide,
onPageNotFound,
onShareAppMessage,
onShareTimeline,
onTabItemTap,
wxNavigateToMiniProgram,
triggerWxEvent,
maxDuplicateCount,
onRouteChange
} = options
validateOption(beforeAppAjaxSend, 'beforeAppAjaxSend', 'function') && (this.beforeAppAjaxSend = beforeAppAjaxSend)
// wx-mini hooks
validateOption(appOnLaunch, 'appOnLaunch', 'function') && (this.appOnLaunch = appOnLaunch)
validateOption(appOnShow, 'appOnShow', 'function') && (this.appOnShow = appOnShow)
validateOption(appOnHide, 'appOnHide', 'function') && (this.appOnHide = appOnHide)
validateOption(pageOnUnload, 'pageOnUnload', 'function') && (this.pageOnUnload = pageOnUnload)
validateOption(pageOnShow, 'pageOnShow', 'function') && (this.pageOnShow = pageOnShow)
validateOption(pageOnHide, 'pageOnHide', 'function') && (this.pageOnHide = pageOnHide)
validateOption(onPageNotFound, 'onPageNotFound', 'function') && (this.onPageNotFound = onPageNotFound)
validateOption(onShareAppMessage, 'onShareAppMessage', 'function') && (this.onShareAppMessage = onShareAppMessage)
validateOption(onShareTimeline, 'onShareTimeline', 'function') && (this.onShareTimeline = onShareTimeline)
validateOption(onTabItemTap, 'onTabItemTap', 'function') && (this.onTabItemTap = onTabItemTap)
validateOption(wxNavigateToMiniProgram, 'wxNavigateToMiniProgram', 'function') &&
(this.wxNavigateToMiniProgram = wxNavigateToMiniProgram)
validateOption(triggerWxEvent, 'triggerWxEvent', 'function') && (this.triggerWxEvent = triggerWxEvent)
// browser hooks
validateOption(onRouteChange, 'onRouteChange', 'function') && (this.onRouteChange = onRouteChange)
validateOption(enableTraceId, 'enableTraceId', 'boolean') && (this.enableTraceId = enableTraceId)
validateOption(traceIdFieldName, 'traceIdFieldName', 'string') && (this.traceIdFieldName = traceIdFieldName)
validateOption(throttleDelayTime, 'throttleDelayTime', 'number') && (this.throttleDelayTime = throttleDelayTime)
validateOption(maxDuplicateCount, 'maxDuplicateCount', 'number') && (this.maxDuplicateCount = maxDuplicateCount)
toStringValidateOption(filterXhrUrlRegExp, 'filterXhrUrlRegExp', '[object RegExp]') && (this.filterXhrUrlRegExp = filterXhrUrlRegExp)
toStringValidateOption(includeHttpUrlTraceIdRegExp, 'includeHttpUrlTraceIdRegExp', '[object RegExp]') &&
(this.includeHttpUrlTraceIdRegExp = includeHttpUrlTraceIdRegExp)
}
}
const options = _support.options || (_support.options = new Options())
export function setTraceId(httpUrl: string, callback: (headerFieldName: string, traceId: string) => void) {
const { includeHttpUrlTraceIdRegExp, enableTraceId } = options
if (enableTraceId && includeHttpUrlTraceIdRegExp && includeHttpUrlTraceIdRegExp.test(httpUrl)) {
const traceId = generateUUID()
callback(options.traceIdFieldName, traceId)
}
}
/**
* init core methods
* @param paramOptions
*/
export function initOptions(paramOptions: InitOptions = {}) {
setSilentFlag(paramOptions)
breadcrumb.bindOptions(paramOptions)
logger.bindOptions(paramOptions.debug)
transportData.bindOptions(paramOptions)
options.bindOptions(paramOptions)
}
export { options }
手动上报错误
/**
*
* 自定义上报事件
* @export
* @param {LogTypes} { message = 'emptyMsg', tag = '', level = Severity.Critical, ex = '' }
*/
export function log({ message = 'emptyMsg', tag = '', level = Severity.Critical, ex = '', type = ERRORTYPES.LOG_ERROR }: LogTypes): void {
let errorInfo = {}
if (isError(ex)) {
errorInfo = extractErrorStack(ex, level)
}
const error = {
type,
level,
message: unknownToString(message),
name: 'MITO.log',
customTag: unknownToString(tag),
time: getTimestamp(),
url: isWxMiniEnv ? getCurrentRoute() : getLocationHref(),
...errorInfo
}
breadcrumb.push({
type: BREADCRUMBTYPES.CUSTOMER,
category: breadcrumb.getCategory(BREADCRUMBTYPES.CUSTOMER),
data: message,
level: Severity.fromString(level.toString())
})
transportData.send(error);
}
公共的格式转换函数
export function httpTransform(data: MITOHttp): ReportDataType {
let message = ''
const { elapsedTime, time, method, traceId, type, status } = data
const name = `${type}--${method}`
if (status === 0) {
message =
elapsedTime <= globalVar.crossOriginThreshold ? 'http请求失败,失败原因:跨域限制或域名不存在' : 'http请求失败,失败原因:超时'
} else {
message = fromHttpStatus(status)
}
message = message === SpanStatus.Ok ? message : `${message} ${getRealPath(data.url)}`
return {
type: ERRORTYPES.FETCH_ERROR,
url: getLocationHref(),
time,
elapsedTime,
level: Severity.Low,
message,
name,
request: {
httpType: type,
traceId,
method,
url: data.url,
data: data.reqData || ''
},
response: {
status,
data: data.responseText
}
}
}
const resourceMap = {
img: '图片',
script: 'js脚本'
}
export function resourceTransform(target: ResourceErrorTarget): ReportDataType {
return {
type: ERRORTYPES.RESOURCE_ERROR,
url: getLocationHref(),
message: '资源地址: ' + (interceptStr(target.src, 120) || interceptStr(target.href, 120)),
level: Severity.Low,
time: getTimestamp(),
name: `${resourceMap[target.localName] || target.localName}加载失败`
}
}
export function handleConsole(data: Replace.TriggerConsole): void {
if (globalVar.isLogAddBreadcrumb) {
breadcrumb.push({
type: BREADCRUMBTYPES.CONSOLE,
category: breadcrumb.getCategory(BREADCRUMBTYPES.CONSOLE),
data,
level: Severity.fromString(data.level)
})
}
}
日志上报类
/**
* 用来传输数据类,包含img标签、xhr请求
* 功能:支持img请求和xhr请求、可以断点续存(保存在localstorage),
* 待开发:目前不需要断点续存,因为接口不是很多,只有错误时才触发,如果接口太多可以考虑合并接口、
*
* ../class Transport
*/
export class TransportData {
queue: Queue
beforeDataReport: unknown = null
backTrackerId: unknown = null
configReportXhr: unknown = null
configReportUrl: unknown = null
configReportWxRequest: unknown = null
useImgUpload = false
apikey = ''
trackKey = ''
errorDsn = ''
trackDsn = ''
constructor() {
this.queue = new Queue()
}
imgRequest(data: any, url: string): void {
const requestFun = () => {
let img = new Image()
const spliceStr = url.indexOf('?') === -1 ? '?' : '&'
img.src = `${url}${spliceStr}data=${encodeURIComponent(JSON.stringify(data))}`
img = null
}
this.queue.addFn(requestFun)
}
getRecord(): any[] {
const recordData = _support.record
if (recordData && variableTypeDetection.isArray(recordData) && recordData.length > 2) {
return recordData
}
return []
}
getDeviceInfo(): DeviceInfo | any {
return _support.deviceInfo || {}
}
async beforePost(data: FinalReportType) {
if (isReportDataType(data)) {
const errorId = createErrorId(data, this.apikey)
if (!errorId) return false
data.errorId = errorId
}
let transportData = this.getTransportData(data)
if (typeof this.beforeDataReport === 'function') {
transportData = await this.beforeDataReport(transportData)
if (!transportData) return false
}
return transportData
}
async xhrPost(data: any, url: string) {
const requestFun = (): void => {
const xhr = new XMLHttpRequest()
xhr.open(EMethods.Post, url)
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8')
xhr.withCredentials = true
if (typeof this.configReportXhr === 'function') {
this.configReportXhr(xhr, data)
}
xhr.send(JSON.stringify(data))
}
this.queue.addFn(requestFun)
}
async wxPost(data: any, url: string) {
const requestFun = (): void => {
let requestOptions = { method: 'POST' } as WechatMiniprogram.RequestOption
if (typeof this.configReportWxRequest === 'function') {
const params = this.configReportWxRequest(data)
// default method
requestOptions = { ...requestOptions, ...params }
}
requestOptions = {
...requestOptions,
data: JSON.stringify(data),
url
}
wx.request(requestOptions)
}
this.queue.addFn(requestFun)
}
getAuthInfo(): AuthInfo {
const trackerId = this.getTrackerId()
const result: AuthInfo = {
trackerId: String(trackerId),
sdkVersion: SDK_VERSION,
sdkName: SDK_NAME
}
this.apikey && (result.apikey = this.apikey)
this.trackKey && (result.trackKey = this.trackKey)
return result
}
getApikey() {
return this.apikey
}
getTrackKey() {
return this.trackKey
}
getTrackerId(): string | number {
if (typeof this.backTrackerId === 'function') {
const trackerId = this.backTrackerId()
if (typeof trackerId === 'string' || typeof trackerId === 'number') {
return trackerId
} else {
logger.error(`trackerId:${trackerId} 期望 string 或 number 类型,但是传入 ${typeof trackerId}`)
}
}
return ''
}
getTransportData(data: FinalReportType): TransportDataType {
return {
authInfo: this.getAuthInfo(),
breadcrumb: breadcrumb.getStack(),
data,
record: this.getRecord(),
deviceInfo: this.getDeviceInfo()
}
}
isSdkTransportUrl(targetUrl: string): boolean {
let isSdkDsn = false
if (this.errorDsn && targetUrl.indexOf(this.errorDsn) !== -1) {
isSdkDsn = true
}
if (this.trackDsn && targetUrl.indexOf(this.trackDsn) !== -1) {
isSdkDsn = true
}
return isSdkDsn
}
bindOptions(options: InitOptions = {}): void {
const {
dsn,
beforeDataReport,
apikey,
configReportXhr,
backTrackerId,
trackDsn,
trackKey,
configReportUrl,
useImgUpload,
configReportWxRequest
} = options
validateOption(apikey, 'apikey', 'string') && (this.apikey = apikey)
validateOption(trackKey, 'trackKey', 'string') && (this.trackKey = trackKey)
validateOption(dsn, 'dsn', 'string') && (this.errorDsn = dsn)
validateOption(trackDsn, 'trackDsn', 'string') && (this.trackDsn = trackDsn)
validateOption(useImgUpload, 'useImgUpload', 'boolean') && (this.useImgUpload = useImgUpload)
validateOption(beforeDataReport, 'beforeDataReport', 'function') && (this.beforeDataReport = beforeDataReport)
validateOption(configReportXhr, 'configReportXhr', 'function') && (this.configReportXhr = configReportXhr)
validateOption(backTrackerId, 'backTrackerId', 'function') && (this.backTrackerId = backTrackerId)
validateOption(configReportUrl, 'configReportUrl', 'function') && (this.configReportUrl = configReportUrl)
validateOption(configReportWxRequest, 'configReportWxRequest', 'function') && (this.configReportWxRequest = configReportWxRequest)
}
/**
* 监控错误上报的请求函数
* @param data 错误上报数据格式
* @returns
*/
async send(data: FinalReportType) {
let dsn = ''
if (isReportDataType(data)) {
dsn = this.errorDsn
if (isEmpty(dsn)) {
logger.error('dsn为空,没有传入监控错误上报的dsn地址,请在init中传入')
return
}
} else {
dsn = this.trackDsn
if (isEmpty(dsn)) {
logger.error('trackDsn为空,没有传入埋点上报的dsn地址,请在init中传入')
return
}
}
const result = await this.beforePost(data)
if (!result) return
if (typeof this.configReportUrl === 'function') {
dsn = this.configReportUrl(result, dsn)
if (!dsn) return
}
if (isBrowserEnv) {
return this.useImgUpload ? this.imgRequest(result, dsn) : this.xhrPost(result, dsn)
}
if (isWxMiniEnv) {
return this.wxPost(result, dsn)
}
}
}
const transportData = _support.transportData || (_support.transportData = new TransportData())