设计一个前端埋点监控插件有哪些要点
前言
大家好这里是阳九,一个文科转码的野路子码农,热衷于研究和手写前端工具.
我的宗旨就是 万物皆可手写
新手创作不易,有问题欢迎指出和轻喷,谢谢
附上本人的git仓库 github.com/lzy19926, 支持的大佬们可以进来给本人的一些学习项目点个star嘛(是的我就是厚颜无耻要star)
首先我们来看一下 一个比较简易的前端监控的流程
现在我们将要把这个前端部分,封装成一个监控插件。
架构设计
首先我们大致知道了,需要有四个主要业务模块
- 用户事件监控模块 (eventWatcher)
- 页面性能监控模块 (performanceWatcher)
- 页面跳转监控模块 (routerWatcher)
- 错误监控模块 (errorWatcher)
为了保证我们程序的健壮性与可拓展性, 我们再给其加上一些辅助模块
- 数据上报模块(uploader) : 处理上报数据
- 控制台打印模块(logger) : 控制台打印上报记录
- 插件模块(plugin) : 可外接其他监控模块
然后他就变成了这样
接下来我们对每一个模块进行业务分析和优化点探索:
各模块优化整理
模块 | 优化点 |
---|---|
事件监控 | 冒泡处理,主动触发 |
页面性能监控 | 首屏渲染 ,异步资源传输 ,向下兼容 ,onload事件检查 |
页面跳转监控 | 路由劫持, 兼容hash和history路由模式 |
错误监控 | 异常监控,Promise异常监控 |
数据上报 | sendBeacon与gif上报, 使用uuid, batch发送, requestIdleCallback等 |
用户事件监控
用户事件有很多我们这里以最常见的click事件举例
埋点
埋点可以分为,代码埋点,可视化埋点和无埋点.
- 代码埋点:就是以嵌入代码的形式进行埋点
// tsx
// 被标记了trackEvent的dom,点击时会被记录事件
<button trackEvent={true}>点击</button>
优点:精确
缺点:工作量大,迭代麻烦
2.可视化埋点:通过可视化交互的手段,代替代码埋点,实际上跟代码埋点还是区别不大。也就是用一个系统来实现手动插入代码埋点的过程。
优点: 简易,可视化
缺点: 不灵活
3.无埋点:无埋点并不是说不需要埋点,而是全部埋点,前端的任意一个事件都被绑定一个标识,所有的事件都别记录下来。
优点:全量埋点,迭代方便,不会错埋
缺点:数据量大,性能和服务器压力。
监控点击事件
- 首先我们给按钮埋点
<button trackEvent={true}>点击</button>
- 拦截点击事件
document.addEventListener('click', (e) => { // 点击事件
// do something
})
- 识别埋点dom(冒泡)
如果点击了按钮内的a标签,我们仍然要将其识别为点击按钮,所以我们需要一层层向上查找dom
const domList:HTMLElement[] = getDomPath(e.target) // 获取点击的元素到最外层的元素数组
const target = domList.find(node=> // 遍历数组,找到拥有trackEvent属性的dom
node.hasAttribute("trackEvent")&&node.hasAttribute
)
if(!target) return // 无埋点直接返回
- 将事件封装为消息对象,传给上报模块进行上报(消息对象大概长这样)
// 我们这里封装一个消息对象message
type EventMessage = {
type:string // 事件类型(click)
eventId:string// 事件domId
triggerTime:number// 事件触发事件
url:string// 触发页面的url
... // 还有什么消息可以自己从e.target中获取
}
// 生成消息对象并传给上报模块
const eventMsg:EventMessage = {type:"click"...}
// 发送
emit(eventMsg)
页面性能监控
Performance API简介
Performance API 是浏览器提供给我们的用于检测网页性能的 API,其中包含四个主要API
-
Resource Timing API:与网页资源(脚本、样式、图片等)加载相关的耗时信息,定义了接口 PerformanceResourceTiming。
-
Navigation Timing API:从页面导航开始一直到 load 事件结束,中间经历过程的耗时信息。定义了接口 PerformanceNavigationTiming,此接口继承自 PerformanceResourceTiming 接口。
-
Performance.timing:老版本的PerformanceNavigationTiming,现已弃用
-
Paint Timing:与网页绘制相关的耗时信息。定义了接口 PerformancePaintTiming。
首屏性能监控
首先我们可以列举一下一些常用的性能指标
export type TimePerformance = {
fmp: number // 首屏渲染时间
fpt?: number // 白屏时间
tti?: number // 首次可交互时间
ready?: number // HTML加载完成时间
onload?: number // 页面完全加载时间
firstbyte?: number // 首包时间
dns?: number // dns查询耗时
appcache?: number // dns缓存时间
tcp?: number // tcp连接耗时
ttfb?: number // 请求响应耗时
trans?: number // 内容传输耗时
dom?: number // dom解析耗时
res?: number // 同步资源加载耗时
ssllink?: number // 同步资源加载耗时
redirect?: number // 重定向时间
unloadTime?: number // 上一个页面的卸载耗时
}
之后通过performance系列API获取关键部分的时间,来计算这些指标
(这里使用performance.timing这个老API做演示,也可以用新API进行替代)
const times:TimePerformance = {} // 记录各项时间数据
const t = window.performance.timing;
// 获取首屏渲染时间
const paintEntries = window.performance.getEntriesByType('paint')
times.fmp = paintEntries.pop().startTime
times.fpt = t.responseEnd - t.fetchStart; // 白屏时间
times.tti = t.domInteractive - t.fetchStart; // 首次可交互时间
times.ready = t.domContentLoadedEventEnd - t.fetchStart; // HTML加载完成时间
times.loadon = t.loadEventStart - t.fetchStart; // 页面完全加载时间
times.firstbyte = t.responseStart - t.domainLookupStart; // 首包时间
times.dns = t.domainLookupEnd - t.domainLookupStart; // dns查询耗时
times.appcache = t.domainLookupStart - t.fetchStart; // dns缓存时间
times.tcp = t.connectEnd - t.connectStart; // tcp连接耗时
times.ttfb = t.responseStart - t.requestStart; // 请求响应耗时
times.trans = t.responseEnd - t.responseStart; // 内容传输耗时
times.dom = t.domInteractive - t.responseEnd; // dom解析耗时
times.res = t.loadEventStart - t.domContentLoadedEventEnd; // 同步资源加载耗时
times.ssllink = t.connectEnd - t.secureConnectionStart; // SSL安全连接耗时
times.redirect = t.redirectEnd - t.redirectStart; // 重定向时间
times.unloadTime = t.unloadEventEnd - t.unloadEventStart; // 上一个页面的卸载耗时
最后我们监视一下页面onload事件,将需要的首屏渲染各项数据发送上去
window.addEventListener('load', () => {
emit(messsg) // 封装好首屏渲染消息,发送给数据上报模块
})
异步资源加载性能监控
首先,每当我们异步资源加载触发时,我们需要去对他们进行监控,触发我们的监控函数callback
浏览器给我们提供了一个 PerformanceObserver(MDN) API 用于创建监视器。
// 创建一个监视器并注册resource事件,每当异步资源加载时会触发回调callback
const observer = new PerformanceObserver(callback);
observer.observe({ entryTypes: ['resource'] });
在callback中,我们可以通过PerformanceResourceTiming(MDN)这个API去获取对应的时间
// 可通过API获取的一些属性
const performanceResourceTimingAttrs = {
transferSize: 0, // 传输内容大小
encodedBodySize: 0,// 删除任何应用的内容编码之前
decodedBodySize: 0,// 在删除任何应用的内容编码之后
duration: 0, // responseEnd和startTime相减
connectStart: 0, // 浏览器检索资源,开始建立与服务器的连接之前
connectEnd: 0, // 浏览器完成与服务器的连接以检索资源之后。
requestStart: 0, // 浏览器开始从服务器请求资源之前
responseStart: 0,// 浏览器收到服务器响应的第一个字节时
responseEnd: 0, // 浏览器收到资源的最后一个字节之后或紧接在传输连接关闭之前
...
};
还是一样,我们可以通过上述时间计算出我们想要的信息,封装起来上报。
页面跳转监控
这个模块就比较简单了,想必背过前端八股文的同学都知道hash路由和history路由的种种API
我们在这里劫持路由的跳转,通过window.history
或window.location
进行劫持,封装为路由跳转事件进行上报
(如果不知道history和location是啥的同学请自行百科前端路由原理谢谢)
const referer = document.referrer; // 获取是从哪个页面跳转来的
// 劫持history.pushState history.replaceState popState
const originPush = window.history.pushState.bind(window.history);
const originRepalce = window.history.replaceState.bind(window.history);
// 修改history的pushState方法
window.history.pushState = (data, title, url)=>{
originPush(data, title, url)// 先正常进行跳转
emit(message) // 再上报数据(自行封装message)
}
// 修改replaceState方法
window.history.replaceState = (data, title, url)=>{
originRepalce(data, title, url)// 先正常进行跳转
emit(message) // 再上报数据(自行封装message)
}
// popState同理
...
hash模式的话我们直接监听hashchange即可
// 监听hashchange
window.addEventListener('hashchange', () => {
...
emit(message)
});
错误监控模块
对于错误,浏览器也提供了监控错误的方法,即
- error事件(捕获资源加载错误)
// 可以获取资源加载错误,script.onError link.onError img.onError
window.addEventListener('error', (e) => {
handleError(e) // 处理错误并上报emit
}, true);
- unhandledrejection事件(捕获promise调用链未捕获异常)
// promise调用链未捕获异常
window.addEventListener('unhandledrejection', (e) => {
handleError(e) // 处理错误并上报emit
});
- 劫持console.error
// 劫持console.error
const originConsoleError = console.error;
// 上报每个error
console.error = (...errors)=>{
errors.forEach((e) => {
handleError(e) // 处理错误并上报emit
} );
originConsoleError.apply(console, errors);
};
数据上报
消息池
我们知道,之前的四个模块会不断发送message,我们在这里创建一个messagePool,将其统一管理,并分批发送
let messagePool = []; // 批次队列
定时发送埋点数据
我们可以将数据分批发送,每5s发送5条,使用定时器进行控制
let timer = setTimeout(() => { send(url) }, 5000)
传统方式Gif发送数据
我们将上报的数据JSON.stringfy()一下,放到Image的src属性中即可
const beacon = new Image();
beacon.src = `${url}?data=${JSON.stringify(data)}`;
web beacon(网络信标)发送数据
除了gif图片,从2014年开始,浏览器逐渐实现专门的API,来更优雅的完成这件事:Navigator.sendBeacon(MDN)
Navigator.sendBeacon(url,data)
相较于图片的src,这种方式的更有优势:
- 不会和主要业务代码抢占资源,而是在浏览器空闲时去做发送;
- 并且在页面卸载时也能保证请求成功发送,不阻塞页面刷新和跳转;
现在的埋点监控工具通常会优先使用sendBeacon,但由于浏览器兼容性,还是需要用图片的src兜底。
优化
-
我们可以将send函数通过requestIdleCallback(MDN) 和 requestAnimationFrame(MDN)等函数,嵌入浏览器空闲时进行发送优化
-
配合定时器setTimeout或者setInterval
后记
这篇文章也是肝了好几天, 我也是自己写了一个埋点插件,边写边输出文章。
后续会更新我手写的埋点监控插件的仓库。
同学们可以自行给架构添加其他模块
比如预留外接模块的位置
比如logger控制台打印
比如专门用来外接数据库的模块等等...
希望大家(自己能够再接再厉) 在这个寒冷的时代找到一丝积极的正能量。
转载自:https://juejin.cn/post/7195908197572427832