基于rrweb录屏与重放页面

项目背景

在使用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.在项目上线后选择了一个小项目进行测试,发现存储和播放效果良好,代码如下

1
2
3
4
5
6
7
import rrweb from 'rrweb';

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

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

1
2
3
4
5
{
timestamp: 1563418490795,
name:'小明',
event:...
}

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

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

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

1
2
3
4
5
6
7
rrweb.record({
emit(event, checkout) {
if(checkout)rrwebSessionSet();
storagePush(event);
},
checkoutEveryNms: 1000 * 60 * 10
});

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

1
2
3
4
5
6
{
timestamp: 1563418490795,
name:'小明',
session:xxxxxxxxxxx,
event:...
}

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

播放页面如下


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

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

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

重新认识caniuse

困惑

相信大家都曾用caniuse网站查询过css、js的一些兼容性问题,并且都从它反馈的兼容性数据中获益,让我们的线上项目更加稳定、和谐的跑在用户电脑里。不过对于caniuse页面上的一些细节,我们可能会感到困惑或者模棱两可,今天就带着大家一起来重新认识caniuse这个网站,并对它的原理和细节做些探究。

1.1从babel-preset-env说起

babel-preset-env是babel6中极力推崇的一个preset,preset代表的是babel plugins的一个集合,相当于一堆plugins的一个统称。在babel最开始打江山的时候,es6标准也发布不久,babelrc的配置中只需要添加es2015这样的preset。但随着es2016、es2017的相继出现,babelrc很快就会变成一堆挂历式的集合体。所以babel给出了env这个杀器,既避免了es20xx的出现,又可以与caniuse的权威数据融合,让配置preseet科学而简单。

1
2
3
4
5
6
7
8
9
{
"presets": [
["env", {
"targets": {
"browsers": ["last 2 versions", "safari >= 7"]
}
}]
]
}

这是babel官网给出的env配置方案,”last 2 versions”, “safari >= 7”,这两个条件是并集的关系,babel将会分别给出满足这两个条件的浏览器及版本,并会进行合并,最后算出一组浏览器及对应最低版本的数据。

babel是用来转换js语法的一个编译器,为什么还能知道满足env条件的浏览器跟版本,这要从browserslist这个库说起。

1.2 Browserslist

这个库不仅仅用在babel-preset-env中,像autoprefixer这样知名的库,也是用到了它。

1
2
3
4
last 1 version
> 1%
maintained node versions
not dead

browserslist能够把上面近似于人类语言的配置,转换成一组浏览器集合。不过它的主要职责就是转化上面的配置内容,按照正则过滤出正确浏览器列表内容,而它本身不提供浏览器列表的来源。

1.3 caniuse-lite

Browserslist的浏览器数据来源就是这个caniuse-lite,而它是caniuse-db库的精简版本,是从caniuse-db库衍化而来,只不过对caniuse-db数据按照一定规则做了简化,使得库的大小减少了许多,并且提供一些查询api供他人使用,每当caniuse-db更新时,也会跟着一起发布版本。

1.4 caniuse-db

caniuse的npm包,提供了caniuse网站查询所需的所有数据。

2.caniuse库的介绍

caniuse-db的github地址在此,caniuse鼓励大家去github上提交pr,经过审核之后就可以被录用到它的官方数据库中。

2.1如何为caniuse贡献数据

首先,它为我们准备了sample-data.json文件,按照此文件格式把需要增加的特性名称、介绍和浏览器兼容性情况填写清楚,保存并放到features-json文件夹中,最后提交pull request即可,审核完毕后会自动把这部分新增特性保存到data.json中。data.json就是caniuse官方的数据库导出文件,供其他库调用,每次json文件变化后,都会release一个新版本。

2.2 sample-data.json

作为新特性发布的样本文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
{
"title":"Sample title",
"description":"Sample description",
"spec":"http://example.com/path/to/spec.html",
"status":"wd",
"links":[
{
"url":"http://example.com/path/to/link.html",
"title":"Link title"
}
],
"bugs":[
{
"description":"Sample bug description"
}
],
"categories":[
"CSS"
],
"stats":{
"ie":{
...
"11":"u"
},
"edge":{
...
"18":"u"
},
"firefox":{
...
"67":"u"
},
"chrome":{
...
"75":"u"
},
"safari":{
...
"TP":"u"
},
"opera":{
...
"58":"u"
},
"ios_saf":{
...
"12.2":"u"
},
"op_mini":{
"all":"u"
},
"android":{
...
"67":"u"
},
"bb":{
"7":"u",
"10":"u"
},
"op_mob":{
...
"46":"u"
},
"and_chr":{
"71":"u"
},
"and_ff":{
"64":"u"
},
"ie_mob":{
...
"11":"u"
},
"and_uc":{
"11.8":"u"
},
"samsung":{
...
"8.2":"u"
},
"and_qq":{
"1.2":"u"
},
"baidu":{
"7.12":"u"
}
},
"notes":"Sample notes for feature, explain partial support here",
"notes_by_num":{
"1":"First note..."
},
"usage_perc_y":0,
"usage_perc_a":0,
"ucprefix":false,
"parent":"parentfeatureid",
"keywords":"example,keywords",
"shown":false,
"ie_id":"",
"chrome_id":"",
"firefox_id":"",
"webkit_id":""
}

简要介绍下其中的几个关键字段:

(1)title:特性名称

(2)description:特性介绍(搜索时的关键字)

(3)spec:跳转到详细介绍页面

(4)links:拓展内容介绍

(5)keywords:搜索时的关键字

(6)status:特性在标准中的状态

  • ls - 标准

  • rec - W3C 推荐

  • pr - W3C 建议

  • cr - W3C 候选

  • wd - W3C 手稿

  • other - 非W3C, 但流行的

  • unoff - 非官方

(7)categories:分类

  • HTML5

  • CSS

  • CSS2

  • CSS3

  • SVG

  • PNG

  • JS API

  • Canvas

  • DOM

  • Other

  • JS

  • Security

从上面分类可以看出,caniuse并不只是一个查询css兼容性的网站。

如果想查看目前caniuse已经支持了多少种特性,以及特性对应的分组信息,可以点击这个网址

(8)stats:浏览器对特性的支持情况

  • y - (Y)es, supported by default 完全支持

  • a - (A)lmost supported (aka Partial support) 部分支持

  • n - (N)o support, or disabled by default 不支持

  • p - No support, but has (P)olyfill 不支持,但有替代方案

  • u - Support (u)nknown 未知

  • x - Requires prefi(x) to work 需要加前缀

  • d - (D)isabled by default (need to enable flag or something)需要打flag

  • ‘#n’ - Where n is a number, starting with 1, corresponds to the notes_by_num note. 支持,请看介绍第n条

(9)stats:浏览器列表

  • ie

  • edge

  • firefox

  • chrome

  • safari

  • opera

  • ios_saf

  • op_mini

  • android

  • bb

  • op_mob

  • and_chr

  • and_ff

  • ie_mob

  • and_uc

  • samsung

  • and_qq

  • baidu

以上浏览器列表是固定的,用户不能增加和缺少某个浏览器类型。

总结

每当增加一个新特性时,都要对以上浏览器列表以及对应版本列表进行实测,特性的测试可使用以下两个官方推荐的网站https://www.browserstack.comhttp://saucelabs.com。

对于第二个网站,可用于因浏览器兼容性造成生产事故的还原测试,在其网站的虚拟机内完成特定浏览器特定版本的实测,在测试完成后可以观看操作视频,并支持导出功能,这对技术解决兼容性问题,提供了第一现场的操作流程,方便问题的解决。

3.caniuse网站介绍

3.1主页面介绍


页面红字标注了4个地方

(1)代表了这个介绍框的内容隶属于一个特性,也就是我们在features-json看到的一个个跟特性相关的文件,没有#标志的不属于特性。

(2)代表这个特性在标准中所处的一个状态,具体参照前文对sample-data.json的介绍

(3)对于这个特性,在全球、中国所有浏览器中,分别有多少完全支持和部分支持,把两部分值加起来,得到总份额。

(4)浏览器基线,代表对应浏览器current状态的版本号。基线往上是该浏览器的低版本,并对相同支持情况的版本进行合并。基线往下是未来的三个版本,并进行状态合并。

3.2浏览器信息统计

caniuse关于浏览器的数据,主要都来源于statcounter,此网站统计了全球以及各国的浏览器使用情况。

上面提到的浏览器基线及版本号列表,都是基于statcounter上个月份的数据统计。
例如chrome v73在3月12号发布了版本,但在caniuse网站里,v72还是作为了current版本,就是因为caniuse的分析数据来源于2月份的统计数据,数据并不是实时更新。

3.3详情

页面红字标注了4个地方

(1)浏览器对特性支持情况相同的版本区间

(2)对特性的支持情况

(3)火狐40-火狐64的发布时间

(4)火狐40-火狐64,在全球、中国的使用份额

4.想法

知道了caniuse的数据来源及原理之后,我们是否可以打造属于自己公司的caniuse,暂且就叫做caniuse-shein

(1)
目前,我司的前端只负责中后台系统,面向的用户群体有限。
而我手头上有一个专门为公司前端而打造的APM项目,里面包含了详尽的浏览器版本及份额数据,将APM项目中的浏览器数据与caniuse的特性数据相结合,可以制作出类似于caniuse官网的特性查询分析页面,但报表数据只关心我司的用户群里使用的浏览器,而非依据全球或者全国。

(2)
babel-preset-env这个插件也可以结合caniuse-shein的数据,给出对应浏览器份额的babel插件列表