探索行为可回溯系统 应用与实现.png

     通过阅读本文你会了解到行为回溯系统的优点和带来的好处是什么?可以学到搭建完整的回溯系统的流程、DOM录制方案的优点和利用FFmpeg转码视频等知识。

前言

     行为可回溯系统最先出现于保险销售行业,对其定义和范围是指 保险机构通过销售页面管理和销售过程记录等方式,对在自营网络平台上销售保险产品的交易行为进行记录和保存, 使其可供查验。通过这种方式减少线上销售模式的风险及其问题的出现并在问题出现后可以很快的找到对应的因素。

     行为可回溯系统为我们后期提供有效的图片、音频或者视频相关的有效资料,以便调查,检查使用。可通过记录用户行为序列,通过会话回放还原用户操作真实场景,分析核心流程转化并呈现用户行为偏好,基于用户行为优化用户体验,根据用户操作轨迹了解异常发生的全部过程,协助研发人员定位处理异常问题,实现通过行为分析达到异常追踪的方式。

     我们通过下面的内容来具体了解可回溯系统,了解它的原​理及使用方式。

系统设计

行为可回溯分析平台:

1. 数据仪表盘:

     数据仪表盘主要用来统计已集成终端提交的数据件数,运营人员/管理员进行查看的数量和归档数量,提交数据的地理分布情况。通过以上指标可以监控和分析抽检/归档是否达标,系统使用的活跃情况及系统使用的区域分布等,以便于下一步的有针对性的进行调查分析。

2. 行为分析:

     行为分析模块主要设计了三个子模块:关键行为分析,普通行为分析和其他行为分析三个模块来进行分别管理。推荐将跟业务强关联的操作,如:下单支付,合同预览等按业务编码来归入关键行为。推荐跟系统模块关联的操作,如:页面跳转,地址修改等按系统编码来归入普通行为。当一些记录未关联业务或系统编码的情况将统一记录到其他行为,保证记录的数据有处可查。

image.png

3. 归档管理:

     基于系统设计的初衷和已集成终端迭代速度的不同,我们需要对关键的业务回溯行为设计一定的指标进行抽查并归档,归档后的数据会被持久化存储,避免终端快速迭代造成的回溯问题,后续将增加定时任务来进行自动归档避免这样的问题。

image.png

4. 用户管理:

     实现常见的管理平台的功能,用来新增运营人员和进行功能授权等操作。

image.png

行为分析平台模块分布图:

模块结构图.png

注:回溯查看在归档后会转为视频,在下方的数据服务中有示例演示。

行为可回溯上报SDK:

SDK API定义:

1. 启动回溯记录【startRecord】:
  1. 单页面应用:业务开始时调用一次,参数first指定为true,开启录制,返回UID用于自行存储,并向cookie中存储当次录制的UID
  2. 多页面应用:业务开始同单页面,额外需要在业务开始后的每个页面进入时调用,参数firstfalse,将同一UID的录制数据进行绑定。
  3. 录制启动后,SDK将以默认值每3秒为一个间隔进行录制数据的自动上报来保证数据完整。

    startRecord(first = false) {
      if (first) {
        this.uid = uuidv4()
        docCookies.setItem('behavior-record-uid', this.uid)
      } else {
        this.uid = docCookies.getItem('behavior-record-uid')
      }
      if (!this.uid) {
        throw new Error('Unable to get UID, refer to development documentation.')
      }
      const ctx = this
      this.stopFn && this.stopFn()
      this.stopFn = rrweb.record({
        emit(event) {
          ctx.events.push(event)
        }
      })
      this.timer = setInterval(() => {
        if (ctx.events.length > 0) {
          this._requestEvents(this.uid, ctx.events)
          ctx.events = []
        }
      }, config.INTERVAL_TIME)
      return this.uid
    }
    2. 停止回溯记录【stopRecord】:
  4. 单页面应用:业务结束时调用一次,参数last指定为true,结束录制,cookie中存储的当次UID会并清空。
  5. 多页面应用:业务结束同单页面,额外需要在业务结束前的每个页面离开时调用,参数last指定为falsecookie中存储的当次UID不被清空。

    stopRecord(last = false) {
      if (!this.stopFn) {
        throw new Error(
          'No startup recording found, refer to development documentation.'
        )
      }
      this.stopFn()
      clearInterval(this.timer)
      this.timer = null
      if (this.events.length > 0) {
        this._requestEvents(this.uid, this.events)
        this.events = []
      }
      if (last) {
        if (docCookies.hasItem('behavior-record-uid')) {
          docCookies.removeItem('behavior-record-uid')
          this.uid = ''
        }
      }
    }
    3. 上报回溯记录【report】:
  6. 当调用停止回溯记录APIlast=true)后,需要将一些用来做统计和分析的公共数据进行提取上报,并与相关的业务ID进行关联。
  7. 当您录制的场景非业务场景时,可自行生成系统ID,并将模块或功能的名称进行上报以便查询分析。

    _requestEvents(uid, events) {
      this._request('track/update', {
        uid,
        timestamp: events[0].timestamp,
        events,
        userAgent: navigator.userAgent
      })
    }
    
    async _request(action, data) {
      return await fetch(`${config.EVENT_API}/${action}`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(data)
      })
    }
  • 把插件安装到需要回溯的客户端后进行简易的配置即可开始回溯操作的记录:

    import { BehaviorRecord } from './behavior-record/index'
    window.rb = new BehaviorRecord()
    
    // 启动/重启回溯时调用
    start(bool) {
      if (bool) {
        const uid = window.rb.startRecord(true)
        console.log('[ uid true ] >', uid)
      } else {
        const uid = window.rb.startRecord(false)
        console.log('[ uid false ] >', uid)
      }
    }
    
    // 暂停/结束回溯时调用
    stop(bool) {
      if (bool) {
        window.rb.stopRecord(true)
      } else {
        window.rb.stopRecord(false)
      }
    }

SDK API使用流程图:

上报流程图.png

数据服务:

     数据服务用来连接上报SDK和分析平台,主要为分析平台提供用户管理和行为管理等数据支持,数据来源由上报SDK集成到需要上报的终端后按场景进行数据定时上报,因存储的内容大量为不需要强关系的JSON文档,这里推荐选用效率更高的MongoDB来做存储介质。

     由于DOM录制受终端大版本迭代影响会造成部分内容无法回溯,如:实时的图片,实时的脚本等,所以需要增加定时任务来做定时的数据归档,持久化后的数据虽然丢失了直接回放DOM的有点,但是也是目前不可缺少的一块内容。

数据服务功能流转图:

数据服务流程图.png

  • 定时归档任务(演示):

    const Subscription = require('egg').Subscription;
    const { spawn } = require('child_process');
    
    // 在EggJs中实现定时任务,目前为每小时执行一次subscribe函数
    // subscribe函数为演示,实际中会受到转码速度限制
    class RegularlyArchive extends Subscription {
    
      // 配置触发规则
      static get schedule() {
        return {
          interval: '60m', // 分钟间隔
          type: 'all', // 指定所有的 worker 都需要执行
        };
      }
    
      // 任务执行函数
      async subscribe() {
        console.log('[ 执行时间 ] >', new Date().getTime());
        /**
         * 通过spawn来执行转换视频的命令
         * input:events数据数组
         * output:视频文件输出地址
         *
         * 转换逻辑:
         *  1.通过puppeteer来加载DOM使用rrweb-player进行视频后台播放
         *  2.定时每秒15次通过DOM元素的screenshot函数获取数据流
         *  3.将每次截取的数据流写入ffmpegProcess进程
         */
        spawn(
          'rrvideo.cmd',
          [ '--input', './data.json', '--output', './rrvideo-output.mp4' ],
          {
            stdio: [ process.stdin, process.stdout, process.stderr ],
          }
        );
      }
    }

    使用ffmpeg转换视频流程图:

     通过后台重放DOM并定时截图再利用ffmpeg库来对图片进行合成为视频后上传到对象存储服务达到数据持久化存储的目的。

定时任务转视频流程.png

视频生成过程稍慢,请等待一下(演示所用):

GIF 2022-2-15 10-30-46.gif

无头浏览器使用及扩展全局函数示例

export async function openPage(
  input: string,
  output: string,
  resolve: Function,
  reject: Function
) {
  _input = input;
  _output = output;
  _resolve = resolve;
  _reject = reject;

  try {
    browser = await puppeteer.launch({
      headless: true,
    });
    page = await browser.newPage();
    await page.goto("about:blank");
    // 扩展启动录制函数
    await page.exposeFunction("onReplayStart", async () => {
      await startReplay();
    });
    // 扩展结束录制函数
    await page.exposeFunction("onReplayFinish", async () => {
      await finishReplay();
    });
    // 读取原数据
    const events = JSON.parse(
      fs.readFileSync(path.resolve(process.cwd(), _input), "utf-8")
    );
    await page.setContent(getHtml(events));
  } catch (error) {
    console.log("openPage:", error);
  }
}

利用FFmpeg转视频处理过程示例:

async function startReplay() {
  state = "recording";
  const wrapperEl = await page.$(".replayer-wrapper");
  console.log("开始转换,请稍等。。。");
  const ffmpegProcess = spawn("D:\\ffmpeg\\bin\\ffmpeg", [
    // fps
    "-framerate",
    "15",
    // 输入配置
    "-f",
    "image2pipe",
    "-i",
    "-",
    // 输出配置
    "-y",
    _output,
  ]);
  let processError: Error | null = null;

  //  每秒15次(帧率)定时执行
  const timer = setInterval(async () => {
    if (state === "recording" && !processError) {
      try {
        const buffer = await wrapperEl?.screenshot({
          encoding: "binary",
        });
        ffmpegProcess.stdin.write(buffer);
      } catch (error) {
        // ignore
      }
    } else {
      clearInterval(timer);
      if (state === "closed" && !processError) {
        console.log("转换结束");
        ffmpegProcess.stdin.end();
      }
    }
  }, 1000 / 15);

  ffmpegProcess.on("close", () => {/* 结束回调 */});
  ffmpegProcess.on("error", (error) => {/* 错误回调 */});
  ffmpegProcess.stdin.on("error", (error) => {/* 错误回调 */});
}

了解rrweb

Record and replay the web

rrweb is an open source web session replay library, which provides easy-to-use APIs to record user's interactions and replay it remotely.
-- rrweb官网

     rrweb 是 'record and replay the web' 的缩写,顾名思义,rrweb是记录并回放web页面中的用户操作的开源库。

入门介绍:

  1. rrweb 主要由 3 部分组成:

  2. 支持Typescript,为保证录制和回放数据结构一致采用了天然支持强类型的Typescript。
  3. 支持隐私保护,关于录制过程中的一些隐私内容,为开发者提供了丰富的隐私保护选项。
  4. 主流开源库,目前GitHub上的Star数达到了11.2k,且已发布稳定版本。在作者受访(rrweb 纪录片)时提到rrweb整个会话的提交量达到每月10亿次且保险行业占比最大,这算不算是对rrweb的一次有力的压测呢?

应用场景:

  1. 用户分析:通过对用户在网站上所做操作的像素级完美回放,很容易了解用户并优化他们的体验。
  2. 复现Bug:可以在浏览器中以一致的行为远程回放问题并对其进行调试。
  3. 网页展示:提供一种更轻量级和像素完美的方式,而不是录制视频来展示您的web页面。
  4. 实时协作:借助 rrweb 的强大功能, 可以很轻松的在网页上与他人进行实时的互动分享。

工作原理分析

     rrweb的录制方案从最初确定录制快照到解决定时快照会造成长时间无效录制导致冗余数据过大造成的存储空间浪费和大量复杂DOM对DIFF造成的性能压力,从而确定了最终版本按照一次DOM快照+监听DOM等相关变化相结合的方案。

rrweb方案确定时间线.png

Oplog的关注点有哪些?

  1. 通过MutationObserver来监听DOM结构的变化;
  2. 通过全局事件的监听来得到鼠标移动,窗口缩放,视图滚动,媒体交互;
  3. 通过监听input事件并劫持set记录输入组件的变化;
  4. 通过代理样式表相关函数来记录样式表的变化;
  5. 特殊的canvas组件和字体也需要代理对应的方法来处理。

operations log.png

MutationObserver API:

属性类型说明
typeString如果是属性变化,则返回 "attributes";如果是 characterData 节点变化,则返回"characterData";如果是子节点树 childList 变化,则返回 "childList"。
addedNodesNodeList返回被添加的节点。
removedNodesNodeList返回被移除的节点。
previousSiblingNode返回被添加或移除的节点之前的兄弟节点,或者 null。
nextSiblingNode返回被添加或移除的节点之后的兄弟节点,或者 null。
attributeNameString返回被修改的属性的属性名,或者 null。
attributeNamespaceString返回被修改属性的命名空间,或者 null。
oldValueString返回值取决于 MutationRecord.type。对于属性 attributes 变化,返回变化之前的属性值。对于 characterData 变化,返回变化之前的数据。对于子节点树 childList 变化,返回 null。
  • MutationObserver API的兼容性如下图所示:

image.png

写在最后

     在这次探索中我们通过调研了解到rrweb开源产品的优点并通过搭建完整的前后端系统来进行验证。使用rrweb录制的数据需要搭建特有的播放系统才能实现DOM重放所以传播性较低,这时我们可以采用puppeteer+FFmpeg将数据转存为视频后再进行分发。

     行为可回溯系统在产品体验优化、系统稳定性、用户操作规范等方面可以增加数据上的支持,让我们很直观的进行分析总结为下一步的提升做铺垫。同样基于rrweb录制方案可以实现的产品或功能还有很多,如基于Web平台的会议屏幕分享,针对Web平台的系统监控和金融保险公司销售行为回溯系统等。

关于我们

     高灯科技交易合规前端团队(GFE), 隶属于高灯科技(北京)交易合规业务事业线研发部,是一个富有激情、充满创造力、坚持技术驱动全面成长的团队, 团队平均年龄27岁,有在各自领域深耕多年的大牛, 也有刚刚毕业的小牛, 我们在工程化、编码质量、性能监控、微服务、交互体验等方向积极进行探索, 追求技术驱动产品落地的的宗旨,打造完善的前端技术体系。

  • 愿景: 成为最值得信任、最有影响力的前端团队
  • 使命: 坚持客户体验第一, 为业务创造更多可能性
  • 文化: 勇于承担、深入业务、群策群力、简单开放

Github: https://github.com/gfe-team<br/>
团队邮箱: gfe@goldentec.com<br/>
特别关注:

  • 文章如涉及源码&案例均可在gfe-team获取。
  • 文章如有引用内容但未声明的现象请直接联系我们处理。
  • 目前各版块正在大力建设中,尽请期待。。。

参考阅读

  1. rrweb-io
  2. rrweb:打开 web 页面录制与回放的黑盒子
  3. 如何将录制的DOM转成视频文件
  4. 浅析Web录屏技术方案与实现
  5. rrweb 带你还原问题现场
  6. 天眼探针基于rrweb实现前端异常视频录制与回放功能

本文发布自高灯科技交易合规前端团队(GFE),文章未经授权禁止任何形式的转载。我们常年招收前端,如果你准备换工作,又恰好喜欢这里,那就加入我们! ! !


GFE团队
17 声望1 粉丝

👀 欢迎来到GFE的思否空间