Better

Ethan的博客,欢迎访问交流

前端监控与方案方案研究

前端监控方案的基本实现原理,同时简单调研一下市面常用方案。

监控目的

错误监控处理

  • 当报错时提供一个友好的信息给用户
  • 为开发者收集重要的数据,提前发现错误

监控组成

一般而言,监控系统分为如下几个部分

  • 日志采集
  • 日志存储
  • 统计分析
  • 监控告警服务

下面看看每个部分具体的细节

日志采集

对日志采集的分类

  • 错误日志
  • 性能日志
  • 产品指标日志
  • 用户自定义打点日志

日志分类之后,还需要对不同类型的日志,做公共字段提取

  • 提升数据的抽象程度,便于以后的检索、聚合、分析等操作
  • 节省存储空间

重视对相关用户、设备环境等信息的采集

  • 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 监听器捕获
  • 框架内部错误:通过框架提供的外部接口进行捕获

页面加载失败:页面整体挂掉,页面主要内容加载失败情况的监控:可以通过 Mutation Observer 这个 API 来进行,比如通过获取页面 dom 结构的变化,动态计算页面已加载的 dom 元素占可视化窗口的占比,如果没有达到理想状态下的占比,就可以看成当前页面加载失败的情况

性能日志

根据 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

日志存储

日志落地方案选择

  • 即时上传
  • 持久化在本地,特定频率上传

接收与存储

  • 对客户端的日志内容进行合法性和安全性校验,防止系统被恶意攻击
  • 消息队列的方式,比如 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

资料



留言