基于rrweb录屏与重放页面

4,039 阅读4分钟

项目背景

在使用ant design文档的过程中发现,antd使用了一个叫做logRocket的录屏框架,于是立马将logRocket用在自己的项目当中,测试它的功能。

logRocket网站将采集到的数据,按照人员和session进行分类,观看各人员的操作回放,可以发现系统中某些操作的不便之处,并且可以发现哪些人员是你的重度用户。

但是logRocket的数据存储在他们的服务器,并且从logRocket回放里,能看到系统中的各种重要数据。如果数据被别有用心之人获取,后果将很严重。

rrweb

如果我们需要基于一个开源框架,并将数据存在自己的服务器中,限制人员查看的权限,这样就尅消除之前的隐患。

下面我要介绍的就是今天的主角rrweb框架,全称record and replay the web。它由三个库组成:

rrweb-snapshot,将页面中的dom转化为可序列化的数据结构 rrweb,提供录屏和重放的api rrweb-player,提供播放的ui页面,支持快进、全屏、拖拽等操作 每次刷新页面时,rrweb会将页面中的dom元素全部转换成文档数据,并给每个dom元素分配一个唯一id。后面当页面发生变化时,只对变化的dom元素进行序列化。当重放页面时,会将数据反序列化并插入到页面中,而原先增量的dom变化,如属性或者文本变化,则根据id找到对应dom元素修改;而子节点的增加或减少,根据父元素id进行dom变更。

开发历程

1.直接使用rrweb记录每次的序列化录屏数据,首先保存到localStorage中,当数据量超过阈值或者超过时间限制,再由sendbeacon发送数据到node,并保存到mongo中。

2.首先遇到的问题是sendbeacon发送数据居然出现了丢失,原因是数据超过65536时,将会发送失败,由于sendbeacon是由后台进程单独发送,无法获取失败状态,所以要进行降级处理,当数据过大时,使用fetch请求发送。

3.由于公司中后台系统的用户分布在世界各地,海外的网络延迟较高,需要解决压缩数据大小的问题,这里使用的是lz-string库。一开始想要在每次存储在localStorage时进行压缩,后来发现压缩后的数据有特殊字符,JSON.parse高频率出错,后改为在每次发送数据到后端之前压缩,并在node端进行解压。

4.一开始的数据库选型为时序数据库influxdb,由于某些不可抗拒原因改为了mongodb。

5.在项目上线后选择了一个小项目进行测试,发现存储和播放效果良好,代码如下

import rrweb from 'rrweb';

rrweb.record({
  emit(event) {
    storagePush(event);
  },
});

存进数据库中的数据结构为

{
timestamp: 1563418490795,
name:'小明',
event:...
}

方便按照用户和时间范围进行查找数据,内容如下

6.但是每次都要播放一整天的数据,第一播放接口获取的数据量巨大,第二播放时间漫长,抓不住重点,一旦数据有误导致后续录屏都播放不了。

查看rrweb源码发现checkoutEveryNms属性可以按照时间进行session切分,于是代码变成了这样

rrweb.record({
  emit(event, checkout) {
    if(checkout)rrwebSessionSet();
    storagePush(event);
  },
  checkoutEveryNms: 1000 * 60 * 10
});

每一次checkoutEveryNms到期时,emit里的第二个参数checkout都会为true,这样就可以知道新的session开始,给session分配一个唯一值,存到数据库中的数据结构改为这样

{
timestamp: 1563418490795,
name:'小明',
session:xxxxxxxxxxx,
event:...
}

有了session概念之后,某个人某一天的操作就可以按照session进行选择

播放页面如下

7.小项目测试完毕后,希望引入一个大项目进行测试,于是开放了一个uv上千、pv几十万的大项目,采集一天的数据后,发现存储数据正常,而播放页面已经获取不到数据,查看mongo的stats发现一天存储量达到了1500万条,每一条数据基本在几十KB到几M之间。

首先对不同的项目进行分表存储,并将索引设置为后台处理,这个方案使用后播放页面变得正常,但人员列表接口还是很慢。

于是在每次存储mongo时,存一份人员和日期的数据到redis中,目前系统已经正常运行,所有接口能在1s内返回所有数据。