rrweb在保险可回溯中的应用

2020年6月30日,中国银保监会发布《关于规范互联网保险销售行为可回溯管理的通知》(以下简称本通知,核心内容如下)

一、本通知所称互联网保险销售行为可回溯,是指保险机构通过销售页面管理和销售过程记录等方式,对在自营网络平台上销售保险产品的交易行为进行记录和保存,使其可供查验。

十五、保险机构应当将投保人、被保险人在销售页面上的操作轨迹予以记录和保存,操作轨迹应当包含投保人进入和离开销售页面的时点、投保人和被保险人填写或点选销售页面中的相关内容及时间等。

二十、互联网保险销售行为可回溯资料应当可以还原为可供查验的有效文件,销售页面应当可以还原为可供查验的有效图片或视频。

总结下来就是用户整个投保过程的页面内容和操作轨迹都需要记录下来,并且可通过视频的方式进行呈现。


实现难点

需求很明确,通常这种跟踪用户行为的需求,会通过埋点采集数据来分析用户行为,埋点对象通常是按钮、链接和图片等单个简单节点。但此次合规监管明确提到的是基于页面的采集录制,特别是用户在页面上的交互轨迹,没办法只通过简单地对节点埋点来完成。

思路一:操作系统录制

最直接的方式当然是通过底层操作系统提供的API对页面直接进行录制,抛开不同浏览器对于录制的支持差异,目前浏览器暴露的录制方式都是需要用户交互参与,需要用户手动选择录制窗口,对于业务流程太割裂了,同时录制完成的视频上传也是一个大问题。

思路二:借助Canvas截图

借助canvas去画网页内容,具体可参考 html2canvas ,实现方式为持续对用户访问的页面进行截图并上传到服务器。为了视频流畅,一秒中我们需要25张图,一张图300KB,也就是60秒产生的图片为440M,这么大的网络开销明显吃不消。

思路三:记录页面变化并重放

将数据埋点的方案升级,在首次访问的页面基础上,持续采集页面变化,并将变化序列化存储,服务端拿到数据后进行重放,这样我们就能模拟复现用户的操作过程。这种方式传输的数据较少,但视频需要在服务端页面重放时进行录制。

综合可行性、网络开销以及方案验证后,确定采用思路三,借助下文介绍的rrweb Puppeteer FFmpeg来完成页面采集、重放录制和视频处理。


技术组件

rrweb

rrweb 是 ‘record and replay the web’ 的简写,旨在利用现代浏览器所提供的强大API录制并回放任意 web 界面中的用户操作。record 用于记录 DOM 中的所有变更(mutation);replay 则是将记录的变更按照对应的时间一一重放。

  • rrweb record采取快照 + Oplog(operations log)相结合,首先对页面进行记录 DOM 树操作,形成一个可序列化的 DOM 初始快照,之后页面的所有操作,包括DOM 变动、⿏标交互、⻚⾯或元素滚动、视窗⼤⼩改变、输入和鼠标移动等,都会通过增量数据的方式记录下来。

  • rrweb replay读取record采集的数据,将记录的操作数据进行重放,也就回放了该操作对视图的改变,达到页面重新播放的效果,同时rrweb-player 为rrweb 提供一套 UI 控件,提供基于 GUI 的暂停、快进、拖拽至任意时间点播放等功能。

Puppeteer

Puppeteer 是一个Node库,它提供了高级API来通过DevTools协议控制Chrome。

rrweb 通过回放 DOM 树操作的方式,实现了页面重放,但并不是真正意义上的视频,需要借助于录制回放的页面来生成,并且这一系列操作还需要能自动化完成。

Puppeteer 多用于自动化页面测试,支持通过API操作浏览器模拟用户进行操作。在上述场景中,借助puppeteer打开浏览器,通过 rrweb-player 播放视频,同时通过浏览器插件对页面进行录制,播放结束下载存储视频文件,从而达到整个录制流程自动化的目的。

FFmpeg

FFmpeg是用于处理多媒体内容(例如音频,视频,字幕和相关元数据)的库和工具的集合。

rrweb 的实现原理决定了采集回放的对象是单个页面,准确的说是单次访问单个页面,我们称之为片段,用户在整个投保过程中,会进行多个页面跳转,所以会产生多个片段,同时录制结束后,会得到多个片段对应的视频文件。

FFmpeg 用于将几个独立的视频文件进行合并,并对视频文件进行转码等处理。


技术实现

(绿色为数据采集,蓝色为视频录制拼接)
  • 数据采集

    • 使用rrweb采集用户行为,采集的结果为 JSON 数据,按照一定频率存储到服务器

    • 用户在整个投保过程中访问多个页面,同时采集到多个数据片段

    • 投保成功创建订单,同时将多个片段关联到投保订单

  • 视频录制

    • 在服务器端puppeteer操控浏览器顺序重放用户行为数据片段

    • 播放同时进行视频录制,播放结束下载保存视频片段文件

    • 订单所有片段录制结束,通过FFmpeg进行视频片段拼接并转码得到完整回溯视频文件

  • 视频上云

    • 上传回溯视频到阿里云OSS

    • 上传成功后回调通知保司下载存档

核心代码片段

无锁高并发存储采集数据

为了实现近实时采集,目前web每秒上报一次数据,典型的高并发场景。同时由于采集的是DOM节点变化,单个片段数据量很大,存储方面采用经典的Mongo + Mysql结构,即将片段数据内容完整存储于Mongo,objectId和其他索引字段存储在Mysql中,优势是可以充分结合各自文档存储和事务索引,劣势是需要操作两种数据代码复杂度较高。

通常的实现方式是结合分布式锁实现double check,大致伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fetch mysql data
if not exist
// 避免重复初始化,竞争分布式锁
lock userId
fetch mysql data
// double check
if not exist
insert mongo document genernate objectId
insert mysql data
return
// mysql 索引表存在,则更新mongo document
fetch mongo document with objectId
document add data
update mongo document with objectId

这段代码存在三个问题:

  1. 首次存储通过竞争分布式锁避免数据插入重复,但用户频繁刷新页面将带来较大性能开销;
  2. mongo & mysql没有分布式事务保障,后者执行失败,将导致mongo脏数据产生;(mongo 4.0开始支持事务,先按下不表)
  3. mongo document update通过 fetch then update两步完成,无法保障原子性。

针对这些问题对应的调整如下:

  • 同一用户采集的数据线性发送到服务器,排除网络阻塞的情况下,竞争初始化的几率不会很高,可以将悲观锁降级为补偿机制,查询未命中则直接创建,若插入冲突即说明数据已创建则再次查询,快速失败也能避免锁竞争失败排队消耗;
  • 拆分mongo & mysql的耦合,两者独立进行insert or update,并且优先执行mysql insert;
    • 若第一步mysql insert失败,后续逻辑不会继续执行,整体状态为未创建;
    • 若第二步mongo insert失败,mysql 数据保持现状,整体接口失败重试,mysql 数据已存在则再次执行mongo insert,保障最终一致性。
  • mongo update 借助$push 操作将上述fetch then update变成原子操作来执行

具体代码实现如下:

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
// deviceId, productId, pageType, pageViewId 是Mysql索引表的联合唯一索引
InsureBehaviorRecord insureBehaviorRecord =
insureBehaviorDB.fetchOne(deviceId, productId, pageType, pageViewId);
String objectId;
if (null != insureBehaviorRecord) {
objectId = insureBehaviorRecord.getObjectId();
} else {
// 首次上报数据,自动生成objectId
objectId = new ObjectId().toString();
try {
insureBehaviorDB.insert(userId, deviceId, productId, pageType, pageViewId,objectId);
} catch (DataIntegrityViolationException e) {
// 唯一索引冲突,数据已存在获取对应的ObjectId
insureBehaviorRecord = insureBehaviorDB.fetchOne(deviceId, productId, pageType, pageViewId);
objectId = insureBehaviorRecord.getObjectId();
}
}
// mongo $push
UpdateResult updateResult = insureBehavioralMongo.pushList(objectId, behaviorJsonData);
// push未成功,mongo document未创建,执行创建逻辑
if (updateResult.getMatchedCount() == 0) {
try {
insureBehavioralMongo.insertData(objectId, behaviorJsonData);
} catch (MongoWriteException e) {
// objectId 冲突,说明数据已存在,执行pushList操作
if (MongoErrorCode.DUPLICATE_KEY.getCode() == e.getCode()) {
insureBehavioralMongo.pushList(objectId, behaviorJsonData);
}
}

PS1:由于补偿机制代码比较繁琐,导致代码量看起来比之前增加了不少,但整体性能和可靠性提升明显

PS2:若接口重复调用,存储时并未对数据直接进行去重,去重操作统一放到录制查询时进行过滤

puppeteer操作录制

借助于Puppeteer 打开浏览器以及新的标签,利用浏览器上的一些录制工具开始录制,同时在页面上运行 rrweb-player 自动播放采集数据,播放结束后通过标志位通知Puppeteer,然后通过window.addEventListener监听message和chrome.runtime的消息传递方法通知Chrome下载视频并储存起来。部分核心代码如下:

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
const options = { // 一些配置
ignoreHTTPSErrors: true,
headless: false,
defaultViewport: null,
args: [
'--enable-usermedia-screen-capturing',
'--allow-http-screen-capture',
'--auto-select-desktop-capture-source=yqg-puppetcam',
'--load-extension=' + __dirname,
'--disable-extensions-except=' + __dirname,
'--disable-infobars',
`--window-size=${width},${height}`,
],
}
const browser = await puppeteer.launch(options);
const [page] = await browser.pages();
await page._client.send('Emulation.clearDeviceMetricsOverride');
// 打开咱们播放视频页面
await page.goto(playUrl, {waitUntil: ['load', 'domcontentloaded']});
await page.setBypassCSP(true);
// timeout设置为0即为一直等待到出现finish-flag元素的出现,finish-flag是播放结束后在页面里添加的一个元素,通过这种方式页面通知puppeteer继续往下走。
await page.waitForSelector('.finish-flag', {timeout: 0});
await page.evaluate(filename => {
window.postMessage({type: 'SET_EXPORT_PATH', filename}, '*');
window.postMessage({type: 'REC_STOP'}, '*');
}, exportname);
await page.waitForSelector('html.downloadComplete', {timeout: 0});
await page.close();
await browser.close();

遇到的问题

  1. rrweb目前不支持采集iframe、pdf,在回放中会展示为空白。用户协议绝大部分都是通过这两种方式来完成的,这就导致协议成为重灾区。

    • iframe 打开的协议,理论上可以通过新打开页面来解决,但这样会导致采集的片段很零散,大量短时间访问的协议片段。解决方案为通通处理为文字内容,协议内容短的直接配置为文字内容,内容长的通过后端服务代理访问,拿到 html 内容直接页面内展示。
    • pdf 文件协议,尝试使用上述方案将pdf文件动态转为html 代码展示,但动辄好几M协议文件转换和页面渲染都非常慢。由于文件太大,另辟蹊径,将pdf文件处理为图片集合,并将图片上传到图床,结合页面懒加载等技术完美模拟了pdf协议。

这两套组合拳结合下来,除开少部分三方带脚本的协议页面只能打开新页面,几乎能处理掉所有协议采集录制问题。

  1. 采集跨多个页面时,每次进页面时要先清一下rrweb引用缓存再引入rrweb,否则会出现输入框里的文字录制不上的诸多奇怪的bug,我们是用的vue Router进行的页面跳转,不知道其他框架会不会有问题;

  2. 从上文我们知道rrweb是基于快照不断更新,所以如果中间有某次数据缺失的情况,那后续可能就会卡顿或者出现一些未知错误,所以要考虑做一些错误补偿机制,比如后续录制阶段不能因为一个视频的问题影响整个流程的继续。

页面性能影响

rrweb性能

使用rrweb页面performance示意图

这是投保过程中最复杂的一个页面。载入时,rrweb的emit方法大概用了小于100ms的计算时间,后续页面上DOM变动rrweb的emit方法需要的时间仅为几ms。整体计算时间在可接受范围内,引入DOM采集机制并未给用户带来明显的加载延迟。

延伸“直播”

在完成上述工作后,我们实现了整套数据采集、视频录制方案,但我们思考能在目前基础上做一些服务用户的功能,“直播”就这么提上日程。

咱们小水花规划师除了给用户量身定做投保方案之外,还承担一个重要责任,就是指导部分大龄用户投保。之前的方式是语音电话沟通,用户描述自己遇到的问题,佐以截图,规划师进行答疑,非常依赖用户描述问题的能力和规划师流程熟悉程度。

用户访问页面后,我们会持续拿到采集数据,借助播放器能在秒级别延迟后看到用户最新操作,也就是“直播”用户的操作,规划师也可以观看“直播”给用户进行指导。

整体流程没有大问题,但投保页面上存在用户很多敏感信息,这部分内容在“直播”中应该用掩码处理,rrweb-player也提供了blockClass参数,只需要在不想被显示的元素上加上rr-block类名即可,但是实际使用中我们还是想看到用户是否输入了内容,只不过不显示文字而已,还是要能看到字数的信息以及输入的情况,所以我们用到了insertStyleRules参数,这个参数可用于自定义回放时的样式,首先在不想被看见文字的元素上加一个类名(eg:hide-info-in-live),然后在回放时加上insertStyleRules参数。

敏感信息打码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
new rrwebPlayer({
target: vm.$refs['rrweb-player'],
props: {
events: data,
insertStyleRules: [
`.hide-info-in-live {${this.hideClass()}}`
],
}
});
...
hideClass() {
const input = document.createElement('input');
const style = window.getComputedStyle(input);
if (style.webkitTextSecurity !== undefined) {
return 'text-security: disc!important;-webkit-text-security: disc!important;-moz-text-security: disc!important;'
} else {// 兼容一下火狐,火狐直接给它隐藏
return 'opacity: 0!important;'
}
}

Tips: 整个流程是在用户明确知晓,并手动授权规划师指导投保才会触发,大家正常投保,并不会出现有人观看,小水花在隐私权限控制这块一直都是作为最高优先级的。


写在最后

为了完成此需求,我们前期做了大量调研尝试,也pass了多个方案,例如用截图的方式生成视频,在其他地方已经看到有人实现了,但是我们考虑到截屏的性能问题以及视频流畅度问题,最终放弃了这一方案,毕竟适合自己需求的方案才是好的方案。

非常感谢rrweb这个开源项目,据了解2020年整个保险行业有不少公司都选择了rrweb,项目主要维护者也称之为机遇。能做到影响整个行业,难能可贵,希望能得到更多人的关注。除了合规回溯,日常流程复现、问题排查也非常适合。

如果不是监管强制要求,对Web页面录制绝对是天马行空需求之一,当然这也从侧面反应了“监管是第一生产力”。对于这类非常规需求,前期方案调研和技术摸索非常重要,大胆构思小心求证,甚至还需要几个方案同时推进,比较考验团队执行力,同时在整个落地过程中也需要持续调整,多沟通多反馈,注重产出。

可回溯管理的出台,有效补齐了此前保险行业监管制度的短板,逐步引导合规安全透明的运营,倒逼各大险企进行保险科技改革,同时也给新崛起的保险技术类公司开辟了新赛道,未来保险+科技才是主旋律。小水花,冲鸭!

小水花保险 彭波、王凯

参考代码与文章:

1.rrweb

2.rrweb-player

3.rrweb:打开 web 页面录制与回放的黑盒子

4.puppetcam

5.web页面录屏实现