背景
当我们收到系统异常通知的时候,普通的监控系统只能告诉你系统出现了错误,但是不能给出错误的复现路径,对于稳定复现的错误而言还好,但对于偶发错误或复现路径隐藏较深的场景我们就较难去解决问题。当我们想要分析用户的系统行为,如果是常见埋点只能分析一些简单的行为例如点击,对于用户完整的行为无法跟踪。所以如果我们可以获取到用户在一定时间内的所有操作行为,也就是录制用户行为,包括整个会话中的每一个点击、滑动、输入等行为,同时支持回放录制的操作行为,完整且真实地重现用户行为以帮助我们回溯或分析某些使用场景。对于我们排查问题的效率有很大提升。
最终期望结果
必须做到用户对录屏无感知尽可能少的影响原本项目的性能,同时需要有一个开关可以全局控制是否开启录屏可以关联录屏时间内的其他相关日志,例如sentry
等可以区分上报类型,例如错误上报/用户行为上报等 方案调研
一:视频录制
录制用户行为最容易想到的就是将屏幕操作通过视频的方式录制下来,目前浏览器本身已经提供了一套基于音视轨的实时数据流传输方案 WebRTC(Web Real-Time Communications)。可以通过调用其原生的API就能实现录屏。
原理分析
在我们的录屏使用场景主要关注以下几个 API:
getDisplayMedia() - 提示用户给予使用媒体输入的许可从而获取屏幕的流;MediaRecorder() - 生成对指定的媒体流进行录制的 MediaRecorder 对象;ondataavailable - 当 MediaRecorder 将媒体数据传递到应用程序以供使用时将触发该事件;整体录制流程如下:
调用mediaDevices.getDisplayMedia()由用户授权选择屏幕进行录制,获取到数据流;生成一个new MediaRecorder()对象录制获取的屏幕的数据流;在 MediaRecorder 对象上设置ondataavailable监听事件用于获取录制的 Blob 数据。播放的时候获取Blob数据进行播放产物大小
1min录制产物大小约为 10M,且视频清晰度低,有失帧的情况出现。
缺点
尽管浏览器原生提供了这样既简单又实用的屏幕录制解决方案,但在我们实际应用场景中仍旧有非常多的问题:
由用户感知并控制:通过 WebRTC 提供的 API 所实现的用户行为录制在开始录制前会通过弹窗来让用户完成对所需录制屏幕的授权,所有的录制行为均由用户自主控制,这种让用户感知到系统录制的方式对于我们预期的使用而言是不合适的,我们预期的录制行为对于用户而言应该是无感的,这种技术方案更适用于考试系统屏幕监控、在线面试屏幕共享等等。录制数据无法脱敏:视频录制过程中直接就将整个页面的内容录制下来,对于一些敏感的数据同样也会直接录制下来,在录制的过程中我们无法进行脱敏,这对于一些数据安全要求比较高或者涉及用户隐私的场景就不适用了。WebRTC 兼容性:在实现录制过程中使用的几个 WebRTC API 都具有一定的兼容性要求,不同的浏览器的支持情况各不相同。其中getDisplayMedia兼容性最低,不到30%。二:页面截图
众所周知,视频是由一帧帧的画面组合而成的,因此我们可以按照一定时间间隔来截图的方式保存当前页面快照,然后将快照按照相同的截取速度播放形成视频就能实现用户行为录制了。最常用的截图方法就是以 html2canvas 库为代表的 canvas 截图。将全量的document按照一定的时间处理为图片。后续将图片一帧一帧展示出来。
原理分析
每隔一段时间,例如每一帧都将全量的dom保存为图片,回放的时候按顺序一张张展示即可。所以最后存储的其实是图片。
产物大小
截图时单张图片体积为200k左右;如果1s截图10次,1min截图产物约为110M
缺点
canvas 截图有较多局限之处,例如无法绘制动画、样式错位等等;每一次都要遍历全量的DOM,截图性能开销较大;三:DOM快照
每一个瞬间我们看到的页面都是浏览器当前渲染的 DOM节点,那么我们完全可以将 DOM 节点保存下来,并持续记录 DOM 节点的变化,然后再将记录的 DOM 节点数据通过浏览器渲染回放,这样即可实现用户行为录制的需求。整个思路非常简单,但具体实现起来是非常复杂的事情,我们需要考虑 DOM 节点数据如何保存、如何捕获用户行为并记录 DOM 节点变换和如何将记录的数据在浏览器上回放出来等。
原理分析
rrweb 主要由有以下三部分:
rrweb-snapshot,包含 snapshot 和 rebuild 两部分,snapshot 用于将 DOM 及其状态转化为可序列化的数据结构并添加唯一标识,rebuild 是将 snapshot 记录的数据结构重建为对应 DOM。rrweb,包含 record 和 replay 两个功能,record 用于记录 DOM 中的所有变更,replay 则是将记录的变更按照对应的时间一一重放。rrweb-player,提供更丰富的播放功能。为 rrweb 提供一套 UI 控件,提供基于 GUI 的暂停、快进、拖拽至任意时间点播放等功能。整体流程如下
rrweb 在录制时会首先进行首屏 DOM 快照,遍历整个页面的 DOM Tree 并通过 nodeType 映射转换为 JSON 结构数据。在获取首屏全量快照之后,我们就需要监听各类变动以获取增量的数据,增量改变的数据也需要同步转换为 JSON 数据进行存储。对于增量数据更新,则是通过 mutationObserver 获取 DOM 增量变化,以及通过全局事件监听例如鼠标移动,鼠标滚动,鼠标交互等等,并将劫持到的增量变化数据存入 JSON 数据中。针对不同类型的变动有这不同的监听处理方式(源码)。
产物大小
1min录制产物大小约为 1.2M,如果加上其他优化配置产物会更小,可以优化到 1M左右。
方案对比
对比内容 | 视频录制 | 页面截图 | Dom 快照录制 ✅ |
---|---|---|---|
开源库 | WebRTC 原生支持✅ | html2canvas | rrweb |
用户感知 | 录制有感 | 录制无感✅ | 录制无感✅ |
产物大小 | 大 | 大 | 相对较小✅ |
兼容性 | 兼容性一般 | 部分场景内容截图无法显示 | 兼容性好✅ |
可操作性 | 弱 | 弱 | 强(支持数据脱敏/加密等)✅ |
回放清晰度 | 录制时决定,有损录制 | 录制时决定,有损录制 | 高保真✅ |
综合各种特性,最终选用 DOM 快照录制的方式进行用户行为录制,选用已有的开源库rrweb进行开发。
开发前需考虑的问题
一:项目多,如何实现多项目快速引入
使用npm包的方式进行开发和引入,每个项目传入自己的配置即可。二:上线后录屏数据如何保存和处理
前端使用indexedDB(不支持indexedDB的使用webSQL降级处理)进行存储,上报时读取后端使用mysql + cos
的当时,将体积比较大的录制数据存在cos
中,mysql
中只存储对应的链接 三:如何防止录屏过程中影响项目性能
通过原理分析,录制时主要耗时在:
mutation对DOM变更的监听处理函数执行对event监听的处理函数执行emit回调函数的执行:把事件写到indexDB上报时刻函数执行:读取indexDB数据并压缩编码,调cgi接口对应策略
使用 requestIdCallback 函数进行事件处理的包裹,防止影响主线程四:如何防止上报的数据量过大
数据进行压缩再上报。非上报全量数据,进行抽样上报。(rrweb已提供对应能力)五:数据上报的时机
系统捕获到重要错误时上报关键行为操作后上报前后1min数据六:上报多长时间的录屏
通过接入项目配置可动态调整策略,默认1min的录屏 / 500个events长度七:录屏数据如何关联其它日志
前端每一次生成全局唯一的session_id,其它日志平台上报时都携带session_id。后续日志查询时通过session_id进行关联八:如何应对特殊情况
录制必须有全局的开关进行控制,当线上发生大规模的重复错误时可以将开关关闭,方式请求过大造成服务器压力有白名单机制,可以针对开白的用户上报全量的数据,方便问题排查。九:其它
思考中…
整体系统架构设计
配置系统
针对每个项目配置单独的策略。包括但不限于以下配置:
录屏策略配置:这个配置主要对齐rrweb是否开启录屏:当业务不需要录屏;或者出现系统bug造成大批量重复报错触发上报,可以及时关掉以免增加服务器压力…配置系统可以自定义实现,可以是一个JSON文件,也可以是一个专业的配置系统。但是前提都要是配置系统易操作且现网生效快。
录制系统
向接入系统暴露出录制类,通过传入定制化配置进行实例化。提供统一的录制和上报函数。接入系统可以自定义触发录制时机和上报时机。每次触发录制之前都清除之前录制的记录,防止不同页面数据污染。播放系统
支持通过uid:(用户唯一Id) + session_id + 时间范围查询具体某次行为的录屏信息支持通过session_id关联其它日志平台日志后续展望
录制系统丰富
提供更加丰富的日志查询,不仅能看到录屏,也能展示每一秒录屏中用户的所有请求/事件/错误。可以实现远程调试,目前已有开源库 PageSpy,方便为用户进行远程调试。五:总结
以上就是关于用户行为录制的一些调研和思考,如果大家对具体代码实现感兴趣可以点点关注,后面会持续更新用户行为录制相关的内容~
最后打个广告,我开了个公众号(微信搜索 “中南滴水哥”),旨在将自己日常学习的内容进行沉淀。这个公众号会经常更新前端相关的技术文章,还请大家多多支持,点点关注?。