美文网首页让前端飞技术文
让前端监控数据采集更高效

让前端监控数据采集更高效

作者: 马蜂窝技术 | 来源:发表于2019-04-19 10:43 被阅读4次

    随着业务的快速发展,我们对生产环境下的问题感知能力越来越关注。作为距离用户最近的一层,前端的表现是否可靠、稳定、好用,很大程度上决定着用户对整个产品的体验和感受。因此,对于前端的监控不容忽视。

    搭建一套前端监控平台需要考虑的方面很多,比如数据采集、埋点模式、数据处理和分析、报警以及监控平台在具体业务中的应用等等。在这所有环节中,准确、完整、全面的数据采集是一切的前提,也为后续的用户精细化运营提供基础。

    前端技术的日新月异给数据采集也带来了变化和挑战,传统的手工打点模式已经不能满足需求。如何在新的技术背景下让前端数据采集工作更加完善、高效,是本文讨论的重点。


    前端监控数据采集

    在采集数据之前,首先要考虑采集什么样的数据。我们重点关注两类数据,一类是与用户体验相关的,如首屏时间、文件加载时间、页面性能等;另外是帮助我们及时感知产品上线后是否出现异常的,比如资源错误、API 响应时间等。具体来说,我们对前端的数据采集具体主要分为:

    1、路由切换 (href、hashchange、pushState)

    2、JsError

    3、性能 (performance)

    4、资源错误

    5、API

    6、日志上报

    路由切换

    Vue、React、Angular 等前端技术的快速发展使单页面应用盛行。我们都知道,传统的页面应用是用一些超链接来实现页面切换和跳转的,而单页面应用是使用各自的路由系统来管理前端的每一个页面切换,例如 vue-router、react-router 等,跳转时仅刷新局部资源 ,js、css 等公共资源只需要加载一次,这就使传统网页进入离开的方式只有第一次打开能被记录。单页应用后续所有路由切换的方式有两种,一种是 Hash,一种是 HTML5 推出的 History API。

    1. href

    href 为页面初始化的第一次进入,这里只需要单纯上报「进入页面」事件即可。

    2. hashchange

    Hash 路由一个明显的标志是带有「 # 」。Hash 的优势是兼容性更好,但问题在于 URL 中一直存在「 # 」并不美观。我们主要通过监听 URL 中的 hashchange 来捕获具体的 hash 值进行检测。

    window.addEventListener('hashchange',function(){

    // 上报【进入页面】事件

    },true)

    需要注意的是,在新版 vue-router 中如果浏览器支持 history,即使 mode 选择 hash 也会优先选择 history 模式,虽然表现形式暂时还是 # 号,但实际上是模拟的,所以千万不要认为自己在 mode 选择了hash 就一定会是 hash。

    3. History API

    History 利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法进行路由切换,是目前主流的无刷新切换路由方式。与 hashchange 只能改变 # 后面的代码片段相比,History API (pushState、replaceState) 给了前端完全的自由。

    PopState 是浏览器返回事件的回调,但是更新路由的 pushState、replaceState 并没有回调事件,因此,还需要分别在 history.pushState() 和 history.replaceState() 方法里处理 URL 的变化。在这里,我们运用到了一种类似 Java 的 AOP 编程思想,对 pushState 和 replaceState 进行改造。

    AOP (Aspect-oriented programming)即面向切面编程,提倡针对同一类问题进行统一处理。AOP 的核心思想是让某个模块能够重用,它采用横向抽取机制,将功能代码从业务逻辑代码中分离出来,扩展功能而不修改源代码,相比封装来说隔离得更加彻底。

    下面介绍我们的具体改造方式:

    // 第一阶段:我们对原生方法进行包装,调用前执行 dispatchEvent 了一个同样的事件

    functionaop(type){

    varsource =window.history[type];

    returnfunction(){

    varevent =newEvent(type);

    event.arguments =arguments;

    window.dispatchEvent(event);

    varrewrite = source.apply(this,arguments);

    returnrewrite;

    };

    }

    // 第二阶段:将 pushState 和 replaceState 进行基于 AOP 思想的代码注入

    window.history.pushState = aop('pushState');

    window.history.replaceState = aop('replaceState');// 更改路由,不会留下历史记录

    // 第三阶段:捕获pushState 和 replaceState

    window.addEventListener('pushState',function(){

    // 上报【进入页面】事件

    },true)

    window.addEventListener('replaceState',function(){

    // 上报【进入页面】事件

    },true)

    window.history.pushState 实际调用关系如图:

    至此,我们对 pushState、replaceState 改造完毕,实现了有效地捕获路由切换。可以看到,我们在不侵入业务代码的情况下,对 window.history.pushState 进行了扩展,在调用的同时会主动 dispatchEvent 一个 pushState。

    但在这里我们也能看到一个弊端,就是如果 AOP 代理函数发生 JS 错误,将会阻断后续的调用关系,使实际的 window.history.pushState 无法被调用。所以在使用此方式的时候,要对 AOP 代理函数的内容做好完善的 try catch,来防止业务上出现异常。

    *Tips:想自动捕获页面停留时间只需要在下一个进入页面事件触发时,通过上一个页面的打点时间和当前时间做差值即可,这时候可以上报一个【离开页面】事件。

    JsError

    前端项目中,由于 JavaScript 本身是一个弱类型语言,加上浏览器环境的复杂性、网络问题等,很容易发生错误。因此做好网页错误监控,不断优化代码,提高代码健壮性是一项很重要的工作。

    JsError 的捕获可以帮助我们分析和监控线上问题,它与我们在 Chrome 浏览器的调试工具 Console 中看到的内容一致。

    1. window.onerror

    我们使用 window.onerror 捕获一般情况下 JS 错误的异常信息。捕获 JS 错误的方式有两种,window.onerror 和 window.addEventListener(‘error’)。一般情况下,捕获 JS 异常不推荐使用 addEventListener(‘error’),主要是因为它没有堆栈信息,而且还需要对捕获到的信息做区分,因为它会将所有异常信息捕获到,包括资源加载错误等。

    window.onerror =function(msg, url, lineno, colno, stack){

    // 上报 【js错误】事件

    }

    2. Uncaught (in promise)

    当 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. 常见问题

    "Script error."

    如果出现捕获的 msg 全部为 "Script error." ,问题在于你的 JS 地址和当前网页不在同一个域下。因为我们要经常在线上的版本做静态资源 CDN 化,会导致常访问的页面跟脚本文件来自不同的域名。这时如果没有进行额外的配置,浏览器出于安全方面的设计就容易出现 "Script error."。我们可以利用目前流行的 Webpack 打包工具来处理此类问题。

    // webpack config 配置

    // 处理 html 注入 js 添加跨域标识

    plugins: [

    newHtmlWebpackPlugin({

    filename:'html/index.html',

    template: HTML_PATH,

    attributes: {

    crossorigin:'anonymous'

    }

    }),

    newHtmlWebpackPluginCrossorigin({

    inject:true

    })

    ]

    // 处理按需加载的 js 添加跨域标识

    output: {

    crossOriginLoading:true

    }

    SourceMap

    大部分场景下,生产环境中的代码都是经过压缩合并的,这使得我们捕获到的错误很难映射到具体的源码,为我们解决问题带来很大困扰,这里简要提出 2 个解决方案的思路。

    生产环境我们需要添加 sourceMap 配置,这会导致安全隐患,因为这样外网就可以通过 sourceMap 进行源码映射。为了降低风险,我们可以通过如下方式:

    将 sourceMap 生成的 .map 文件设置公司内网访问,降低源码安全风险

    在发布代码到 CDN 的时候,将 .map 文件存储到公司内网下

    这时我们已经拥有了 .map 文件,后续要做的就是通过捕获到的 lineno、colno、url 调用 mozilla/source-map 库进行源码映射,即可拿到真实的源码错误信息。

    性能

    性能指标的获取相对比较简单,在 onload 之后读取 window.performance 即可,里面包含了性能、内存等信息。这部分内容在很多现有的文章中都有介绍,因篇幅所限不在本文做过多展开,之后在相关主题文章中我们会有相关探讨,感兴趣的朋友可以添加「马蜂窝技术」公众号持续关注。

    资源错误

    首先我们要明确下资源错误捕获的使用场景,更多的是感知 DNS 劫持 及 CDN 节点异常等,具体方式如下:

    window.addEventListener('error',function(e){

    vartarget = e.target || e.srcElement;

    if(targetinstanceofHTMLScriptElement) {

    // 上报 【资源错误】事件

    }

    },true)

    这里只做基本演示,实际环境中我们会关心更多的 Element 错误,如 css、img、woff 等,大家可以根据不同的场景自行添加。

    *资源错误的使用场景更多依赖其他几个维度,如:__地域、运营商等,后续的篇幅中我们会具体讲解。

    API

    市面上主流的框架(如 Axios、jQuery.ajax 等)中,基本上所有的 API 请求都是基于xmlHttpRequest 或者 fetch,所以捕获全局接口错误的方式就是封装 xmlHttpRequest 或者 fetch。这里,我们的 SDK 仍然使用到上文提及的 AOP 思想,对 API 进行拦截。

    1. XmlHttpRequest

    varxhr =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){

    // 可以存储response的status、计算客户端实际响应时间

    attr.status =this.status;

    attr.duration =newDate().getTime() - attr.duration;

    // 上报【API】事件

    }.bind(this, ,JSON.parse(JSON.stringify(attr))));

    _send.apply(this,arguments);

    }

    xmlhttp.prototype.open = openReplacement;

    xmlhttp.prototype.send = sendReplacement;

    2. Fetch

    需要注意的是,API 拦截一定要对 SDK 自己上报的 API 设置好忽略,否则将会导致循环上报问题。

    var_fetch =window.fetch;

    window.fetch =function(){

    varattr = {

    method:arguments[1].method,

    url:arguments[0],

    duration:newDate().getTime()

    };

    return_fetch.apply(this,arguments).then(res=>{

    attr.status = res.status;

    attr.duration =newDate().getTime() - attr.duration;

    // 上报【API】事件

    returnres;

    });

    }

    日志上报

    为了监控前端应用是否正常运行,通常会在前端收集错误与性能等数据,最终将这些数据上报到服务端。因为日志上报并不是应用的主要功能逻辑,优先级比较低,所以我们在确保日志数据上报更高效的同时,还应该考虑如何尽可能地减少与其他关键操作的资源争抢。

    1. sendBeacon

    navigator.sendBeacon() 方法主要用于满足统计和诊断代码的需要。这些代码通常会在卸载文档之前,尝试通过 HTTP 将少量数据异步传输到 Web 服务器。它解决了日志上报在 unload 时成功率很低的问题。我们在埋点时有很多对离开页面时上报的需求,因为 SendBeacon 是异步的,不会影响当前页到下一个页面的跳转速度,可以更可靠地保障事件上报成功率,并且不影响路由切换。

    window.navigator.sendBeacon('上报事件的api','数据参数')

    2. img.src

    当浏览器不支持 navigator.sendBeacon 时,我们可以采用模拟图片加载的方式发送日志上报事件,且不会存在跨域问题。

    varimg =newImage();

    img.src = API +'?'+'数据参数'

    3. 关于 XmlHttpRequest

    这里不推荐用 XmlHttpRequest。XHR 虽然支持异步请求,直接发送数据到后端,但是会受到跨域和同源的限制。而通过日志上报 API 跟业务是不在一个域下的,如果采用这种模式需要设置 Access-Control-Allow-Origin:* 跨域,非常不方便,并且在 unload 情况下上报发生的丢包率最高。

    总结来看,日志上报推荐采用 sendBeacon -> img.src。在不影响用户路由切换和阻塞用户的情况下丢包率可以控制在 10%-30%,具体要看用户群体对应的环境。


    小结

    高效的前端数据采集对于搭建前端监控平台来说非常关键。本文我们分享了马蜂窝在保证数据采集及时、准确、全面等方面的一些思路和实践。需要提示大家注意的是,文中涉及到的演示只做了核心代码的关键描述,不具备生产使用,我们在实际使用中需要做好兼容及容错。

    本文也将作为马蜂窝前端监控平台系列文章的开篇,今后还将陆续推出埋点模式、数据处理和分析、报警以及监控平台在具体业务中的应用等内容,欢迎大家持续关注。

    本文作者:王峥,马蜂窝大数据平台前端技术专家。

    (马蜂窝技术原创内容,转载务必注明出处保存文末二维码图片,谢谢配合。)

    关注马蜂窝技术,找到更多你想要的内容

    相关文章

      网友评论

        本文标题:让前端监控数据采集更高效

        本文链接:https://www.haomeiwen.com/subject/xezfgqtx.html