当前互联网产品竞争激烈的环境下,前端研发在一个产品的生产链上承担了越来越重要的角色。作为直达用户的一层应用,跟安卓IOS的原生App一样,可以说是直接衡量一个项目产品好坏的第一道关口。
和原生应用不同的是,前端应用在运行环境方面存在太多不可控的复杂因素,并没有一个相对稳定运行环境来保证我们的项目一定不出问题。有时候哪怕我们自测再充分,在不同用户复杂的运行环境和操作之下,也难免会出现开发者意想不到的问题。
出现问题不可怕,可怕的是我们解决线上问题的手段贫乏,效率低下,导致项目的体验和质量低下。这也是一个产品初期,前端这一块发生比较普遍的现象。
业务场景中的痛点
在没有一个好的解决方案之前,通常一个解决线上问题的发生和解决的过程是这样:
这已经是一个比较理想且和谐的线上问题解决案例了。事实上很多时候,我们很难那么顺利地解决用户端的问题,测试现有的资源无法复现,用户端和我们研发之间相隔太多层级,都使我们无法及时准确地了解错误本身。想要让产品的质量和服务更上一层楼,不能等总是用户发现错误,我们需要及时快速地响应解决错误,这时候一个好的监控平台十分必要。
更重要的一点是,这些功能是很通用,可未必能够满足我们自己的需求。所以我们打算搭建一套属于映客前端自己的监控平台。
总的来说,前端监控平台可以帮我们解决了如下问题:
快速发现线上问题,并在用户反馈之前解决,减小线上问题带来影响
详细地了解用户端的页面信息,方便协助排查解决问题
统计项目数据,评估项目质量,进一步可以生成多维度的数据分析报表
减少研发阶段异地沟通的成本,测试中的项目也能很快发现并解决问题,加快研发效率
消除购买第三方产品的成本,定制化我们需要的功能
为了打造一个轻量级、易接入的监控平台,我们设计了异常上报过程的整个流程架构,如下:
如图展示,在项目中只需要引入一段js,即可开始监听错误事件,记录用户行为,检查运行环境信息,录屏(选择接入)等四个模块的任务。当一旦检测到错误发生,或者用户主动调用我们暴露给开发者的一个全局方法,即可实现上报。上报信息除了错误本身的信息以外,还包括此时记录下来的所有页面信息。
三、基本原理
JS错误包含两种:
JS引擎在检测语法没有错误的情况下,开始运行代码内容。由于JS是一门解释型语言,JS引擎执行前并不知道要执行下一行代码的逻辑是否能被识别,所以可能会出现运行时的错误,如:引用错误,类型错误等。
JS的内置对象Error,就是用来生成描述所有错误对象的构造函数,包含内建的标准错误类型有:
虽然全局的error事件能够帮助我们监听到Js运行时的异常,可是一个页面里的错误可不仅仅就这些,因此需要其他方式检测到这些错误。
前端页面的网络请求的方式有两种:XMLHttpRequest(简称XHR)和fetch。都可以通过劫持这两个请求,来自定义事件,来实现监听(下文详细说明)
在开始搭建之前,我们内部确定了这个项目的基本功能和目标,总结如下:
我们希望插入其他项目的代码量在10k以内(不包含录屏模块)。轻量的资源才能更快的加载,减少性能的影响
即项目内部报错,绝不影响接入的项目的任何功能,内部功能防干扰的同时对全局影响做到最小。所以项目内部需要一个完善的自我catch错误的机制,保证项目的稳定性。
虽然现在我们的项目很多都是面向新型浏览器的业务,但是我们也没放弃老版本浏览器的兼容,包括IE。因此要求我们的项目里使用的核心API都是兼容性很强的。
接下来,我将针对流程图里初始化后的四个模块逐个讲解实现。
错误监听是本项目的核心,我们所有功能都是围绕这个展开。
如3.3.1提到的原理,Promise的,Promise监听的实现代码如下:
如3.3.2提到的原理,在捕获期间监听全局error事件,代码如下:
从前端逻辑代码来说,这并不算是前端的错误。但是作为一个完整的webview应用,网络请求是页面功能实现非常重要的部分,上报网络请求的错误,可以协助我们排查服务端的问题。网络请求的监听不仅仅在错误的时候需要,用户行为记录里也需要,因此现在这里统一讲述吧。
看的出来,经过这一段js的注入,全局的XHR请求都会在各个阶段触发对应的事件,这样我们就可以监听我们需要的内容,包括跨域的错误。
通过这种方法,已经能够监听到大部分的ajax请求了,然而却无法监听到fetch的请求事件,这是怎么回事呢?明明fetch也是基于XMLHttpRequest实现的一层封装啊。原来事实上,fetch的代码实现是内置在浏览器中的,它必然先用监控代码执行,所以,我们在添加监听事件的时候,是无法监听fetch里边的XMLHttpRequest对象的。怎么办呢?其实也简单,重写一个fecth即可,因此我们只需要类似于上面的方式重写一个fetch方法即可,代码如图:
经过上面两个方式的封装,我们即可监听到网络请求的各个事件了,包括错误事件。
在请求拦截方面还有很多的业务场景可以使用,例如我们团队整理的大数据项目时序控制解决方案(未公布),针对了业务中请求资源浪费的痛点进行请求优化。
没有看错,确实是录屏,能够还原用户所有操作的“视频”,我们能够获取到用户在报错过之前的所有行为。这是我们这个项目的黑科技,目前市面上极少有项目有这个功能,可以非常直观地看到发生在用户端的错误现象,从此告别让用户截屏录屏等繁琐操作了!问题反馈链路大大缩短。
话不多说,先看看我们实际的效果吧。
有录屏并不是最惊讶的,惊讶的是,上传这样一段录屏数据,花费了多少数据量呢,答案是20kb~50kb。也就是说,我们并不会占用多少宽带。那对页面的性能会有影响吗,到底怎么实现的呢?
由于这个功能是选择性接入的,因此并不影响主要流程。只要引入我们提供的cdn链接或者npm模块,录屏记录就开始操作,同时全局就会注入一个获取录屏数据方法。同时我们还不允许这个方法被重写和配置来保护我们的全局属性。
监控平台我们安装了主动检测模块,可以利用requestIdleCallback函数中执行浏览器空闲队列任务,我们在任务中插入一些主动检测模块
主动检测模块可以让我们收集到更多的环境信息,更加精准的定位到问题的根源。
计算网络速度也是类似的想法,只是图片要大一些,不过我们可以不用选择img标签的形式,而是用ajax请求的方式。原因主要是两点:
此外,chrome65+也提供了一个API,navigator.connection.downlink来得到网络宽带的信息;
在模块化的开发方式下,数据对象作用域隔离不是问题,但为了方便添加记录,做到既可以添加数据,又可以根据唯一标识覆盖记录来保证数据是可以实时刷新,同时内部实现溢出自动出栈的效果,我们需要给目标对象添加一个add方法,项目中各个维度的记录都调用此方法,代码如下:
有了这样的对象存贮,接下来就可以着手如何添加记录了。
一个前端页面里比较隐藏的错误往往发生在用户在进行了某些交互操作后出现,因此为了清晰还原用户端的行为,记录用户的交互是第一要素。我们这里暂时只记录了点击事件,其他行为如滚动页面行为,元素进入用户视野等等,或者有些操作是在TouchStart,TouchMove,TouchEnd等事件中进行,这些大多跟业务场景比较贴近,后续会用可配置的方式来记录上传这类行为,首先我们来看看如何实现记录点击事件。
由于开发者可能会对一些点击事件进行阻止冒泡,因此在监听点击事件时必须得在捕获阶段,同时,我们需要记录一些点击事件目标Target的详情,如:
在说明如何监听网络请求错误的事件时,我们讲解了如何对网络请求的各个阶段进行了事件监听。与点击这类事件不同,网络请求是一个异步的过程,不能只监听一次事件。设想一下,我们如果只监听开始请求事件,则将无法得知请求是否完成,耗时多少;同样地如果只监听请求结束事件,万一在请求过程中页面发生了错误,开始触发上报,那我们此时就没有记录到此次请求。这都是不合理的,因此需要不断跟踪这一个请求从发起到结束的全部状态。在这之前我们需要了解XHR的请求事件的触发顺序,如图:
由图可见,在没有发生网络错误的情况下,我们需要监听不同的事件来记录网络请求的所有过程,其中关键的节点包括:loadstart,progress(upload),progress,loadend。不过我们发现监听这些事件时,发现以下几点问题:
有一点需要注意,我们的上报和网络监测等内部功能,不应该添加到记录里,因此需要有白名单控制
fetch请求的监听跟XHR类似,只需监听fetchStart和fetchEnd事件,只是抓取到的参数没有那么丰富。其他跟XHR一样,记录类型不同而已。添加记录的代码如下图:
为什么要记录页面跳转呢?有时候我们的页面错误,并不是因为当前页面的逻辑出错了,而可能受其他页面的逻辑影响。在单页应用中就很常见,状态管理使多个页面的数据得以共享,当我能够清晰地了解到我的路由跳转是什么样的顺序,我们就能评估报错会是受到了哪里的影响。
除此之外,还有一个很重要的原因,页面之间的通信经常是以url的形式完成,如果发生原子参数丢失,或者拼接错误,如果我们不知道页面之前的跳转,自然很难找到原因。这曾经在我们一个项目中就困扰了非常之久,不了解报错的页面参数如何丢失,从哪里跳转过来,url如何发生了变化,加上这一个检测,我们就能解决这一痛点。
于是我们可以通过什么样的方式监听,页面url的变化呢?
单页应用的前端路由里的hash模式就是基于hashchange事件来更改视图,用这个事来检测路由变化自然没有任何问题。但是history里的pushState和replaceState两个更改url的方法并不会任何事件,因此可以重写这两个方法,触发自定义事件。重写这两个方法的代码如下:
但是如果在IE浏览器环境下,是IE8以前不支持hashchange的。这种情况下就用定时器时刻监听url是否有变化。那么代码如下:
为什么没有用popstate事件?
最开始的想法里,popstate肯定是需要的,也确实能够监听URL变化,但是有问题的是:
我们分两期来做异常捕获平台项目,本阶段主打客户端数据上报监控。下阶段主打数据统计平台。初步计划: