clarity-js源码分析系列(一)之代码模块

123次阅读
2条评论

共计 4558 个字符,预计需要花费 12 分钟才能阅读完成。

前端监控一直是前端不可或缺的一部分,这里我调研了微软的 clarity,它们主要是针对用户的行为监控进行录制回放,并且能生成热力图分析。为了彻底搞清楚其中的原理,对clarity-js 进行了源码分析。

话不多说,直接开始!

整体代码结构

clarity-js
    -- src
        -- core
        -- data
        -- diagnostic
        -- interaction
        -- layout
        -- performance
        clarity.ts
        global.ts
        index.ts

下面围绕入口文件 index.ts 开始逐步分析。

代码分析

index.ts

入口文件,主要导出三个对象:

export {clarity, version, helper};

这里我们主要关注 clarity 对象。

clarity关键导出四个方法:start、pause、resume、stop,从字面上也能猜出他们分别代表的功能:开始、暂停、继续、停止。

先来看源码:

export function start(config: Config = null): void {
  // 先检查浏览器是否支持相关 api
  // 保证不会多次执行 start
  if (core.check()) {core.config(config);
    core.start();
    data.start();
    modules.forEach(x => measure(x.start)());
  }
}

export function pause(): void {if (core.active()) {data.event(Constant.Clarity, Constant.Pause);
    task.pause();}
}


export function resume(): void {if (core.active()) {task.resume();
    data.event(Constant.Clarity, Constant.Resume);
  }
}

export function stop(): void {if (core.active()) {
    // 以与 modules 初始化相反的顺序去停止
    modules.slice().reverse().forEach(x => measure(x.stop)());
    data.stop();
    core.stop();}
}

从源码很容易看出,这里主要就是针对整个监控流程的一个生命周期操作。主要是对 core、data、modules 这几个对象进行操作,其实最关键的部分就是初始化,我们来分模块看下。

core

core.config

说白了就是支持自定义配置项,具体配置内容先不深入讲,之后用到的时候再讨论。这里给出 Config 实例:

export interface Config {
  projectId?: string;
  delay?: number;
  lean?: boolean;
  track?: boolean;
  content?: boolean;
  mask?: string[];
  unmask?: string[];
  regions?: Region[];
  metrics?: Metric[];
  dimensions?: Dimension[];
  cookies?: string[];
  report?: string;
  upload?: string | UploadCallback;
  fallback?: string;
  upgrade?: (key: string) => void;
}

core.start

初始化操作:

export function start(): void {
  status = true;
  time.start();    // 时间打点开始
  task.reset();    // 重置任务队列
  event.reset();   // 移除所有事件绑定
  report.reset();  // 清除缓存的上报数据
  history.start(); // 开始记录 url 的 history state}

这里其他的方法都好理解,关键来看看最后的history.start

总共做了两件事:

1、绑定 window.popstate 事件

st=>start: 绑定 window.popstate 事件
cond=>condition: url 是否发生变化?sub1=>subroutine: 停止当前 clarity 实例
sub2=>subroutine: 250ms 后重新开启 clarity 实例
e=>end: 结束
st->cond
cond(yes)->sub1->sub2->e
cond(no)->e

2、代理 history.pushStatehistory.replaceState事件

st=>start: 代理 pushState、replaceState 事件
op1=>operation: 正常执行 pushState、replaceState 事件
cond1=>condition: 调用堆栈是否小于 20?cond2=>condition: url 是否发生变化?sub1=>subroutine: 停止当前 clarity 实例
sub2=>subroutine: 250ms 后重新开启 clarity 实例
e=>end: 结束
st->cond1
cond1(yes)->op1->cond2
cond1(no)->e
cond2(yes)->sub1->sub2->e
cond2(no)->e

这样就能确保当 url 地址发生变化时,能及时重启 clarity 实例,保证跟踪到每个页面的状态。

data

export function start(): void {metric.start(); // 初始化所有与性能相关的信息
  modules.forEach(x => measure(x.start)()); // 初始化数据,并且测量耗时
}

这里的主要目的就是初始化所有数据,包括了一系列需要记录的信息:如页面浏览器、页面来源、userid、页面长宽、鼠标指针等等。数据非常繁多,同时也支持自定义,总之是尽可能地去收集页面数据,方便之后的日志分析。

但看到这里同时引入一个问题:这么庞大的数据是怎么保存和上传分析的呢?别急,之后会拿来专门分析。

modules

这里加载了一些模块,然后进行初始化。模块包括:

diagnostic, layout, interaction, performance

那么这些模块在初始化时又做了些什么呢,来看看他们的操作。

diagnostic

通过代码发现,主要做了两件事:

1、绑定 window.error 事件,记录一些错误堆栈和相关信息。

2、初始化历史缓存,用来之后打 log

layout

这个模块跟页面元素的变化息息相关,又细分了很多模块。

首先看源码:

export function start(): void {
  // 这里的执行顺序非常重要
  doc.start();
  region.start();
  dom.start();
  mutation.start();
  discover.start();
  box.start();}

通过源码分析,可以得到各个模块的大概作用:

doc:记录整个页面的最大宽度和高度。

region:利用了IntersectionObserver,来观察元素的变化,记录元素的交互状态,方便之后的数据重放与还原。

dom:遍历所有元素,记录需要遮罩的元素和监听记录所有元素的属性、状态、性能变化。

mutation:利用MutationObserver,监听 DOM 树和 CSS 的变化。

discover:记录 dom 和 region 变化函数的耗时。

box:利用 ResizeObserver 监听元素 size 的变化。

interaction

这个模块主要是做一些跟交互有关的操作,先看代码:

export function start(): void {timeline.start();
  click.start();
  clipboard.start();
  pointer.start();
  input.start();
  resize.start();
  visibility.start();
  scroll.start();
  selection.start();
  submit.start();
  unload.start();}

分别做了以下事情:

timeline:记录跟踪 click 事件的时间线。

click:监听点击事件,记录点击元素相关信息。这里要着重看下记录了哪些信息,来看这段关键代码。

if (x !== null && y !== null) {
  state.push({time: time(), event, data: {
      target: t,  // 当前元素
      x,          // pageX
      y,          // pageY
      eX,         // 点击时相对元素坐标 X
      eY,         // 点击时相对元素坐标 Y
      button: evt.button,      // 点击按钮元素
      reaction: reaction(t),   // 是否是点击无交互元素,比如纯文本,或者非 "input", "textarea", "radio", "button", "canvas" 元素
      context: context(a),     // link 标签 a 元素的 target 类型,比如:blank、parent、top
      text: text(t),           // 点击文本,截取前 25 个非空字符
      link: a ? a.href : null, // 跳转链接
      hash: null
    }
  });
  schedule(encode.bind(this, event));
}

这样一来,就能相对完整地记录点击的元素信息,方便之后还原。

clipboard:监听 cut、copy、paste 事件,并记录相应的 event 对象。

pointer:监听所有跟鼠标指针交互相关的事件:mousedown、mouseup、mousemove、mousewheel、dblclick、touchstart、touchend、touchmove、touchcancel,并记录指针位置。

input:监听 input 事件,包括 value、attr、placeholder 等方面的隐私处理,主要记录 value。

resize:监听 window.resize 事件,记录 window 视窗变化。

visibility:监听 visibilitychange 事件,记录document.visibilityState

scroll:监听元素的 scroll 事件,记录当前滚动元素和滚动位置。

selection:监听元素 selectstart、selectionchange 事件,记录选区起始和结束锚点和元素。

submit:监听元素 submit 事件,记录当前元素。

unload:监听 window.pagehide 事件,记录事件,停止 clarity 实例。

performance

这里的模块很容易理解,就是记录页面的各种性能,主要包括以下两部分:

navigation:利用 PerformanceNavigationTiming 记录页面首屏性能指标,包括:DNS 解析时间、请求时间、DOM 解析时间、重定向时间等等。

observer:利用 PerformanceObserver 观测页面性能指标,包括:浏览器、资源、长任务、首次输入延迟、累积布局偏移、最大内容绘制。

总结

到此,我们分析了 clarity 的代码结构,和初始化时各个模块的分工。

下一篇,我将着重分析关键的数据存储和上报方式,并且回顾整个系统架构,整体分析 clarity 的设计理念。

正文完
 
西蒙
版权声明:本站原创文章,由 西蒙 2022-02-24发表,共计4558字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(2条评论)
验证码
前端终结者 评论达人 LV.1
2023-07-25 10:37:26 回复

上报的数据格式能给一下吗?

   
    西蒙 博主
    2023-09-18 19:43:04 回复

    @前端终结者 数据结构比较复杂,且有编码处理,建议直接看github源码,或者使用clarity在自己的页面上试一下。

     Macintosh  Chrome