前端项目异常监控-全局捕获Promise错误

发布时间 2023-10-07 14:28:03作者: ChoZ

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('^')
}