前端项目异常监控-全局捕获Promise错误
1.核心
全局监听unhandledrejection,该事件为Promise被reject时但没有reject处理器时(没有被catch处理),则触发该事件。( async 函数内部的异步任务一旦出现错误,那么就等同于 async 函数返回的 Promise 对象被 reject。)
2.编写辅助函数
2.1getLastEvent获取最后一个事件
let lastEvent; ['click', 'touchstart', 'mousedown', 'keydown', 'mouseover'].forEach( (eventType) => { document.addEventListener( eventType, (event) => { lastEvent = event }, { capture: true, // 是在捕获阶段还是冒泡阶段执行 passive: true // 默认不阻止默认事件 } ) } ) export default function () { return lastEvent }
2.2getSelector获取操作元素
function getSelectors(path) { // 反转 + 过滤 + 映射 + 拼接 return path .reverse() .filter((element) => { return element !== document && element !== window }) .map((element) => { console.log('element', element.nodeName) let selector = '' if (element.id) { return `${element.nodeName.toLowerCase()}#${element.id}` } else if (element.className && typeof element.className === 'string') { return `${element.nodeName.toLowerCase()}.${element.className}` } else { selector = element.nodeName.toLowerCase() } return selector }) .join(' ') } export default function (pathsOrTarget) { if (Array.isArray(pathsOrTarget)) { return getSelectors(pathsOrTarget) } else { let path = [] while (pathsOrTarget) { path.push(pathsOrTarget) pathsOrTarget = pathsOrTarget.parentNode } return getSelectors(path) } }
2.3tracker报错处理器,针对捕获到的错误后进行的操作统一处理
// 主机 // let host = 'cn-guangdong-log.aliyuncs.com' // 项目名 // let project = 'yymonitor' // 存储名 // let logstore = 'yymonitor-store' let userAgent = require('user-agent') function getExtraData() { return { title: document.title, url: location.href, timestamp: Date.now(), userAgent: userAgent.parse(navigator.userAgent).name } } class SendTracker { // constructor() { // // 上报的路径 // this.url = `http://${project}.${host}/logstores/${logstore}/track` // this.xhr = new XMLHttpRequest() // } send(data = {}) { let extraData = getExtraData() let log = { ...data, ...extraData } // 阿里云要求值不能为数字 for (const key in log) { if (typeof log[key] === 'number') { log[key] = `${log[key]}` } } console.log('log', log) // 接入日志系统,此处以阿里云为例 // let body = JSON.stringify({ // __logs__: [log] // }) // this.xhr.open('POST', this.url, true) // this.xhr.setRequestHeader('Content-Type', 'application/json') // this.xhr.setRequestHeader('x-log-apiversion', '1.0.0') // this.xhr.setRequestHeader('x-log-bodyrawsize', body.length) // this.xhr.onload = function () { // // console.log(this.xhr.response); // } // this.xhr.onerror = function (error) { // console.log(error) // } // this.xhr.send(body) } } export default new SendTracker()
3.编写全局监听函数,附带捕获JS资源加载异常
import getLastEvent from '../utils/getLastEvent' import getSelector from '../utils/getSelector' import tracker from '../utils/tracker' export function injectJsError() { // 监听全局未捕获的错误 window.addEventListener( 'error', (event) => { console.log('error+++++++++++', event) let lastEvent = getLastEvent() // 获取到最后一个交互事件 // 脚本加载错误 if (event.target && (event.target.src || event.target.href)) { tracker.send({ kind: 'stability', // 监控指标的大类,稳定性 type: 'error', // 小类型,这是一个错误 errorType: 'resourceError', // js执行错误 filename: event.target.src || event.target.href, // 哪个文件报错了 tagName: event.target.tagName, selector: getSelector(event.target) // 代表最后一个操作的元素 }) } else { tracker.send({ kind: 'stability', // 监控指标的大类,稳定性 type: 'error', // 小类型,这是一个错误 errorType: 'jsError', // js执行错误 message: event.message, // 报错信息 filename: event.filename, // 哪个文件报错了 position: `${event.lineno}:${event.colno}`, // 报错的行列位置 stack: getLines(event.error.stack), selector: lastEvent ? getSelector(lastEvent.path) : '' // 代表最后一个操作的元素 }) } }, true ) window.addEventListener( 'unhandledrejection', (event) => { console.log('unhandledrejection-------- ', event) let lastEvent = getLastEvent() // 获取到最后一个交互事件 let message let filename let line = 0 let column = 0 let stack = '' let reason = event.reason if (typeof reason === 'string') { message = reason } else if (typeof reason === 'object') { message = reason.message if (reason.stack) { let matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/) filename = matchResult[1] line = matchResult[2] column = matchResult[3] } stack = getLines(reason.stack) } tracker.send({ kind: 'stability', // 监控指标的大类,稳定性 type: 'error', // 小类型,这是一个错误 errorType: 'promiseError', // js执行错误 message, // 报错信息 filename, // 哪个文件报错了 position: `${line}:${column}`, // 报错的行列位置 stack, selector: lastEvent ? getSelector(lastEvent.path) : '' // 代表最后一个操作的元素 }) }, true ) } function getLines(stack) { return stack .split('\n') .slice(1) .map((item) => item.replace(/^\s+at\s+/g, '')) .join('^') }