【序】:前端异常监控是很重要的一部分,今天刚好看到一篇文章,就在此总结学习,文章地址
【疑惑】:什么是前端监控?
【解惑】:前端监控包括行为监控、异常监控、性能监控等,本文主要讨论异常监控。对于前端而言,和后端处于同一个监控系统中,前端有自己的监控方案,后端也有自己等监控方案,但两者并不分离,因为一个用户在操作应用过程中如果出现异常,有可能是前端引起,也有可能是后端引起,需要有一个机制,将前后端串联起来,使监控本身统一于监控系统。因此,即使只讨论前端异常监控,其实也不能严格区分前后端界限,而要根据实际系统的设计,在最终的报表中体现出监控对开发和业务的帮助。
【疑惑】:异常监控有哪些部分组成?
【解惑】:一般而言,一个监控系统,大致可以分为四个阶段:日志采集、日志存储、统计与分析、报告和警告。
- 收集阶段:收集异常日志,先在本地做一定的处理,采取一定的方案上报到服务器。
- 存储阶段:后端接收前端上报的异常日志,经过一定处理,按照一定的存储方案存储。
- 分析阶段:分为机器自动分析和人工分析。机器自动分析,通过预设的条件和算法,对存储的日志信息进行统计和筛选,发现问题,触发报警。人工分析,通过提供一个可视化的数据面板,让系统用户可以看到具体的日志数据,根据信息,发现异常问题根源。
- 报警阶段:分为告警和预警。告警按照一定的级别自动报警,通过设定的渠道,按照一定的触发规则进行。预警则在异常发生前,提前预判,给出警告。
【疑惑】:什么是前端异常?
【解惑】:前端异常是指在用户使用Web应用时无法快速得到符合预期结果的情况,不同的异常带来的后果程度不同,轻则引起用户使用不悦,重则导致产品无法使用,使用户丧失对产品的认可。
前端异常可分为几类:
a. 出错
界面呈现的内容与用户预期的内容不符,例如点击进入非目标界面,数据不准确,出现的错误提示不可理解,界面错位,提交后跳转到错误界面等情况。这类异常出现时,虽然产品本身功能还能正常使用,但用户无法达成自己目标。
b. 呆滞
界面出现操作后没有反应的现象,例如点击按钮无法提交,提示成功后无法继续操作。这类异常出现时,产品已经存在界面级局部不可用现象。
c. 损坏
界面出现无法实现操作目的的现象,例如点击无法进入目标界面,点击无法查看详情内容等。这类异常出现时,应用部分功能无法被正常使用。
d. 假死
界面出现卡顿,无法对任何功能进行使用的现象。例如用户无法登陆导致无法使用应用内功能,由于某个遮罩层阻挡且不可关闭导致无法进行任何后续操作。这类异常出现时,用户很可能杀死应用。
e. 崩溃
应用出现经常性自动退出或无法操作的现象。例如间歇性crash,网页无法正常加载或加载后无法进行任何操作。这类异常持续出现,将直接导致用户流失,影响产品生命力。
【疑惑】: 产生异常的原因有哪些?
【解惑】:
【疑惑】: 如何采集异常?
【解惑】:
当异常出现的时候,我们需要知道异常的具体信息,根据异常的具体信息来决定采用什么样的解决方案。在采集异常信息时,可以遵循4W原则:
"WHO" "did WHAT" and "get WHICH exception" in "WHICH environment?"
话句话说就是:是谁做了什么和报了什么错,在那个环境?
第一点:是谁发现的错误?
a. 用户信息
出现异常时该用户的信息,例如该用户在当前时刻的状态、权限等,以及需要区分用户可多终端登录时,异常对应的是哪一个终端。
第二点:当时做了什么操作?
b. 行为信息
用户进行什么操作时产生了异常:所在的界面路径;执行了什么操作;操作时使用了哪些数据;当时的API吐了什么数据给客户端;如果是提交操作,提交了什么数据;上一个路径;上一个行为日志记录ID等。
第三点:获取哪些异常信息?
c. 异常信息
产生异常的代码信息:用户操作的DOM元素节点;异常级别;异常类型;异常描述;代码stack信息等。
第四点:是在什么环境发现的?
d. 环境信息
网络环境;设备型号和标识码;操作系统版本;客户端版本;API接口版本等。
【疑惑】:如何捕获异常呢?
【解惑】:分为两种捕获方式,单点捕获与全局捕获
1. 全局捕获
- window.addEventListener(‘error’) / window.addEventListener(“unhandledrejection”) / document.addEventListener(‘click’) 等
- 框架级别的全局监听,例如aixos中使用interceptor进行拦截,vue、react都有自己的错误采集接口
- 通过对全局函数进行封装包裹,实现在在调用该函数时自动捕获异常
- 对实例方法重写(Patch),在原有功能基础上包裹一层,例如对console.error进行重写,在使用方法不变的情况下也可以异常捕获
2. 单点捕获
- try…catch
- 专门写一个函数来收集异常信息,在异常发生时,调用该函数
- 专门写一个函数来包裹其他函数,得到一个新函数,该新函数运行结果和原函数一模一样,只是在发生异常时可以捕获异常
跨域脚本异常
产生的场景:由于浏览器安全策略限制,跨域脚本报错时,无法直接获取错误的详细信息,只能得到一个Script Error。例如,我们会引入第三方依赖,或者将自己的脚本放在CDN时。
方案一:
- 将js内联到HTML中
- 将js文件与HTML放在同域下
方案二:
- 为页面上script标签添加crossorigin属性
- 被引入脚本所在服务端响应头中,增加 Access-Control-Allow-Origin 来支持跨域资源共享
问题解决神器
异常录制
git上有一个项目主要用于这个
异常级别划分
不同级别的告警使用不同级别处理方式,特别紧急和非常重要的影响用户的问题要及时上报。
异常上报
-
首先要存储日志,这里推荐indexeddb,配合hello-indexeddb工具异步调用,高性能
-
合理使用indexedDB
上图展示了前端存储日志的流程和数据库布局。当一个事件、变动、异常被捕获之后,形成一条初始日志,被立即放入暂存区(indexedDB的一个store),之后主程序就结束了收集过程,后续的事只在webworker中发生。在一个webworker中,一个循环任务不断从暂存区中取出日志,对日志进行分类,将分类结果存储到索引区中,并对日志记录的信息进行丰富,将最终将会上报到服务端的日志记录转存到归档区。而当一条日志在归档区中存在的时间超过一定天数之后,它就已经没有价值了,但是为了防止特殊情况,它被转存到回收区,再经历一段时间后,就会被从回收区中清除。
3.前端整理日志
上文讲到,在一个webworker中对日志进行整理后存到索引区和归档区,那么这个整理过程是怎样的呢?
由于我们下文要讲的上报,是按照索引进行的,因此,我们在前端的日志整理工作,主要就是根据日志特征,整理出不同的索引。我们在收集日志时,会给每一条日志打上一个type,以此进行分类,并创建索引,同时通过object-hashcode计算每个log对象的hash值,作为这个log的唯一标志。
- 将所有日志记录按时序存放在归档区,并将新入库的日志加入索引
- BatchIndexes:批量上报索引(包含性能等其他日志),可一次批量上报100条
- MomentIndexes:即时上报索引,一次全部上报
- FeedbackIndexes:用户反馈索引,一次上报一条
- BlockIndexes:区块上报索引,按异常/错误(traceId,requestId)分块,一次上报一块
上报完成后,被上报过的日志对应的索引删除 - 3天以上日志进入回收区
- 7天以上的日志从回收区清除
- rquestId:同时追踪前后端日志。由于后端也会记录自己的日志,因此,在前端请求api的时候,默认带上requestId,后端记录的日志就可以和前端日志对应起来。
-
traceId:追踪一个异常发生前后的相关日志。当应用启动时,创建一个traceId,直到一个异常发生时,刷新* traceId。把一个traceId相关的requestId收集起来,把这些requestId相关的日志组合起来,就是最终这个异常相关的所有日志,用来对异常进行复盘。
图举例展示了如何利用traceId和requestId找出和一个异常相关的所有日志。在上图中,hash4是一条异常日志,我们找到hash4对应的traceId为traceId2,在日志列表中,有两条记录具有该traceId,但是hash3这条记录并不是一个动作的开始,因为hash3对应的requestId为reqId2,而reqId2开始于hash2,因此,我们实际上要把hash2也加入到该异常发生的整个复盘备选记录中。总结起来就是,我们要找出同一个traceId对应的所有requestId对应的日志记录,虽然有点绕,但稍理解就可以明白其中的道理。
我们把这些和一个异常相关的所有日志集合起来,称为一个block,再利用日志的hash集合,得出这个block的hash,并在索引区中建立索引,等待上报。
日志上报
上报日志也在webworker中进行,为了和整理区分,可以分两个worker。上报的流程大致为:在每一个循环中,从索引区取出对应条数的索引,通过索引中的hash,到归档区取出完整的日志记录,再上传到服务器。
按照上报的频率(重要紧急度)可将上报分为四种:
a. 即时上报
收集到日志后,立即触发上报函数。仅用于A类异常。而且由于受到网络不确定因素影响,A类日志上报需要有一个确认机制,只有确认服务端已经成功接收到该上报信息之后,才算完成。否则需要有一个循环机制,确保上报成功。
b. 批量上报
将收集到的日志存储在本地,当收集到一定数量之后再打包一次性上报,或者按照一定的频率(时间间隔)打包上传。这相当于把多次合并为一次上报,以降低对服务器的压力。
c. 区块上报
将一次异常的场景打包为一个区块后进行上报。它和批量上报不同,批量上报保证了日志的完整性,全面性,但会有无用信息。而区块上报则是针对异常本身的,确保单个异常相关的日志被全部上报。
d. 用户主动提交
在界面上提供一个按钮,用户主动反馈bug。这有利于加强与用户的互动。
或者当异常发生时,虽然对用户没有任何影响,但是应用监控到了,弹出一个提示框,让用户选择是否愿意上传日志。这种方案适合涉及用户隐私数据时。
即时上报虽然叫即时,但是其实也是通过类似队列的循环任务去完成的,它主要是尽快把一些重要的异常提交给监控系统,好让运维人员发现问题,因此,它对应的紧急程度比较高。
批量上报和区块上报的区别:批量上报是一次上报一定条数,比如每2分钟上报1000条,直到上报完成。而区块上报是在异常发生之后,马上收集和异常相关的所有日志,查询出哪些日志已经由批量上报上报过了,剔除掉,把其他相关日志上传,和异常相关的这些日志相对而言更重要一些,它们可以帮助尽快复原异常现场,找出发生异常的根源。
用户提交的反馈信息,则可以慢悠悠上报上去。
为了确保上报是成功的,在上报时需要有一个确认机制,由于在服务端接收到上报日志之后,并不会立即存入数据库,而是放到一个队列中,因此,前后端在确保日志确实已经记录进数据库这一点上需要再做一些处理。
图展示了上报的一个大致流程,在上报时,先通过hash查询,让客户端知道准备要上报的日志集合中,是否存在已经被服务端保存好的日志,如果已经存在,就将这些日志去除,避免重复上报,浪费流量。
压缩上报数据
一次性上传批量数据时,必然遇到数据量大,浪费流量,或者传输慢等情况,网络不好的状态下,可能导致上报失败。因此,在上报之前进行数据压缩也是一种方案。
对于合并上报这种情况,一次的数据量可能要十几k,对于日 pv 大的站点来说,产生的流量还是很可观的。所以有必要对数据进行压缩上报。lz-string是一个非常优秀的字符串压缩类库,兼容性好,代码量少,压缩比高,压缩时间短,压缩率达到惊人的60%。但它基于LZ78压缩,如果后端不支持解压,可选择gzip压缩,一般而言后端会默认预装gzip,因此,选择gzip压缩数据也可以,工具包pako中自带了gzip压缩,可以尝试使用。