阅读 165

前端错误监控sdk初步实践

前言

作为前端经常遇到线上报错却无法复现的情况,要是这个时候有错误监控能快速帮我们定位问题所在,再查找错误监控的相关资料尝试去实现一个自己的sdk。下面我以 错误监控设备信息两方面作为关注点去实现。 完整代码

错误监控

运行时报错

  window.onerror = (msg,url,lineNo,columnNo,e) => {
    // ...上报错误
  }
复制代码

promise reject 未被处理

  window.addEventListener('unhandledrejection',(event) => {
    // 报错原因,当前路径,报错时间
    const { message,config:{method,url} } = event.reason
    // ...上报错误
  })
复制代码

重写原生的监听事件


  // 保存原生的 addEventListener 事件
  const originAddEventListener = EventTarget.prototype.addEventListener
  // 重写原生的监听事件
  EventTarget.prototype.addEventListener = (type,listener,options) => {
    const wrappedListenner = (...args) => {
      try {
        return listener.apply(this,args)
      } catch (error) {
        const { name,message } = error
        // ...上报错误
        throw error
      }
    }
    return originAddEventListener.call(this,type,wrappedListenner,options)
  }
复制代码

劫持 Vue.config.errorHandler

  const vueErrorHandler = Vue.config.errorHandler
  const wrapErrorHandler = function(err,vm,info) {
    const componentRouteInfo = vm.$route
    // 组件路径,路由名称
    const { fullPath,name } = componentRouteInfo
    // ...上报错误
    vueErrorHandler.call(this,err,vm,info)
  }
  _Vue.config.errorHandler = wrapErrorHandler
复制代码

设备信息的获取

页面滚动信息

  // 滚动事件的监听
  let scrollPosition  = []
  // 将滚动的一组数据进行上报,滚动时间超过设定时间进行上报 throttle 是自定义的节流函数
  const scrollLog = throttle(() => {
    // ...数据上报,并清空历史的位置点
    scrollPosition = []
  }, scrollTime)
  function scrollHandler(e) {
    // 如果监听的是 window 那么会在前三个值中拿到滚动距离
    // 如果是设置了监听滚动的对象,那么会在 e.taget.scrollTop 中拿到滚动高度
    const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop 
    // 将滚动的距离位置收集起来,可以进行间断上报,减少请求次数
    // 记录的位置取整
    scrollPosition.push(scrollTop.toFixed(0))
    // ...数据上报
    scrollLog(e)
  }
  const throttleScrollHandler = throttle(scrollHandler)
  window.addEventListener('scroll',throttleScrollHandler)
复制代码

点击事件

  function clickHandler(e:any) {
    const { target } = e
    const clickInfo = judgeDomType(target)
    logGif(clickInfo)
  }
  window.addEventListener('click',clickHandler)
  // 获取目标元素的相关信息如: 当前元素的类名、元素标签、遍历第一个子元素的内容
  function judgeDomType(target:HTMLElement) {
    const { nodeType, nodeName, nodeValue,className,id} = target
    let firstChild
    switch (nodeType) {
      // 元素类型
      case Node.ELEMENT_NODE:
        firstChild = searchBottomNestChild(target)
        break
    }
    return {
      firstChild,
      selector: `class-${className};id-${id}`,
      nodeName
    }
  }
  // 获取该元素下嵌套的子元素的中第一个元素的文字
  function searchBottomNestChild(dom:any) {
    let current = dom
    while (current.firstChild) {
      current = current.firstChild
    }
    return current.nodeValue
  }
复制代码

设备相关信息

const pageLog = () => {
  const userAgent = navigator.userAgent
  let webview = ''
  if (userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)) {
    webview = 'ios'
  }
  if (userAgent.match(/MicroMessenger\/([^\s]+)/)) {
    webview = 'weixin'
  }
  if (userAgent.match(/QQ\/([\d\.]+)/)) {
    webview = 'qq'
  }
  // 获取网络相关的信息
  const connection = navigator.connection
  return {
    logType:LogType.page,
    path:location.href,
    platform:navigator.platform,
    webview,
    connection
  }
}
复制代码

发送数据

  • 通过 new Image 方式创建的元素,只需要赋值 src 属性即可发送请求,无需插入文档中。
  • 需要注意在拼接参数的时候,需要使用 encodeURIComponent 对值进行转移否则将 location.href 这类url作为值时会造成错误。
function parseJsonToString(dataJson) {
  if (!dataJson ) { dataJson = {} }
  var dataArr = Object.keys(dataJson).map(function(key) { return key + '=' + encodeURIComponent(dataJson[key]) })
  return dataArr.join('&')
}

const logGif = (params) => {
  const upload = parseJsonToString(params)
  const img = new Image(1,1)
  img.src = 'https://view-error?' + upload
}
复制代码

打包sdk文件

完成了基本的错误监控功能后,我们可以把这些文件打包成一个js文件,其他项目需要应用的时候通过 script 引入的方式直接使用即可。

webpack.config.js 的配置会比我们项目中的少很多,由于我是使用的 typescript + vue 的方式,配置如下:

const path = require('path')
const resolve = dir => path.resolve(__dirname, dir)
module.exports = {
  entry:{
    // sdk文件的主入口
    'view-error':resolve('../src/utils/index.ts')
  },
  output:{
    path:resolve('../dist')
  },
  resolve: {
    extensions:['.ts','.js']
  },
  module: {
    rules:[
      // 处理 .ts 结尾的文件
      {
        test: /\.tsx?$/,
        loader: 'ts-loader'
      }
    ]
  }
}
复制代码

服务端的处理

连接数据库

const mysql = require('mysql')
const pool = mysql.createPool({
  host: 'ip',
  user: 'admin',
  password: 'password',
  database: 'view_error'
})
// 接受 sql 语句,后续执行
function connect(sql) {
  return new Promise((resolve, reject) => {
    pool.getConnection((err, connection) => {
      if (err) reject(err)
      console.log('连接 成功')
      connection.query(sql, function(error, results, fields) {
        if (error) reject(error)
        resolve(results)
        connection.release()
      })
    })
  })
}
复制代码

插入数据

exports.insert = async function (ctx) {
  const query = JSON.stringify(ctx.request.query)
  // 这里直接将客户端传来的值插入,前期实验用,后续需要优化的地方
  await connect(`insert into view_error (root) values('${query}')`)
  // 图片作为返回
  const file = fs.readFileSync('upload/icon-image.gif')
  ctx.type = 'image/gif'
  ctx.body = file
}
复制代码

疑问和待优化

问题

  1. 目前是通过 vue-router 来监听页面跳转的。我试了下window.addEventListener('popstate',()=>{})window.addEventListener('hashchange',()=>{}) 都监听不到 vue 页面路由的变化。js原生能否监听路由变化?
  2. 想记录单个用户的操作链路,如何将用户链路关联起来,通过同ip关联吗,
  3. 服务端存储的时候该怎么归类存储查询比较方便快捷,服务端这块不是很熟悉。

待优化

  1. 前端发送数据的时候统一格式,将请求分为几类或者相应的等级。
  2. 优化客户端采集的信息,以及一些报错信息的捕获。
  3. 服务端相同信息归类存储,优化存储空间。