前端监控系列,SDK,服务、存储 ,会全部总结一遍,写文不易,点个赞吧
前端监控上报数据的时候,是怎么发请求的呢,是每产生一条监控数据就上报一次吗
当然不是了,如果监控点很多,那估计请求都快发爆炸了,请求发得多,不仅会加重服务器压力,数据丢失的概率也大,毕竟10条请求的成功率肯定比 一条请求 的成功率小嘛
所以才会出现日志池,这篇内容不属于前端监控的一部分,属于是其中的一个优化点
不多说了,开始正文
本文其实挺简单的,分为三个部分
1、基本思路
2、具体逻辑
3、代码Demo仓库
基本思路
最简单的说法就是
每次上报数据的时候,不会立即发送,把数据存到一个数组里面
然后设置 setTimeout,定时从数组里面取固定数量的日志(比如20条数据),然后再进行上报
简单画个图,如下
在这个基础上再进行优化
1、错误重试。上报请求发生错误的时候,会进行重试,以免日志就这么丢失,这里在离线日志中有过相关处理
2、页面关闭发送剩余日志。因为我们使用定时发送的方式,可能会存在用户关闭界面的时候,还有缓存的日志没有发送。
所以需要在最后一刻发送一波
下面就来详细说下具体的处理逻辑
具体逻辑
看了上面基本就知道这里就有三个步骤
1、定时发送
2、错误重试
3、监听页面关闭发送日志
1定时发送
1、把所有日志数据都会先缓存到一个数组中,调用上报方法的时候,直接 push 进去
const LOG_CACHE = []
function report(data){
LOG_CACHE.push(data)
}
2、初始化的时候会设置一个循环定时器,从 日志数组中,取一定数量的数据 进行上报
let timer = null;
const duration = 500;
const limit = 20;
let LOG_CACHE = [];
function init() {
loop();
}
function loop() {
timer = setTimeout(() => {
timer = null;
LOG_CACHE.length && send();
loop();
}, duration);
}
function send() {
const logs = LOG_CACHE.splice(0, limit);
if (!logs.length) return;
fetch("www.test.com/report", {
body: JSON.stringify(logs),
});
}
这个 定时器时间 和 单次上报的数量 可以自己定义
时间不能太长,导致积压的数据过多,丢失风险大
数量不能太大,不然请求体太大,响应时间长,丢失的成本大
我们这里设置的是,500ms 单次发送20条数据
2错误重试
当上报请求发生错误的时候,有两种选择
1、直接重试
2、缓存等待重试
直接重试
如果是偶尔的错误,可以直接重试,但是如果是频繁报错,可能是接口服务或者用户网络的问题
所以仍然直接重试没有什么意义
直接重试条件是,如果3s内发生错误没有超过3次,那么就直接重试。
每次报错叠加一次failCount,并且重设定时器,定时器会把错误次数置为0。
重试的做法是,把上报的数据,重新 push 进 数组
缓存等待重试
定时器内超过3次的,会存入本地。
缓存进本地的日志,什么时候会重试?
1、页面初始化时,读取本地数据,push 进数组
2、每次存本地时,会开启一个定时器,从本地读取数据,push 进数组
所以缓存进本地,是为了把数据延迟更久上报,同时保证数据不丢失
看下大概的实现代码
其中操作 indexdb 的代码是我简化写的,实际不是这么操作
更详细的关于存日志到 indexdb 的部分在 离线日志
let failCount = 0; // 上报错误次数
function send() {
const logs = LOG_CACHE.splice(0, limit);
if (!logs.length) return;
fetch("www.test.com/report", {
body: JSON.stringify(logs),
}).catch((e) => {
handlerReportError();
if (failCount < 3) {
LOG_CACHE.push(...logs); // 伪代码
} else {
saveLogs(logs)
}
});
}
let failTimer = 0; // 错误定时器
function handlerReportError() {
failCount++;
// 发生错误就重置定时器
clearTimeout(failTimer);
failTimer = setTimeout(() => {
failTimer = null;
failCount = 0;
}, 3000);
}
let readTimer = null; // 读取本地数据库定时器
function saveLogs(data) {
indexDBStore.add("fail_log", data);
if (!readTimer) return;
readTimer = setTimeout(() => {
const logs = indexDBStore.get("fail_log");
LOG_CACHE.push(...logs); // 伪代码
}, 3000);
}
3监听页面关闭发送日志
因为定时发送请求的原因,可能存在用户关闭页面,仍然有部分日志没有发送,所以需要监听页面关闭,把剩余日志发送一下
具体处理就是
1、监听页面关闭事件
2、使用 navigator.sendBeacon 发送请求
关于这部分存在的兼容性,我写在另一篇文章了,请看【兼容性】监听页面关闭发送请求
具体代码如下
四个事件都是为了监听页面关闭,但是各端支持程度不一样,所以全部监听,谁生效就用谁
使用一个标志位,只要有事件触发过了,其他事件就不用了
window.addEventListener("beforeunload", sendRemainLogs);
window.addEventListener("pagehide", sendRemainLogs);
window.addEventListener("unload", sendRemainLogs);
// IOS14 之前不会冒泡,只能监听document
document.addEventListener("visibilitychange ", () => {
if (document.visibilityState !== "visible") {
sendRemainLogs();
} else {
// 如果界面又显示了,说明没有关闭,重置标志位
sendRemainLogsSuccess = false;
}
});
let sendRemainLogsSuccess = false;
function sendRemainLogs(data) {
if (sendRemainLogsSuccess) return;
if (!navigator.sendBeacon) {
indexdbStore.add("fail_logs", data);
return;
}
const logs = LOG_CACHE;
const result = navigator.sendBeacon(
"www.test.com/report",
JSON.stringify(logs)
);
if (!result) {
indexdbStore.add("fail_logs", data);
}
}
现在所有的处理都说完了
现在来看下这个日志池的流程图
代码demo错误
把大概的实现写了一个demo,可以参考下,相对于上面出现的代码片段,做了一些封装和错误判断,是一个可以走通整个逻辑的
https://gitee.com/hoholove/study-code-snippet/blob/master/LOGGER/pool.js