郁郁青青 长过千寻

数据埋点

    笔记本

    1. 前提
    2. 怎么做呢
      1. 在业务程序里埋一次点
    3. 我的实现
      1. tracker.js
      2. tracker-config.js
        1. 检查原始埋点数据
        2. 和页面元素 DOM 关联
      3. tracker-tools.js
    4. 注意
    5. 尾声
    6. Reference

前提

应用变得更受欢迎,了解用户的行为就更迫切,埋点的事就更重要了。前后端都会进行埋点,也有各自适合的场景,例如 api 回调里的埋点显然更适合后端,相比前端,这样更快也更精准。而前端的原住业务代码也很关注侵入的埋点代码,会希望它们能矜持一些,不至于扎堆妨碍业务。把一份埋点文档转译成埋点代码,这份代码最好就能像文档一样方便定位添加修改删除查看,并且和业务代码保持距离

怎么做呢

这里用各个元素DOMid来区分各个埋点数据,即用 DOM id 来进行埋点,这里介绍这种埋点方案。

根据前端埋点文档看,有三类埋点类型:

  • 点击事件埋点;
  • 页面进入离开式的绑定埋点;
  • 元素曝光的统计时长埋点。

之所以选择DOM id,是因为id和埋点性质相似,上面三类埋点也都是埋在DOM上:

  • 每个埋点是不同的,DOM id也都是唯一的;
  • 点击事件点击的是具体某个DOM
  • 页面绑定埋点将利用组件,组件是DOM
  • 被曝光的元素依然是DOM
  • DOM idDOM 树上,离JS业务代码最远;
  • 关联DOM更符合声明式编码。

后面的内容会围绕第一种“点击事件埋点”做介绍。

在业务程序里埋一次点

所以怎么做呢?下面是业务里的(一次)埋点部分代码(Vue):

1
2
3
4
5
6
7
8
9
10
11
12
// ... 省略
<Icon
id="icon"
@click="iconClickHandler($event, 0, 'firstTitle', 1632028563744)"
/>
// ... 省略
import { sendEventTagByIdClick } from './tracker.js'
function iconClickHandler(e, i, title, time) {
sendEventTagByIdClick({ id: e.currentTarget.id, iconIdx: i, iconTitle: title, iconTime: time }) // 埋点
// ... 业务程序
}
// ... 省略

上面是在用户点击 Icon 的时候触发一次埋点,这段埋点代码只是一次埋点流程的一部分,也是这部分和业务最亲密,所以抓住让读写代码费力的发源地,分开它们。

代码里idicon被传入到埋点函数sendEventTagByIdClick,这个id将被用来辨识埋点数据(“用 DOM id 来进行埋点”),并且该函数的其它入参只传入了最基础的数据(原始埋点数据),例如ititle和时间戳time,不过我想埋点的服务端是不会想收到一份时间戳的,所以格式化时间戳成可读时间的工作会留给整个埋点流程里的其它环节,只是这儿只想解耦埋点和业务。

我的实现

像下面这样创建 3 个文件,这 3 个文件拆分流程,分离业务和埋点:

1
2
3
4
5
6
...
--libs
|--tracker.js // 埋点业务
|--tracker-config.js // 这儿是个埋点数据表,埋点数据都列在这,自文档化
|--tracker-tools.js // 处理埋点数据的工具箱
...

接下来通过这仨文件来走过一次埋点的一生。

tracker.js

tracker.js向用户(业务程序开发者)提供接口函数,用户在业务层成功调用函数就完成 1 次埋点。

这个函数会接收入参,入参是原始埋点数据,然后传递生成最终埋点数据,最后将数据发送给服务端。下面是主要代码(js):

1
2
3
4
5
6
7
8
import { genTrackingFinalData } from './tracker-config.js'

export async function sendEventTagByIdClick(data) {
const eventTrackingData = await genTrackingFinalData(data)
if (eventTrackingData != null) {
api.trackEvent(eventTrackingData).then().catch(res => console.error(res))
}
}

这里之所以使用async/await同步代码,是因为在生成最终埋点数据的过程可能存在异步程序,例如向服务端请求一些数据。

可以看到这个文件像是一根通向服务端的管道,把埋点数据从业务程序流向服务端。

tracker-config.js

tracker-config.js是和埋点文档同步的文件,同时向用户(tracker.js 开发者)提供生成最终埋点数据的函数。下面是主要代码(js):

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
import { timeFormatter } from './tracker-tools.js'

export async function genTrackingFinalData(data) {
const eventTrackingName = genEventTrackingName(data) // 获取代表某一埋点的名称,例如`ICON`
await genEventTrackingData(data, eventTrackingName)
}

async function genEventTrackingData(data, eventTrackingName) {
const {
platform,
iconIdx, iconTitle, iconTime,
// ...
} = data

switch(eventTrackingName) {
// 最终埋点数据,和埋点文档同步的内容
case 'ICON': return {
aid: '按钮点击',
a_attrs: {
'参与渠道': platform,
'项目名称': '深夜诗人俱乐部',
'页面名称': '首页',
'按钮名称': `功能区${iconIdx}-${iconTitle}`,
'触发时间': timeFormatter(iconTime),
}
}
// ...
}
}

上面代码里的caseICON唯一表示了后面的埋点数据,是根据埋点数据起的名字;另外代码里能看到埋点数据存储使用了switch/case语句,没有使用下面这样的Map数据类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ... 省略
async function genEventTrackingData(data, eventTrackingName) {
// ...省略
const eventTrackingDataMap = new Map([
['ICON', {
aid: '按钮点击',
a_attrs: {
'参与渠道': platform,
'项目名称': '深夜诗人俱乐部',
'页面名称': '首页',
'按钮名称': `功能区${iconIdx}-${iconTitle}`,
'触发时间': timeFormatter(iconTime),
}
}],
// ...
])
return eventTrackingDataMap.get(eventTrackingName)
}

相较于switch/case会发现Map更适合陈列数据,更少的关键字读起来是线性的、无逻辑的,但这也带来运行时将加载所有数据的代价。由于tracker.js里向函数genTrackingFinalData传递的是原始埋点数据,不免需要在生成最终埋点数据前进行一些加工,当埋点数据很多、加工很多的时候,Map就浪费了性能。

就像这里的时间戳iconTime,它需要利用timeFormatter来生成经过排版格式化的可读时间字符串,当面对众多更重量级的加工函数,胜出的会是switch/case语句。

检查原始埋点数据

原始埋点数据从业务程序汇入tracker.js,从tracker.js传递给tracker-config.js分流开来,就像下图:

1
2
3
4
5
6
7
8
业务   tracker.js  tracker-config.js  

-->--| |-->--
| |
-->--|->--[]-->-|-->--
| |
-->--| |-->--
> 接收 > 传递 > 处理

埋点数据从进入tracker.js起已经经历了接收传递处理,然而在最后想要处理得安全还不能少了检查。之所以检查,是因为需要处理的埋点数据往往是服务端的返回数据,前端要检查这些数据保证符合埋点文档的规范,检查之后前端再决定怎样继续程序,例如不合规范就不埋点。

从上图看tracker.js只传递,不了解数据细节,所以业务程序和tracker-config.js都能检查数据,考虑到业务程序要减少接触埋点程序,最后应该在tracker.js之后的程序执行检查。上面的代码还没有实现检查的部分。

和页面元素 DOM 关联

上面提到过用DOM id来辨别埋点数据,下面就是DOM id和埋点数据关联的主要代码(js):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function genEventTrackingName(data) {
const { id } = data
const trackingCodeVerifierMap = new Map([
['ICON', id => /^icon/.test(id)], // 对应页面以`icon`为前缀的`DOM id`
// ...
])
const defaultTarget = '(*゚∀゚*)'
let eventTrackingName = defaultTarget
for (let [code, verifier] of trackingCodeVerifierMap) {
eventTrackingName = verifier(id) ? code : eventTrackingName
if (eventTrackingName !== defaultTarget) { break }
}
return eventTrackingName
}

在之前埋点数据已经和switch/caseCode映射,这里就让DOM idCode映射,这样id和埋点数据就映射了,这么做可以命名自由,让埋点程序内部使用Code、外部的业务程序使用DOM id,各自规范。

tracker-config.js和页面对DOM id的依赖看似不能并行开发,但只要在开始和服务端约定好原始打点数据,和埋点文档约定好最终打点数据,和页面约定好DOM id,页面、tracker.jstracker-config.js都是可以并行开发的。

tracker-tools.js

这个文件里就放一些奇奇怪怪的埋点工具吧,导出去给其它埋点文件使用。


上面的各文件拆分了埋点流程,业务程序和埋点程序保持距离了之后,相处变得和睦,定位添加修改删除查看的事情也更顺心了。

  • 定位:tracker-config.js -> 埋点数据 -> Code -> DOM id -> 业务
  • 添加:tracker-config.js -> 业务
  • 修改:tracker-config.js
  • 删除:
    • 只改 tracker-config.js,断开一环让程序找不到埋点数据
      • 改 Code,例如约定好在 Code 前加DELETE:::,例如DELETE:::ICON
      • 改 id,例如约定好在 id 前加DELETE:::,例如DELETE:::icon
      • 页面里改 id,例如约定好在 id 前加DELETE:::,例如DELETE:::icon
    • 直接删除对应埋点所有相关代码,改动 tracker.js、tracker-config.js
  • 查看:tracker-config.js

这么看来,埋点的事情都可以从tracker-config.js里开始。

注意

埋点不能影响正常业务流程,注意浏览器的 tcp 请求限制机制,即时取消超时请求,防止业务请求被挂起。

尾声

希望收到这样一份埋点代码的开发者,会像收到一封“情书”愉悦,润润色给下一位开发者。

Reference

关于数据埋点

基于指令和混合的前端通用埋点方案

网络分析参考-分析请求:“已为同一来源打开了六个(上限)TCP 连接。 仅适用于 HTTP/1.0 和 HTTP/1.1。”

美团点评前端无痕埋点实践

数据平台网站埋点统计实现原理与应用:ppt,介绍了异常 js 异常统计。

2021年 9月19日 星期日 20时32分26秒 CST - 我应该写一份「不怂理由清单」,最近反复回荡着一些话,像是“不要犹豫,等着等着时间一晃眼又过去了”、“世界很大,你要说话”、“总是低着头,总是贪生怕死”、“动辄心惊的弱者”之类的。

页阅读量:  ・  站访问量:  ・  站访客数: