通常来说前端页面想要知道用户在页面里的交互行为时一般会在不同的节点上进行埋点上报,然后在后台进行埋点数据分析,但是埋点只能记录在特定时间点的特定事件,虽然能把这些事件给串联起来,但是我们还是无法知道2个时间点之间用户到底发生了什么样的操作行为,我们只知道结果而不知道过程
。针对这个问题前端一般会使用录制与回放的能力来录制用户在前端页面里的交互行为,并在后台进行回放,比较有名的开源库就是rrweb了。
本文不介绍rrweb的基本使用或代码解读,而是基于rrweb的基础能力,在基础的录制回放以及利用之前一篇文章浏览器生命周期API里的一些生命周期回调来做额外扩展,下面只展示基本的代码片段,我们主要还是介绍一些扩展思路、实践及不同场景可能的解决方案,也可以当做一个Story来听🙉。
本文的一些扩展实践主要来解决以下几个问题:
- 录制的同时追加页面里的脚本错误,回放时能更好的重现线上bug
- 通过回放能为产品设计提供实际的用户交互行为参考,而不是YY各种UI/UX
- 批量数据压缩,而不是针对rrweb的单个事件
- 针对页面刷新、打开新Tab等在浏览器里的同一、不同页面之间操作在回放时能体现串联起来
- 多种录制模式:
- 全量,用户无感知(数据量较大)
- 仅发生错误前后一段时间(主要用于错误的快速定位)
- 通过UI用户手动触发(得看用户愿不愿意)
页面录制时追加错误监控
在排查线上问题时有时仅仅有错误信息可能不够,很多问题依赖于用户的具体操作过程,但是仅仅有用户操作回放也是不够的,无法知道错误到底发生在哪个时间点,我们把这2个能力结合起来再排查问题时就能做到事半功倍的效果。
错误的录制我们一般在发生错误时添加一个rrweb的自定义事件作为记录,包括时间点、错误信息、堆栈等,在一个前端应用中,一般会有以下几种错误需要记录:
- 全局错误包含
error
、unhandledrejection
事件 - 框架里面捕获到的错误,比如Vue里面catch到的错误,React ErrorBoundry里catch到的错误等
- 业务应用层面的错误,比如接口报错,业务里try/catch掉的错误
全局错误我们通常可以直接在封装的SDK里处理掉;而捕获框架里的错误一般由SDK提供对应的插件或方法来让接入方简单调用即可,也无需接入方写代码手动捕获;而最后一个应用层的错误一般需要接入方手动在特定的地方添加(如在发请求的地方统一处理)错误事件。
这些错误上报后,在后台回放时就能在回放进度条上进行展示,就可以很直观的看到用户操作到哪里时发生了错误,可以快速进行错误回溯,更进一步的可以自定义进度条样式,为不同的错误显示不同的颜色,甚至对不同的错误进行过滤。
Session会话串联
当用户在浏览器里浏览我们的网站时,如果能把同一Tab的进入,刷新,打开不同Tab, 前后切换在回放时串联起来,会大大提升回放的价值,可以让我们全面的了解用户在网站上的生命周期,会话串联大致可以分为以下几点:
- 刷新页面
- 打开新Tab
- 同项目串联(同域名)
- 跨项目串联(不同二级域名)
当然对于不同项目的定义可能在不同的公司有不同的处理方式,比如域名相同时通过不同的/path
来区分不同项目,这样就可以用同域来处理跨项目的串联。
刷新页面
首先我们可能希望区分第一次进来还是用户刷新了页面,在特定的几次刷新内(也不能过多,避免同一session数据量过大)我们希望在回放时能在一个会话里回放,避免来回切换,可以通过一下代码获取页面进来的类型:
js
// type = "back_forward" | "navigate" | "prerender" | "reload"
const type = performance.getEntriesByType(
'navigation'
)[0].type
我们主要关注里面的2种情况:navigate
及reload
,navigate就是正常导航进来的情况,用户在地址栏输入链接,或者从收藏栏直接打开等,reload就是用户点击浏览器的刷新按钮或快捷键刷新或者通过代码location.reload()
刷新,当然back_forward
有时也需要关注,通过浏览器的前进后退进来,但是一般在SPA的情况下case不多。
打开新Tab
当用户在当前页面中打开了新Tab后,我们希望在回放时能有相应的体现,并可以直接跳到新Tab的回放,那么首先我们应该如何知道用户打开了新Tab呢,一般会有以下几种场景:
<a>
标签有target="_blank"
- 使用
window.open()
a标签的话简单粗暴的就是每隔几秒钟获取页面里的所有a标签,然后给他们绑定事件(在最新一次未出现a标签需要解除事件监听以避免内存泄露),点击的时候也是以rrweb自定义的事件记录下来;
window.open()
我们可以通过覆盖的方式来拦截该方法的调用,例如下面的拦截:
ts
function overrideWindowOpen() {
// 把原始的放到_open下,当不需要的时候需要撤回去
const _open = window.open;
window.open = function myWindowOpen(
url?: string | URL | undefined,
target?: string | undefined,
features?: string | undefined,
) {
// 这个方法就是拦截记录打开新Tab的数据并上报
beforeOpen(url, target, features);
return _open(url, target, features);
};
// 用来重置open
return () => {
window.open = _open;
};
}
const reset = overrideWindowOpen()
// 在后面的某个时间点
reset()
同项目串联(同域名)
同项目把多个会话串联起来主要利用上面的页面刷新
及打开新Tab
的操作,打开新Tab比较直观,但是需要提前生成sessionId,并存储到sessionStorage
,新的Tab直接获取并使用上一个页面存储下的sessionId,这样新的会话就知道我是由谁打开的。
但是利用sessionStorage
的能力有个局限性,它只能用于window.open
打开的情况,通过a标签在新Tab是获取不到上一个Tab的sessionStorage
的,而且由于a标可能直接打开的是外链,所有这里a标签的处理在回放时简单提示,并且也能跳转到外链查看。
对于a标签的同项目也可以使用localStorage
来存储上面的数据,但是得确保用完即清理,不然可能会出现串数据的情况,因为所有的Tab都可以获取同域名localStorage里的数据,所以这里在sessionStorage
跟localStorage
之间可以根据实际的需求做选择,或在不同的场景下使用不同的storage。
跨项目串联(不同二级域名)
对于跨项目来说相当于是同项目串联的扩展,如果一个顶级域名(a.com)下有多个项目(b1.a.com, b2.a.com)接入了录制与回放,这种情况通过storage就不太好搞了,这里可以用有几种方案选择:
- 相关串联数据放到cookie里面顶级域名下(a.com),这样其他子域名也可以访问到;
- URL参数的形式这种就不限域名和项目了,只要目的项目能识别即可,但是存在的问题就是侵入性太强,肉眼可见,实际应用时不太适合;
- 利用浏览器指纹再配合服务端的一些能力来传递相关串联数据;
有其他好的方法大佬们可以评论区交流下🙋
批量数据压缩
rrweb自带压缩是针对单个event来做的,但是实际使用下来有很多事件就一点点数据,相反的事件数量会很多,不太适合做单个事件的压缩,效果不明显,实际使用时更适合做批量数据压缩,比如每10s的数据一起压缩,效果更明显,而且更容易在上面做封装,或者添加自己的数据结构字段等。
基本的gzip压缩可以利用开源库fflate
等,当然你也可以使用浏览器自带的压缩能力,具体可以参考纯前端如何原生处理数据压缩。
多种录制模式
至于录制的方式(何时录制),rrweb本身未提供任何的选项,我们需要根据自己的录制需求提供不同的接入方式,主要有以下几种:
全量,用户无感知(数据量较大)
全量录制就是录制一个会话的整个生命周期,好处是不仅仅可以用来排查页面错误,还可以用这些回放来分析产品交互,用户卡点等场景,以此来为后续的产品设计优化提供参考,而不是仅靠主观想法。但由于这种方式数据量较大,接入前需要用项目的pv/uv数据做参考,以评估数据量大小、并发程度等,以及上面说到的批量压缩能力强烈建议用起来。
除了预估外,还可以增加白名单的方式,由服务端下发当前页面是否需要录制;亦可以在SDK层面添加采样率的形式来控制是否需要录制的开关。
仅发生错误前后一段时间(主要用于错误的快速定位)
这种形式一般是开发在现有错误上报的基础上进行扩展,比如说接入了sentry或自研的错误上报SDK,仅仅通过错误信息有时无法定位错误的真正来源,或者说有些问题是只在用户特定的操作路径下发生的,这种如果没有完整的操作回放的话就比较难以快递定位错误原因。
至于实现方式是在错误SDK上集成回放的SDK,还是在回放SDK上添加监控错误的能力,取决于开发团队技术需求及成本。 以sentry为例,sentry本身有实验性的录制的能力,最初的版本应该也是基于rrweb的,只是把他集成到了sentry里,但是一直处于一种测试阶段,而且有些功能被砍掉了,比如说canvas的录制。反过来我们可以把自己的回放数据集成进sentry, 可以通过调用sentry的一些setUser()
、setContext()
等方法把回放的一些id, 用户信息塞进去,这样在sentry的issue错误页面,就可以看到错误对应的回放链接及相关信息,可以快速跳转至回放页面进行查看。
不管哪种方式,它们依然可以相互独立使用,只是说提供了对应的插件使其能调用对方的接口。
通过UI用户手动触发(得看用户愿不愿意)
这种方式就取决于用户了,一般会在页面右下角有个按钮,用户可以点击开始录制,这种对页面的侵入性就比较强,如果是对C端的页面一般不建议使用了或者小范围使用,这个比较适合内部的一些系统。
获取系统版本 win 11+/macOS 11+
除了扩展功能外,我们一般还需要收集用户浏览器的一些基本信息,最常见的就是我们通过user-agent
获取用户的设备,系统,浏览器版本,操作系统版本等,我们可以通过ua-parser-js
这类库来实现,但是这两年由于2大操作系统大的新版本的出现,主要就是macOS 11+, windows 11+,传统的user-agent已无法区分,主要体现在系统版本上,例如macOS 11+拿到的user-agent还是10.15.x
这种,我们需要新的方法去获取,现代浏览器主要有以下2种方式获取:
- 通过Javascript User-Agent Client Hints API(
页面需HTTPS
)
js
async function getModernUserAgentData() {
let userAgentData = {}
try {
userAgentData = await navigator.userAgentData.getHighEntropyValues([
'architecture',
'model',
'platform',
'platformVersion',
])
} catch (err) {
console.error(err)
}
return userAgentData
}
- 通过浏览器与服务端的Header来交互,这个可以参考获取win11版本的文档
通过回放提升产品交互,拒绝YY
产品在设计产品交互需求时,一般都是凭自己的直觉或者经验来画交互,很多时候这些产品或设计所谓的很牛逼的交互行为在实际用户使用时可能完全跟预先的设想背道而驰,这种case既浪费了开发的时间也影响了用户的体验,甚至影响整个产品的成功与否。
鉴于以上问题,我们可以利用录制回放的能力让产品设计直接去参考用户的实际操作,在配合上大数据分析统计的能力,能够归纳出哪些交互是能提升用户体验的,哪些是block用户交互的,甚至在需求初期就可以通过这些数据来提前设计具有更高概率对用户实际有效的交互,再配合各种数据图表能够直观有效的查看优化前后的效果。
更进一步的,鉴于最近很多的AI,我们甚至可以利用AI的能力从海量的回放数据里训练并能提取出对产品设计以及开发最关心的一些回放,这种能力在例如国外LogRocket等的产品中已经应用起来,我们不用再面对海量的回放数据浪费时间看我们并不关心的数据,而是直击重点花最少的时间做最有效的事情,可以用最近大厂比较火的降本增效
来形容😂。
用户隐私
既然需要录制用户页面,特别是前2种用户无感知的情况(应该特别是第一种😂),一个逃不过的问题就是用户隐私问题,虽然从开发的角度说SDK会提供基本的隐身保护,比如密码回放时也是mask的,用户(接入开发者)可以自定义哪些元素需要录制哪些直接以占位符替换掉等能力。
但这只是从开发的角度出发,从产品甚至公司的层面来说,用户不知道还好,一旦用户发现了,如果没有预先的隐私协议写清楚就尴尬了,可能面临被投诉的风险,所以在上线功能前需要产品或法务这些人制定好隐私协议或者一套应对方案。
总结
本文主要讲了在前端录制与回放功能的基础上能够扩展出的进阶功能,如果仅仅是无脑录制,可能对于使用这些回放的人的意义不大,需要配合额外的数据展现及分析能力才能更大的发挥回放数据的效果,能够真正解决产品设计、运营、开发的实际问题。
本文对于有这方面需求的团队或公司来说提供了一些思路及解决方案。大家如果有其他的一些场景应用或想法可以在评论区一起讨论讨论,cheers🍻。