前端错误监控sdk初步实践

250 阅读8分钟

作为前端经常遇到线上报错却无法复现的情况,要是这个时候有错误监控能快速帮我们定位问题所在,再查找错误监控的相关资料尝试去实现一个自己的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 } 复制代码 疑问和待优化 问题 目前是通过 vue-router 来监听页面跳转的。我试了下 window.addEventListener('popstate',()=>{}) 和 window.addEventListener('hashchange',()=>{}) 都监听不到 vue 页面路由的变化。js原生能否监听路由变化? 想记录单个用户的操作链路,如何将用户链路关联起来,通过同ip关联吗, 服务端存储的时候该怎么归类存储查询比较方便快捷,服务端这块不是很熟悉。 待优化 1前端发送数据的时候统一格式,将请求分为几类或者相应的等级。 2优化客户端采集的信息,以及一些报错信息的捕获。 3服务端相同信息归类存储,优化存储空间。