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

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

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

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

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

[[262933]]

前端监控数据采集

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

  • 路由切换 (href、hashchange、pushState)
  • JsError
  • 性能 (performance)
  • 资源错误
  • API
  • 日志上报

 路由切换

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

1. href

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

2. hashchange

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

  1. window.addEventListener('hashchange'function() { 
  2.     // 上报【进入页面】事件 
  3. }, 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 的核心思想是让某个模块能够重用,它采用横向抽取机制,将功能代码从业务逻辑代码中分离出来,扩展功能而不修改源代码,相比封装来说隔离得更加彻底。

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

  1. // ***阶段:我们对原生方法进行包装,调用前执行 dispatchEvent 了一个同样的事件 
  2. function aop (type) { 
  3.     var source = window.history[type]; 
  4.     return function () { 
  5.         var event = new Event(type); 
  6.         event.arguments = arguments; 
  7.         window.dispatchEvent(event); 
  8.         var rewrite = source.apply(this, arguments); 
  9.         return rewrite; 
  10.     }; 
  11.  
  12. // 第二阶段:将 pushState 和 replaceState 进行基于 AOP 思想的代码注入 
  13. window.history.pushState = aop('pushState'); 
  14. window.history.replaceState = aop('replaceState'); // 更改路由,不会留下历史记录 
  15.  
  16. // 第三阶段:捕获pushState 和 replaceState 
  17. window.addEventListener('pushState'function() { 
  18.     // 上报【进入页面】事件 
  19. }, true
  20. window.addEventListener('replaceState'function() { 
  21.     // 上报【进入页面】事件 
  22. }, 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’),主要是因为它没有堆栈信息,而且还需要对捕获到的信息做区分,因为它会将所有异常信息捕获到,包括资源加载错误等。

  1. window.onerror = function (msg, url, lineno, colno, stack) { 
  2.     // 上报 【js错误】事件 

2. Uncaught (in promise)

当 Promise 内发生 JS 错误或者 reject 信息未被业务处理的情况时,会抛出一个 unhandledrejection,并且这个错误不会被 window.onerror 以及 window.addEventListener('error')  捕获,这里需要用专门的 window.addEventListener('unhandledrejection')  进行捕获处理:

  1. window.addEventListener('unhandledrejection'function (e) { 
  2.     var reg_url = /\(([^)]*)\)/; 
  3.     var fileMsg = e.reason.stack.split('\n')[1].match(reg_url)[1]; 
  4.     var fileArr = fileMsg.split(':'); 
  5.     var lineno = fileArr[fileArr.length - 2]; 
  6.     var colno = fileArr[fileArr.length - 1]; 
  7.     var url = fileMsg.slice(0, -lno.length - cno.length - 2);}, true); 
  8.     var msg = e.reason.message; 
  9.     // 上报 【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 打包工具来处理此类问题。

  1. // webpack config 配置 
  2. // 处理 html 注入 js 添加跨域标识 
  3. plugins: [ 
  4.     new HtmlWebpackPlugin({ 
  5.       filename: 'html/index.html'
  6.       template: HTML_PATH, 
  7.       attributes: { 
  8.         crossorigin: 'anonymous' 
  9.       } 
  10.     }), 
  11.     new HtmlWebpackPluginCrossorigin({ 
  12.       inject: true 
  13.     }) 
  14.  
  15. // 处理按需加载的 js 添加跨域标识 
  16. output: { 
  17.     crossOriginLoading: true 
  • SourceMap

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

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

  • 将 sourceMap 生成的 .map 文件设置公司内网访问,降低源码安全风险
  • 在发布代码到 CDN 的时候,将 .map 文件存储到公司内网下

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

 性能

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

 资源错误

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

  1. window.addEventListener('error'function (e) { 
  2.     var target = e.target || e.srcElement; 
  3.     if (target instanceof HTMLScriptElement) { 
  4.         // 上报 【资源错误】事件 
  5.     } 
  6. }, true

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

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

 API

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

1. XmlHttpRequest

  1. var xhr = window.XMLHttpRequest; 
  2. var _open = xhr.prototype.open
  3. var _send = xhr.prototype.send; 
  4. var attr = {}; 
  5. var openReplacement = function (method, url) { 
  6.     // 可以存储method、url、时间打点等信息 
  7.     attr.duration = new Date().getTime(); 
  8.     _open.apply(this, arguments); 
  9. var sendReplacement = function () { 
  10.     methods.addEvent(this, 'readystatechange'function (attr) { 
  11.         // 可以存储response的status、计算客户端实际响应时间 
  12.         attr.status = this.status; 
  13.         attr.duration = new Date().getTime() - attr.duration; 
  14.         // 上报【API】事件 
  15.     }.bind(this, , JSON.parse(JSON.stringify(attr)))); 
  16.     _send.apply(this, arguments); 
  17. xmlhttp.prototype.open = openReplacement; 
  18. xmlhttp.prototype.send = sendReplacement; 

2. Fetch

  1. var _fetch = window.fetch
  2. window.fetch = function () { 
  3.     var attr = { 
  4.         method: arguments[1].method, 
  5.         url: arguments[0], 
  6.         duration: new Date().getTime() 
  7.     }; 
  8.  
  9.     return _fetch.apply(this, arguments).then(res => { 
  10.         attr.status = res.status; 
  11.         attr.duration = new Date().getTime() - attr.duration; 
  12.         // 上报【API】事件 
  13.         return res; 
  14.     }); 

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

 日志上报

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

1. sendBeacon

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

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

2. img.src

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

  1. var img = new Image(); 
  2. img.src = API + '?' + '数据参数' 

3. 关于 XmlHttpRequest

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

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

小结

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

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

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

(题图来源于网络)

【本文是51CTO专栏作者马蜂窝技术的原创文章,作者微信公众号马蜂窝技术(ID:mfwtech)】

戳这里,看该作者更多好文

责任编辑:武晓燕 来源: 51CTO专栏
相关推荐

2018-05-08 14:58:07

戴尔

2015-09-30 14:22:44

Qlik数据

2024-07-31 11:29:11

监控数据库PostgreSQL

2016-06-30 16:54:49

UCloud爱数云计算

2023-11-24 11:20:04

functoolsPython

2022-06-10 14:09:18

前端监控异常数据

2016-09-29 13:44:23

数据中心

2017-11-02 10:23:48

冷热分层存储

2023-09-12 16:20:04

边缘AI深度学习

2011-07-21 13:52:43

组策略网络打印机

2013-04-03 09:49:48

LinkedIn大数据

2024-06-24 00:05:00

Python代码

2017-12-21 14:36:10

大数据健身智慧

2010-12-12 09:40:00

Android UI设

2011-08-29 09:33:48

2015-12-31 11:57:17

华为eLTE物联网

2010-12-23 15:55:00

上网行为管理

2015-04-02 12:42:26

HDFS分层存储高效

2024-04-26 07:54:07

ZustandReact状态管理库

2018-10-23 15:20:29

SparkShuffleSpark SQL
点赞
收藏

51CTO技术栈公众号