基于rrweb框架,搭建前端技术运营监控体系的实践分享
本文作者:何家伟,碧桂园服务前端开发高级工程师,拥有10年开发经验。
1 背景
在工程化的前端项目中,通常使用webpack进行打包优化并上线。打包后的产物经过压缩和优化,对于一般开发者来说难以理解。当这样的产物交付到线上生产时,由于生产环境的状态是不可监控的,且代码已被压缩,导致如果发生前端js报错,报错信息无法准确地映射到源代码中的具体位置,从而给问题的定位带来了很大的挑战。因此,迫切需要一种前端监控手段来记录和收集这些报错,以便快速定位问题。
为了实现前端监控和快速定位问题,必须解决各种类型的数据记录、收集和监控,例如js报错、接口报错、文件加载报错、用户行为等。同时,如果要实现实时的视频回放能力,还需要解决视频文件过大不方便保存和上传的问题。本方案的目标就是解决这些问题,实现前端全方位的监控。
2 总体技术方案
总体的技术方案包括两部分:
第一部分是页面报错、静态资源加载、用户行为、接口报错、埋点等数据的收集记录,这部分已经有技术方案实现,后面工程实践会具体介绍原理和实现。
第二部分是用户视频回放能力,将以视频的方式无失真地呈现用户的操作路径。在本方案中会解决视频的上传体积、存储位置、上传时机等问题。
第二部分的数据会作为第一部分数据的一个类型嵌入,一起上传到服务端。这两部分数据的收集将会统一到集成监控工具下。
2.1 视频回放方案选择
本方案的核心是实现用户操作视频回放。下图是实现视频回放能力的业界方案对比:
从实验结果来看,可以得出一下结论:
• rrweb压缩后的文件大小跟其他方案文件大小差异比较大。
• 时间越长,rrweb压缩算法的优势越明显。
实际上,除了压缩方法,官网还提供了其他优化存储容量的方式:
• 通过屏蔽DOM元素,减少录制的内容。
• 通过sampling配置抽样策略,减少录制的数据。
• 通过去冗、压缩,减少数据存储体积。
2.1.2 结论
通过前面的方案对比可以看到,rrweb无论从体验(是否需要授权)、兼容性或者录制数据大小都有较为明显的优势。
2.2 数据收集和存储
集成的前端监控工具收集包括页面报错监控、静态资源加载监控、用户行为监控、接口报错监控等方面的数据,这些数据会存储在浏览器indexDb中。因为rrweb数据的量一般会达到几百kb,所以会先用fflate库对数据进行压缩处理,然后再存放在indexDb中等待上传。每个类型的监控都是独立的表结构存储,rrweb的具体数据是放到文件服务器中,以json文件格式存储。
2.3 数据上传
存在indexDb的数据会根据创建时间来进行筛选,每次产生新数据的时候都会判断第一个数据时间是否超过10分钟,这个时间是可以配置的。如果超过10分钟,就会把数据出栈,然后才会把新数据进栈,这样就能固定数据量大少,不至于过大。
整体的数据要经过fflate压缩,包括rrweb数据和其他类型的数据,然后用blob类型转换成文件的形式上传(后续要支持断点续传),后台express服务接收数据生成json文件形式存储在服务器文件系统中(后期可以考虑保存在阿里云)。
2.4 用户操作路径实现
(作者开发了后台监控中心专门用于监控数据的呈现,下文会有介绍。)
rrweb的数据呈现是先读取数据库中rrweb表的。每条数据有用户系统和页面url的维度,还有对应生成的json文件的url路径。播放rrweb数据的时候先通过axios库加载json文件,再通过rrwebPlayer播放json文件数据。下部的用户操作数据也在json文件中,只是存在不同字段下。rrwebPlayer有播放时间回调功能,在回调函数中调用相同时间段的用户操作数据就能实现rrweb和用户操作数据双屏播放。
2.5 总体技术架构图
3 前端监控工程实践
3.1 集成监控工具,嵌入rrweb功能
上屏是rrweb回放用户的真实的操作,下屏是记录用户的操作路径数据,例如点击是什么按钮,调用了什么接口,页面报了什么错误。
3.1.1 集成监控工具的原理
监控工具用vite框架库进行快速打包,与webpack相比能极速的服务启动。如果使用原生ESM文件,则无需打包。此外。轻量快速的热重载无论应用程序大小如何,都始终以极快的模块热重载,具有 “库” 模式的预配置Rollup构建,所以用来编写监控工具比较合适。
项目结构如下:
页面性能监控:
这个监控的主要目的是要页面从请求开始,到加载完成的各个环节耗时,一般来说会用到Performance API。Performance是前端性能监控的API,它可以检测页面中的性能,也可以检测到白屏时间、首屏时间、用户可操作的时间节点,页面总下载的时间、DNS查询的时间、TCP链接的时间等关键性能指标,页面性能监控会用到这个API的timing属性。
Performance.time属性如下图所示:
原理:监听window.addEventListener('load', ()=>{}); 时间,这个时候页面加载完成,在回调函数中读取。
window.performance.timing的各个属性值,用相关的属性相减得到网页各个阶段的用时。
页面报错监控:
经过了大量测试及联调的项目在有些时候还是会有十分隐蔽的Bug存在,这种复杂而又不可预见性的问题唯有通过完善的监控机制才能有效的减少其带来的损失。因此对于直面用户的前端而言,异常捕获与上报是至关重要。window.onerror提供了全局监听异常的能力,通常情况下监听其回调事件,即可获得有用的参数:
原理:通过监听全局错误对象window.onerror 在回调函数中,取得错误的信息,位置(列号,行好),错误文件名字进行保存。在进一步的实现中,最好的能把错误位置打源代码上下6行,记录下来。
Promise的全局错误信息用window.addEventListener(‘unhandledrejection’, function (event) {}) 进行捕获。
用户行为监控:
记录用户行为路径,对于产品优化具有重要意义,前端通过监控用户点击的元素,记录用户行为路径,用户点击按钮的位置可以用xpath来描述。
前端原理:点击每个元素记录其在页面的xpath。
接口监控:
前端对应接口的监控有非常重要的意义,现在行业流行的单页应用非常依赖接口的速度。
对接口的监控可以定位系统流程问题,定位到具体哪一个接口有问题。前端现在还没有对接口监控专门的API处理,需要重写送接口的API,在重写的API上加上发送的时间,在成功回调的时候减去发送的时间,得到整个接口的请求时间。
前端原理:通过装饰器模式,缓存原来的方法,记录开始发送时间,监听完成时刻,最终算出接口用时。
3.1.2 嵌入rrweb功能
点击查看代码
{
init() {
if (this.config.listenModules === "all") {
this.config.listenModules = this.moduleMap
}
let upModules = this.config.listenModules
if (upModules) {
Object.keys(upModules).forEach((key) => {
if (upModules[key]) {
this.moduleMap[key].init((data) => { //监听每个模块的回调事件
this.moduleEvent(key, data, this.config.callback)
})
}
})
}
if (this.config.rrweb) {
let vm = this;
//嵌入rrweb功能
this.stopFn = rrweb.record({
emit(event) {
// 保存获取到的 event 数据,event里面是序列号后的DOM和鼠标事件等
let inputEvent = Object.assign({},event)
inputEvent.createTime = new Date().getTime()
vm.rrwebEvent.system = vm.system
vm.rrwebEvent.userName = vm.userName
vm.rrwebEvent.pageUrl = window.location.pathname
vm.rrwebEvent.createTime = new Date().getTime()
vm.rrwebEvent.dataType="rrweb"
vm.arrPush(vm.rrwebEvent.rrwebData,inputEvent)//收集rrweb数据
}
})
}
}
}
3.2 监控数据和rrweb的数据上传的服务器
当监控工具在项目中运行时,发生页面报错或者接口报错时,监控数据上传到我们专门写的后台服务中,代码如下:
点击查看代码
export const uploadData = async (req: Request | any, res: Response | any, next: NextFunction) => {
//monitorData是监控数据,包括行为监控和接口监控,错误监控等等,通过dataType字段区分
//rrwebData是rrweb监控数据
let { monitorData = [], rrwebData = [] } = req.body;
//rrweb和监控数据保存在file文件中,方便上传以及存储
let fileUrl = `${req.protocol}://${req.headers.host}/uploads/${req?.file?.filename}`;
const fileJSON = require(path.join(__dirname, '../public', 'uploads')+`/${req?.file?.filename}`)
const atouData= JSON.parse(atou(fileJSON.utoaData))
if(monitorData.length==0&&rrwebData.length==0){
monitorData = atouData.monitorData
delete atouData.rrwebData.rrwebData
rrwebData = atouData.rrwebData
}
let moduleMap = {
behavior: Behavior, errorCatch: ErrorCatch, pref: Pref, resources: Resources, xhrHook: XhrHook
}
let mapArr = {
behavior: [],
errorCatch: [],
pref: [],
resources: [],
xhrHook: []
}
monitorData.forEach(element => {//保存各个监控模块的数据
if (element.dataType) {
mapArr[element.dataType].push(element)
}
});
Object.keys(mapArr).forEach(key => {
moduleMap[key].create(mapArr[key])
});
rrwebData.fileUrl = fileUrl
Rrweb.create(rrwebData) ///保存rrweb数据
try {
res.json({
fileUrl,
success: true,
code: 200
});
} catch (error) {
next(error);
};
}
在后台监控管理中心中播放,代码如下:
点击查看代码
methods: {
userPlay() {
let inputItem = this.monitorData[this.userPlayIndex]
if (!inputItem) return
if(this.userPlayIndex==0){
this.userDataPlayArr.push(inputItem)
}
let fCreateTime = inputItem.createTime;
let allLen = this.monitorData.length
this.userTimer = setInterval(() => {
let next = this.monitorData[this.userPlayIndex + 1]
if (next) {
if (fCreateTime + 100 >= next.createTime) {
this.userDataPlayArr.push(next)
this.userPlayIndex++
if (this.userDataPlayArr.length > 10) {
this.userDataPlayArr.shift()
}
}
fCreateTime+=100
}
if (this.userPlayIndex >= allLen||!next) {
if (this.userTimer) {
clearInterval(this.userTimer)
}
}
}, 100);
}
},
后台监控中心与最终的播放效果,如下图所示:
后台监控中心
最终播放效果
3.3 系统接入
其他业务系统要接入,参考下面的引入和使用方法,同时关注对应的API。
3.3.1 自研监控工具+rrweb的引入
支持npm和cdn的方式直接引入。cdn方式引入注意是放到index.html的body结束标签前面。
点击查看代码
<link
rel="stylesheet"
href="https://unpkg.com/monitor-bgy/dist/style.css"
/>
//这个标签放到body结束标签前面
<script src="https://unpkg.com/monitor-bgy/dist/monitor-bgy.js"></script>
//也可以使用npm 引入
npm install --save monitor-bgy
3.3.2 使用方法
点击查看代码
//1.cdn直接引用方式
let monitorObj = new window.MonitorBgy({
userName: "hejiawei",//登录用户名称
uploadData(data) {//rrweb已经其他监控数据上传回调
},
})
monitorObj.init()
//2.另外也可以使用npm包的形式使用
import MonitorBgy from 'monitor-bgy'
let monitorObj = new MonitorBgy({
userName: "hejiawei",//登录用户名称
uploadData(data) {//rrweb已经其他监控数据上传回调
},
})
monitorObj.init()
3.3. 主要API
点击查看代码
let config ={
saveDataTime:1000 * 60 * 10, //当报错的时候,上传用户前n分钟操作 默认10分钟
rrweb:true, //监控是否加上rrweb功能
system:window.location.hostname,//系统名称,默认系统域名
userName:"XXX",//用户名,当前的登录用户,或者用户唯一标识
uploadUrl:"/rrweb/uploadData",//传出数据url,默认/rrweb/uploadData
listenModules:"all",//配置监控的类目,对象类型,可取的值如下,默认all是全部类型监控
// {
// pref, //页面性能监控
// resources,//静态资源监控
// errorCatch,//js错误监控
// xhrHook, //接口请求监控
// behavior //用户行为监控
// }
uploadmMonitorFile:()=>{ //监控数据自动上传函数,可以自定义
}
}
let monitorObj = new MonitorBgy(config)
monitorObj.init()
4 总结
以下是传媒系统接入监控工具(初步实现)后的实际监控成果展示:
页面报错监控
系统发生js报错的时候,就会上传对应的报错信息,通过系统、页面、用户能具体定位报错信息。
用户行为监控
用户点击了按钮、进入了页面、进行的操作,都会被记录,通过系统、页面、用户、时间等能查询出用户一段时间的操作
接口监控
当接口发生500错误的时候,会产生一条对应的接口报错信息,包括了url地址和参数信息。
rrweb监控
系统在登录的时候,记录了系统、用户和页面等维度信息,当用户操作过程中产生产生报错信息的时候,例如页面报错、接口报错等,会触发监控工具上报数据,数据上报完成后,就会生成一条rreweb数据,通过查询系统、用户、页面和发生时间等条件就能查询具体用户数据。
rrweb双屏监控
在rrweb监控页面定位用户数据后,点击查看按钮,就能看到双屏监控功能,上屏是用户操作视频,下屏是用户操作数据,因为视频文件存储量较大,后台express会有一个定时服务,删除创建日期超过1个月的数据,包括视频json文件。