华为云通过TinyMonitor看板来对用户前端性能数据、异常等做监控。与传统拨测形式的监控方式不同,Tiny Monitor采用用户上报的方式,能够获取全量的、真实的用户数据,并提供丰富的数据看板,为业务的性能监控提供强大的支持。
1. 用户性能数据收集
在真实的用户监测场景下如何采集和计算用户数据,实际上包括以下几个方面的问题:
- 如何评估用户性能?
- 如何设定性能指标?
- 如何采集和计算用户数据?
1.1 如何评估用户性能?
前端性能会直接影响用户体验,如下两组对比图,我们感受到性能对页面展示的直接影响:
当然前端性能也会比用户本身的条件影响,例如从3G网络到4G网络等带宽影响。既然性能与人相关联,我们如何检测所有的性能数据呢?
在模拟的环境中进行检测(实验室数据):
- Lighthouse
- WebPage Test
- PageSpeed Insights
汇集实际的用户数据(真实数据):
- Chrome User Experience Report (CrUX)
- Analytics data (e.g. Google Analytics)
- PageSpeed Insights
1.2 如何设定性能指标?
主要从首屏时间、全加载时间、用户设备信息等几个维度来探讨。通过 Navigation Timing API分析:
1. 确定统计时间点:
页面性能统计的起始时间点,应该是用户输入网址回车后开始等待的时间。一个是通过navigationStart获取,相当于在URL输入栏回车或者页面按F5刷新的时间点;另外一个是通过 fetchStart,相当于浏览器准备好使用 HTTP 请求获取文档的时间。从开发者实际分析使用的场景,浏览器重定向、卸载页面的耗时对页面加载分析并无太大作用,通常建议使用 fetchStart 作为统计起始点。
2. 首屏时间:
有如下集中测量方式确定首屏时间:
- 开发者手动打点(最佳方式)
- window.performance.getEntriesByType(‘paint’)[0] (PerformancePaintTiming API ,MDN Web标准)
- window.chrome.loadTimes().firstPaintTime (老API,不推荐)
- responseEnd – fetchStart (保底方案)
3. DOM READY —— 页面解析完成的时间
DOM Ready,指的是页面解析完成的时间,在高级浏览器里有对应的DOM事件 – DOMContentLoaded。该事件在文档解析完成时会触发。那么文档解析到底包括哪些操作呢?虽然暂不能给出一个完全的答案,但文档的解析至少应该包括以下操作:HTML文档分析以及DOM树的创建、外链脚本的加载、外链脚本的执行以及内联脚本的执行,但是不包括图片、iframe等其它资源的加载。正因为如此,该事件触发的时机一般比window.onload要早,而且是在所有DOM元素都可以操作之时。DOM Ready指标影响的是交互功能的最早可用时间,DOM Ready时间如果过长的话,用户会发现页面已经出来了,但是很多功能却是不可用的。
4. PAGE LOAD
Page Load 时间指的就是window.onload事件触发的时间。与DOM Ready时间相比,Page Load的时间往往要更靠后一些,因为Page Load不仅仅是HTML文档解析完毕还包括了所有资源加载所需要的时间,例如图片资源的加载、iframe的加载等鉴于window.onload事件要等到所有资源加载完成后才会触发,因此资源加载的时间越长则Page Load的时间越长。如果没有任何外链资源,则Page Load时间与DOM Ready时间几乎是相等的,随着图片等资源的增加,Page Load与DOM Ready的差距也会越来越大。Page Load的时间越久,浏览器状态栏显示加载中的时间也就越久,因此会影响用户对页面整体速度的体验。
5. 用户环境信息
用户环境信息也是影响性能指标的一大因素:
- 客户端类型(PC、手机)
- 操作系统OS(Windows、Mac、Unix)
- 浏览器类型(Chrome、IE)
- 国家(国家代码)、地区(地区代码)
- 渠道(用户来源)
- 局点(北京一、北京四、上海一等)
- 网络类型(3G、4G、5G、Wifi)
1.3 如何采集和计算用户数据?
华为云的数据采集架构如下图所示,然后将采集信息通过 TinyMonitor 看板来展示。TinyMonitor 工具是华为自研的性能监控平台具有如下特点:
- 提供基于业务分类下的性能数据上报、展示、分析等一站式功能,作为性能持续看护和定位问题平台,助力BU产品性能,提升体验。
- 通过在前端页面js非侵入式埋点,来上报用户访问页面的真实性能数据
- 数据上报到全球鹰后台进行统计分析,并提供查询接口
- 实现前端性能看板统一呈现各站点、各页面的性能数据,并提供趋势分析、性能数据分布、地域性能分析等查询能力
- 实时采集真实用户体验数据;端到端多维度数据分析展示;记录页面加载全过程,建立页面和资源的加载时序图;支持多种告警规则和告警间隙,通过应用号推送的方式通知业务负责人
2. 前端异常监控
2.1 异常监控的背景
- 目前现状 —— 华为云工程师在处理线上问题的时候基本上都来自于工单,工程师只是被动地接收客户发现的问题。
- 目前痛点 —— 兼容性问题自测难覆盖、接口报错难复现、问题发现不及时、客户反馈太紧急。
- 实现目标 —— 做一个异常监控平台,化被动为主动,主动优化和修复线上问题,并且建立报警机制,提高工程师线上服务意识。
2.2 异常监控的意义
2.3 采集内容
在进行异常采集过程中,我们需要注意如下几个方面问题:1. 性能和信息全面性之间做出取舍; 2. 获取异常的自动化、不遗漏任何一处报错;3. 需要高效、准确和全面的捕获异常;4. 正确选择上报的时机、收集的异常日志等;基于此,我们要采集的内容有哪些呢?归纳有如下几点:
- 用户信息 —— 当前用户状态、权限、用户信息等。
- 行为信息 —— 用户所在的界面路径、执行了什么操作、操作时运用或者产生的相关数据等。
- 异常信息 —— 用户操作的DOM元素节点、异常级别、异常类型、代码stack信息等。
- 环境信息 —— 网络环境、设备型号、标志码、操作系统版本、客户端版本、API接口版本等。
2.4 异常捕获
- try-catch 异常处理
try-catch 在我们的代码中经常见到,当代码块发生出错时 catch 将能捕捉到错误的信息,页面也将可以继续执行。但是 try-catch 处理异常的能力有限,只能捕获捉到运行时非异步错误,对于 语法错误 和 异步错误 就显得无能为力。如下图所示,只有左图可以捕捉异常:
- window.onerror 异常处理
window.onerror 捕获异常能力比 try-catch 稍微强点,无论是异步还是非异步错误,onerror 都能捕获到运行时错误。window.onerror 函数只有在返回 true 的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示。关于window.onerror 还有两点需要值得注意:
- 对于 onerror 这种全局捕获,最好写在所有 JS 脚本的前面,因为你无法保证你写的代码是否出错,如果写在后面,一旦发生错误的话是不会被 onerror 捕获到的。
- 另外 onerror 是无法捕获到网络异常的错误和语法错误。
- Promise 异常处理 —— window.addEventListener(“unhandledrejection”)
通过 Promise 可以帮助我们解决异步回调地狱的问题,但是一旦 Promise 实例抛出异常而你没有用 catch 去捕获的话,onerror 或 try-catch 也无能为力,无法捕捉到错误。当你用到很多的 Promise 实例的话,特别是你在一些基于 promise 的异步库比如 axios 等一定要小心,因为你不知道什么时候这些异步请求会抛出异常而你并没有处理它,所以你最好添加一个 Promise 全局异常捕获事件 unhandledrejection。
- 监听错误事件 —— window.addEventListener(“error”)
资源(img 或 script)加载失败时,加载资源的元素会触发一个 Event 接口的 error 事件,并执行该元素上的 onerror() 处理函数。但这些 error 事件不会向上冒泡到 window,但能被window.addEventListener(‘error’) 捕获。也就是说,面对资源加载失败的错误,只能用window.addEventListerner(‘error’),而用 window.onerror 是无效的。
- iframe 异常
- 卡顿和页面崩溃 (load和beforeload, service worker)
- 前端框架的异常捕获 —— Vue.config.errorHandler
在 React v16以前,可以使用 unstable_handleError 来处理捕获的错误。React v16以后,使用componentDidCatch 来处理捕获的错误。若需全局捕获错误,可以在最外层包裹一层组件,在 componentDidCatch 中捕获错误信息。
Vue 的源码中,在关键函数(比如钩子函数等)执行的时候,都加上try{} catch(){}
,在cacth
中处理捕获到的错误。看下面的源码。
...
// vue源码片段
function callHook (vm, hook) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget();
var handlers = vm.$options[hook];
if (handlers) {
for (var i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm);
} catch (e) {
handleError(e, vm, (hook + " hook"));
}
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook);
}
popTarget();
}
...
function globalHandleError (err, vm, info) {
if (config.errorHandler) {
try {
return config.errorHandler.call(null, err, vm, info)
} catch (e) {
logError(e, null, 'config.errorHandler');
}
}
logError(err, vm, info);
}
function logError (err, vm, info) {
{
warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm);
}
/* istanbul ignore else */
if ((inBrowser || inWeex) && typeof console !== 'undefined') {
console.error(err);
} else {
throw err
}
}
复制代码
Vue 中提供了Vue.config.errorHandler
来处理捕获到的错误。
// err: 捕获到的错误对象。
// vm: 出错的VueComponent.
// info: Vue 特定的错误信息,比如错误所在的生命周期钩子
Vue.config.errorHandler = function (err, vm, info) {
//
}
复制代码