随着业务的快速发展,我们对生产环境下的问题感知能力越来越关注。作为距离用户最近的一层,前端的表现是否可靠、稳定、好用,很大程度上决定着用户对整个产品的体验和感受。因此,对于前端的监控不容忽视。
搭建一套前端监控平台需要考虑的方面很多,比如数据采集、埋点模式、数据处理和分析、报警以及监控平台在具体业务中的应用等等。在这所有环节中,准确、完整、全面的数据采集是一切的前提,也为后续的用户精细化运营提供基础。
前端技术的日新月异给数据采集也带来了变化和挑战,传统的手工打点模式已经不能满足需求。如何在新的技术背景下让前端数据采集工作更加完善、高效,是本文讨论的重点。
在采集数据之前,首先要考虑采集什么样的数据。我们重点关注两类数据,一类是与用户体验相关的,如首屏时间、文件加载时间、页面性能等;另外是帮助我们及时感知产品上线后是否出现异常的,比如资源错误、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 值进行检测。
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 了一个同样的事件
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:想自动捕获页面停留时间只需要在下一个进入页面事件触发时,通过上一个页面的打点时间和当前时间做差值即可,这时候可以上报一个【离开页面】事件。
前端项目中,由于 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) {
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. 常见问题
"Script error."
如果出现捕获的 msg 全部为 "Script error." ,问题在于你的 JS 地址和当前网页不在同一个域下。因为我们要经常在线上的版本做静态资源 CDN 化,会导致常访问的页面跟脚本文件来自不同的域名。这时如果没有进行额外的配置,浏览器出于安全方面的设计就容易出现 "Script error."。我们可以利用目前流行的 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 进行源码映射。为了降低风险,我们可以通过如下方式:
这时我们已经拥有了 .map 文件,后续要做的就是通过捕获到的 lineno、colno、url 调用 mozilla/source-map 库进行源码映射,即可拿到真实的源码错误信息。
性能指标的获取相对比较简单,在 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 等,大家可以根据不同的场景自行添加。
*资源错误的使用场景更多依赖其他几个维度,如:__地域、运营商等,后续的篇幅中我们会具体讲解。
市面上主流的框架(如 Axios、jquery.ajax 等)中,基本上所有的 API 请求都是基于xmlHttpRequest 或者 fetch,所以捕获全局接口错误的方式就是封装 xmlHttpRequest 或者 fetch。这里,我们的 SDK 仍然使用到上文提及的 AOP 思想,对 API 进行拦截。
1. XmlHttpRequest
var xhr = window.XMLHttpRequest;
var _open = xhr.prototype.open;
var _send = xhr.prototype.send;
var attr = {};
var openReplacement = function (method, url) {
// 可以存储method、url、时间打点等信息
attr.duration = new Date().getTime();
_open.apply(this, arguments);
}
var sendReplacement = function () {
methods.addEvent(this, 'readystatechange', function (attr) {
// 可以存储response的status、计算客户端实际响应时间
attr.status = this.status;
attr.duration = new Date().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 () {
var attr = {
method: arguments[1].method,
url: arguments[0],
duration: new Date().getTime()
};
return _fetch.apply(this, arguments).then(res => {
attr.status = res.status;
attr.duration = new Date().getTime() - attr.duration;
// 上报【API】事件
return res;
});
}
为了监控前端应用是否正常运行,通常会在前端收集错误与性能等数据,最终将这些数据上报到服务端。因为日志上报并不是应用的主要功能逻辑,优先级比较低,所以我们在确保日志数据上报更高效的同时,还应该考虑如何尽可能地减少与其他关键操作的资源争抢。
1. sendBeacon
navigator.sendBeacon() 方法主要用于满足统计和诊断代码的需要。这些代码通常会在卸载文档之前,尝试通过 HTTP 将少量数据异步传输到 Web 服务器。它解决了日志上报在 unload 时成功率很低的问题。我们在埋点时有很多对离开页面时上报的需求,因为 SendBeacon 是异步的,不会影响当前页到下一个页面的跳转速度,可以更可靠地保障事件上报成功率,并且不影响路由切换。
window.navigator.sendBeacon('上报事件的api', '数据参数')
2. img.src
当浏览器不支持 navigator.sendBeacon 时,我们可以采用模拟图片加载的方式发送日志上报事件,且不会存在跨域问题。
var img = new Image();
img.src = API + '?' + '数据参数'
3. 关于 XmlHttpRequest
这里不推荐用 XmlHttpRequest。XHR 虽然支持异步请求,直接发送数据到后端,但是会受到跨域和同源的限制。而通过日志上报 API 跟业务是不在一个域下的,如果采用这种模式需要设置 Access-Control-Allow-Origin:* 跨域,非常不方便,并且在 unload 情况下上报发生的丢包率最高。
总结来看,日志上报推荐采用 sendBeacon -> img.src。在不影响用户路由切换和阻塞用户的情况下丢包率可以控制在 10%-30%,具体要看用户群体对应的环境。
高效的前端数据采集对于搭建前端监控平台来说非常关键。本文我们分享了马蜂窝在保证数据采集及时、准确、全面等方面的一些思路和实践。需要提示大家注意的是,文中涉及到的演示只做了核心代码的关键描述,不具备生产使用,我们在实际使用中需要做好兼容及容错。
本文也将作为马蜂窝前端监控平台系列文章的开篇,今后还将陆续推出埋点模式、数据处理和分析、报警以及监控平台在具体业务中的应用等内容,欢迎大家持续关注。
本文作者:王峥,马蜂窝大数据平台前端技术专家。
在react中是单向数据绑定,而在vue和augular中的特色是双向数据绑定。为什么会选择两种不同的机制呢?我猜测是两种不同的机制有不同的适应场景,查了一些资料后,总结一下。
双向数据绑定是非常重要的特性 —— 将JS模型与HTML视图对应,能减少模板编译时间同时提高用户体验。我们将学习在不使用框架的情况下,使用原生JS实现双向绑定 —— 一种为Object.observe
js判断数据类型的多种方法,主要包括:typeof、instanceof、 constructor、 prototype.toString.call()等,下面就逐一介绍它们的异同。
由于js为弱类型语言拥有动态类型,这意味着相同的变量可用作不同的类型。 typeof 运算符返回一个用来表示表达式的数据类型的字符串,目前typeof返回的字符串有以下这些: undefined、boolean、string、number、object、function、“symbol
在js中我们直接这样写typeof obj===‘object’有什么问题呢?发现Array, Object,null都被认为是一个对象了。如何解决这种情况,能保证判断obj是否为一个对象
js要处理十六进制,十进制,字符之间的转换,发现有很多差不多且书写不正确的方法.一个一个实践才真正清楚如何转换,现在来记录一下它们之间转换的方法。
奇数和偶数的判断是数学运算中经常碰到的问题,这篇文章主要讲解通过JavaScript来实现奇偶数的判断。2种判断方法:求余% 、&1
质数又称素数。指在一个大于1的自然数中,除了1和此整数自身外,没法被其他自然数整除的数。比如100以内共25个,js实现代码如下。
JavaScript自动类型转换真的非常常见,常用的一些便捷的转类型的方式,都是依靠自动转换产生的。比如 转数字 : + x 、 x - 0 , 转字符串 : \\\"\\\" + x 等等。现在总算知道为什么可以这样便捷转换。
XML是标准通用标记语言 (SGML) 的子集,非常适合 Web 传输。XML 提供统一的方法来描述和交换独立于应用程序或供应商的结构化数据。 这篇文章主要介绍Js中实现XML和String相互转化
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!