前言
收集哪些数据
性能
错误
辅助信息
小结
客户端SDK(探针)相关原理和API
Web
微信小程序
编写测试用例
单元测试
流程测试
提供Web环境的方式
Mock Web API的方式
结语
随着前端的发展和被重视,慢慢的行业内对于前端监控系统的重视程度也在增加。这里不对为什么需要监控再做解释。那我们先直接说说需求。
对于中小型公司来说,可以直接使用三方的监控,比如自己搭建一套免费的<code>sentry</code>就可以捕获异常和上报事件,或者使用阿里云的<code>ARMS</code>,功能比较全面也并不会太贵。类似的开源系统或者付费系统还很多,都能满足我们一定的需求。
假如这个公司逐渐成长,已经成为一个中大型的公司,用户量、业务服务、公司整体架构全部都在升级,这样三方的监控系统可能就慢慢的出现一些不能满足需求的问题。比如企业内部各种系统之间的关系太独立和分散,不能使用内部的统一登陆、不能相互跳转,想要增加一些字段收集并不能很快得到支持等等。这些问题都会导致效率上不能满足企业发展要求。一个内部可控并且能高速响应企业需求的前端监控系统就显得很有必要。
我们在内部的前端监控系统上已经投入了一定的精力和时间,今天分享一下前端监控<code>SDK</code>部分的内容,主要三个方面:
客户端SDK(探针)及原理
前端监控系统最核心的首要是收集客户端的相关数据,我们现在支持的客户端探针有:<code>web</code>、微信小程序、<code>andriod</code>和<code>ios</code>。它们主要收集如图以下信息:

收集页面加载、静态资源、<code>ajax</code>接口等性能信息,指标有加载时间、<code>http</code>协议版本、响应体大小等,这是为业务整体质量提升提供数据支撑,解决慢查询问题等。
收集<code>js</code>报错、静态资源加载错误、<code>ajax</code>接口加载错误,这些常规错误收集都很好理解。下面主要说明一下"业务接口错误(bussiness)":
客户端发送<code>ajax</code>请求后端业务接口,接口都会返回<code>json</code>数据结构,而其中一般都会有<code>errorcode</code>和<code>message</code>两个字段,<code>errorcode</code>为业务接口内部定义的状态码。正常的业务响应内部都会约定比如<code>errorcode==0</code>等,那如果不为<code>0</code>可能是一些异常问题或者可预见的异常问题,这种错误数据就是需要收集的。
由于不同团队或者接口可能约定都不一样,所以我们只会提供一个预设方法,预设方法会在<code>ajax</code>请求响应后调用,业务方自己根据约定和响应的<code>json</code>数据,在预设的方法中编写判断逻辑控制是否上报。像是下面这样:
除了上面两类硬指标数据,我们还需要很多其它的信息,比如:用户的访问轨迹、用户点击行为、用户ID、设备版本、设备型号、UV/UA标识、<code>traceId</code>等等。很多时候我们要解决的问题并不是那么简单直接就能排查出来,甚至我们需要前端监控和其它系统在某些情况下能够关联上,所以这些软指标信息同样很重要。
在这里专门解释一下<code>traceId</code>:
现在的后端服务都会使用<code>APM</code>(应用性能管理)系统,<code>APM</code>工具会在一次完整请求调用之初生成唯一的<code>id</code>,通常叫做<code>traceId</code>,它会记录整个请求过程服务端的链路细节。如果前端能够获取到它,就能通过它去后端<code>APM</code>系统中查询某次请求的日志信息。只要后端做好相关的配置,后端接口在响应客户端<code>http</code>请求时,可以把<code>traceId</code>返回给客户端,SDK便可以去收集<code>ajax</code>请求的<code>traceId</code>,这样前后端监控就能够关联上了。
收集以上的信息并开发一套管理台,能够达到监控前端性能和异常错误的目的。想象一个场景,当我们收到监控系统的告警或者相关同事的问题反馈时,我们能打开管理台,首先查看到实时的错误,如果发现是<code>js</code>的代码导致的问题,我们能很快找到前端代码错误的地方。如果不是前端的错误,我们通过收集的业务接口错误发现是后端接口的问题,我们也能及时的通知后端同事,在什么时间哪个接口报出<code>errorcode</code>为xx的错误,并且我们还能通过<code>traceId</code>直接查到这次<code>ajax</code>请求的后端链路监控数据。如果实在不是明显就能排查到的问题,我们还能通过收集到的用户轨迹、设备信息和网络请求等数据,多方面的分析还原用户当时的场景,来辅助我们排查代码中的难以复现的<code>bug</code>或者兼容问题。
在以上这个场景中,我们能够提高前端排查问题的能力,甚至能辅助后端同学。在大部分时候,出现<code>bug</code>,很可能第一时间首先是找到前端做反馈,前端是排查问题的先头部队。当我们有这样的前端监控系统之后,不至于每次遇到问题手足无措,解决问题的时间也会快许多。
【具体字段一览】
确定好了要收集哪些信息,接下来就需要去实现客户端<code>SDK</code>,它能够在业务项目中自动收集数据上报给服务端。
所谓探针,是因为我们的<code>SDK</code>要依托于监控的前端项目的运行环境,在其运行环境的底层<code>API</code>中加入探针函数来收集信息,下面分享<code>WEB</code>和微信小程序<code>SDK</code>实现的主要原理和使用的<code>API</code>。
下图是<code>SDK</code>主要使用的<code>Web API</code>,通过这几个<code>API</code>我们就能分别获取到:页面性能信息、资源性能信息、<code>ajax</code>信息、错误信息。
通过<code>performance.timing</code>可以拿到页面首次加载的性能数据,<code>dns</code>、<code>tcp</code>、白屏时间等,而在最新的标准中<code>performance.timing</code>已经被废弃,因此我们也改造为使用<code>performance.getEntriesByType('navigation')</code>。这里的白屏时间可能和实际真正的用户感官的白屏时间是有差异的,仅供参考。
通过<code>new PerformanceObserver</code>监听器,我们可以监听所有资源(<code>css</code>,<code>script</code>,<code>img</code>,<code>ajax</code>等)加载的性能数据:加载时间,响应大小,<code>http</code>协议版本(<code>http1.1</code>/<code>http2</code>)等。而后我们需要通过一个数组去管理资源性能数据,在完成数据上报后,清空数组。
由于浏览器并没有提供一个统一的<code>API</code>使我们能够收集到<code>ajax</code>请求和响应数据,并且不管我们是用<code>axois</code>还是使用其他的<code>http</code>请求库,他们都是基于<code>fetch</code>和<code>xmlHttpRequest</code>实现的。因此只能通过重写<code>fetch</code>和<code>xmlHttpRequest</code>,并在对应的函数和逻辑中插入自定义代码,来达到收集的目的。相关的文章很多,这里就不再细说了。
最后这几个API都是收集js相关错误信息的。需要注意两个问题:
一是<code>onerror</code>会获取不到跨域的<code>script</code>错误,解决方案也很简单:为跨域的<code>script</code>标签设置<code>crossorigin</code>属性,并且需要静态服务器为当前资源设置<code>CORS</code>响应头。
二是代码压缩后的报错信息需要通过<code>sourceMap</code>文件解析出源代码对应的行列和错误信息,<code>sourceMap</code>本身是一种数据结构,存储了源代码和压缩代码的关系数据,通过解析库能够很轻松转换它们。但如何自动化管理和操作<code>sourceMap</code>文件才是前端监控系统核心需要解决的问题。这里就需要结合企业内部的静态资源发布系统和前端监控系统,来解决低效率的手动打包上传问题。
微信小程序底层使用<code>js</code>实现,有着它自己的一套生命周期,也提供了全局的<code>API</code>。通过重写它的部分全局函数和相关<code>API</code>我们能获取到:网络请求、错误信息、设备和版本信息等。由于微信小程序的加载流程是由微信<code>APP</code>控制的,<code>js</code>等资源也被微信内部托管,因此和<code>web</code>不同,我们没有办法获取到<code>web</code>中<code>performance</code>能获取到的页面和资源加载信息(后来发现小程序已经在v2.11.0 (2020-04-24)版本中,新增 API 提供performance性能对象指标,以后可以使用了)。下图是<code>SDK</code>主要使用的<code>API</code>
通过重写全局的<code>App</code>函数,绑定<code>onError</code>方法监听错误,重写它的<code>onShow</code>方法执行小程序启动时<code>SDK</code>需要的逻辑。通过重写<code>Component</code>的<code>onShow</code>方法,可以在页面组件切换时执行我们的路径收集和执行上报等逻辑。
这里也是因为和 <code>fetch/xmlHttpRequest</code> 一样,并没有一个全局的<code>API</code>能让我们捕获到请求信息,因此只能通过重写<code>wx.request</code>来达到监听收集的功能。
当我们已经实现了<code>SDK</code>之后或者说在实现的过程中,就需要编写测试代码了,下面说说编写测试用例。
<code>SDK</code>属于一个需要长期维护和更新的独立库,它被使用在很多业务项目中,要求更加稳定,当出现问题的时候,它的更新成本很高。需要经历:更新代码->发布新版本->业务方更新依赖版本,等流程,而如果在这个流程中,假如<code>SDK</code>又改出其它问题,那将会再启上述循环,业务同事肯定会被麻烦死。随着接入监控的系统增多,在迭代过程中改动任何的代码已经让人开始发慌,因为存在很多流程性的关联逻辑,害怕改出问题。在一次代码的重构和优化过程中,决心完善单元测试和流程测试。
单元测试主要是对一些有明显输入输出的通用方法,比如<code>SDK</code>的<code>utils</code>中的常用方法,<code>SDK</code>的参数配置方法等。而对于监控<code>SDK</code>来说,更多的测试代码主要集中在流程测试,对于单元测试这里就不具体说明了。
监控<code>SDK</code>在业务项目中初始化之后,主要是通过加入探针监听业务项目的运行状态而收集信息并进行上传的,它在大部分情况下并不是业务方调用什么就执行什么。比如我们页面初次加载,<code>SDK</code>在合适的时机会执行首次加载相关信息的收集并上传,那我们需要通过测试代码来模拟这个流程,保障上报的数据是预期的。
我们的<code>SDK</code>运行在浏览器环境中,在<code>node</code>环境下是不支持<code>Web</code>相关<code>API</code>的。因此我们需要让我们的测试代码在浏览器中运行,或者提供相关<code>API</code>的支持。下面我们将会介绍两种不同的方式,来支持我们的测试代码正常运行。
假如我们使用<code>mocha</code>或者<code>jest</code>作为测试框架,可以通过<code>mocha</code>自带的<code>mocha.run</code>方法在<code>html</code>中编写和执行我们的测试代码,并在浏览器中打开运行;<code>jest-lite</code>也可以支持让<code>jest</code>运行在浏览器中。
但有时候我们不想让它打开浏览器,希望在终端中就能完成测试代码运行,可以使用无头浏览器,在<code>node</code>中加载浏览器环境,比如<code>phontomjs</code>或者<code>puppeteer</code>。他们提供了相关的工具,比如<code>mocha-phantomjs</code>就能直接在终端中运行<code>html</code>执行测试流程。
基于写好的<code>html</code>测试文件,再使用<code>mocha-phantomjs</code>和<code>phantomjs</code>,以下是<code>package.json</code>的命令配置。
<code>phontomjs</code>已经被废弃了,不被推荐使用。推荐<code>puppeteer</code>,相关的功能和类似工具都有支持。
举例说明:
以前有在<code>WebSocket</code>的代码库中使用过这种方式。因为依赖Web Api: <code>WebSocket</code>。需要通过<code>new WebSocket()</code>,来完成测试流程,而<code>node</code>环境下没有此<code>API</code>。于是使用<code>mocha</code>在<code>html</code>中写测试用例,如果希望全程使用终端跑测试,还可以配合使用<code>mocha-phantomjs</code>让测试的<code>html</code>文件可以在终端中执行而不用打开本地的网页运行。
当然其实完全可以直接在浏览器中打开<code>html</code>查看测试运行结果,而且<code>phantomjs</code>相关的依赖包非常大、安装也比较慢。但当时我们使用了持续继承服务travis,当我们的代码更新到远程仓库以后,<code>travis</code>将会启动多个独立容器并在终端中执行我们的测试文件,如果不使用<code>mocha-phantomjs</code>在终端中跑测试没有办法在<code>travis</code>中成功通过。
在这次完善监控<code>SDK</code>测试的过程中,尝试了另一种方式,全程使用<code>Mock</code>的方式。
上面的<code>Web</code>环境运行方式需要提供浏览器或者无头浏览器。但实际我们需要测试的代码并不是<code>Web API</code>,我们只是使用了它们。我们假定它们是稳定的,我们只需要在乎它的输入输出,如果它们内部出<code>bug</code>了,我们也是不能控制的,那是浏览器开发商的事情。因此我要做的事情仅仅是在<code>node</code>环境中模拟相关的<code>Web API</code>。
拿前面说到的<code>WebSocket</code>举例,因为<code>node</code>中不支持<code>WebSocket</code>,我们没有办法<code>new WebSocket</code>。那假如有完全模拟<code>WebSocket</code>的三方<code>node</code>库,我们就可以在<code>node</code>代码中,直接让执行环境支持<code>WebSocket</code>: <code>const WebSocket = require('WebSocket')</code>。这样我们就不需要在浏览器或者无头浏览器环境下运行了。
下面就具体拿我们的监控<code>SDK</code>中的<code>fetch</code>举例,是如何模拟流程测试的,总的来说要支持下面3个内容,
启动一个httpserver服务提供接口服务
引入三方库,让node支持fetch
node中手动模拟部分performance API
首先说明一下<code>SDK</code>中<code>fetch</code>的正常流程,当我们的<code>SDK</code>在业务项目中初始化了之后,<code>SDK</code>会重写<code>fetch</code>,于是业务项目中真正使用<code>fetch</code>做业务接口请求的时候,<code>SDK</code>就能通过之前重写的逻辑获取到<code>http</code>请求和响应信息,同时也会通过<code>performance</code>获取到<code>fetch</code>请求的性能信息,并进行上报。我们要写的测试代码,就是验证这个流程能够顺利完成。
因为是验证<code>fetch</code>完整流程,我们需要启动一个<code>httpserver</code>服务,提供接口来接收和响应这次<code>fetch</code>请求。
<code>node</code>环境中支持<code>fetch</code>的话,我们可以直接使用三方库node-fetch,在执行环境的顶部,我们就可以提前定义<code>fetch</code>。
而<code>performance</code>就比较特殊一点,没有一个三方的库能够支持。对于<code>fetch</code>流程来说,我们如果要模拟<code>performance</code>,只需要模拟我们使用的<code>PerformanceObserver</code>,甚至一些入参和返回我们也可以只模拟我们需要的。下面的代码是<code>PerformanceObserver</code>的使用例子。在<code>SDK</code>中,我们主要也是使用这一段代码。
在浏览器内部<code>performance</code>底层会自动去监听资源请求,我们只是通过它提供<code>PerformanceObserver</code>去收集它的数据。本质上来说,主动收集的行为探针在<code>performance</code>内部实现。
下面我们模拟<code>PerformanceObserver</code>一部分功能,来支持我们需要的测试流程。定义<code>window.PerformanceObserver</code>为构造函数,把传入方法参数<code>fn</code>加入到数组中。<code>mockPerformanceEntriesAdd</code> 是我们需要手动调用的方法,当我们发起一次<code>fetch</code>,我们就手动调用一下此方法,把<code>mock</code>数据传入给注册的监听函数,这样就能使<code>PerformanceObserver</code>的实例接收到我们的<code>mock</code>数据,以此来模拟浏览器中<code>performance</code>内部的行为。
通俗点举例来说,十号公司要给打工人银行卡发工资的,打工人的工资银行卡第二天就会被扣房贷。打工人最关心的保障正常扣房贷否则影响征信。本来打工人只需要关注银行是否成功完成扣款,但是打工人最近丢工作了公司不会打款到工资卡,所以只能拿积蓄卡给自己的扣贷银行卡转钱,让后续银行可以扣钱还房贷。公司就是浏览器<code>performance</code>底层,打工人给自己转钱就是<code>mockPerformanceEntriesAdd</code>,把公司发工资到银行卡替换为自己转钱进去,从被动接收变为主动执行。细品,你细品~
<code>mockPerformanceEntriesAdd</code>就是模拟浏览器的主动行为,入参是性能信息,我们可以直接写死(下方<code>mockData</code>)。
看看测试代码
当<code>mockPerformanceEntriesAdd</code>执行的时候,<code>SDK</code>内部的<code>PerformanceObserver</code>便能收集到mock的性能信息了。( 这里注意,我们还需要启动一个<code>httpserver</code>的服务,服务提供<code>http://localhost:xx/api/getData</code>接口 )
当上面的测试代码运行的时候,<code>SDK</code>能够获取地址为<code>http://localhost:xx/api/getData</code>的<code>fetch</code>的请求、响应和性能信息,并且<code>SDK</code>也会发送一次<code>fetch</code>请求把收集的数据上报给后端服务。我们可以再次重写<code>window.fetch</code>,来拦截<code>SDK</code>的上报请求,就可以获取到请求内容,用请求内容来做预期测试判断
合并后的测试代码
如上图所示,我们主要是以这样的模式进行<code>SDK</code>的流程测试和代码编写。有了测试代码后,能够在很大程度上保障代码维护迭代过程中的稳定性可控性,也能省去很多后期测试成本。
以上分享是我们在做监控<code>SDK</code>时比较核心的这三个方面,还有很多其它的细节和实现,比如:如何节流、上报时机、数据合并、初始化配置等。开发迭代过程中,要避免客户端<code>SDK</code>或者后端服务因为迭代造成的兼容性问题。还比较重要的是要考虑后期数据库查询和存储方面的需求,收集、存储和查询才能完整的构成这套前端监控系统。
- End -
有没有人打赏?没有的话,那我晚点再来问问。
关注大诗人公众号,第一时间获取最新文章。
如果你有购买钢琴的打算,可以从这里了解到在售信息,价格实惠品质保障。
---转发请标明,并添加原文链接---