前端监控理论¶
统计信息:字数 30347 阅读61分钟
原文来源: https://www.zhihu.com/column/c_101987637 加入了自己的笔记和整理
(一)场景和类型¶
最近半年一直都在研究前端监控,发现对于前端来说,监控真的很重要,萌发了想写一系列文章全面梳理一下对前端监控的了解和总结。
1、主要场景¶
产品,首先要建立在使用场景上,所以首先讨论下监控使用的几个主要使用场景。纵观当前市面上各个前端监控产品,无外乎是满足两个方面的需求,监察**用户**的使用情况,监察**系统**的运行状态。所以当前也从 用户 和 系统 两个维度来讨论主要的使用场景。
1.1 用户使用情况¶
系统前端直面用户,即是和用户交流的窗口,也是产品运营数据第一手数据收集地,所以这是任何完备版的监控系统都绕不过去的话题。
浏览量访问量¶
百度统计行为监控
运维一个网站,有多少人来访问了,每天访问的人次是多少,这些数据是衡量一个网站运行好坏的晴雨表。当前比较主要的维度列举如下:
- pv: 访问人次,一般每次刷新页面都会 + 1
- uv: 访问人数,每人一天内只计数一次,一般小于等于pv
- ip数:访问的ip数量
- 跳出率:打开一个网页的概率/总的网页访问数
- 平均访问时长:访问总时长 / 访问页数
- 平均访问深度:在当前域名跳转的次数 / 访问次数
- 会话数:用户发起的session的数量
- 路由切换量(rpv):由于当前很多较复杂的网站都是采用spa结构,使用传统的pv,uv不能反映用户真实使用状况,所以这个指标就显得非常重要了,一般来说路由切换一次 rpv + 1,最终得到一个路由访问的次数
各个统计网站都根据不同的维度和认知对行为数据进行统计,然后通过时间维度,按(时/天/月)来对网站的运行数据进行分析,得到当前网站的运行状态。
埋点,点击流¶
为了要记录用户在网站上的点击,拖动,跳转等行为,对页面上元素绑定的一些事件并上报记录,称作埋点。有的网站只针对重点操作进行埋点,用作审计和时候追查,而另一些网站做全量事件拦截,对用户的所有行为数据都进行记录。通过埋点,可以知道用户在网站上做了哪些操作,为事后审计和查找问题提供帮助。
点击流,是基于以上的埋点的记录,把一系列事件串起来,形成一个用户操作的链条。
通过点击流和埋点,可以分析用户的使用习惯,找到用户在网站使用上碰到的问题,帮助网站做的更好。比如,在点击购买链接前,如果多数都会点击对比商品的按钮,那么对比按钮的位置就需要得到凸显等等。
场景回放¶
基于埋点和点击流,我们可以做到对用户使用场景的回放和还原,比如用户点击了一些敏感操作按钮,可以帮助用户找到操作失误记录,再比如对于较难复现的bug,可以通过场景回放来重放用户的操作步骤,复现bug。
录屏¶
场景回放的进阶版,终极大法。通过视频对用户操作进行还原,直观且强大。不过这种科技是要付出代价的,在实现上,可能会碰到不小的麻烦。后面我们在讲实现的时候再具体讨论。
1.2 系统运行情况¶
除了对用户行为的监看,前端监控和后端监控有很多类似场景,存在很多监控系统运行的场景,据当前收集的资料和对一些系统的调研来看,包含了以下几个主要方面:
错误感知¶
感知前端页面运行时产生的错误,错误包含多个维度,根据不同系统监控程度不同,一般分成:
- 控制台错误: runtime报出的错误,一般会打印在浏览器开发工具的控制台,这样
- 网络错误: 一般包括 http 等与服务器交互产生的请求错误,比如 http response 返回值为4xx,5xx等错误
- 业务系统自定义错误:一般是各系统自己定义的需要上报的业务错误,比如 http 返回 200,但其实接口返回错误码
耗时统计¶
时间,是影响用户网站体验的主要因素之一。所以打开速度、各种响应速度,都成了监控统计的主要项目,对于耗时,一般根据侧重点不同,分成:
- 页面加载耗时:主要是收集由 performace.timing 对象提供计算出的各种时间实现的,具体时间可以参考下图
浏览器自带performance对象拆解图
- 接口请求耗时:主要用于统计对后端接口的耗时情况
全链路状态感知¶
全链路状态感知,也称作端到端的全链路监控,基本会收集从请求从 web server 到 db 的全生命周期的状态,基本会记录整个请求在处理过程中各个环节的具体耗时等信息。
2、对应场景的监控类型¶
以上说明了当前了解到的各个监控的使用场景,我的理解,当前监控的类型大致可以分为以下3类:行为监控,异常监控,链路监控。按照之前的场景说明来归纳和对应下。
2.1、行为监控¶
主要负责监控用户的使用情况,比如点击流,pv,uv等指标都属于此类。还有上面提到的场景回放和录屏,也可以归到这一类上面。行为监控一般的展现形式都是图表,同时提供基于时间等维度的对比功能,能够比较直观的看出数据的变化和趋势对比。
2.2、异常监控¶
包括浏览器主动抛出的错误和接口错误的情况,一般来说异常监控会以列表的形式展示收集到的错误信息,可能包括用户 UA,Ip,网络状态,请求url等等信息的展示。
很多异常监控也结合sourcemap来还原错误堆栈和现场,提供快速查找错误的能力。
2.3、链路监控¶
链路监控对应上面全链路感知的场景,一般的展示场景可以参考开发者工具中的timeline类似,可以直观的看出各阶段耗时情况。
链路监控耗时
同时由于链路监控涉及到后端系统的改造工作,当前大部分前端监控系统是缺少这方面能力的,如何将链路监控很好的融合到当前监控系统中去,是一个值得思考的命题。
(二)行为监控的技术实现¶
上一篇梳理了前端监控的主要场景和类型,从本文开始,讨论下我知道的一些技术实现。
前端黑科技层出不穷,个人眼界有限,尽量把了解到的实现方式都罗列出来,希望对大家有些启发,同时也欢迎流言讨论。
限于篇幅,按照第一篇的场景来进行拆分,本文只讨论行为监控的技术实现方案:
0、总体思路¶
监控系统设计的总体思路上,最重要的是**“无痛”或者“无侵入”**。
任何监控代码,不在业务系统需要自定义的情况下,需要侵入到业务代码里面,都是不可取的设计。同时接入配置应该尽量简单。
如何做到**无痛**?主要方式是**拦截和重写**。
比如,很多监控系统都会**重写** xhr(XMLHttpRequest)/ fetch
来拦截请求, 例子:xhr 重写上报示例代码
proxyAjax.send = XMLHttpRequest.prototype.send;
proxyAjax.open = XMLHttpRequest.prototype.open;
// 重写 open
XMLHttpRequest.prototype.open = function(){
// 先在此处取得请求的url、method等信息并记录等处理
// 调用原生 open 实现重写
proxyAjax.open.apply(this, arguments);
}
// 重写 send
XMLHttpRequest.prototype.send = function () {
// 调用原生send
proxyAjax.send.apply(this, arguments);
// 在onleadend ontimeout等事件中上报,上报处理函数 handleMonitor
this.onloadend = function() {
handleMonitor(someParams)
}
}
// 上报函数
handleMonitor = function(params) {
this.send(params)
}
比如,在对页面事件的点击记录时,给document
对象绑定click
事件收集点击就是**拦截**的一种。
例子:要注意的是在捕获阶段绑定,防止业务代码中的阻止冒泡捕获不到事件
document.addEventListener("click", function(event) {
handleClick(event);
}, false);
1、用户使用场景¶
下面按照统计维度来说明一下统计指标的技术实现:
1.1 pv,uv¶
通常的方式对访问当前域名的**一个用户**植入**一个cookies**用于标识用户身份,以传统的统计口径来看,对于pv,每次刷新页面都 + 1,对于uv,在今天内访问的用户只会 + 1。这里有几个注意点:
- 1、统计口径:对于不同的产品,pv的统计口径可能是不一样的,有的要求首页完全加载完才算一个pv,有的要求曝光¼,有的需要dom加载完。根据需求的不同,绑定上报事件的时机也不同。对于监控系统来说,一般会实现通用口径的pv统计,如果有其他不同需求,可以走后文提到的自定义上报流程。
- 2、防刷:对于pv/uv,有很多刷流量的方式,比如删掉cookies,重新加载一次。对有账号体系的系统,cookies和账号绑定就可以防刷。对没有账号体系的系统,可以使用ip来限制,同ip发起多少次都算一个uv。防刷是个比较有趣的话题,限于篇幅,这儿简略提一下,有兴趣的同学可以一起进一步探讨。
- 3、spa网页:由于前端路由的存在,spa结构的网页,传统的pv很难反应网站的真实状况,推荐使用uv或者rpv来观测。
1.2 ip 数¶
访问ip数的统计有多种方式,这里介绍两种主要方式:
- 1、接入层直接记录:在接入层入口直接记录来源ip,收到就 + 1,如有需要详情也可以记录更多信息,这种方式可能会增加当前系统的一些负担。改造成MQ或者其他的异步方式,可以减轻对主干系统的影响。
- 2、分析日志:主流做法,分析接入层日志,对日志做统计即可得出ip数。
1.3 跳出率¶
根据上文提到的跳出率公式,需要计算当前页面的打开次数,对于非spa且非hash的页面,都可以用接入层统计的方式来计算url的打开次数。对使用hash路由的spa页面,需要绑定hashchange
事件或者框架提供的路由事件来进行上报。总访问页数同理。
1.4 平均访问时长 / 平均访问深度¶
根据计算公式,统计方法类似跳出率。
1.5 会话数量¶
这个没太多好说的,服务端统计就完事了。
1.6 路由切换量(rpv)¶
重点讲下这个,随着前端路由系统的普及,当前 spa 是web系统的主要形态之一,对spa系统来说,统计的实现方式和 mpa 系统有很多的不同,一般来说统计路由切换量(rpv)需要手动开启配置,比如阿里云arms就需要配置enableSPA = true
。
前端路由主要是通过**hash**和**history api**来实现的,使用hash路由时hash值不会上传服务器,需要前端来做捕获上报,而history api的情况url是变化的,可以在后台统计到。
hash路由的捕获上报实现:
// hash路由绑定onhashchange事件
if("onhashchange" in window) {
window.onhashchange = handler
}
// history api类型路由的上报
// 监听popstate
window.addEventListener('popstate', (event) => {
// 上报处理
handler()
})
如果前端需要通过 history api来统计,这里也给出一些代码实例
// history 只监听 popstate事件可以处理掉大部分的api触发
window.addEventListener("popstate", function() {
// 上报逻辑
});
// pushState 和 replaceState 不会触发 popstate 事件,可以采用类似xhr的方式重写
2、埋点,点击流¶
埋点的实现上面其实已经提到了,本质上就是对**事件的拦截**,这里主要提一下自定义埋点上报的实现。
2.1 自定义埋点¶
自定义埋点上报,涉及到各监控系统api设计,一般来说,各监控系统的接入sdk都会给出自定义上报的方法,供业务系统自己控制上报时机和上报内容。 举例:
// 自定义埋点实例,指定类型type,服务器解析数据并呈现
monitor.diysend({type:'monitor', value: 't1=1&t2=2'})
2.2 点击流¶
点击流其实是通过根据统一的用户标识把一系列的事件上报的用户行为串起来的一种方式,结合以上的数据上报和页面切换,可以构造出一个基于时间轴的用户点击操作流程。 示例页面
点击流示例页面
3、场景回放,录屏¶
场景回放和录屏的技术实现,总的来主要有如下实现方式:
- dom 背景 + 回放操作:用当前页面做背景,方法:1、iframe加载目标页面放在下层做背景,2、用phantomjs截取当前页面做背景,在背景之上根据上报数据重现用户操作。这种实现不用特殊上报,只需要有点击流的坐标数据即可。但其最大的问题在于回放操作和背景没有交互,即使在背景中实现模拟操作,也可能存在一定的延迟。
- html2canvas:顾名思义,此方案的思路是把当前dom结构转化成一张截图,然后按照每秒24帧上传图片,后端和用户操作组合一下,组成一个可播放视频。这种方法的悲催在于上传的图片体积过大,导致出现一些性能问题。
- chrome 插件方式:使用chrome的插件权限实现录屏,缺点是完全没有兼容性,而且装插件对用户体验不友好。
- dom 上报重建:思路是上报dom并重建,实现:上报首次的dom结构做基础,后续使用增量上报方式,上报dom结构的变化,然后通过后端平台,将数据组装成可播放的视频,这种方式的典型代表有rrweb等实现。这种方式对于canvas之类的非dom表现元素,需要做特殊处理,但已经是个比较成熟可用的方案了。
总的来说,以上录屏方案中,dom上报回放是一个比较成熟的方案了,也有类似rrweb等成熟实践,比较不容易遇到坑,可以考虑使用。
(三)异常监控的实现¶
前两篇已经总结了前端监控的主要使用场景和行为监控的实现方式,这一篇,主要来聊一下异常监控的实现方式。
总体思路¶
和行为监控的设计的思路一致,**无侵入**也是最值得思考的系统设计重点。和前面一样,由于篇幅,这里只简单的谈一谈设计实现,如果需要了解细节,可以留言一起探讨。在本文最后,会说明一下,当捕获到错误之后的上报的方法。
全局异常¶
全局异常是异常漏斗的最下一层,基本上用于捕获抛到window.onerror
事件里面捕获的异常,同时采用这种方式捕获的异常最简单的方式,一般异常捕获系统都会实现一个托底异常捕获。具体实现方式如下:
// 覆盖window.onerror函数及注意点
window.onerror = function(message, source, lineno, colno, error) {
// Script error 不需要上报,因为同源限制,上报了也没有意义
if("Script error."=== messaage && !source) {
return false;
}
// 这里要注意用setTimeout包装一下,防止报错太多,卡住主线程
setTimeout(function(){
// do something 上报
});
}
重写window.onerror函数,确实可以使异常监控有一定的托底作用,但是受到同源等安全策略的限制,很多异常不会如我们预期的一样报错,即使上报上来也没有什么意义。如果报上来都是没有意义的数据,那就无端增加了监控系统的消耗。
这里也提供另一个全局捕获思路,来自于 sentry raven.js,可以参看 _instrumentTryCatch 函数,基本思路是通过包装各类事件,来捕获事件异常,构造自己的异常捕获系统,这里就不再展开了,有兴趣的同学可以自己阅读相关源码。这种方式相比直接重写window.error更加先进,但是也更加复杂,好在有sentry帮忙维护了。
总的来说,通过全局sdk的方式,我们可以简单直接的获取一层托底异常。但是可能有几个问题:
- 1、代码中有try{}catch(e){}类的异常不会抛到最外层,window.onerror无法捕获错误
- 2、有些需要自定义的上报信息无法及时捕获
自定义异常上报¶
为了解决以上全局上报的问题,自定义异常上报作为全局上报系统的补充,是各个监控系统必不可少的功能,这个功能的完善与否,直接决定这框架的好坏。
一般来说,自定义上报都会提供如下信息:
- 1、自动增加上报时间
- 2、自动增加用户信息
- 3、可配置增加错误信息
- 4、用户上报的信息
值得一提的是,自定义上报信息的解析很多监控系统的后端做法都不一样,有些是让用户导出数据,自己格式化,拆解做分析,更智能一些的是让用户上传结构数据,然后对结构智能拆解,输出报表。个人觉得没有什么优劣,智能拆解相对来说使用成本变低,但是易用性要求和开发难度也会相应变高。
异常分级¶
写过后端代码的同学都知道,异常一般分为很多级别的,对于各种级别和类型的异常处理方式也不一样。普遍来说,一般 info,warn,error 几个级别都是有的。
这里主要说下error类错误的处理方式,一般来说,error类错误会实时上报后端,因为和监控系统联动的告警系统很需要这类实时数据,以便及时告警。其他类型的错误,如果不是致命类异常错误,可能会用到离线(空闲)上报的策略,以减少消耗,提高性能。
实时上报¶
上节说到,上报方式有两种,这一节介绍下实时上报的方式。实时上报本质上就是系统的一个sendMessage的方式,一般来说,只要构造一个get请求就可以了。
举个例子:
// 使用image的方式send info
function sendInfo(url) {
var _image = new Image(1, 1);
// 挂载到监控全局上,保证同时只有一个,减少消耗
monitor.img=_image;
// 发送完成后清除掉相关函数,离线上报需要改造一些
_image.onload = _image.onerror = _image.onabort = function() {
_image.onload = _image.onerror = _image.onabort = null;
monitor.img = null;
}
}
以上,一般的系统上报方式大同小异,本质上原理是提一个发后不理的get请求,但是由于浏览器的并发数量限制,如果上报触发太多的话,有可能会影响宿主系统的性能。为了解决这个问题,离线(空闲)上报方式就诞生了。
离线上报¶
离线上报主要是为了解决两个问题:
- 1、上文提到的宿主系统性能的问题
- 2、补充一些失败的日志,这儿要在上文提到的上报部分做一些改造
代码比较多,这里就提一下思路先:对于非error类的需要实时收集的异常,可以采用本地存储的方式存储起来,寻机上传。至于寻机,可能是根据网络和系统的情况变化,比如从弱网情况恢复到良好网络情况,可以统一上传,这里注意上传时的节奏即可。这儿还会有一些退化重试的上传的时间间隔的算法。
总的来说,就是不必要实时上报、当前无法上报、上报失败的数据可以先存起来,择机上报。至于存在哪里,什么时候上报,是不是可以手动触发,看各个系统自己的实现了。
综上,异常监控的基本已经说清楚了,下一篇看看聊聊性能监控的实现。
(四)性能监控的实现¶
本篇聊聊性能监控相关的一些实现方式和常用的性能监控工具
总体思路¶
和行为监控和异常监控的思路不太一样,实现上无侵入不是太重点,因为本身就不会有太多的侵入性。 性能监控的重点在于对各类环境的适配和监控的准确性,因为对系统性能的监控更多是一个面的监控,比如说对cgi接口的平均响应时间,如果由于部分获取时间的毛刺拉高导致数据不准就很麻烦。
性能监控的主要意义¶
性能监控的主要意义有两个:
-
改善用户体验。这方面,衍生出来的主要是客户端性能监控,说客户端不说页面主要是因为当前前端复杂的运行环境,比如说小程序,H5,pc端页面都是典型的运行环境。 一般来说,改善体验的循环是:收集数据 -> 分析数据 -> 改代码 -> abtest -> 分析数据 -> 上线,性能监控是在收集数据阶段起作用,分析数据当前阶段,绝大部分是后台系统加体验者人肉的方式。
-
监控系统异常。这方面,主要谈的的cgi的性能监控,监控内容主要是各个cgi的响应时间,主要也有两点左右,1、帮助优化接口性能,如果普遍响应很慢,就需要做一些优化措施了,2、帮助发现不可用错误,如果接口响应时间长的超过阈值,那说明可能接口挂掉了,这时候应该触发告警,让运维或者开发哥哥去查问题了。
主要指标¶
指标主要由三大块组成: * 系统获取 系统部分主要指浏览器提供的一系列高精度的网页性能指标,一般来说常规使用如下图解析 performance 对象:
根据图中的各个时间戳的end-start
,可以获得网页从加载到渲染各阶段的精确时间,从而评估网页在各阶段的性能。
- 监控系统定义 各监控系统会自定义一些采集指标,同时在前端的js-sdk里面进行一些采集和统计 比如客户端维度的: cpu 使用率,fps监控,memory消耗等(这儿的实现挺有趣的,后续专门写一篇来讲讲怎么弄) 比如业务维度的:首屏加载时间,通常使用
performance.timing
中的domContentLoadedEventEnd - domContentLoadedEventStart
页面停留时间,一般用unloadEventEnd - domContentLoadedEventEnd
来统计。
以上只是举例,各系统定义的指标还有很多,不同的监控目的对于指标的制定会有很多不一样的地方,也会导致很多实现方案上的差异。
- 自定义获取 为了满足不同业务的需要,监控系统一般都会给各业务代码提供灵活的自定义接口,毕竟,通用接口是不能满足所有情况的。 和之前文章中提到的自定义上报方式类似,一般来说上报是由前端代码根据一定规则解析后上报,也可以上报到后端来解析,这儿就看各监控系统的实现和约定了
主要实现方式¶
- performance对象 这里其实很简单,就是根据performance.timing和图的指引,减减减就完事了。 这儿可以产生出很多标准数据,是监控系统的标配。
- 自定义模拟实现 举个例子:监控系统提供标准api,收集key和value用于上报和展示自定义指标。 产品要求,只有展示出来首页的最后一张图才算完全加载完成。
考虑到这个实现,一般来说前端会自己打点,使用 image.onload
记录结束时间点,然后减去记录的startTime得到首页展示时间,根据{key, value}
的格式上报后在后端查看。
这样的需求有很多,比如页面访问深度,页面停留时间,都可以通过自定义上报的方式来完成采集。
性能测试工具¶
接下来介绍一些性能测试工具,性能测试监控更多是事后的发现和处理,而上线前期需要一些工具对页面进行性能检测和验证 * lighthouse
Lighthouse 是一个开源的自动化工具,用于改进网络应用的质量。 您可以将其作为一个 Chrome 扩展程序运行,或从命令行运行。 您为 Lighthouse 提供一个您要审查的网址,它将针对此页面运行一连串的测试,然后生成一个有关页面性能的报告。 google 官方出品,对项目进行全方位的体检。 特别好处是可以做上线前体检,通过
npm
安装后,可以直接通过本地执行命令的方式来获取页面报告,不过要注意一些细节带来的结果不准,比如页面登录和其他一些原因带来的网络延迟。 同时,lighthouse默认验证的是mobile,可能在pc端会有少许偏差
- pagespeed pageSpeed 老牌页面性能测试网站,听说底层是lighthouse,输入网址就能测试, 输出页面性能的各类指标,结果如下图 :
pagespeed这儿有个问题,对国内的页面不友好,出不来结果,需要的话建议还是用lighthouse。
ps:早年的免费页面测试都收归到各大云厂商旗下去收钱了,说真的,还是本地测试靠谱。
最近也比较忙点,文章更新有点慢,后续会继续推进这个系列,写一些更详细的点的深入解决方案。
(五)前端异常监控实际操作-知乎¶
虽然上线前会有自测 QA等 但是还是会有线上异常情况,如何快速发现 或者监控到这些异常的出现?
原文来源:https://www.zhihu.com/question/29953354
方法1¶
可以用 Sentry(GitHub - getsentry/sentry: Sentry is cross-platform crash reporting built with love) 来收集错误 。
每次发布把 sourcemap 文件上传到 Sentry 上还能还原代码压缩后的错误。
每次发布创建一个 release
把每次发布的 soucemap 文件传上去
收集到的错误
根据 soucemap 把压缩代码的错误栈还原出来,上面是用户信息
还能发送自定义的数据来帮助定位错误
方法2¶
推荐几个成熟的**异常监控平台** ——
“Web 前端 异常监控/远程日志”这个领域 我从近一年前就开始关注、尝试,先列一下参考文献 ——
- 前端代码异常监控
- 前端相关数据监控
- 阿里巴巴(中国站)用户体验设计部博客(文中提到的 FdSafe 并没有被 阿里巴巴 UED 开源)
- hlg-front/前端性能监控/monitor.js-master at master · huanleguang/hlg-front · GitHub
- sap1ens/javascript-error-logging · GitHub
就目前来看,无论是 开源项目 还是 开放平台,适合国内程序猿使用的此类成熟系统依然缺乏,是一个值得探索的领域~(我在考虑用 Node.JS 写一个)
初期若不考虑记录**调用堆栈**这样复杂的信息,可以直接简单粗暴地让前端页面把报错信息作为 URL 参数,去 GET 一个固定且不存在(404 Not Found)的 URL,然后就可以直接在服务器用命令行过滤 HTTP 服务程序(Apache、LightHTTPd、IIS、Nginx 等)的 Access Log 来分析 Web 前端日志 —— 简单、高效、可靠~
分享一段我修改自上文所提【参文 1】的“Web 前端全局异常监控”代码(基于 jQuery)——
(function (BOM, $) {
var Console_URL = $('head link[rel="console"]').attr('href');
BOM.onerror = function (iMessage, iURL, iLine, iColumn, iError){
BOM.setTimeout(function () {
var iData = {
message: iMessage,
url: iURL,
line: iLine,
column: iColumn || (BOM.event && BOM.event.errorCharacter) || 0
};
if (iError && iError.stack)
iData.stack = (iError.stack || iError.stacktrace).toString();
if (Console_URL) {
if (iData.stack)
$.post(Console_URL, iData);
else
$.get(Console_URL, iData);
}
}, 0);
return true;
};
})(self, self.jQuery);
方法3¶
浏览器端 JavaScript 异常监控 For Dummies
演讲视频预计在12月底发布