前端监控01-如何实时捕获页面中的异常并进行上报?

463 阅读9分钟

1、为什么要做前端监控?

  • 为了更快的发现和解决问题
  • 做产品的决策依据
  • 提升前端工程师的技术深度与广度,打造简历亮点
  • 为业务扩展带来更多可能性

2、前端监控目标

2.1 稳定性(stability)

错误名称备注
JS错误JS执行错误或者Promise异常
资源异常script、link等资源加载异常
接口错误ajax或者fetch接口请求异常
白屏页面空白

2.2 用户体验性(experience)

错误名称备注
加载时间各个阶段的加载时间
TTFB(time to first byte)首字节加载时间是指浏览器发起第一个请求到数据返回第一个字节所需要消耗的时间
FCP(First Content Paint)首次内容绘制首次绘制包括任何用户自定义的背景绘制,它是第一个像素点绘制到屏幕的时刻
FMP(First Meaningful paint)首次有意义的绘制首次有意义绘制是页面可用性的度量标准
FID(First Input Delay)首次输入延迟用户首次和页面交互到页面响应交互的时间
卡顿超过50ms的长任务

2.3 业务(business)

错误名称备注
PVpage view即页面浏览量或者点击量
UV指访问某个站点的不同IP地址的人数
页面的停顿时间用户在某一个页面的停顿时间

3. 埋点

3.1代码埋点

  • 代码埋点: 就是以嵌入代码的形式进行埋点。比如在需要监控用户的点击事件。插入一段代码。保存这个监听行为以某一种数据格式直接传送给服务器端。
  • 优点就是可以在任意时刻。精准的发送或者保存所需要的数据。
  • 缺点是工作量比较大

3.2可视化埋点

  • 通过可视化的交互手段,代替代码埋点
  • 将业务代码和埋点代码进行分类。提供一个可视化的操作页面。输入为业务代码。通过这个可视化系统。可以在业务代码中自定义的增加埋点事件等等。最后输出的代码耦合了业务代码和埋点代码。
  • 可视化埋点其实是系统用来代替手工插入埋点代码。

3.3 无痕埋点

  • 前端的任意一个事件都被绑定为一个标识。所有的事件都被记录下来。
  • 通过定期上传记录文件。配合文件解析,解析出来我们想要的数据。并生成可视化报告供专业人员分析。
  • 无痕埋点的优点是采集全量数据。不会出现漏埋或者错误埋点的情况

4.简单的前端监控小案例

这里为了方便演示,使用的Vite创建的Vue工程化项目,其实如果使用webpack创建项目还是创建react项目实现的功能都是一样的。

4.1 用Vite创建一个Vue项目

  • 打开命令行,输入npm create @vitejs/app 得到下面这个目录结构。然后我们使用webstorm/vscode来打开这个目录:

image.png

  • 在控制台输入package.json中的dev指令来启动这个脚手架创建的项目后面会专门开一个专栏讲如何搭建自己的cli 在浏览器搜索localhost:3000 就可以得到这样一个页面.

image.png

4.2 创建自定义monitorSDK

上面说了,前端监控的本质就是通过window.addEventListener来监听系统中各种各样的异常错误。在最上面的时候,我们说了系统开发中最常见的错误就是前端稳定性错误。那么,这个SDK的功能就是对稳点性错误中最常见的JS执行错误进行监听.

  • 创建一个SDK目录,在src目录下,目录结构如下所示
 |--- monitor
     |---utils //这个目录存放处理对象或者字符串或者数字的函数
     |---lib // 这个目录存放实际需要监听的各种各样的异常错误
     |---index.js // 这个对象暴露出去给Main.js进行导入
  • 在src=>main.js中引入monitorSDK

import { createApp } from 'vue'
import App from './App.vue'
import './sdk/monitor/index'
createApp(App).mount('#app')
  • 修改App.vue 放一个点击按钮,让他点击一下就可以抛出异常
<template>
  <input type="button" value="抛出错误" @click="throwError" />
</template>
<script>
import { defineComponent, onMounted, reactive, toRefs } from "vue";
export default defineComponent({
  setup(){
    const throwError=()=>{
      throw Error()
    }
    return {
      throwError
    }
  }
})
</script>
<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

image.png

  • 通过window.addEventListener('error')来监听这个JS异常,监听这个异常属于JS稳定性错误。我们可以在monitorSDK下面的lib包下新建一个文件,命名为JSError。然后里面默认导出一个方法,命名为injectJsError。里面写入如下代码:
export function injectJsError(){
    window.addEventListener('error',function(event){
        console.log(event)
    })
}
  • 在monitorSDK中的index.js中使用这个方法,由于这个方法在main.js中进行了导入。那么我们页面启动的时候,就会加载这个index.js,执行里面所有的的监听器了 monitor\index.js
import {injectJsError} from './lib/jsError'
injectJsError()

这样我们抛出异常的时候,就会打印这个错误的具体event了。这个是我们开展后面各种各样异常监控的关键。

image.png

4.3构造错误类型数据结构

上面我们已经可以捕获到错误了,下面我们来对捕获到的错误进行进一步的处理,然后将这个错误的处理结果进行上报。捕获-> 处理-> 上报

{
    "kind": "", // 错误的大类
    "type": "", // 错误的小类
    "errorType": "", // 错误的具体类型
    "filename": "", // 错误在哪个文件进行发生的
    "tagName": "", // 错误在哪个页面标签发生的
    "selector": "", // 最后执行了什么操作导致了这个错误的发生
    "position": "", // 错误代码是在执行哪一段代码出错的
    "stack": "", //错误发生的时候JavaScript的堆栈信息
    "url": "", // 访问哪一个路由的时候出现的错误
    "extra": "", // 错误的额外标注信息
}

下面我们将一个一个的说明这些值是怎么通过event获取的。

  • 可以自定义命名,没有通用标准的部分: kind,type,errorType,extra
  • 错误代码产生的位置,position: positon:${event.lineno}:${event.colno}
  • 错误代码发生的文件 filename: event.target.src||event.target.href
  • 错误代码的堆栈信息 event.error.stack
  • 发生这个错误时最后一个操作的元素selector:

这里我们要写一个方法对window的点击,触摸,键盘点击,鼠标浮动做一个监听了。在utils包下面新建一个文件。命名为LastEvent.js

let lastEvent
['click','touchstart','mousedown','keydown','mouseover'].forEach(eventType=>{
    document.addEventListener(eventType,(event)=>{
        lastEvent=event;
    },{
        capture:true, // 在捕获阶段执行
        passive:true // 默认不阻止事件
    })
})
export  default  function(){

    return lastEvent;
}

里面导出一个方法,用lastEvent变量记录用户最后执行的时什么操作,然后将这个操作给他返回回去.然后我们再通过这个最后执行的操作,获取到是在页面上哪个元素的位置进行的操作。然后再将他的元素列表,倒序变成一个字符串,填充到我们这个需要上报的对象当中去。记住,传入的对象是最后一个操作的path数组,它代表我们dom相对于整个操作元素的文档结构。但是这个path数组在某些情况下也可能不存在,比如说js文件加载错误resourceLoadError。所以说我们就需要对整个path进行一个是不是数组的判断。

  • 在utils包下面新建一个文件,命名为getSelector.js
function getSelectors(path){
    return path.reverse().filter(element=>{
        return element!=document &&element!=window
    }).map(element=>{
        let selector=""
        // 如果点击的元素有ID的话
        if(element.id){
            return `${element.tagName.toLowerCase()}#${element.id}`
        // 如果点击的元素没有ID有Class的话
        }else if(element.className&&typeof element.className==='string'){
            return `${element.tagName.toLowerCase()}.${element.className}`
        // 如果元素上面两个都没有 那么就将他的元素名称小写返回
        }else {
            selector=element.tagName
        }
        return selector
    }).join(' ')
}

export default function getSelector(pathOrTarget){
    if(Array.isArray(pathOrTarget)){
        return getSelectors(pathOrTarget)
    }else{
        let path=[]
        while(pathOrTarget){
            path.push(pathOrTarget)
            pathOrTarget=pathOrTarget.parentNode
        }
        return getSelectors([pathOrTarget])
    }
}
  • 错误发生元素的标签名称获取方法 event.target.name

至此,我们需要上报的错误数据结构,就已经构造完毕了。

5.阿里云SLS服务

image.png

由于目前准备的不是很充分,所以先使用阿里云的这个日志服务来存储我们的JS错误信息。那么大家第一眼看到这个SLS的时候肯定很懵逼,因为他这里可以上报的地方太多了。有点眼花撩乱的感觉。那么我们就从开通这个服务开始,到怎么往这个SLS数据库里面存数据,一步一步的来。

  • 首先开通阿里云SLS日志服务
  • 然后创建一个项目

5.1 配置阿里云SLS日志服务并且配置web-trakcer上传错误信息

image.png

  • 这里会出现一个弹窗问你要不要开通logStore用于数据日志存储,我们选择开通。并且全部勾选

image.png

image.png

  • 然后会让你选择是否接入数据,这里我们选择是,并且搜索web-tracker

image.png

  • 配置web-tracker数据源

image.png

image.png

  • 点击查询日志按钮,进入我们的一个后台管理面板。这里就可以查看我们前端项目的报错了。

image.png

5.2 编写tracker上报类

image.png

好,这个规范知晓之后,我们在utils包下面新建一个类。命名为tracker.js

let host=''
let project=''
let logStore=''
let userAgent = require('user-agent');
function getExtraData() {
    return {
        title: document.title,
        url: location.href,
        timestamp: Date.now(),
        userAgent: userAgent.parse(navigator.userAgent).name,
        //用户ID
    }
}
//gif图片做上传 图片速度 快没有跨域 问题,
class SendTracker {
    constructor() {
        this.url = `http://${project}.${host}/logstores/${logStore}/track`;//上报的路径
        this.xhr = new XMLHttpRequest;
    }
    send(data = {}) {
        let extraData = getExtraData();
        let log = { ...extraData, ...data };
        //对象 的值不能是数字
        for (let 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', '0.6.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();
  • 然后我们在处理好错误数据结构之后调用这个类,进行错误上报:
tracker.send({
    kind: 'stability', // 监控指标的大类
    type: 'error',
    errorType:'jsError', // JS执行错误
    url: '', //访问哪个路径报错了
    message: event.message,
    filename: event.filename,
    position: `${event.lineno}:${event.colno}`,
    stack: getLines(event.error.stack),
    selector: lastEvent?getSelector(lastEvent.path): '' // 代表最后一个操作的元素
})
  • 最后修改过后的jsError这个lib如下:
import getLastEvent from "../utils/getLastEvent";
import getSelector from "../utils/getSelector";

export function injectJsError(){
    window.addEventListener('error',function(event){
        let lastEvent=getLastEvent();
        tracker.send({
            kind: 'stability', // 监控指标的大类
            type: 'error',
            errorType:'jsError', // JS执行错误
            url: '', //访问哪个路径报错了
            message: event.message,
            filename: event.filename,
            position: `${event.lineno}:${event.colno}`,
            stack: event.error.stack.toString(),
            selector: lastEvent?getSelector(lastEvent.path): '' // 代表最后一个操作的元素
        })
    })
}
  • 测试看logStore是否存在数据 前端显示

image.png

logStore显示

image.png

  • 简单错误收集- 上报- 处理 框架搭建完毕.

5. 总结

本质上前端监控就是在页面加载的时候插入一段代码,对系统中出现的各种可能的错误进行监控。然后根据机器上不同的端口运行着各种不同的项目,每个项目出现错误都对其错误发生的位置,原因JS堆栈信息进行记录。然后上报到Mysql或者其他数据库进行保存。

后面会写更多的前端监控方面的内容,这是一个系列,文章对你有帮助的话可以点一个赞支持下哦