关于前端监控方案

当有人问起:你们的公司的这款应用用户体验怎么样呀?访问量怎么样?此时,你该怎么回答呢?你会回答:UV(独立访客数,以cookie或token为依据)、PV(访问量,页面浏览次数) ,IP(独立IP数)等页面流量指标。

秒开率、RTT(延迟,数据往返时间)、TTI(页面可交互时间,用户与页面进行互动之前所花费的时间) 、FCP(首屏时间)、 FP(白屏时间)等性能指标。详见:前端页面的常见性能指标

那么,这些数据是哪里来的呢?显而易见,这些数据都来自前端监控系统。

前端监控的意义

  1. 性能监控,避免由性能问题影响用户的留存率
  2. 错误监控,避免用户流失
  3. 数据上报,根据用户行为和数据进行分析

错误采集方式

错误监控可以分为:脚本错误监控请求错误监控资源错误监控

脚本错误监控

编译时错误

一般在开发阶段就会发现,配合 lint 工具比如 eslint、tslint 等以及 git 提交插件比如 husky 等,基本可以保证线上代码不出现低级的编译时错误。大厂一般都有发布前置检测平台,能够在发布前提前发现编译时错误。

运行时错误

搭建前端检测平台。

前端检测平台错误捕获的机制有:

  • Js 通过 try catch 捕获错误。但是,try catch 捕获错误是侵入式的,需要在开发代码时即提前进行处理,无法做到在所有可能产生错误的代码片段中都嵌入 try catch
  • 全局捕获脚本错误:根据 window.onerror 事件进行全局捕获,但只能绑定一个回调函数,且回调函数的参数过于离散,使用不方便,且不能监听到资源错误。可以使用addEventListener,但只有 window.onerror 才能阻止抛出错误到控制台
  • 以上两种方法无法捕获 Promise 错误,需要用到 Promise 错误事件:unhandledrejection 以及 rejectionhandled

try-catch缺点:

  1. 需要提前进行处理,无法全局捕获
  2. 通过 try catch 包裹,影响代码可读性
  3. 无法处理语法错误,比如使用了中文标点
  4. 无法处理异步中的错误,比如 setTimeout
/**
 * @description window.onerror 全局捕获错误
 * @param event 错误信息,如果是
 * @param source 错误源文件URL
 * @param lineno 行号
 * @param colno 列号
 * @param error Error对象
 */
window.onerror = function (event, source, lineno, colno, error) {
  // 上报错误
  // 如果不想在控制台抛出错误,只需返回 true 即可
};

//-----------------------------------

/**
 * @param event 事件名
 * @param function 回调函数
 * @param useCapture 回调函数是否在捕获阶段执行,默认是false,在冒泡阶段执行
 */
window.addEventListener('error', (event) => {
  // addEventListener 回调函数的离散参数全部聚合在 error 对象中
  // 上报错误
}, true)

// Promise错误捕获------------------------
// 当 Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件。
// 当 Promise 被 reject 且有 reject 处理器的时候,会触发 rejectionhandled 事件。

// unhandledrejection
window.addEventListener('unhandledrejection', (event) => {
  console.log(event)
}, true);

// unhandledrejection 备选处理方案
window.onunhandledrejection = function (error) {
  console.log(error)
}

// rejectionhandled 推荐处理方案
window.addEventListener('rejectionhandled', (event) => {
  console.log(event)
}, true);

// rejectionhandled 备选处理方案
window.onrejectionhandled = function (error) {
  console.log(error)
}

框架错误

框架提供了 API 来捕获全局错误。在 Vue 中,提供了 errorHandler  来捕获错误(vue2vue3)。

微信小程序提供了app.onError的方法。

React 提供了错误边界组件,它可以捕获其子组件任何位置的 JS 错误,并且会渲染出备用 UI。如果在 class 组件中定义了static getDerivedStateFromError()或者componentDidCatch()这两个方法中任何一个时,该组件就变成了错误边界。

Vue.config.errorHandler = function (err, vm, info) {
  // handle error
  // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
  // 只在 2.2.0+ 可用
}

// Vue 3.x
app.config.errorHandler = (err, instance, info) => {
  // 处理错误,例如:报告给一个服务
}

请求错误监控

前端请求有两种方案 ajax 或者 fetch ,只需重写两种方法,进行代理,即可实现请求错误监控。

代理的核心在于使用 apply 重新执行原有方法,并且在执行原有方法之前进行监听操作。

在请求错误监控中,我们关心三种错误事件:aborterror 以及 timeout,所以,只需在代理中对这三种事件进行统一处理即可。

ps:可以使用请求工具如 axios ,不需要重写 ajax 或者 fetch 只需在请求拦截器以及响应拦截器进行处理上报即可。

资源错误监控

资源错误一般是指页面上的静态资源路径错误,如图片src。可以通过监控error错误事件实现错误捕获。

我们可以通过 instanceof 区分脚本错误和资源错误,脚本错误参数对象 instanceof ErrorEvent,而资源错误的参数对象 instanceof Event

由于 ErrorEvent 继承于 Event ,所以不管是脚本错误还是资源错误的参数对象,它们都 instanceof Event,所以,需要先判断脚本错误。此外,两个参数对象之间有一些细微的不同,比如,脚本错误的参数对象中包含 message ,而资源错误没有,这些都可以作为判断资源错误或者脚本错误的依据。

/**
 * @param event 事件名
 * @param function 回调函数
 * @param useCapture 回调函数是否在捕获阶段执行,默认是false,在冒泡阶段执行
 */
window.addEventListener('error', (event) => {
  if (event instanceof ErrorEvent) {
    console.log('脚本错误')
  } else if (event instanceof Event) {
    console.log('资源错误')
  }
}, true);

Ps:使用 addEventListener 捕获资源错误时,一定要将 useCapture 即第三个选项设为 true,因为资源错误没有冒泡,所以只能在捕获阶段捕获。同理,由于 window.onerror 是通过在冒泡阶段捕获错误,所以无法捕获资源错误。

跨域脚本错误捕获

为了性能方面的考虑,我们一般会将脚本文件放到 CDN ,这种方法会大大加快首屏时间。但是,如果脚本报错,此时,浏览器出于于安全方面的考虑,对于不同源的脚本报错,无法捕获到详细错误信息,只会显示 Script Error。那么,有解决该问题的方案吗?

  1. 方案一:所有的脚本全部放到同一源下,但是,该方案会放弃 CDN ,降低性能。(可以使用Nginx做代理)
  2. 方案二:在 script 标签中,添加 crossorigin 属性(推荐使用 webpack 插件自动添加);同时,配置 CDN 服务器,为跨域脚本配上 CORS

方案二基本可以完美解决跨域脚本错误捕获的问题。但是,crossorigin 属性对于 IE 以及 Safari 支持程度不高。

错误上报方式

采用Ajax上报、使用image对象模拟图片请求方式上报。

使用Ajax请求上报,要对错误进行防抖操作,避免频繁上报重复错误。

一般来说,大厂都是采用利用image对象的方式上报错误的;使用图片发送get请求,上报信息,由于浏览器对图片有缓存,同样的请求,图片只会发送一次,避免重复上报。

var entry = {};
function report(url, data) {
    if (!url || !data) {
        return;
    }
    // @see http://jsperf.com/new-image-vs-createelement-img
    var image = document.createElement('img');
    var items = [];
    for (var key in data) {
        if (data[key]) {
            items.push(key + '=' + encodeURIComponent(data[key]));
        }
    }
    var name = 'img_' + (+new Date());
    entry[name] = image;
    image.onload = image.onerror = function () {
      console.log(arguments);
        entry[name] =
            image =
            image.onload =
            image.onerror = null;
        delete entry[name];
    };
    image.src = url + (url.indexOf('?') < 0 ? '?' : '&') + items.join('&');
}

错误上报需要的数据内容有:时间、message、错误类型、错误文件路径、source-map文件等。

另外,前台可建立数据可视化平台,对错误进行实时监控。

加载中...
加载中...