前端日志上报的实现

前端日志上报的实现

在前端开发中,日志上报是一个常见的需求,它涵盖了从按钮点击到整个操作流程追踪的范围。为了达到这个目的,需要进行日志记录、埋点和日志上报等工作。

为什么我们需要前端日志

因为绝大多数用户都是通过前端来与系统进行交互的,前端的质量和稳定性就成了用户对于系统最直观的体现。同时前端代码又是直接运行在用户的电脑上的,很多情况下出现了错误或者页面崩溃的情况,开发人员都没有办法第一时间发现和进行处理。同时由于用户的系统和浏览器显示设备等多样性,使得很多情况未必能在开发时候就考虑周到。

综上所述,前端日志就显得格外重要。它能够及时反馈用户的错误,包括用户的浏览器,操作系统,显示分辨率等参数,以便后期复现以及修复问题。

日志内容

为了收集准确且有用的信息,以便用于故障排除和调试,前端日志应包括以下内容:

时间戳:错误发生的时间。

错误消息:关于所发生错误的简明描述。

错误类型:错误的类型,例如语法错误、运行时错误或逻辑错误。

严重程度:错误对系统或用户体验造成的影响级别。

用户信息:关于遇到错误的用户的详细信息,例如他们的浏览器、设备、操作系统和 IP 地址。

堆栈跟踪:导致错误的代码和函数的列表。

上下文信息:其他信息,可以帮助开发人员了解发生错误的情况,例如应用程序的当前状态、用户的操作以及任何以前的错误或事件。

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
export interface AppInfo {
appID?: string
version?: string
appName?: string
[key: string]: any
}

export interface SystemInfo {
ua?: string
isCookie?: number
cookie?: string
screenHeight?: number | string
screenWidth?: number | string
[key: string]: any
}

export interface UserInfo {
userID?: string
userLanguage?: string
token?: string
[key: string]: any
}

export interface EasyEvent {
eventType: string
logLevel?: LOG_LEVEL
elemId?: string
createTime?: string
extraParams?: object
}

日志上报的实现

我们可以相信的到,日志上报最方便的就是做成一个console类似的类,把错误信息直接传入方法中即可

1
2
3
4
5
6
7
8
easyLogReport.log({
eventType: 'onLoad',
elemId: 'App',
extraParams: {
// other extra params
type: 'onLoad'
}
})

eventType - 埋点事件类型,它可能是点击事件、页面初始化、页面激活、请求事件…

elemId - 埋点标识,后续进行埋点数据提取的重要标识

extraParams - 额外的上报内容,丰富埋点的含义帮助后续做更加精细、完善的分析

获取到了这些日志之后,我们可以在前端过滤一下这个日志,因为某些错误有可能在短时间内触发了很多次。我们就只需要把这些相同内容的日志聚合一下,没必要100个相同的报错一起给后端发去,减少网络流量以及后端处理压力。
实践中选择了后端进行处理,主要担心前端性能问题,而且队列也不能设置太大,其实对于重复生成的错误日志也不能很好地过滤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 过滤1秒内连续抛出同一错误
debounce = ()=>{
try {
if(this.timeout !== null){
clearTimeout(this.timeout);
}
this.timeout = setTimeout(()=>{
const max = this.props.max || 1;
if (this.queue && this.queue.size >= max) {
this.catchBack()
}
}, 1000);
} catch (error) {
console.log(this.selfError + "debounce",error)
}
}

之后就是给服务器发送日志了。这里并没有所谓最正确的做法,因为最终发送日志上报请求的方式有很多种,有些是GET请求、有些是请求一张图片而有些则是发送POST请求。不同的请求方式,发送日志内容的时机也不太一样。

如果是GET请求的方式进行日志上报,则一般是埋点触发后,产生日志条目就直接发送到日志服务。

如果是POST请求的方式,则有大多数会在项目中维护一个日志上报队列,从日志条数、上报间隔等维度进行日志条目的统一上报。

无论是使用哪种方式进行日志上报,都需要在上报前对对埋点的内容进行预处理,让它符合日志服务的要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* report log by img url function */
export const sendImage = (data: ReportContent, sendUrl: string)=>{
let image = new Image();
image.src = sendUrl + "/" + encodeURIComponent(JSON.stringify(data));
image.onerror = () => {
console.error('EasyLogReport - log data send from image src error')
}
}

/* report log by sendBeacon function */
export const beacon = (data: ReportContent, sendUrl: string)=>{
if (!navigator?.sendBeacon) {
console.error('EasyLogReport - current env don\'t support sendBeacon!')
return
}
navigator.sendBeacon(sendUrl, encodeURIComponent(JSON.stringify(data)))
}

上面的代码分别是通过图片get的方法进行返回,以及使用因为Beacon的方式进行返回。

这里建议使用因为Beacon的方式,因为Beacon API 是用来向 server 端传输小批量数据的,并且不需要等待 server 端的响应。相比与xhr的请求,Beacon的优点不接受响应,浏览器可以把请求队列起来,然后发送,而不会阻塞任何其他代码的执行。

比如说你需要记录 unload 或 beforeunload 的事件日志记录。这些事件通常在用户跳转链接时触发。这里的问题是,unload 事件中的代码可以阻塞代码执行且推迟页面的卸载。如果当前页面的卸载被推迟了,那也就是说,下个页面的加载也被推迟了,最后导致页面跳转的体验变得迟缓。

最后用一张图来总结一下整个日志上报的实践过程。

5c6f01d93d1d4de1bf8eb2d49e714e59~tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0

最后,插件地址在 https://www.npmjs.com/package/easy-log-report,这个只是日志上报插件,后续会进行错误捕捉,性能监控插件的开发。

参考资料:

前端错误日志收集方案

前端日志上报实践

手撸一个前端日志上报组件

react-error-catch


前端日志上报的实现
https://zjw93615.github.io/2023/04/13/杂谈/前端日志上报的实现/
作者
前菜
发布于
2023年4月13日
许可协议