前端监控平台搭建方案研究

WGenji Lv4

概述

应用上线后,我们常常需要通过了解应用的运行状况、用户访问情况等信息来修正应用异常、优化应用体验、针对用户使用情况进行功能完素和作为需求决策依据等。 所以建立一个能获取和分析这些数据的监控平台对企业的发展与决策尤为重要。
对于前端而言,前端监控包括:行为监控、异常监控、性能监控等。
不同的监控对应的重点不同:
行为监控主要用于获取用户行为以及跟踪产品在用户端的使用情况,并以监控数据为基础,指明产品优化的方向。
异常监控则是用于监听和发现产品存在的隐藏缺陷,以便提前消除BUG隐患、提高故障处理能力和保障服务质量。
性能监控则是用于收集和了解产品在客户端下的运行情况和性能表现,及早发现性能瓶颈,指明代码逻辑的优化的重点与方向

第三方监控平台

目前市面上已有大量成熟、免费、且应用广泛的第三方监控平台,其中包括:

这些平台都非常专业,而且很多都可以对多端、多环境进行全方位的监控,对于开发时间紧迫、开发人手不足又或缺乏相关开发经验团队来说,在无特别需求的情况下,直接使用第三方监控平台是最快的监控平台搭建方案,只需按第三方平台的使用说明接入一个脚本即可实现相应的监控需求。

然而,不少企业还是会选择自建监控平台,这是为什么呢?

下面通过一个表格来列举自建监控平台和第三方平台的优劣:

序号 对比项 第三方监控平台 自建监控平台
1 开发周期 视具体需求而定,总体来说非常长
2 人员投入 基本为零 视具体需求而定,需求越复杂投入的开发人员越多
3 数据可控性 完全可控
4 数据安全性 应用数据须暴露给第三方平台,安全性待评估 完全由平台的开发和掌控能力决定,无需对外暴露
5 特殊数据采集分析 相对较弱 完全可根据需求针对采集与分析
6 收益情况 短期内收益高,且相对固定 短期内投入大,收益低,但长远来看收益高

上面列举了部分对比项,通过对比分析可得:对于注重数据安全、希望自主掌握平台数据、对平台所产生的数据深入挖掘和分析的企业来说都是通过自建监控平台来实现企业的监控需求的。

那么前端要自建监控平台,需要怎么规划才能做好行为、异常、性能等数据的采集和分析的基础工作呢?

基本架构分析

一般来说,一个监控系统,大致可以分为四个阶段:数据采集、数据存储、统计与分析、报告/警告。

  • 采集阶段:采集监控数据,先在数据采集端做一定的处理,采取一定的方案上报到服务器。
  • 存储阶段:后端接收前端上报的监控数据,经过一定处理,按照一定的存储方案进行存储。
  • 分析阶段:分为机器自动分析和人工分析。机器自动分析,通过预设的条件和算法,对存储的监控信息进行统计和筛选,发现问题,触发报警或根据分析结果生成报表进行报告输出。人工分析,通过提供一个可视化的数据面板,让系统用户可以看到具体的日志数据,根据数据,梳理出有用的信息或发现异常问题根源等。
  • 报警阶段:报警指的是针对异常或性能数据进行的报警,分为告警和预警。告警按照一定的级别自动报警,通过设定的渠道,按照一定的触发规则进行。预警则在异常发生前,提前预判,给出警告。

而前端要做的主要是第一阶段:采集阶段,这是整个监控平台的起点,尤为重要。

数据采集

监控主要有:行为、异常、性能这三大类别,所以对应的数据采集类型类型也有三大类型。

行为数据采集

要采集用户的行为数据,常见的监控项有:

  • 用户属性类相关数据,包括:
  • 设备/机型信息
  • 操作系统
  • 浏览器信息
  • 屏幕分辨率信息
  • 语言信息
  • 地理位置
  • 使用的ip/网络服务商
  • 用户事件类相关数据,包括:
  • 访问的页面/路由
  • 访问的页面/路由的来源
  • 在页面/路由的停留时间
  • 在页面/路由下的TAB切换时间
  • 某个按钮/链接/区域的点击次数
  • 鼠标活动轨迹
  • 内容输入过程

统计这些数据可以让我们知道用户来源的渠道,可以促进产品的推广,知道用户在每一个页面停留的时间,可以针对停留较长的页面,增加广告推送、调整内容展示顺序,知道某个功能是用户频繁使用,哪个功能少人使用等。
这些数据对于了解用户交互过程,还原异常产生原因、分析性能表现等也有十分重要的用途。

异常监控

异常监控主要监听和发现产品存在的隐藏缺陷,常见的需要监控的异常包括:

  • Javascript的异常监控
  • js运行时错误(windown.onerror)
  • trycatch错误
  • Promise错误
  • 框架内部报错(vue、react)
  • js错误消息(console.error)
  • 死循环错误
  • 接口异常监控
  • 接口HTTP状态异常(400/404/500/502/503/504等)
  • 接口返回数据异常(返回数据结构异常等)
  • 接口请求并发异常
  • 资源加载异常监控
  • js加载出错
  • css加载出错
  • 图片资源加载出错
  • 音视频资源加载出错
  • iframe加载出错
  • 字体文件加载出错

统计这些异常信息可以让产品开发和维护人员快速了解产品当前存在的BUG,提前或第一时间解决产品故障,为客户提供高质量的产品应用。
结合用户行为数据还可以准确地评估异常的影响范围,可以根据影响的严重程度,启动相应级别的故障处理预案。

性能监控

性能监控主要包括监听网页或者产品在用户端的体验。常见的性能监控项包括:

  • 页面加载时间
  • js,css,img等资源的加载时间
  • 白屏时间
  • 接口请求的响应时间
  • DNS查询时间
  • 页面渲染时间
  • 页面交互动画完成时间
  • 应用的内存占用情况
  • 应用的cpu空闲时长

这些性能监控的结果,可以展示前端性能的好坏,根据性能监测的结果可以进一步的去优化前端性能,比如兼容低版本浏览器的动画效果,加快首屏加载等等。
结合行为监控采集到的数据还可以分析出应用在不同用户、不同机型、不同系统、不同地区、不同时间下的性能表现情况,进而针对性地优化产品性能,更好地做到渐进增强、优雅降级

数据临时存储

由于我们要采集的数据内容较多,所以我们不能每采集到一条数据,就立即将该数据传到服务器,对于具有大量用户同时在线的应用,如果采集到数据就立即上传,无异于是对后台服务器进行DDOS攻击。这“自制的DDOS攻击工具”随时可将后台服务器D爆! 因此,我们须先将这些采集到的数据临时存储在用户客户端上,达到一定条件之后,再一次性上传这些收集好的数据。
那么,如何进行前端数据存储呢?使用变量临时存储起来是最简单便捷的方案,但是如果存储的数据量太过庞大,这样操作会很容易吧内存挤爆,而且一旦用户进行刷新操作,这些日志就会丢失,因此,我们需要使用前端数据持久化方案。
目前,可用的持久化方案可选项也比较多了,主要有:Cookie、localStorage、sessionStorage、IndexedDB、webSQL 、FileSystem 等等。那么该如何选择呢?我们通过一个表来进行对比:

存储方式 cookie sessionStorage localStorage IndexedDB webSQL FileSystem
存储类型 string key-value key-value NoSQL SQL -
数据格式 string string string Object - -
存储容量 4KB 5MB 5MB 500MB 60MB -
操作进程 同步 同步 同步 异步 异步 -
检索方式 key key key,index field -
读写性能 极快 读快写慢 读快写慢 读慢写快 - -

虽然都可以进行数据存储,但是cookie和sessionStorage的应用场景决定了他们不适合用来做数据持久化存储。综合来看,IndexedDB无疑是最好的选择,它具有容量大、异步的优势,异步的特性保证它不会对界面的渲染产生阻塞。而且IndexedDB是分库的,每个库又分store,还能按照索引进行查询,具有完整的数据库管理思维,比localStorage更适合做结构化数据管理。另外,IndexedDB是被广泛支持的HTML5标准,兼容大部分浏览器,因此不用担心它的发展前景。但是它也有自身的缺点,就是api非常复杂,不像localStorage那么简单直接,兼容性来说也没localStorage好,所以如果同时兼容两种存储无疑是最好的解决方案。
如果你的应用不需要考虑兼任IE8、9、10这些浏览器,那么直接使用Dexie.js (opens new window) ,它可以简化IndexedDB的操作。但如果你需要同时兼容IndexedDB和localStorage,那么使用localForage (opens new window) 将会是你的最佳选择。
但我们根本不需要搞这么多要蛾子,很多时候我们直接使用localStorage就完全足够了,我们不会在用户的电脑上存储大量的离线数据。一方面是存储离线数据太多,容易暴露我们收集的数据内容,另一方面是占据太大用户空间,一旦用户发现容易让用户抓狂,对应用产生不信任心态,最后技术上来说,如果产生了那么多数据,数据回传、宽带成本、后台对数据的存储、处理等问题都会被无限放大,最终得不偿失,所以,我们直接采用localStorage存储即可。
不管采用IndexedDB还是localStorage,还是两者并用,我们都需要对存储的数据进行预处理,即建立一套:数据存储、数据查询、数据过期、大小控制 等一系列的机制,大致可以通过下面图示来理解:

数据存储机制

上图展示了前端存储日志的流程和数据库布局。当一个条数据产生后,形成一条初始数据,被立即放入暂存区里面(可以是临时变量),一个循环任务不断从暂存区中取出数据,对数据进行加工处理(归类、大小控制等),将处理结果存储到索引区中(可以是另外一个数据对象),最后通过数据格式转换存储离线存储空间里(可以是IndexedDB或localStorage),最终,等到时机成熟将会上报到服务端将数据数据存储起来,这时候数据已被真正归档,数据将被转移到回收区,数据在回收区超过一定时间后,它就已经没有存在的价值了,就会被从回收区中清除,数据管理机制就是如此循环往复地工作。当然数据上报后你就可以直接删除这些已上报的数据了,毕竟如果是使用localStorage的话,储存空间有限,而且我们也可以通过后台随时调出对应的数据。

数据上报

因为我们已采用了数据持久方案对数据进行离线存储管理,所以并不会实时上报收集到的数据,而是通过一定的条件进行触发上报,这些条件包括:

  • 数据收集达到了一定的条数上限
  • 收集的数据超过了一定的占用空间,急需上报然后进行存储空间回收
  • 当前数据存在某些需要优先上报的日志类型

为了不影响页面性能,用户进入应用一段时间后才会去持久化存储库里提取出当前需要上报的数据内容,整个上报流程大致如下:

数据上报流程

数据采集的技术实现

上面只是分析和罗列了要采集的数据内容,至于技术上如何实现还需要进一步的深入研究与了解。

行为数据采集的技术实现

用户属性类的数据采集

用户属性类相关数据的采集一般使用navigatorscreen等api,即可采集到一系列的用户属性信息

1
2
3
4
5
6
7
8
// 设备信息/操作系统信息/浏览器信息
window.navigator.userAgent

// 当前使用的语言信息
window.navigator.languages[0] || window.navigator.language

// 屏幕分辨率信息
window.screen

对于要采集用户所在的地理位置、ip、网络服务商则需要后台的支持,一般来说后台通过拦截数据上报接口,再进行相应的逻辑处理,即可提取出用户的地理位置、ip、网络服务商等信息。

用户事件类的数据采集

要采集用户事件类相关数据则可利用下面的相关事件或api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 访问的页面/路由
window.location.href

// 访问的页面/路由的来源
document.referrer

// 在页面/路由的停留时间,TAB切换时间
document.addEventListener('readystatechange', (event) => {}, true);
document.addEventListener('DOMContentLoaded', (event) => {}, true)
window.addEventListener('load', (event) => {}, true)
window.addEventListener('pushstate', (event) => {}, true)
window.addEventListener('replacestate', (event) => {}, true)
document.addEventListener('visibilitychange', (event) => {}, true)
window.addEventListener('beforeunload', (event) => {}, true)

// 某个按钮/链接/区域的点击次数
document.addEventListener('touchstart', (event) => {}, true)
document.addEventListener('click', (event) => {}, true)

// 鼠标活动轨迹
window.addEventListener('mousemove', (event) => {}, true)

// 内容输入过程
window.addEventListener('keyup', (event) => {}, true)

页面停留时间的具体实现可参考:https://cloud.tencent.com/developer/article/1408482

异常数据采集的技术实现

Javascript异常数据采集

Javascript的异常数据采集可利用下面的相关事件或api

1
2
3
4
5
6
7
8
9
10
11
// js运行时错误
window.addEventListener('error', (event) => {}, true)

// Promise错误
window.addEventListener('unhandledrejection', (event) => {}, true)

// 框架内部报错(vue、react)
// vue
Vue.config.errorHandler = (err, vm, info) => {}
// react
componentDIdCatch(err, info)

要捕获try catch里面的错误,则必须通过提供try catch包裹器,让用户自行调用该包裹器对代码进行包裹才能实现,且必须注意:try catch只能捕获同步代码发生的错误,异步无效,下面提供try catch包裹器的示例代码,以供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 将函数使用 try..catch 进行包裹
* 包装后的函数如果出错会进行错误收集起来并上报
* @param {Function} fn 需要进行包装的函数
* @return {Function} 包装后的函数
*/
function tryCatchWraper (fn) {
if (fn instanceof Function && !fn._wrapped_ && !fn._isWraperFunc_) {
const wrapped = function () {
try {
return fn.apply(this, arguments)
} catch (error) {
// 此处编写成你要对catch信息进行收集的代码
}
}

wrapped._isWraperFunc_ = true
fn._wrapped_ = wrapped

return wrapped
}

return fn._wrapped_ || fn
}

对于要捕获js里的console消息,需要通过hook console 方法来实现,下面提供一段hook代码,以供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function consoleHook (cb) {
const log = console.log
cb = cb || function () {
log.call(this, 'consoleHook message:', arguments)
}

const methodList = ['log', 'info', 'warn', 'debug', 'error']
methodList.forEach(function (item) {
var method = console[item]
console[item] = function () {
cb.apply(null, arguments)
method.apply(console, arguments)
}
})
}

注意,一般使用了webpack进行打包的工程,进行构建的时候都会移除掉console相关的方法,所以如果你在开发过程中可以采集到console消息的数据,而打包发布后无法采集,请检查工程是否开启了 drop_console: true 这样的配置项 drop_console 配置说明参考:https://github.com/mishoo/UglifyJS2

如果你不想修改默认配置,只想要收集某处特定的console输出,则可以通过:

1
2
// window.console 不会被 drop_console 配置干掉
window.console.error('msg')

对于死循环错误一般不能直接收集,只能通过收集到的日志数据进行相关性判断。

跨域脚本异常

由于浏览器安全策略限制,跨域脚本报错时,无法直接获取错误的详细信息,只能得到一个Script Error。 例如,我们会引入第三方依赖,或者将自己的脚本放在CDN时。

解决Script Error的方法:
方案一:

  • 将js内联到HTML中
  • 将js文件与HTML放在同域下

方案二:
1、为页面上script标签添加crossorigin属性
2、被引入脚本所在服务端响应头中,增加 Access-Control-Allow-Origin 来支持跨域资源共享

各种javascript异常的处理可参考:
https://blog.fundebug.com/2018/12/07/how-to-handle-frontend-error/

#### 接口异常数据的采集 原理上,我们可以通过hook掉XMLHttpRequest来实现接口请求的全局监听,但是这样做一旦发生了意外就会造成整个应用的接口请求都不可用,副作用太大,所以不建议采用此法。
一般来说通过对接口请求的方法进行统一封装,然后在封装函数上进行异常数据采集。 如果使用的是axios库,则直接通过request和response拦截器进行统一的拦截处理即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
axios.interceptors.request.use(function (config) {
// 这里可编写并发统计的相关逻辑
return Promise.resolve(config)
}, function (error) {
return Promise.reject(error)
})

axios.interceptors.response.use(function (response) {
// 这里可增加接口返回数据校验的逻辑
// 校验不通过则可进行异常数据采集
return Promise.resolve(response)
}, function (error) {
// 这里编写接口异常数据采集逻辑
return Promise.reject(error)
})

资源加载异常数据采集

通过 window.addEventListener(‘error’, (event) => {}, true) 可以监控到大部分资源的加载异常情况。
js、css、img、audio、video、iframe等实体标签的资源加载出错都会被该事件监控到。基础的采集代码示例如下:

1
2
3
4
5
6
window.addEventListener('error', (event) => {
const target = event.target
if(target && target.nodeName){
// 此处编写资源加载异常的数据采集逻辑
}
}, true)

性能数据采集的技术实现

现代浏览器提供了performance api,可以提供全方位的性能数据,非常强大。
下面通过一个示例来展示使用performance获取到的相关性能数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const result = []
performance.getEntries().forEach(function (item) {
result.push({
// 资源加载耗时
duration: item.duration,
// DNS查询用时
domainLookup: item.domainLookupStart - item.domainLookupEnd,
// 解码后资源内容大小
decodedBodySize: item.decodedBodySize / 1024,
// 编码时资源内容大小
encodedBodySize: item.encodedBodySize / 1024,
// 资源名词
name: item.name,
// 资源类型
initiatorType: item.initiatorType
})
})
console.table(result)

更多性能数据的采集需要建立在全面了解performance的基础之上
下面提供performance api的说明以备开发参考: https://juejin.im/post/5c8fa71d5188252d785f0ea3

事件处理的注意要点

进行数据采集的时候会利用到各个事件,因此需要深入了解各个事件的特性才能做到准确无误地采集相关数据。下面提供一些注意要点的关键线索,以供开发参考:
1、注意事件触发顺序:

readystatechange -> DOMContentLoaded -> load -> beforeunload
touchstart -> touchend -> click -> dblclick
另外,通过设置在事件捕获阶段触发事件,这样能第一时间处理相关事件,防止被禁止冒泡的事件“占了先机”,阻止了事件的触发。

1
2
// 第三个参数为true则表示在捕获阶段触发事件
window.addEventListener('error', (event) => {}, true)

2、关于beforeunload beforeunload 必须执行同步操作,异步操作不能确保100%成功
3、注意各个事件的兼容处理
4、注意阻止事件冒泡(stopPropagation)与阻止派发默认事件(preventDefault)对全局事件监听的影响
5、注意节流与防抖

事件处理的相关参考资料:

  • Title: 前端监控平台搭建方案研究
  • Author: WGenji
  • Created at : 2024-08-26 13:51:51
  • Updated at : 2024-08-26 13:51:51
  • Link: https://redefine.ohevan.com/2024/08/26/前端监控/前端监控平台搭建方案研究/
  • License: This work is licensed under CC BY-NC-SA 4.0.