随着业务的快速发展,我们越来越重视生产环境中的问题感知能力。作为离用户最近的一层,前端的性能是否可靠、稳定、易用很大程度上决定了用户对整个产品的体验和感受。因此,前端的监控也不容忽视。搭建前端监控平台需要考虑的方面有很多,比如数据采集、埋点模式、数据处理分析、告警、监控平台在具体业务中的应用等等。在所有这些环节中,准确、完整、全面的数据采集是一切的前提,也为后续用户精细化运营提供了基础。前端技术的快速发展也给数据采集带来了变化和挑战,传统的人工管理模式已经不能满足需求。如何在新的技术背景下,让前端的数据采集工作更加完善和高效,是本文关注的重点。前端监控数据采集在采集数据之前,首先要考虑采集什么样的数据。我们关注两类数据,一类是与用户体验相关的,比如首屏时间、文件加载时间、页面性能等;二是帮助我们及时感知产品上线后是否出现异常,如资源错误、API响应时间等。具体来说,我们的前端数据采集主要分为:路由切换(href、hashchange、pushState)JsError性能(性能)资源错误API日志上报路由切换Vue、React、Angular等前端技术的快速发展使得单页App盛行。我们都知道,传统的页面应用都是通过一些超链接来实现页面的切换和跳转,而单页面应用则是在前端使用自己的路由系统来管理每次页面的切换,比如vue-router、react-router等,跳转时只刷新本地资源,而js、css等公共资源只需要加载一次,这使得传统的进入和离开网页的方式只在第一次打开时被记录。单页应用的所有后续路由切换有两种方式,一种是Hash,一种是HTML5推出的HistoryAPI。1、hrefhref是页面初始化的第一个入口,这里只需要简单的上报“页面入口”事件即可。2、hashchangeHash路由的一个明显标志是“#”。Hash的优点是兼容性更好,但问题是URL中总是有“#”,不美观。我们主要是通过监听URL中的hashchange来抓取具体的hash值进行检测。window.addEventListener('hashchange',function(){//Report[Enterpage]event},true)需要注意的是,在新版本的vue-router中,如果浏览器支持history,即使mode选择了hash,history会优先mode,虽然表达形式暂时还是#,但其实是模拟出来的,所以不要以为mode里选择hash就一定是hash。3、HistoryAPIHistory使用HTML5HistoryInterface中新增的pushState()和replaceState()方法进行路由切换,是目前主流的不刷新路由切换方式。与hashchange只能改变#后面的代码片段相比,HistoryAPI(pushState、replaceState)给了前端完全的自由。PopState是浏览器返回事件的回调,但是没有更新路由的pushState和replaceState的回调事件。因此,需要分别在history.pushState()和history.replaceState()方法中处理URL变化。这里,我们采用类Java的AOP编程思想,对pushState和replaceState进行改造。AOP(Aspect-orientedprogramming)即面向切面编程,提倡对同一类问题统一处理。AOP的核心思想是让某个模块能够被复用。它采用水平抽取机制,将功能代码与业务逻辑代码分离,在不修改源代码的情况下扩展功能,比封装隔离更彻底。下面介绍我们具体的改造方法://***阶段:我们封装native方法,在调用同一个事件函数aop(type){varsource=window.history[type];returnfunction(){varevent=newEvent之前执行dispatchEvent(type);event.arguments=arguments;window.dispatchEvent(event);varrewrite=source.apply(this,arguments);returnrewrite;};}//第二阶段:基于AOP思想的pushState和replaceState代码注入窗口。history.pushState=aop('pushState');window.history.replaceState=aop('replaceState');//改变路由不留history//第三阶段:捕获pushState和replaceStatewindow。addEventListener('pushState',function(){//上报[进入页面]事件},true)window.addEventListener('replaceState',function(){//上报[进入页面]事件},true)window.历史。pushState的实际调用关系如图:至此,我们完成了pushState和replaceState的转换,实现了路由切换的有效捕获。可以看出我们在不侵入业务代码的情况下,扩展了window.history.pushState,调用的时候会主动dispatchEvent一个pushState。但是这里我们也可以看到一个缺点,就是如果AOP代理函数出现JS错误,会阻塞后续的调用关系,从而无法调用到实际的window.history.pushState。所以在使用这种方式的时候,需要对AOP代理函数的内容进行完善的trycatch,防止业务出现异常。*Tips:如果想自动抓取页面停留时间,只需要在触发下一个页面进入事件时,将上一个页面的check时间和当前时间做差值即可。这时,你可以报告一个[离开页面]事件。在JsError前端项目中,由于JavaScript本身是弱类型语言,再加上浏览器环境复杂、网络问题等,很容易出现错误。因此,做好网页错误监控,不断优化代码,提高代码的健壮性是非常重要的。JsError的捕获可以帮助我们分析和监控线上问题,这与我们在Chrome浏览器的调试工具Console中看到的是一致的。1.window.onerror我们一般使用window.onerror来捕获JS错误的异常信息。捕获JS错误有两种方法,window.onerror和window.addEventListener('error')。一般来说,不建议使用addEventListener('error')来捕获JS异常,主要是因为它没有堆栈信息,而且还需要区分捕获的信息,因为它会捕获所有的异常信息,包括资源加载错误等待。window.onerror=function(msg,url,lineno,colno,stack){//上报[jserror]event}2.Uncaught(inpromise)当Promise出现JS错误或者reject信息没有被业务处理,会抛出一个unhandledrejection,这个错误不会被window.onerror和window.addEventListener('error')捕捉到,这里需要使用一个特殊的window.addEventListener('unhandledrejection')来进行捕捉处理:window.addEventListener('unhandledrejection',function(e){varreg_url=/\(([^)]*)\)/;varfileMsg=e.reason.stack.split('\n')[1].match(reg_url)[1];varfileArr=fileMsg.split(':');varlineno=fileArr[fileArr.length-2];varcolno=fileArr[fileArr.length-1];varurl=fileMsg.slice(0,-lno.length-cno.length-2);},true);varmsg=e.reason.message;//报[js错误]事件}我们注意到unhandledrejection继承自PromiseRejectionEvent,PromiseRejectionEvent继承自Event,所以msg,url,lineno,colno,stack都是以字符串形式放在e.reason.stack中。我们需要将上述参数解析出来与onerror参数对齐,为后续监控平台指标的统一打下基础。3.常见问题“脚本错误”。如果所有捕获的消息都是“脚本错误”,问题是你的JS地址和当前网页不在同一个域下。因为我们经常为在线版本做静态资源CDN,会导致经常访问的页面和脚本文件来自不同的域名。这时候,如果没有额外的配置,浏览器很容易出现“Scripterror”。由于安全设计。我们可以使用流行的Webpack打包工具来处理此类问题。//webpackconfig配置//处理html注入js添加跨域识别插件:[newHtmlWebpackPlugin({filename:'html/index.html',template:HTML_PATH,attributes:{crossorigin:'anonymous'}}),newHtmlWebpackPluginCrossorigin({inject:true})]//处理js的按需加载并添加跨域识别output:{crossOriginLoading:true}SourceMap大多数场景下,生产环境的代码被压缩合并,这使得我们的错误捕捉很小。很难映射到具体的源码,给我们解决问题带来了很大的麻烦。在此我们简要提出两种解决方案。在生产环境中,我们需要添加sourceMap配置,这会带来安全隐患,因为外网可以使用sourceMap进行源映射。为了降低风险,我们可以采用以下方法:将sourceMap生成的.map文件设置为公司内网访问,以降低源代码安全风险。发布代码到CDN时,将.map文件存放在公司内网。这时,我们已经有了.map文件创建完成,接下来要做的就是通过抓取的lineno、colno、url调用mozilla/source-map库进行源码映射,然后就可以得到真正的源代码错误信息。performance性能指标的获取比较简单,onload后读取window.performance即可,里面包含了性能、内存等信息。这部分内容在已有的很多文章中都有介绍。限于篇幅,本文不做过多展开。稍后我们将在相关文章中讨论相关主题。感兴趣的朋友可以加“马蜂窝技术”公众号敬请期待。资源错误首先要明确资源错误捕获的使用场景,更多的是感知DNS劫持和CDN节点异常等,具体方法如下:window.addEventListener('error',function(e){vartarget=e.target||e.srcElement;if(targetinstanceofHTMLScriptElement){//report[resourceerror]event}},true)这里只是一个基本的演示,在实际环境中我们会关心更多的Element错误,比如css,img,woff等,大家可以根据不同的场景自行添加。*资源错误的使用场景更多取决于其他几个维度,例如:地域、运营商等,我们将在后续页面详细说明。API市场上的主流框架(如Axios、jQuery.ajax等),基本上所有的API请求都是基于xmlHttpRequest或者fetch,所以捕获全局接口错误的方式就是封装xmlHttpRequest或者fetch。在这里,我们的SDK还是沿用了上面提到的AOP思想来拦截API。1.XmlHttpRequestvarxhr=window.XMLHttpRequest;var_open=xhr.prototype.open;var_send=xhr.prototype.send;varattr={};varopenReplacement=function(method,url){//可以存储method,url,和时间attr.duration=newDate().getTime();_open.apply(this,arguments);}varsendReplacement=function(){methods.addEvent(this,'readystatechange',function(attr){//可以存储响应status,计算客户端实际响应时间attr.status=this.status;attr.duration=newDate().getTime()-attr.duration;//上报[API]事件}.bind(this,,JSON.解析(JSON.stringify(attr))));_send.apply(this,arguments);}xmlhttp.prototype.open=openReplacement;xmlhttp.prototype.send=sendReplacement;2。Fetchvar_fetch=window.fetch;window.fetch=function(){varattr={method:arguments[1].method,url:arguments[0],duration:newDate().getTime()};return_fetch.apply(这,arguments).then(res=>{attr.status=res.status;att??r.duration=newDate().getTime()-attr.duration;//Report[API]eventreturnres;});}需要注意的是SDK自身上报的API一定要设置API拦截忽略掉,否则会造成循环上报问题。日志上报为了监控前端应用是否正常运行,通常会在前端收集错误、性能等数据,最后将这些数据上报给服务器。由于日志上报不是应用的主要功能逻辑,优先级相对较低,因此我们还应该考虑如何在保证日志数据上报更高效的同时,尽量减少与其他关键操作的资源争用。1、sendBeaconnavigator.sendBeacon()方法主要用于满足统计和诊断代码的需要。这些代码通常会尝试在卸载文档之前通过HTTP将少量数据异步传输到Web服务器。解决了日志报告卸载时成功率低的问题。我们在埋点的时候对于离开页面的时候上报有很多要求,因为SendBeacon是异步的,不会影响当前页面到下一页的跳转速度,可以更可靠的保证事件上报的成功率和不影响路由切换。window.navigator.sendBeacon('apiforreportingevents','dataparameters')2.img.src当浏览器不支持navigator.sendBeacon时,我们可以通过模拟图片加载发送日志上报事件,不会有Cross域问题。varimg=newImage();img.src=API+'?'+'数据参数'3.关于XmlHttpRequest,这里不推荐使用XmlHttpRequest。XHR虽然支持异步请求,直接向后端发送数据,但是受到跨域同源的限制。通过日志上报的API和业务不在同一个域。如果采用这种模式,需要跨域设置Access-Control-Allow-Origin:*,非常不方便,而且卸载时出现丢包率报**。综上,日志上报推荐使用sendBeacon->img.src。丢包率可控制在10%-30%,不影响用户路由切换,不阻塞用户,具体取决于用户组对应的环境。小结高效的前端数据采集对于搭建前端监控平台至关重要。在这篇文章中,我们分享了马蜂窝在确保及时、准确、全面收集数据方面的一些想法和做法。需要提醒的是,本文涉及的演示仅提供核心代码的关键说明,不适合生产使用。我们在实际使用中需要兼容和容错。本文也将作为马蜂窝前端监控平台系列文章的开篇。后续我们将继续介绍埋点模式、数据处理分析、告警,以及监控平台在具体业务中的应用。欢迎继续关注。本文作者:王征,马蜂窝大数据平台前端技术专家。(题图来源于网络)【本文为专栏作者马蜂窝科技原创文章,微信公众号马蜂窝科技(ID:mfwtech)原创】点此查看作者更多好文
