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

话不多说,直接开始!

整体代码结构

1
2
3
4
5
6
7
8
9
10
11
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,从字面上也能猜出他们分别代表的功能:开始、暂停、继续、停止。

先来看源码:

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
30
31
32
33
34
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实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

初始化操作:

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

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

总共做了两件事:

1、绑定window.popstate事件

2、代理history.pushStatehistory.replaceState事件

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

data

1
2
3
4
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

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

首先看源码:

1
2
3
4
5
6
7
8
9
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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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:监听点击事件,记录点击元素相关信息。这里要着重看下记录了哪些信息,来看这段关键代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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的设计理念。