前端監控系列,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,可以參考下,相對於上面出現的代碼片段,做了一些封裝和錯誤判斷,是一個可以走通整個邏輯的
https://gitee.com/hoholove/study-code-snippet/blob/master/LOGGER/pool.js
鑑於本人能力有限,難免會有疏漏錯誤的地方,請大家多多包涵, 如果有任何描述不當的地方,歡迎後臺聯繫本人,領取紅包
本文分享自微信公衆號 - 神仙朱(skying-zhu)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。