前端监控方案的基本实现原理,同时简单调研一下市面常用方案。
为什么
为什么需要前端监控呢?
- 测试再充分,也一定会有问题
- 产品的特殊性,不允许出现问题!
错误监控处理
- 当报错时提供一个友好的信息给用户
- 为开发者收集重要的数据,提前发现错误
监控组成
一般而言,监控系统分为如下几个部分
- 日志采集
- 日志存储
- 统计分析
- 监控告警服务
日志采集
对日志采集的分类
- 错误日志
- 性能日志
- 产品指标日志
- 用户自定义打点日志
日志分类之后,还需要对不同类型的日志,做公共字段提取
- 提升数据的抽象程度,便于以后的检索、聚合、分析等操作
- 节省存储空间
重视对相关用户、设备环境等信息的采集
- who:哪些用户
- when:什么时间点
- where:什么页面或环境
- what:发生什么
提供给用户干预处理流程的 hook 能力
- 数据过滤处理
- 额外信息补足
注意用户敏感数据的采集
- 隐私数据加密防护
- 独立部署,不和其他应用共享监控系统
- 不采集具体敏感数据,只采集用户操作数据
错误日志
采集的方式包括监听或劫持原始方法,获取需要上报的数据。
错误日志分类
- JavaScript 运行错误(Runtime Error):通过 error 监听器捕获
- 资源加载错误(Load Error):通过 error 监听器捕获(尤其是样式文件和代码文件)
- Console Error:console.error 抛出的错误,可通过 patch 方法捕获
- HTTP 请求错误(API Error):网络请求错误,可通过 Patch fetch/XMLHttpRequest 方法捕获
- Promise Error:promise reject 错误,可通过 unhandledRejection 监听器捕获
- 异步未捕获错误:重写 setTimeout、setInterval 等异步方法,用同步的写法包裹 try 来捕获异步函数中发生的错误
- 框架内部错误:通过框架提供的外部接口进行捕获
页面加载失败:页面整体挂掉,页面主要内容加载失败情况的监控:可以通过 Mutation Observer 这个 API 来进行,比如通过获取页面 dom 结构的变化,动态计算页面已加载的 dom 元素占可视化窗口的占比,如果没有达到理想状态下的占比,就可以看成当前页面加载失败的情况
行为收集:分为用户行为、浏览器行为、控制台打印行为。监控这些主要是为了在排查错误时,能还原用户当时的各个动作,从而能更好的找出问题出错的原因。
- 用户行为:例如 window 监听 click 事件
- 浏览器行为:例如重写 XMLHttpRequest 对象
- 控制台打印行为:例如重写 console
通常前端运行的代码都是经过压缩的,错误收集上来后,需要通过对应的 sourcemap 解析才能理解
性能日志
根据 w3c 中 Timing Performance API 定义的规范来进行统计和计算
- DNS 查询耗时:domainLookupEnd - domainLookupStart
- TCP 链接耗时:connectEnd - connectStart
- 请求响应耗时:responseStart - requestStart
- 内容传输耗时:responseEnd - responseStart
- DOM 解析耗时:domInteractive - responseEnd
- 资源加载耗时:loadEventStart - domContentLoadedEventEnd
- 首包时间耗时:responseStart - domLookupStart
- 首次渲染耗时:responseEnd - fetchStart
- 首次可交互耗时:domInteractive - fetchStart
- DOM_READY 耗时:domContentLoadedEventEnd - fetchStart
- 页面完全加载耗时:loadEventStart - fetchStart
- 页面首屏加载耗时:firstScreenLoadingTime - fetchTime
- SSL 连接耗时:connectEnd - sourceConnectionStart
产品日志
产品日志大致如下几种
- UV 数据
- DISTINCT(ucid)
- DISTINCT(cookie) 有误差
- DISTINCT(ip) 有误差
- PV 数据
- SPA:hash 路由监听 hashChange 事件,history 路由监听 popState 事件,框架提供的路由钩子也可以
- MPA:执行即可
- Online Time:click
埋点行为
常见埋点行为
- 点击触发埋点
- 页面停留时间上报埋点
- 错误监听埋点
- 内容可见埋点
数据上报
目前常用的上报方式
- 通过 xhr 上报
- 通过 img 上报
- 通过 sendBeacon 上报
通过 xhr 上报:一般而言,埋点域名并不是当前域名,因此请求会存在跨域风险,且如果 ajax 配置不正确可能会浏览器拦截,因此使用 ajax 这类请求并不是万全之策。
通过 img 上报:即把参数拼接到一张 img 地址后,传送到后台。
- 兼容性高,所有的浏览器都支持。
- 无需挂载在页面上。
- 不存在跨域问题。
- 不会携带当前域名中的 cookie。
- 不会阻塞页面加载。
- 通常埋点上报会使用 GIF 图,合法的 GIF 只需要 43 个字节:相比于其他类型的图片格式(BMP、PNG等),能节约更多的网络资源。
Navigator.sendBeacon 是目前通用的埋点上报方案,Navigator.sendBeacon 方法接受两个参数,第一个参数是目标服务器的 URL,第二个参数是所要发送的数据(可选),可以是任意类型(字符串、表单对象、二进制对象等等)。
- 使用 navigator.sendBeacon 会更规范,数据传输上可传输资源类型会更多。
- 对于 ajax 在页面卸载时上报,ajax 有可能没上报完,页面就卸载了导致请求中断,因此 ajax 处理这种情况时必须作为同步操作。
- sendBeacon 是异步的,不会影响当前页到下一个页面的跳转速度,且不受同域限制。这个方法还是异步发出请求,但是请求与当前页面脱离关联,作为浏览器的任务,因此可以保证会把数据发出去,不拖延卸载流程。
日志存储
日志落地方案选择
- 即时上传
- 持久化在本地,特定频率上传
接收与存储
- 对客户端的日志内容进行合法性和安全性校验,防止系统被恶意攻击
- 消息队列的方式,比如 kafka
- 存储:单库单表 -> 分库分表 -> 数据库集群(读写分离)
- 关系型数据库:管理方便、易于实现复杂查询、存储容量相对较少
- 非关系性数据库:性能较高、易于水平扩展、存储容量相对较大
对于海量数据而言,关系型数据库不太能够满足对应的存储以及检索需求了。业界成熟方案有 Hbase 系,Lucene 系列等。针对日志系统,建议优先选择非关系性数据库进行处理。或者两者结合使用,比如将原始日志数据暂存到 Hbase 中,将清洗后的格式化数据持久化到 MySQL 中去
统计分析
elastic search:基于 Apache Lucene 的全文检索引擎,可以完成对海量日志的检索分析与存储工作
用户维度:每一个用户的所有操作和请求,都会行成一条条不同的故事线。收集用户使用过程中的某些重点操作,在后续用户碰到问题时,可以通过聚合该用户前后时间范围内的数据,很快分析出问题出现的原因
时间维度:用户在什么时间,做了什么事情,导致了什么结果
运行环境维度:应用及服务所运行的环境情况,例如网络环境、操作系统、设备信息、地理位置等
监控告警
告警服务让我们尽早发现问题
- 默认触发条件
- 自定义触发条件
- 日志中包含指定内容
- 日志达到一定量级(数量达到阀值、持续时间达到阀值)
- 向哪些用户告警(业务负责人、告警订阅人)
告警推送方式
- 邮件
- 短信
- 企业微信
- 钉钉
- 电话
告警消息推送频率
- 不同级别对应不同的频率
- 沉默时间设置:在沉默时间内不重复报警
方案研究
有了以上的知识储备后,总结了一个合格的前端监控系统至少应该具备哪些能力呢
- 日志采集的完整性,不仅仅指错误日志,还应该包括性能日志,产品日志等,同时敏感信息需要被过滤
- 支持 source map 解析
- 报警方式与规则:比如异常等级划分,对于严重的问题,可以直接发送邮件和短信给相关人员
- 出错场景链路跟踪,用于复现 BUG
- 接入简单
市面上提供的错误监控的方案有很多,了解了如下几种方案
- ARMS
- Fundebug
- Rollbar
- Sentry
错误监控已经是很成熟的市场的,收集的信息都十分完整,比如如下信息
- 错误信息堆栈
- 链路跟踪信息:页面上发生的各个事件节点定义为用户行为,包括页面加载、路由跳转、页面单击、API 请求、控制台输出等信息,按照时间顺序将用户行为串联起来就是用户的行为链路。
- 终端信息:浏览器类型、是否开启 cookie、所用语言、screen 分辨率、浏览器开启插件
- 性能指标
- 请求信息
- ……
ARMS 大致有如下几种类别
- api:接口请求信息
- behavior:分辨率
- error:错误信息
- perf:性能指标
- pv:pv 统计
ARMS
ARMS 的接入并没有具体说明不同框架之间是否会所不同,采取的方式都是一样的,直接安装对应的 node_module,然后初始化一下即可。
但亲测还是有一些平台相关性的,比如 Angular 中,还是需要声明 ErrorHandler,然后手动上报,否则错误不会被收集
框架集成
针对 React、Vue 和 Angular,不同的监控方案可能会提供一些针对特定框架的支持。大致表现为如下几种
- React:使用 16 版本后提供的 error boundary
- static getDerivedStateFromError:当错误产生后渲染一个 fallback UI
- componentDidCatch:记录错误信息
- Vue.config.errorHandler
- Angular:ErrorHandler 和 HttpInterceptor
小小研究
通过简单浏览下 Roolbar 的源码,了解到一些实用工具
- stacktrace-js:生成、解析、增强 JavaScript 错误堆栈,支持所有浏览器
- source-map:将压缩后文件的错误信息翻译对应源文件中的位置
- error-stack-parser:解析错误堆栈,得到更多有意义的信息
一开始本计划,不使用市面服务,而是自己来实现,source-map 的解析工作中需要行号和列号,我们可以通过运用正则从 stack 中获取,简单代码如下
const reg = /(http:\/\/.*\.js\??\d*):(\d+):(\d+)/;
const errorInfo = stack.match(reg)
const errorData = {
errorObj: stack,
scriptURI: errorInfo[1] || 'unknown script url',
lineNumber: errorInfo[2] || -1,
columnNumber: errorInfo[3] || -1
}
// report errorData