前端SDK开发|青训营笔记

157 阅读7分钟

这是我参与第五届青训营伴学笔记创作活动的第十六天

前端监控系统最核心的首要是收集客户端的相关数据,我们现在支持的客户端探针有:web、微信小程序、andriodios。它们主要收集如图以下信息:

性能

收集页面加载、静态资源、ajax接口等性能信息,指标有加载时间、http协议版本、响应体大小等,这是为业务整体质量提升提供数据支撑,解决慢查询问题等。

错误

收集js报错、静态资源加载错误、ajax接口加载错误,这些常规错误收集都很好理解。下面主要说明一下"业务接口错误(bussiness)":

客户端发送ajax请求后端业务接口,接口都会返回json数据结构,而其中一般都会有errorcodemessage两个字段,errorcode为业务接口内部定义的状态码。正常的业务响应内部都会约定比如errorcode==0等,那如果不为0可能是一些异常问题或者可预见的异常问题,这种错误数据就是需要收集的。

由于不同团队或者接口可能约定都不一样,所以我们只会提供一个预设方法,预设方法会在ajax请求响应后调用,业务方自己根据约定和响应的json数据,在预设的方法中编写判断逻辑控制是否上报。像是下面这样:

errcodeReport(res) {
  if (Object.prototype.toString.call(res) === '[object Object]' && res.hasOwnProperty('errcode') && res.errcode !== 0) {
    return { isReport: true, errMsg: res.errmsg,code: res.errcode };
  }
  return { isReport: false };
}
复制代码

辅助信息

除了上面两类硬指标数据,我们还需要很多其它的信息,比如:用户的访问轨迹、用户点击行为、用户ID、设备版本、设备型号、UV/UA标识、traceId等等。很多时候我们要解决的问题并不是那么简单直接就能排查出来,甚至我们需要前端监控和其它系统在某些情况下能够关联上,所以这些软指标信息同样很重要。

在这里专门解释一下traceId:

现在的后端服务都会使用APM(应用性能管理)系统,APM工具会在一次完整请求调用之初生成唯一的id,通常叫做traceId,它会记录整个请求过程服务端的链路细节。如果前端能够获取到它,就能通过它去后端APM系统中查询某次请求的日志信息。只要后端做好相关的配置,后端接口在响应客户端http请求时,可以把traceId返回给客户端,SDK便可以去收集ajax请求的traceId,这样前后端监控就能够关联上了。

收集以上的信息并开发一套管理台,能够达到监控前端性能和异常错误的目的。想象一个场景,当我们收到监控系统的告警或者相关同事的问题反馈时,我们能打开管理台,首先查看到实时的错误,如果发现是js的代码导致的问题,我们能很快找到前端代码错误的地方。如果不是前端的错误,我们通过收集的业务接口错误发现是后端接口的问题,我们也能及时的通知后端同事,在什么时间哪个接口报出errorcode为xx的错误,并且我们还能通过traceId直接查到这次ajax请求的后端链路监控数据。如果实在不是明显就能排查到的问题,我们还能通过收集到的用户轨迹、设备信息和网络请求等数据,多方面的分析还原用户当时的场景,来辅助我们排查代码中的难以复现的bug或者兼容问题。

【具体字段一览】

确定好了要收集哪些信息,接下来就需要去实现客户端SDK,它能够在业务项目中自动收集数据上报给服务端。

所谓探针,是因为我们的SDK要依托于监控的前端项目的运行环境,在其运行环境的底层API中加入探针函数来收集信息,下面分享WEB和微信小程序SDK实现的主要原理和使用的API

WEB

下图是SDK主要使用的Web API,通过这几个API我们就能分别获取到:页面性能信息、资源性能信息、ajax信息、错误信息。

Performance

通过performance.timing可以拿到页面首次加载的性能数据,dnstcp、白屏时间等,而在最新的标准中performance.timing已经被废弃,因此我们也改造为使用performance.getEntriesByType('navigation')。这里的白屏时间可能和实际真正的用户感官的白屏时间是有差异的,仅供参考。

通过new PerformanceObserver监听器,我们可以监听所有资源(css,script,img,ajax等)加载的性能数据:加载时间,响应大小,http协议版本(http1.1/http2)等。而后我们需要通过一个数组去管理资源性能数据,在完成数据上报后,清空数组。

fetch/xmlHttpRequest

由于浏览器并没有提供一个统一的API使我们能够收集到ajax请求和响应数据,并且不管我们是用axois还是使用其他的http请求库,他们都是基于fetchxmlHttpRequest实现的。因此只能通过重写fetchxmlHttpRequest,并在对应的函数和逻辑中插入自定义代码,来达到收集的目的。相关的文章很多,这里就不再细说了。

let _fetch = fetch;
window.fetch = function () {
  // custom code
  return _fetch
    .apply(this, arguments)
    .then((res) => {
      // custom code
      return res;
    })
};
复制代码

window.onerror | unhandledrejection | console.error | 以及框架自带的监听函数

最后这几个API都是收集js相关错误信息的。需要注意两个问题:

一是onerror会获取不到跨域的script错误,解决方案也很简单:为跨域的script标签设置crossorigin属性,并且需要静态服务器为当前资源设置CORS响应头。

二是代码压缩后的报错信息需要通过sourceMap文件解析出源代码对应的行列和错误信息,sourceMap本身是一种数据结构,存储了源代码和压缩代码的关系数据,通过解析库能够很轻松转换它们。但如何自动化管理和操作sourceMap文件才是前端监控系统核心需要解决的问题。这里就需要结合企业内部的静态资源发布系统和前端监控系统,来解决低效率的手动打包上传问题。

微信小程序

微信小程序底层使用js实现,有着它自己的一套生命周期,也提供了全局的API。通过重写它的部分全局函数和相关API我们能获取到:网络请求、错误信息、设备和版本信息等。由于微信小程序的加载流程是由微信APP控制的,js等资源也被微信内部托管,因此和web不同,我们没有办法获取到webperformance能获取到的页面和资源加载信息(后来发现小程序已经在v2.11.0 (2020-04-24)版本中,新增 API 提供performance性能对象指标,以后可以使用了)。下图是SDK主要使用的API

App和Component

通过重写全局的App函数,绑定onError方法监听错误,重写它的onShow方法执行小程序启动时SDK需要的逻辑。通过重写ComponentonShow方法,可以在页面组件切换时执行我们的路径收集和执行上报等逻辑。

// SDK初始化函数
init(){
    this.appMethod = App;
    this.componentMethod = Component;
    const ctx = this;
    //重写微信小程序Component
    Component = (opts) => {
      overrideComponent(opts, ctx);
      ctx.componentMethod(opts);
    };
    //重写微信小程序App
    App = (app) => {
      overrideApp(app, ctx);
      ctx.appMethod(app);
    };
}  

//注意ctx是sdk的this
overrideComponent(opts, ctx) => {
  const compOnShow = opts.methods.onShow;
  opts.methods.onShow = function(){
    // do something
    //注意这里的this是实际调用方
    compOnShow.apply(this, arguments)
  }
})

overrideApp(app, ctx) => {
  const _onError = app.onError || function () {};
  const _onShow = app.onShow || function () {};
  app.onError = function (err) {
    reportError(err, ctx);
    return _onError.apply(this, arguments);
  };
  app.onShow = function () {
    //do something
    return _onShow.apply(this, arguments);
  };
})