随着前端技术日新月异迅猛发展,为了实现更好的前端性能,最大程度提高用户体验,支持单页应用的框架逐渐占领市场,如众所周知的React,Vue等等。但是在单页应用的趋势下,快速定位并解决JS错误却成为一大难题。在当下的互联网行业,对前端性能要求越来越高,前端性能监控的产品层出不穷,javascript错误诊断更是其中举足轻重的一个环节。帮助开发者排查线上bug,实现快速定位问题,高效解决问题,是我们努力的方向。
一、JS错误诊断
目前已经有了许多诸如Arms,Sentry等前端性能监控框架,都在一定程度上对JS错误诊断提供了相应的 支持,总体来说,大家的思路比较相似,可以总结为以下几个步骤:
- 统计JS错误
每当上线一个新的产品或新的业务功能,往往伴随着一些不可避免的线上bug,这些bug发生的频率有多高、发生在什么页面、影响了多少用户等等,对于判断解决问题的优先级、帮助排查问题和优化性能来说是非常关键的。
图1是Arms前端性能监控的JS错误诊断诊断页面,可以清晰地看到错误发生的次数、影响用户数以及错误分布情况等信息,开发者可以根据这些统计数据更好地决策问题的轻重缓急,使性能优化有条不紊进行。
图1. JS错误总览
- 精准定位错误
前端报错的原因有很多,网络因素、浏览器兼容性、用户操作逻辑、业务代码本身的问题等等都可能导致故障发生,在JS错误统计中我们已经知道了错误发生的页面,这在一定程度上缩小了排查范围,但这样还是不够的,我们还想要知道更多错误详情,比如:
1)现场信息
包括报错的设备、操作系统、浏览器等等,这些信息无疑可以帮助开发者更好地复现问题,进而修复bug。
图2. JS错误概要
2)代码追踪
利用error stack和source map来精准定位发生错误的代码位置。
a. 上传source map文件
b. 在代码中定位错误
图3. source map错误定位
- 还原报错现场
还原错误发生时用户行为上下文是JS错误诊断的最后一步,也是最关键和最困难的一步。
假设以下场景:我们收到用户反馈说点了某按钮后没有反应……
面对用户反馈,我们不由得会想,没反应是什么原因呢,是点击事件的handler出错了导致接口请求没发出?还是接口挂了?或者是接口返回数据渲染失败了?
类似以上的困惑应该很多开发者都会时常遇到,我们不可能去指挥用户打开开发者工具再一步步debug给我们看,这就需要我们的前端监控平台具备还原报错现场的能力,帮助开发者了解错误发生时候的用户行为上下文,进而可以预想一下刚才的场景——
根据用户的uid等信息,我们可以回溯到该用户报错前后的一系列操作以及前端行为:
进入页面->点击按钮->发出api请求成功-> js error……
于是我们可以知道报错是发生在api请求成功后的数据处理环节,再依据步骤2中提供的错误详情快速解决问题。
二、用户行为回溯
已经有一些前端性能监控平台接入了用户行为监控,实现的方式也各有千秋,主要流程可以分为三个步骤:行为采集、行为上报、行为回溯。
- 行为采集
1)哪些行为需要采集?
我们站在用户的立场去考虑一个单页应用的浏览周期内的可能流程:进入应用首页——加载页面内容——浏览页面内容——用户交互(鼠标交互/键盘交互等)——跳转到新页面……
要将用户行为串联成完整的行为链来为js error提供上下文,我们需要知道什么时间,什么位置,发生了什么事情。由此,以上用户浏览过程中的所有的页面行为(包括但不仅限于用户交互)可以用以下几类来大致概括:
Api请求,鼠标事件,键盘事件,路由跳转,error 等。
2)如何采集?
在确定了哪些行为需要上报以后,我们再在来看如何完成行为打点。
在过往的时代,我们有传统的手动埋点方法,它的缺点也是不言而喻的:手动埋点是容易混乱的,有时可能会出现错埋、漏埋等情况,每每上线一个新功能,需要开发团队和数据团队进行埋点沟通,徒增了时间和人力成本;其次时常因为产品排期紧张,功能急于上线,就不得不先砍掉了埋点的需求,在后续的版本更新中再补上,这使得新上线的功能得不到验证,而新上线的功能对业务和产品性能的影响都很关键。
现如今,一个好的前端监控产品需要实现“无埋点”监控,充当一双眼睛,时时刻刻监控着产品的运作情况,全量采集页面事件和用户行为,为业务分析和错误诊断都提供充足的信息。
图4. 用户行为采集流程图
以上流程图为Arms做行为采集的大致步骤,首先需要在正常的html页面中插入一小段js代码,即引入我们具有行为日志采集功能的SDK,如下代码所示,通过createElement(“script”)在Dom节点添加script的元素,并将SDK的js文件引入进来,用于收集用户行为,并在适当的时候上报到后端,具体方法如下代码所示,其中bl.js为SDK文件。
<script>
!(function(c,b,d,a){c[a]||(c[a]={});c[a].config={pid:"xxxxxxx",appType:"web",imgUrl:"https://arms-retcode.aliyuncs.com/r.png?"};
with(b)with(body)with(insertBefore(createElement("script"),firstChild))setAttribute("crossorigin","",src=d)
})(window,document,"https://retcode.alicdn.com/retcode/bl.js","__bl");
</script>
3)SDK中行为采集的实现
API行为采集
重写原生XMLHttpRequest或fetch方法,采集必要的API信息(如url,状态码,状态message等)。
reWrite(XMLHttpRequest.send,
originalSend =>
function(...args){
const xhr = this;
function onreadystatechangeHandler(){
if (xhr.readyState === 4) {
//记录API行为
}
}
if ('onreadystatechange' in xhr && typeof xhr.onreadystatechange === 'function') {
reWrite(xhr.onreadystatechange, original=> function(...innerargs){
//记录API行为
original.apply(this, innerargs);
});
} else {
xhr.onreadystatechange = onreadystatechangeHandler;
}
return originalSend.apply(this, args);
});
控制台行为采集
重写console的常见控制台输出方法,比如: log,warning,error等。
var consoleType = ['debug', 'info', 'warn', 'log', 'error', 'assert'];
for (var i = 0; consoleType.length; i++) {
var type = consoleType[i];
reWrite(console.type, function (orig) {
var thisType = type;
return function (...args) {
// 添加console行为
if (orig) {
Function.prototype.apply.call(orig, window.console, args);
}
};
});
}
注意,通过重写 console 对象监控浏览器控制台的打印信息,这样会导致在控制台下打印的日志无法正确看到原代码文件中的位置,console的位置会定位到SDK的代码中,可以通过配置浏览器 Blackboxing来解决。
图5. console定位在SDK代码中
用户交互行为采集(鼠标键盘事件等):
可以在顶层的document上全面监听各类用户交互事件,如click,keypress,mousemove,scroll等等。但是这种方法也有一个明显的缺陷,假设用户监听了某个dom上的click事件,并且设置了event.stopPropagation(),这种情况的点击事件是无法被document监听到的,而往往这类行为对于错误诊断和业务分析都尤为重要。解决方法是在addEventListener中埋入钩子。
var bhEventHandler = function(){
//记录用户行为
}
document.addEventListener('click', bhEventHandler, false);
var types = ['EventTarget', 'Node'];
for (var i = 0; i < types.length; i++) {
var type = types[i];
var proto = window[type] && window[type].prototype;
reWrite(proto.addEventListener, function (orig) {
//重写addEventListener,记录用户行为
});
}
路由跳转行为采集
路由跳转无疑会触发浏览器历史记录的改变,每当处于激活状态的历史记录条目发生变化时,window的popstate事件会触发,但是调用history.pushState()或者history.replaceState()不会触发popstate事件。因此路由跳转的监控可以分为两个方面,一方面在window. onpopstate中埋入钩子,另一方面在history.pushState和history.replaceState中埋入钩子。
var origPopstate = window.onpopstate;
window.onpopstate = function () {
//记录路由行为
if (origPopstate) {
return origPopstate.apply(this, args);
}
};
var dosomething = function (orig) {
return function (...args) {
//记录路由行为
return orig.apply(this, args);
};
};
reWrite(window.history.pushState, dosomething);
reWrite(window.history.replaceState, dosomething);
js error
全局监听js error:
window.addEventListener('error', function(event){...})
监听全局未处理的rejection:
window.addEventListener('unhandledrejection', function(event){...})
- 行为上报
SDK采集到用户行为后,以一定的格式进行信息拼接,然后伪装成图片发送给后端。为什么要使用图片来发送日志信息而不是直接使用ajax呢?这是因为SDK的script文件和后端分析的代码可能不在相同的域内,而将image对象的src属性指向后端脚本并携带参数,可以轻松实现跨域请求。不同平台对于前端监控行为的类别都比较类似,但上报的方式还是不尽相同的,服务于不同的业务需求。以下是两种比较典型的行为上报形式:
1)持续全量上报
图6是截取的New relic的行为日志上报信息,为什么称之为持续全量上报呢?“持续”是指它的行为日志是定时上报的,每隔几秒便会上报一次,上报请求非常密集。“全量”则是指它采集的行为类型覆盖面很广,可以从上报的Request Payload中看到,它采集的不仅限于我们上面提到的一些主要的页面行为,它几乎覆盖了所有的页面事件,包括鼠标移动等等。而这种上报方式也是利弊参半:
优点:它可以更真实地还原用户在整个页面周期内的行为,保真度很高,对于还原现场来说是有一定优势的。
缺陷:首先是无差别的行为采集,这显然增大了行为日志的数据体量,在上报中对流量的消耗很大,日志存储成本也很高,而这巨大的损耗所带来的信息价值呢?几乎大部分都是鼠标移动或者滑轮滚动的大量重复信息,无论是对业务分析还是错误诊断都没有太大贡献,甚至增加了分析问题的复杂性。
其次是行为日志的频繁上报,这对业务方来说可能也是不太友好的,性能监控请求发的比本身的业务请求都多。
图6. 行为日志全量上报
2)场景触发上报
场景触发上报即是指,当符合一定条件或者场景时才上报行为日志,对于JS错误诊断中的用户行为回溯场景而言,当然就是错误触发上报,当页面监听到error时,便把当前采集到的行为列表伴随error详情一并上报,作为错误诊断的辅助信息。
行为日志伴随错误一起上报的方式需要维护一个行为队列,队列应设有最大长度,当捕获到js error的时候,将行为队列作为js错误日志的一个补充信息一起上报,图7所示为sentry错误日志上报的request payload,其用户行为队列包含在错误日志的“breadcrumbs”字段中。
这种用户行为伴随错误上报的方式就相对轻量很多,首先错误和行为一起上报减少了请求数量;其次对用户行为选择性采集,避免了大量冗余信息对流量和存储的消耗,就js错误诊断而言会使得错误现场变得更清晰,对分析错误有利。
当然这种行为上报的方式也是有不可忽视的问题,即行为和js error的强耦合,使得用户行为信息失去了业务分析的扩展性,仅适用于JS错误诊断。
图7. 行为日志breadcrumbs上报
三、用户行为回溯在js错误诊断中的应用
图8展示了Arms前端性能监控的JS错误诊断中用户行为回溯的情况,在错误详情中复原出错现场,辅助排查。下图展示了用户在进行了“接口请求-控制台输出-点击按钮-控制台输出”的一系列行为后发生了JS错误,即可从按钮的点击事件的处理函数中进行错误排查。
图8. Arms前端性能监控的用户行为回溯
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。