hc-web-log-mon
Version:
基于 JS 跨平台插件,为前端项目提供【 行为、性能、异常、请求、资源、路由、曝光、录屏 】监控手段
282 lines (255 loc) • 8.35 kB
text/typescript
import { EVENTTYPES, SEDNEVENTTYPES, SENDID } from '../common'
import { map, filter, getLocationHref, getTimestamp } from '../utils'
import { _global } from '../utils/global'
import { sendData } from './sendData'
import { eventBus } from './eventBus'
import { isArray, isRegExp } from '../utils/is'
import { options } from './options'
import { zip, getEventList } from './recordscreen'
import { debug } from '../utils/debug'
import { initBatchError, batchError } from './err-batch'
import { RecordEventScope } from '../types'
interface ErrorStack {
errMessage: string
errStack: string
}
type InstabilityNature = {
lineNumber: string
fileName: string
columnNumber: string
}
/**
* 格式化错误对象信息
* @param err Error 错误对象
*/
function parseStack(err: Error): ErrorStack {
const { stack = '', message = '' } = err
const result = { eventId: SENDID.CODE, errMessage: message, errStack: stack }
if (stack) {
const rChromeCallStack = /^\s*at\s*([^(]+)\s*\((.+?):(\d+):(\d+)\)$/
const rMozlliaCallStack = /^\s*([^@]*)@(.+?):(\d+):(\d+)$/
// chrome中包含了message信息,将其去除,并去除后面的换行符
const callStackStr = stack.replace(
new RegExp(`^[\\w\\s:]*${message}\n`),
''
)
const callStackFrameList = map(
filter(callStackStr.split('\n'), (item: string) => item),
(str: string) => {
const chromeErrResult = str.match(rChromeCallStack)
if (chromeErrResult) {
return {
triggerPageUrl: chromeErrResult[2],
line: chromeErrResult[3], // 错误发生位置的行数
col: chromeErrResult[4] // 错误发生位置的列数
}
}
const mozlliaErrResult = str.match(rMozlliaCallStack)
if (mozlliaErrResult) {
return {
triggerPageUrl: mozlliaErrResult[2],
line: mozlliaErrResult[3],
col: mozlliaErrResult[4]
}
}
return {}
}
)
const item = callStackFrameList[0] || {}
return { ...result, ...item }
}
return result
}
/**
* 分析错误信息
* @param e 错误源信息
* @returns 相对标准格式的错误信息
*/
function parseError(e: any) {
if (e instanceof Error) {
// fileName: 引发此错误的文件的路径 (此属性为非标准,所以下面得区分)
const { message, stack, lineNumber, fileName, columnNumber } = e as Error &
InstabilityNature
if (fileName) {
return {
errMessage: message,
errStack: stack,
eventId: SENDID.CODE,
line: lineNumber, // 不稳定属性 - 在某些浏览器可能是undefined,被废弃了
col: columnNumber, // 不稳定属性 - 非标准,有些浏览器可能不支持
triggerPageUrl: fileName // 不稳定属性 - 非标准,有些浏览器可能不支持
}
}
return parseStack(e)
}
if (e.message) return parseStack(e)
// reject 错误
if (typeof e === 'string') return { eventId: SENDID.REJECT, errMessage: e }
// console.error 暴露的错误
if (isArray(e))
return { eventId: SENDID.CONSOLEERROR, errMessage: e.join(';') }
return {}
}
/**
* 判断是否为 promise-reject 错误类型
*/
function isPromiseRejectedResult(
event: ErrorEvent | PromiseRejectedResult
): event is PromiseRejectedResult {
return (event as PromiseRejectedResult).reason !== undefined
}
function parseErrorEvent(event: ErrorEvent | PromiseRejectedResult) {
// promise reject 错误
if (isPromiseRejectedResult(event)) {
return { eventId: SENDID.CODE, ...parseError(event.reason) }
}
// html元素上发生的异常错误
const { target } = event
if (target instanceof HTMLElement) {
// 为1代表节点是元素节点
if (target.nodeType === 1) {
const result = {
initiatorType: target.nodeName.toLowerCase(),
eventId: SENDID.RESOURCE,
requestUrl: ''
}
switch (target.nodeName.toLowerCase()) {
case 'link':
result.requestUrl = (target as HTMLLinkElement).href
break
default:
result.requestUrl =
(target as HTMLImageElement).currentSrc ||
(target as HTMLScriptElement).src
}
return result
}
}
// 代码异常
if (event.error) {
// chrome中的error对象没有fileName等属性,将event中的补充给error对象
const e = event.error
e.fileName = e.filename || event.filename
e.columnNumber = e.colno || event.colno
e.lineNumber = e.lineno || event.lineno
return { eventId: SENDID.CODE, ...parseError(e) }
}
// 兜底
// ie9版本,从全局的event对象中获取错误信息
return {
eventId: SENDID.CODE,
line: (_global as any).event.errorLine,
col: (_global as any).event.errorCharacter,
errMessage: (_global as any).event.errorMessage,
triggerPageUrl: (_global as any).event.errorUrl
}
}
/**
* 判断错误源信息是否为需要拦截的
* @param error 错误源信息
*/
function isIgnoreErrors(error: any): boolean {
if (!options.value.ignoreErrors.length) return false
let errMessage = error.errMessage || error.message
if (!errMessage) return false
errMessage = String(errMessage)
return options.value.ignoreErrors.some(item => {
if (isRegExp(item)) {
if ((item as RegExp).test(errMessage)) {
debug(`ignoreErrors拦截成功 - 截条件:${item} 错误信息:${errMessage}`)
return true
} else {
return false
}
} else {
if (errMessage === item) {
debug(`ignoreErrors拦截成功 - 截条件:${item} 错误信息:${errMessage}`)
return true
} else {
return false
}
}
})
}
/**
* 获取错误录屏数据
*/
function getRecordEvent(): RecordEventScope[] {
const _recordscreenList: RecordEventScope[] = JSON.parse(
JSON.stringify(getEventList())
)
return _recordscreenList
.slice(-2)
.map(item => item.eventList)
.flat()
}
/**
* 发送错误事件信息
* @param errorInfo 信息源
*/
function emit(errorInfo: any, flush = false): void {
const info = {
...errorInfo,
eventType: SEDNEVENTTYPES.ERROR,
recordscreen: options.value.recordScreen ? zip(getRecordEvent()) : null,
triggerPageUrl: getLocationHref(),
triggerTime: getTimestamp()
}
options.value.scopeError
? batchError.pushCacheErrorA(info)
: sendData.emit(info, flush)
}
/**
* 初始化错误监听
*/
function initError(): void {
if (!options.value.error.core) return
if (options.value.scopeError) {
initBatchError()
// 如果开启了检测批量错误 则要挂载卸载事件以防缓存池内的错误丢失
eventBus.addEvent({
type: EVENTTYPES.BEFOREUNLOAD,
callback: () => {
batchError.sendAllCacheError()
}
})
}
// 捕获阶段可以获取资源加载错误,script.onError link.onError img.onError,无法知道具体状态
eventBus.addEvent({
type: EVENTTYPES.ERROR,
callback: (e: ErrorEvent) => {
const errorInfo = parseErrorEvent(e)
if (isIgnoreErrors(errorInfo)) return
emit(errorInfo)
}
})
// promise调用链未捕获异常
// 只捕获未处理的 reject的错误,如果对reject进行了回调处理这边不进行捕获
eventBus.addEvent({
type: EVENTTYPES.UNHANDLEDREJECTION,
callback: (e: PromiseRejectedResult) => {
const errorInfo = parseErrorEvent(e)
if (isIgnoreErrors(errorInfo)) return
emit(errorInfo)
}
})
// 劫持console.error
eventBus.addEvent({
type: EVENTTYPES.CONSOLEERROR,
callback: e => {
const errorInfo = parseError(e)
if (isIgnoreErrors(errorInfo)) return
emit({ eventId: SENDID.CODE, ...errorInfo })
}
})
}
/**
* 主动触发错误上报
* @param eventId 事件ID
* @param message 错误信息
* @param options 自定义配置信息
*/
function handleSendError(options = {}, flush = false): void {
emit(options, flush)
}
export { initError, handleSendError, parseError }