埋点方式

​ 所谓’埋点’是数据采集领域(尤其是用户行为数据采集领域)的术语,指的是针对特定用户行为或事件进行捕获、处理和发送的相关技术及其实施过程。. 比如用户某个icon点击次数、观看某个视频的时长等等。

​ 埋点实际上是对特定事件或者行为的数据监控和上报,常见的埋点上报方式有ajax,img,navigator.sendBeacon

基于ajax的埋点上报

介绍

​ 因为埋点实际上是对关键节点的数据进行上报是和服务端交互的一个过程,所以我们可以和后端约定一个接口通过ajax去进行数据上报。

代码实现

我们可以封装一个方法,代码如下:

1
2
3
4
5
6
7
8
9
10
function buryingPointAjax(data) {
return new Promise((resolve, reject) => {
// 创建ajax请求
const xhr = new XMLHttpRequest();
// 定义请求接口
xhr.open("post", '/buryingPoint', true);
// 发送数据
xhr.send(data);
});
}

使用时,直接调用即可

1
2
let info = {}
buryingPointAjax(info) // 这样就成功上报了info的对象

缺点

​ 一般而言,埋点域名并不是当前域名,因此请求会存在跨域风险,且如果ajax配置不正确可能会浏览器拦截。因此使用ajax这类请求并不是万全之策。

基于img的埋点上报

​ 上面可以看到如果使用ajax的话,会存在跨域的问题。而且数据上报前端主要是负责将数据传递到后端,并不过分强调前后端交互。
因此我们可以通过一些支持跨域的标签去实现数据上报功能。

1
script,link,img就是我们上报的数据的最好对象

先说结论,这里推荐使用img标签去实现。

script及link的缺陷

​ 因为埋点涉及到请求,因此我们需要保证script和link标签的src可以正常请求。如果使用script和link,我们需要将标签挂载到页面上。

验证缺陷

不妨验证下,我们在管理台中加入以下代码:

1
2
let a = document.createElement('script')
a.src = 'https://lf-headquarters-speed.yhgfb-cn-static.com/obj/rc-client-security/web/stable/1.0.0.28/bdms.js'

创建一个script标签,未挂载中页面上,并不会发起请求

image.png

书接上文,当我们将这个标签挂载中页面上时:

1
document.body.appendChild(a)

这时发起了请求

image.png

结论

​ 当我们使用script和link进行埋点上报时,需要挂载到页面上,而反复操作dom会造成页面性能受影响,而且载入js/css资源还会阻塞页面渲染,影响用户体验,因此对于需要频繁上报的埋点而言,script和link并不合适。

基于img做埋点上报

​ 通常使用img标签去做埋点上报,img标签加载并不需要挂载到页面上,基于js去new image(),设置其src之后就可以直接请求图片。

验证img优势

控制台去创建一个image标签,如下:

1
2
var img=new Image();
img.src="https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/img/MaskGroup.13dfc4f1.png";

可以看到即便未被挂载到页面上依旧发起了请求。

image.png

结论

因此当我们做埋点上报时,使用img是一个不错的选择。

  1. img兼容性好
  2. 无需挂载到页面上,反复操作dom
  3. img的加载不会阻塞html的解析,但img加载后并不渲染,它需要等待Render Tree生成完后才和Render Tree一起渲染出来

注:通常埋点上报会使用gif图,合法的 GIF 只需要 43 个字节

基于Navigator.sendBeacon的埋点上报

​ Navigator.sendBeacon是目前通用的埋点上报方案,Navigator.sendBeacon方法接受两个参数,第一个参数是目标服务器的 URL,第二个参数是所要发送的数据(可选),可以是任意类型(字符串、表单对象、二进制对象等等)。

介绍

navigator.sendBeacon() 方法可用于通过 HTTP POST 将少量数据 异步 传输到 Web 服务器。

作用

​ 它主要用于将统计数据发送到 Web 服务器,同时避免了用传统技术(如:XMLHttpRequest)发送分析数据的一些问题。

补充

​ sendBeacon 如果成功进入浏览器的发送队列后,会返回true;如果受到队列总数、数据大小的限制后,会返回false。返回ture后,只是表示进入了发送队列,浏览器会尽力保证发送成功,但是否成功了,不会再有任何返回值。

例子

以掘金为例:

image.png

这里发了一个post请求,将小量的数据发到服务端,用于统计数据

image.png

优势

​ 相较于img标签,使用navigator.sendBeacon会更规范,数据传输上可传输资源类型会更多。对于ajax在页面卸载时上报,ajax有可能没上报完,页面就卸载了导致请求中断,因此ajax处理这种情况时必须作为同步操作.

​ sendBeacon是异步的,不会影响当前页到下一个页面的跳转速度,且不受同域限制。这个方法还是异步发出请求,但是请求与当前页面脱离关联,作为浏览器的任务,因此可以保证会把数据发出去,不拖延卸载流程。

总结

  • 前端埋点上报常使用ajax,img,navigator.sendBeacon。
  • 不推荐使用ajax
  • 如果考虑兼容性的话,img是不二之选。
  • 目前最合适的方案是navigator.sendBeacon,不仅是异步的,而且不受同域限制,而且作为浏览器的任务,因此可以保证会把数据发出去,不影响页面卸载。

常见埋点行为

点击触发埋点

绑定点击事件,当点击目标元素时,触发埋点上报。

1
2
3
function clickButton(url, data) {
navigator.sendBeacon(url, data)
}

页面停留时间上报埋点

路由文件中,初始化一个startTime,当页面离开时通过路由守卫计算停留时间。

1
2
3
4
5
6
7
8
9
10
11
let url = ''// 上报地址
let startTime = Date.now()
let currentTime = ''
router.beforeEach((to, from, next) => {
if (to) {
currentTime = Date.now()
stayTime = parseInt(currentTime - startTime)
navigator.sendBeacon(url, {time: stayTime})
startTime = Date.now()
}
})

错误监听埋点

通过监听函数去接收错误信息。

vue错误捕获

1
2
3
app.config.errorHandler = (err) => { 
navigator.sendBeacon(url, {error: error.message, text: 'vue运行异常' })
}

JS异常与静态资源加载异常

1
2
3
4
5
6
7
window.addEventListener('error', (error) => { 
if (error.message) {
navigator.sendBeacon(url, {error: error.message, text: 'js执行异常' })
} else {
navigator.sendBeacon(url, {error: error.filename, text: '资源加载异常' })
}
}, true)

请求错误捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
axios.interceptors.response.use(
(response) => {
if (response.code == 200) {
return Promise.resolve(response);
} else {
return Promise.reject(response);
}
},
(error) => {
// 返回错误逻辑
navigator.sendBeacon(url, {error: error, text: '请求错误异常' })
}
);

内容可见埋点

通过交叉观察器去监听当前元素是否出现在页面

1
2
3
4
5
6
7
8
9
10
11
12
// 可见性发生变化后的回调 
function callback(data) {
navigator.sendBeacon(url, { target: data[0].target, text: '内容可见' })
}
// 交叉观察器配置项
let options = {};
// 生成交叉观察器
const observer = new IntersectionObserver(callback);
// 获取目标节点
let target = document.getElementById("target");
// 监听目标元素
observer.observe(target);

后续

开发的时候可以封装这三种上报方法

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
// sendBeacon 上报
export async function sendBeacon( {url = '', params }: reportParams) {
if (navigator?.sendBeacon && url) {
const isSuccess = await navigator?.sendBeacon(url, JSON.stringify(params));
if (isSuccess) return true;
}
return false;
}

// img 上报
export function sendImg({ img = '', params }: reportParams) {
return new Promise<boolean>((resolve, reject) => {
const imageData = objectToQueryString(params)
const img_o = new Image();
img_o.onload = () => resolve(true);
img_o.onerror = () => reject(false);
img_o.src = `${img}?${imageData}`;
})
}

// ajax 上报
export function sendAjax({ req = '', params }: reportParams) {
return new Promise<boolean>((resolve, reject) => {
if (req) {
postAction(req, params)
.then(() => resolve(true))
.catch(() => reject(false));
} else {
reject(false);
}
});
}

使用的时候再导出一个真实上报函数,有由用户决定使用什么上报组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 基础上报函数
export async function reportEvent(params: reportParams, reportType:string[] = [IMG, BEACON, AJAX]) {
let finalType = false
for (const key in reportType) {
if (!finalType) {
try {
await EVENT_REPORT_FUNCTION_MAP[key](params).then(()=>{
finalType = true
})
} catch (error) {
console.error(error)
}
}
}
return finalType
}

​ 用户卸载网页的时候,有时需要向服务器发一些数据。很自然的做法是在unload事件或beforeunload事件的监听函数里面,使用XMLHttpRequest对象发送数据。但是,这样做不是很可靠,因为XMLHttpRequest对象是异步发送,很可能在它即将发送的时候,页面已经卸载了,从而导致发送取消或者发送失败。

​ 解决方法就是unload事件里面,加一些很耗时的同步操作。这样就能留出足够的时间,保证异步 AJAX 能够发送成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
function log() {
let xhr = new XMLHttpRequest();
xhr.open('post', '/log', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('foo=bar');
}
window.addEventListener('unload', function(event) {
log();
// a time-consuming operation
for (let i = 1; i < 10000; i++) {
for (let m = 1; m < 10000; m++) { continue; }
}
});

​ 上面代码中,强制执行了一次双重循环,拖长了unload事件的执行时间,使得异步 AJAX 能够发送成功。

​ 上面这种做法卸载时间被硬生生拖长了,后面页面的加载被推迟了,用户体验不好。

​ 为了解决这个问题,浏览器引入了Navigator.sendBeacon()方法。这个方法还是异步发出请求,但是请求与当前页面线程脱钩,作为浏览器进程的任务,因此可以保证会把数据发出去,不拖延卸载流程。

1
2
3
4
5
window.addEventListener('unload', logData, false);

function logData() {
navigator.sendBeacon('/log', analyticsData);
}