公司系统需求加上埋点功能,用来统计各页面功能的使用情况。于是,结合网上资料以及之前使用埋点系统的经历,仔细研究研究。
调研
埋点分类
常见的埋点类型有三种
- 代码埋点
- 通过 JavaScript 代码主动将所需要的信息上报给服务器。
- 优点:可以精确的上报所需的数据,对于少量埋点需求较为合适。
- 缺点:代码遍布项目各处,不好维护管理。且埋点只能通过开发人员手动完成。
- 可视化埋点
- 需要另外一个可视化埋点圈选系统来圈选需要埋点的 DOM 元素。然后通过在系统中集成 SDK 来主动上报这些区域的埋点信息。其实算是另一种意义上的代码埋点。
- 优点:有圈选系统可以让产品、运维同学自行决定埋点区域。
- 缺点:适用范围有限,如内网系统、移动端 Hybrid 页面这些就很难用外部的可视化埋点来做。
- 无埋点
- 其实也叫全量埋点,即全局监听系统事件,把用户所有行为都进行上报。
- 优点:行为数据记录全面,无需增加或维护埋点代码。
- 缺点:上报数据量大,对服务器有一定压力。且无法精确上报某一功能的特停数据。
埋点目标
- 数据监控:通过埋点让产品运维同学知道项目当前的具体情况,从而有针对性的去优化项目。
- 异常监控:从开发角度去收集项目中发生的 JS 报错、接口报错等异常情况。发现问题、解决问题、优化项目。
- 性能监控:收集项目运行中的各种性能指标,如白屏时间、首屏加载时间、接口请求时间等等。
埋点 SDK 实现猜想
以我之前工作中用到过的埋点系统 GrowingIO 为例。我们可以通过它的 SDK 文档 来验证上面的理论。
- 它通过全局引入 JS 代码的方式来进行集成,它会在 window 全局对象下加上一个 gio 函数处理各种埋点行为。
- 由于埋点系统会为很多项目服务,所以需要初始化的时候加上
gio('init', 'your projectId', {})
。 - 它要求在需要圈选的 DOM 元素上
data-growing-container
属性,这其实是 HTML 元素的 dataset 属性,可以用来对元素进行自定义数据属性的读写操作。有了圈选标记,埋点事件拦截的时候就可以指哪打哪了。 - 它通过
gio('track', eventId, eventLevelVariables);
函数实现了主动埋点行为,这个自然是必不可少的。总有埋点需求是自动埋点做不了的。 - 它的无埋点记录的是所有元素的点击量和浏览量,应该是全局监听了元素的点击和可视事件。
- 它的可视化圈选是通过 XPath 来唯一定位一个元素的,那么可视化圈选其实就是将目标 DOM 的 xPath 保存起来,在埋点的时候去获取指定 DOM 元素的点击量和访问量。(关于 xpath 的使用可以看 Introduction to using XPath in JavaScript - XPath | MDN)
我的埋点
方案选择
由于项目的埋点只需要记录一些指定的行为,所以全埋点方案被我 PASS 了。同时也没有必要另外写一个页面去做埋点的圈选,最终,选择了最简单粗暴地主动埋点。
主动埋点 1.0
一开始埋点其实很简单,通过在 JavaScript 代码中写埋点代码来进行实现。
定义一个埋点工具对象。
// logger.js
export default {
...,
track(data) {
const configInfo = this.getConfigInfo() // 一些公共配置信息,如用户名、token、时间、url 等
return fetch.post('/api/v1/web/log', {
...data,
...configInfo,
})
},
}
将 logger 对象绑到 Vue 的原型中。
// main.js
Vue.prototype.$logger = logger
在需要的地方主动埋点。
<template>
<div>
<el-button @click="download">download</el-button>
</div>
</template>
<script>
export default {
name: 'demo',
methods: {
download() {
window.open('file url')
this.trackLogger()
},
trackLogger() {
this.$logger.track({
component_id: '2',
component_name: '下载按钮',
})
},
},
}
</script>
<style lang="scss" scoped></style>
遇到的问题
其实主动埋点应该就是如此,但随着埋点代码的逐渐增多(已经从起初的 20 条增加到 203 条了……)。看代码的时候就非常难受了。描述一个场景:
- 需要检查同事代码中的埋点情况,由于不清楚他的代码,就需要一点点找了。
- 全局搜索埋点代码
$logger.track()
,得到 n 个包含有埋点代码的函数。 - 再逐个跟踪这些包含埋点代码的函数的触发位置(有时候还会是函数嵌函数),最终找到绑定函数的 DOM 元素。
- 如此才算是确定了一个元素拥有埋点行为。
声明式 vs 命令式
面对上面的场景,我在想有没有办法能够省去逐个查函数的步骤,让主动埋点代码更加直观呢。这里就得提到另外一个点了:声明式代码与命令式代码的区别了。
- 声明式代码:如 HTML、XML、CSS,它的特点是可读性更强,描述的时候更符合直觉、更形象。
- 命令式代码:如 JavaScript,它的特点是更符合行为步骤的思考模式,适合处理一些逻辑性强的功能。
举几个栗子
比如画一幅画,用声明式的方式来描述是“我要画一幅画,它有青草、大树和天空”;而用命令式的方式描述是"我要画一幅画,首先需要画青草,然后再画大树,最后加上蓝色的天空。"
还有一个例子,在 vue 中有一个 createElement
函数,它可以在 vue 的 render 函数中命令式的创建 DOM 元素。
createElement(
'anchored-heading',
{
props: {
level: 1,
},
},
[createElement('span', 'Hello'), ' world!'],
)
但这种命令式的写法可读性很差。vue 官方也发现了这个问题,于是引进了 JSX 来弥补这个缺陷。
import AnchoredHeading from './AnchoredHeading.vue'
new Vue({
el: '#demo',
render: function (h) {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
},
})
JSX 的写法明显就更偏向于声明式。
那么回过来复习下主动埋点的目的:通过代码主动上报指定 DOM 元素的行为事件。所以个人感觉用声明式写法会更好一些。
主动埋点 2.0
说干就干,我试着将命令式埋点改为声明式埋点。
首先在入口文件 main.js
中引入全局注册逻辑。
// 事件名称
const COMPONENT_MAP = {
1: '图表切换',
2: '下载按钮',
}
// 修复点击子元素不上报埋点信息的问题
function bindDataset(el, value) {
el.dataset.loggerId = value
// 递归绑定 dataset 到所有子集上
el.children.forEach((child) => {
bindDataset(child, value)
})
}
// 全局注册指令,在需要埋点的 DOM 上加上 dataset
Vue.directive('logger', {
bind: function (el, binding) {
const { value } = binding
bindDataset(el, value)
},
})
// 全局监听组件点击事件,加入防抖是为了避免短时间内快速重复点击
document.addEventListener(
'click',
throttle((e) => {
if (e.target.dataset.loggerId) {
this.$logger.track({
component_id: e.target.dataset.loggerId,
component_name: COMPONENT_MAP[e.target.dataset.loggerId],
})
}
}, 2000),
)
在上面代码中,我将埋点通过vue 指令的方式将埋点信息绑定到目标 DOM 的 dateset 上面。然后通过全局 click 事件拦截来获得目标元素的点击行为,并上报埋点信息。
- 由于没有找到如何直接在 Vue 组件上直接操作 DOM 的方式(ref 不算,那个需要写很多的 ref='xxx' 很不划算),所以想到了 Vue 指令。
- 在点击 DOM 元素的时候,如果元素中有子节点那么全局 click 事件只能捕获到子节点的事件,于是我偷懒将子节点都加上了 dataset。(组件的子元素不会太多,偷个懒了)
以上遇到的两个问题个人感觉不是最佳方案,如果有好的解决方案欢迎讨论呀!
使用方式如下,可读性上强了不少。
<div class="filter-wrap" @click="setFilterPopupVisible(true)" v-logger="1">
<img class="filter-icon" :src="filterIconUrl" />
<img
class="filter-icon-checked"
:src="filterSelectedIconUrl"
v-show="isFilterActive"
/>
</div>
如此,以后在看埋点代码的时候只要全局搜索 v-logger
就可以很方便的看到有哪些 DOM 元素或者 vue 组件是进行了埋点的了。不需要反复去查各种事件了。
最后
折腾了一圈,主要就是想解决看主动埋点代码太恶心的问题。然后顺便复习一些知识点。
- 声明式编程和命令式编程
- 埋点相关知识
- dataset
- xpath
网友评论