前端埋点监控

埋点

  • 针对特定用户行为或事件进行捕获、处理和发送的相关技术及其实施过程
基于Ajax的埋点上报
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function buryingPointAjax(data) {
return new Promise((resolve,reject) => {
// 创建ajax请求
const xhr = new XMLHttpRequest()
// 定义请求接口
xhr.open('post','/buryingPoint',true)
// 发送数据
xhr.send()
})
}

// use
let info = {}
buryingPointAjax(info)

缺点

  • 埋点域名有可能不是当前域名,因此请求会存在跨域风险
基于img的埋点上报

通过支持跨域的标签去实现数据上报功能

script link

  • script、link 进行埋点上报,需要挂载到页面上,但反复操作dom会造成性能开销,并且载入js/css资源会阻塞页面渲染,影响用户体验

img

  • img加载并不需要挂载到页面上,通过new Image,设置src可以直接请求图片
1
2
const img = new Image()
img.src = "https://www.baidu.com/assets/png/test.png"
  • img 的加载不会阻塞html的解析,但img加载后并不渲染,它需要等待render tree生成完后才和render tree 一起渲染出来
  • 通常埋点上报会使用gif图,合法的GIF只需要43个字节
基于Navigator.sendBeacon 的埋点上报
  • 它是目前通用的埋点上报方案,该方法可用于通过HTTP POST将少量数据异步传输到Web服务器
  • navigator.sendBeacon(url,data)
    • url:请求地址
    • data:数据类型
  • navigator.sendBeacon 如果成功进入浏览器的发送队列后,会返回true;如果受到队列总数、数据大小的限制后,会返回false。返回true后,只是表示进入了发送队列,浏览器会尽力保证发送成功,但是否成功了,不会再有任何返回值。
  • navigator.sendBeacon是异步的,不会影响当前页到下一个页面的跳转速度,且不受同域限制。这个方法还是异步发出请求,但是请求与当前页面脱离关联,作为浏览器的任务,因此可以保证会把数据发出去,不拖延卸载流程

常见埋点行为

点击触发埋点
1
2
3
function clickButton(url,data) {
navigator.sendBeacon(url, data)
}
页面停留时间上报埋点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 路由文件中,初始化一个startTime,当页面离开时,通过路由守卫计算停留时间
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
})
}
})
错误监听埋点
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// vue错误捕获
app.config.errorHandler = (err) => {
navigator.sendBeacon(url,{
error:error.message,
text:'一些错误信息'
})
}

// js异常与静态资源加载异常
window.addEventListener('error',(error) => {
if(error.message) {
navigator.sendBeacon(url, {
error:error.message,
text:'一些JS错误信息'
})
} else {
navigator.sendBeacon(url, {
error:error.message,
text:'一些资源错误信息'
})
}
})

// 请求错误捕获
axios.interceptors.response.use(
(response) => {
if(response.code == 200) {
return Promise.resolve(response)
} else {
return Promise.reject(response)
}
},
(error) => {
// 返回错误逻辑
navigator.sendBeacon(url,{
error:error.message,
text:'一些请求错误信息'
})
}
)

// 内容可见埋点
// 通过交叉观察器去监听当前元素是否出现在页面
// 可见性发生变化后的回调
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// ajax上报
export function sendAjax({ req:'',params }: reportParams) {
return new Promise<bolean>((resolve,reject) => {
if (req) {
postInfoApi(req, params)
.then(() => resolve(true))
.catch(() => reject(false))
} else {
reject(false)
}
})
}

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

// sendBeacon上报
export function sendBeacon({ url='',params }:reportParams) {
if(navigator?.sendBeacon && url) {
const isSuccess = await navigator?.sendBeacon(url, JSON.stringify(params))
if(!isSuccess) return true
}
return false
}

// 最终导出
// 基础上报函数
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
}