乐趣区

录屏与重放你的页面

项目背景

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

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

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

rrweb

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

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

  1. rrweb-snapshot,将页面中的 dom 转化为可序列化的数据结构
  2. rrweb,提供录屏和重放的 api
  3. 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 的 status 发现一天存储量达到了 1500 万条,每一条数据基本在几十 KB 到几 M 之间。

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

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

退出移动版