随着前端技术突飞猛进迅猛发展,为了实现更好的前端性能,最大程度提升用户体验,支持单页应用的框架逐渐占领市场,如众所周知的React,Vue等等。可是在单页应用的趋势下,快速定位并解决JS错误却成为一大难题。在当下的互联网行业,对前端性能要求愈来愈高,前端性能监控的产品层出不穷,javascript错误诊断更是其中举足轻重的一个环节。帮助开发者排查线上bug,实现快速定位问题,高效解决问题,是咱们努力的方向。javascript
1、JS错误诊断
目前已经有了许多诸如Arms,Sentry等前端性能监控框架,都在必定程度上对JS错误诊断提供了相应的 支持,整体来讲,你们的思路比较类似,能够总结为如下几个步骤:html
每当上线一个新的产品或新的业务功能,每每伴随着一些不可避免的线上bug,这些bug发生的频率有多高、发生在什么页面、影响了多少用户等等,对于判断解决问题的优先级、帮助排查问题和优化性能来讲是很是关键的。前端
图1是Arms前端性能监控的JS错误诊断诊断页面,能够清晰地看到错误发生的次数、影响用户数以及错误分布状况等信息,开发者能够根据这些统计数据更好地决策问题的轻重缓急,使性能优化有条不紊进行。java
图1. JS错误总览web
前端报错的缘由有不少,网络因素、浏览器兼容性、用户操做逻辑、业务代码自己的问题等等均可能致使故障发生,在JS错误统计中咱们已经知道了错误发生的页面,这在必定程度上缩小了排查范围,但这样仍是不够的,咱们还想要知道更多错误详情,好比:ajax
1)现场信息
包括报错的设备、操做系统、浏览器等等,这些信息无疑能够帮助开发者更好地复现问题,进而修复bug。后端
图2. JS错误概要api
2)代码追踪
利用error stack和source map来精准定位发生错误的代码位置。跨域
a. 上传source map文件浏览器
b. 在代码中定位错误
图3. source map错误定位
还原错误发生时用户行为上下文是JS错误诊断的最后一步,也是最关键和最困难的一步。
假设如下场景:咱们收到用户反馈说点了某按钮后没有反应……
面对用户反馈,咱们不禁得会想,没反应是什么缘由呢,是点击事件的handler出错了致使接口请求没发出?仍是接口挂了?或者是接口返回数据渲染失败了?
相似以上的困惑应该不少开发者都会时常遇到,咱们不可能去指挥用户打开开发者工具再一步步debug给咱们看,这就须要咱们的前端监控平台具有还原报错现场的能力,帮助开发者了解错误发生时候的用户行为上下文,进而能够预想一下刚才的场景——
根据用户的uid等信息,咱们能够回溯到该用户报错先后的一系列操做以及前端行为:
进入页面->点击按钮->发出api请求成功-> js error……
因而咱们能够知道报错是发生在api请求成功后的数据处理环节,再依据步骤2中提供的错误详情快速解决问题。
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上报
3、用户行为回溯在js错误诊断中的应用
图8展现了Arms前端性能监控的JS错误诊断中用户行为回溯的状况,在错误详情中复原出错现场,辅助排查。下图展现了用户在进行了“接口请求-控制台输出-点击按钮-控制台输出”的一系列行为后发生了JS错误,便可从按钮的点击事件的处理函数中进行错误排查。
图8. Arms前端性能监控的用户行为回溯