文章采集api(搭建一套前端监控平台需要考虑的几个问题?|本文)
优采云 发布时间: 2021-11-29 02:05文章采集api(搭建一套前端监控平台需要考虑的几个问题?|本文)
随着业务的快速发展,我们越来越重视生产环境中的问题感知能力。作为离用户最近的一层,前端性能是否可靠、稳定、易用,在很大程度上决定了用户对整个产品的体验和感受。因此,前端的监控不容忽视。
搭建前端监控平台需要考虑的方面有很多,比如数据采集、埋点模式、数据处理分析、告警、监控平台在具体业务中的应用等。在所有这些环节中,准确、完整、全面的数据采集是一切的前提,也为用户后续精细化操作提供了基础。
前端技术的飞速发展也给数据带来了变化和挑战采集。传统的人工管理模式已不能满足需求。如何让前端数据采集在新的技术背景下工作更完整、更高效,是本文的重点。
前端监控数据采集
在采集数据之前,我们首先要考虑采集是什么样的数据。我们关注两类数据,一类是与用户体验相关的数据,比如首屏时间、文件加载时间、页面性能等;另一个是帮助我们及时感知产品上线后是否有异常,比如资源错误、API响应时间等。具体来说,我们的前端数据采集具体分为:
路由交换机
Vue、React、Angular 等前端技术的快速发展,使得单页应用大行其道。我们都知道传统的页面应用使用一些超链接来实现页面切换和跳转,而单页面应用则使用自己的路由系统来管理前端的各个页面切换,比如vue-router、react-router等,跳转时只刷新部分资源,js、css等公共资源只需要加载一次,这就使得传统网页的进出方式只能在第一次打开时记录。单页应用所有后续路由的切换有两种方式,一种是Hash,一种是HTML5推出的History API。
1. href
href是页面初始化的第一个入口,这里只需要上报“页面入口”事件即可。
2. 哈希变化
哈希路由的一个明显标志是带有“#”。Hash 的优点是兼容性比较好,但问题是 URL 中总有一个“#”,不美观。我们主要是监控URL中的hashchange,捕获具体的hash值进行检测。
window.addEventListener('hashchange', function() {
// 上报【进入页面】事件
}, true)
需要注意的是,在新版本的vue-router中,如果浏览器支持history,即使选择了hash模式,也会先选择history模式。虽然表达式暂时还是#,但实际上是模拟的,所以不要以为你在模式选择hash的时候就会是hash。
3. 历史 API
History使用HTML5 History Interface中新增的pushState()和replaceState()方法进行路由切换,是目前主流的非刷新切换路由方式。相比hashchange后面的代码片段只能改#,History API(pushState、replaceState)给了前端完全的自由。
PopState是浏览器返回事件的回调,但是update路由的pushState和replaceState没有回调事件。因此,需要分别在 history.pushState() 和 history.replaceState() 方法中处理 URL 更改。在这里,我们使用了类似Java的AOP编程思想来转换pushState和replaceState。
AOP(Aspect-Oriented Programming)是指面向方面的编程,主张对同一类型的问题进行统一处理。AOP的核心思想是允许某个模块被复用。它采用横向抽取机制,将功能代码与业务逻辑代码分离,在不修改源代码的情况下扩展功能,隔离比封装更彻底。
下面介绍我们具体的改造方法:
// 第一阶段:我们对原生方法进行包装,调用前执行 dispatchEvent 了一个同样的事件
function aop (type) {
var source = window.history[type];
return function () {
var event = new Event(type);
event.arguments = arguments;
window.dispatchEvent(event);
var rewrite = source.apply(this, arguments);
return rewrite;
};
}
// 第二阶段:将 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:如果要自动捕捉页面停留时间,只需要计算下一页进入事件触发时上一页的tick时间与当前时间的差值即可。这时候可以举报【离开页面】事件。
错误
在前端项目中,由于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) {
// 上报 【js错误】事件
}
2. 未捕获(承诺)
当Promise发生JS错误或者业务没有处理reject信息时,会抛出unhandledrejection,window.onerror和window.addEventListener('error')不会捕捉到这个错误。这里需要一个特殊的窗口。addEventListener('unhandledrejection') 用于捕获处理:
window.addEventListener('unhandledrejection', function (e) {
var reg_url = /\(([^)]*)\)/;
var fileMsg = e.reason.stack.split('\n')[1].match(reg_url)[1];
var fileArr = fileMsg.split(':');
var lineno = fileArr[fileArr.length - 2];
var colno = fileArr[fileArr.length - 1];
var url = fileMsg.slice(0, -lno.length - cno.length - 2);}, true);
var msg = e.reason.message;
// 上报 【js错误】事件
}
我们注意到,因为 unhandledrejection 继承自 PromiseRejectionEvent 和 PromiseRejectionEvent 继承自 Event,msg、url、lineno、colno、stack 以字符串的形式放在 e.reason.stack 中。我们需要解析出上面的参数来与 onerror 参数对齐。为后续监测平台各项指标的统一奠定基础。
3.常见问题
如果抓到的msg都是“Script error.”,问题是你的JS地址和当前网页不在同一个域。因为我们经常需要对网络版做静态资源CDN化,会导致经常访问的页面和脚本文件来自不同的域名。如果此时不进行额外的配置,浏览器很容易出现“脚本错误”。由于安全设计。我们可以使用目前流行的Webpack打包工具来处理此类问题。
// webpack config 配置
// 处理 html 注入 js 添加跨域标识
plugins: [
new HtmlWebpackPlugin({
filename: 'html/index.html',
template: HTML_PATH,
attributes: {
crossorigin: 'anonymous'
}
}),
new HtmlWebpackPluginCrossorigin({
inject: true
})
]
// 处理按需加载的 js 添加跨域标识
output: {
crossOriginLoading: true
}
大多数场景下,生产环境中的代码都是经过压缩和合并的,这使得我们捕捉到的错误很难映射到具体的源代码上,给我们解决问题带来了很大的麻烦。这里简单介绍2个解决思路。
在生产环境中,我们需要添加sourceMap的配置,这会造成安全隐患,因为外网可以通过sourceMap映射源代码。为了降低风险,我们可以做到以下几点:
设置sourceMap生成的.map文件访问公司内网,降低源代码安全风险
将代码发布到CDN时,将.map文件存放在公司内网下
这时候我们已经有了 .map 文件。后面我们要做的就是调用mozilla/source-map库,通过抓到的lineno、colno、url来映射源码,然后我们就可以得到真正的源码错误信息了。
表现
性能指标的获取比较简单,只需要在onload后读取window.performance,里面收录性能、内存等信息。这部分内容在很多现有的文章中都有介绍。限于篇幅,本文不会展开过多。稍后我们将在相关话题文章中进行相关讨论。感兴趣的朋友可以添加“马蜂窝技术”公众号继续关注。
资源错误
首先需要明确资源错误捕获的使用场景,更多的是感知DNS劫持、CDN节点异常等,具体方法如下:
window.addEventListener('error', function (e) {
var target = e.target || e.srcElement;
if (target instanceof HTMLScriptElement) {
// 上报 【资源错误】事件
}
}, true)
这里只是一个基本的演示。在实际环境中,我们会关心更多的Element错误,比如css、img、woff等,可以根据不同的场景添加。
_*资源错误的使用场景更多地依赖于其他几个维度,例如:_region、operator等,我们将在后面的页面中详细说明。
应用程序接口
在市面上的主流框架(如Axios、jQuery.ajax等)中,基本上所有的API请求都是基于xmlHttpRequest或者fetch,所以捕获全局接口错误的方式是封装xmlHttpRequest或者fetch。在这里,我们的SDK还是采用了上面提到的AOP思想来拦截API。