轉(zhuǎn):https://zhuanlan.zhihu.com/p/40273861
利用serviceWorker.
本文是如何監(jiān)控網(wǎng)頁的卡頓?的下篇格带。今天我們把話題聚焦在如何監(jiān)控網(wǎng)頁的崩潰上撤缴。
崩潰和卡頓有何差別?
卡頓也就是網(wǎng)頁暫時(shí)響應(yīng)比較慢践惑,JS 可能無法及時(shí)執(zhí)行腹泌,這也是上篇網(wǎng)頁卡頓監(jiān)控所依賴的技術(shù)點(diǎn)。
但崩潰就不一樣了尔觉,網(wǎng)頁都崩潰了凉袱,頁面看不見了,JS 都不運(yùn)行了侦铜,還有什么辦法可以監(jiān)控網(wǎng)頁的崩潰专甩,并將網(wǎng)頁崩潰上報(bào)呢?
但钉稍,天無絕人之路涤躲,方法總是有的。
load 與 beforeunload 事件
搜遍互聯(lián)網(wǎng)贡未,幾乎找不到方法种樱,最終碰上了這篇文章。本文利用 window 對(duì)象的 load 和 beforeunload 事件實(shí)現(xiàn)了網(wǎng)頁崩潰的監(jiān)控俊卤。
window.addEventListener('load', function () {
sessionStorage.setItem('good_exit', 'pending');
setInterval(function () {
sessionStorage.setItem('time_before_crash', new Date().toString());
}, 1000);
});
window.addEventListener('beforeunload', function () {
sessionStorage.setItem('good_exit', 'true');
});
if(sessionStorage.getItem('good_exit') &&
sessionStorage.getItem('good_exit') !== 'true') {
/*
insert crash logging code here
*/
alert('Hey, welcome back from your crash, looks like you crashed on: ' + sessionStorage.getItem('time_before_crash'));
}
一圖勝千言:
<figcaption style="margin-top: calc(0.666667em); padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">使用 load 和 beforeunload 事件實(shí)現(xiàn)崩潰監(jiān)控</figcaption>
這個(gè)方案巧妙的利用了頁面崩潰無法觸發(fā) beforeunload 事件來實(shí)現(xiàn)的嫩挤。
在頁面加載時(shí)(load 事件)在 sessionStorage 記錄 good_exit 狀態(tài)為 pending,如果用戶正常退出(beforeunload 事件)狀態(tài)改為 true消恍,如果 crash 了岂昭,狀態(tài)依然為 pending,在用戶第2次訪問網(wǎng)頁的時(shí)候(第2個(gè)load事件)狠怨,查看 good_exit 的狀態(tài)约啊,如果仍然是 pending 就是可以斷定上次訪問網(wǎng)頁崩潰了!
但這個(gè)方案有問題:
- 采用 sessionStorage 存儲(chǔ)狀態(tài)佣赖,但通常網(wǎng)頁崩潰/卡死后恰矩,用戶會(huì)強(qiáng)制關(guān)閉網(wǎng)頁或者索性重新打開瀏覽器,sessionStorage 存儲(chǔ)但狀態(tài)將不復(fù)存在憎蛤;
- 如果將狀態(tài)存儲(chǔ)在 localStorage 甚至 Cookie 中枢里,如果用戶先后打開多個(gè)網(wǎng)頁,但不關(guān)閉,good_exit 存儲(chǔ)的一直都是 pending栏豺,完了彬碱,每有一次網(wǎng)頁打開,就會(huì)有一個(gè) crash 上報(bào)奥洼。
全民直播 一開始采用的就是這個(gè)方案巷疼,發(fā)現(xiàn)就算頁面做了優(yōu)化,crash 不下降灵奖,與 PV 保持比例嚼沿,才意識(shí)到這個(gè)方案的問題之處。
基于 Service Worker 的崩潰統(tǒng)計(jì)方案
隨著 PWA 概念的流行瓷患,大家對(duì) Service Worker 也逐漸熟悉起來骡尽。基于以下原因擅编,我們可以使用 Service Worker 來實(shí)現(xiàn)網(wǎng)頁崩潰的監(jiān)控:
- Service Worker 有自己獨(dú)立的工作線程攀细,與網(wǎng)頁區(qū)分開,網(wǎng)頁崩潰了爱态,Service Worker 一般情況下不會(huì)崩潰谭贪;
- Service Worker 生命周期一般要比網(wǎng)頁還要長耳高,可以用來監(jiān)控網(wǎng)頁的狀態(tài)排抬;
- 網(wǎng)頁可以通過 navigator.serviceWorker.controller.postMessage API 向掌管自己的 SW 發(fā)送消息哆键。
基于以上幾點(diǎn)凡傅,我們可以實(shí)現(xiàn)一種基于心跳檢測(cè)的監(jiān)控方案:
[圖片上傳失敗...(image-73fddc-1658822703499)]
- p1:網(wǎng)頁加載后,通過 postMessage API 每 5s 給 sw 發(fā)送一個(gè)心跳挫望,表示自己的在線驾锰,sw 將在線的網(wǎng)頁登記下來扮碧,更新登記時(shí)間磁椒;
- p2:網(wǎng)頁在 beforeunload 時(shí)凑阶,通過 postMessage API 告知自己已經(jīng)正常關(guān)閉,sw 將登記的網(wǎng)頁清除衷快;
- p3:如果網(wǎng)頁在運(yùn)行的過程中 crash 了,sw 中的 running 狀態(tài)將不會(huì)被清除姨俩,更新時(shí)間停留在奔潰前的最后一次心跳蘸拔;
- sw:Service Worker 每 10s 查看一遍登記中的網(wǎng)頁,發(fā)現(xiàn)登記時(shí)間已經(jīng)超出了一定時(shí)間(比如 15s)即可判定該網(wǎng)頁 crash 了环葵。
一些簡(jiǎn)化后的檢測(cè)代碼调窍,給大家作為參考:
// 頁面 JavaScript 代碼
if (navigator.serviceWorker.controller !== null) {
let HEARTBEAT_INTERVAL = 5 * 1000; // 每五秒發(fā)一次心跳
let sessionId = uuid();
let heartbeat = function () {
navigator.serviceWorker.controller.postMessage({
type: 'heartbeat',
id: sessionId,
data: {} // 附加信息,如果頁面 crash张遭,上報(bào)的附加數(shù)據(jù)
});
}
window.addEventListener("beforeunload", function() {
navigator.serviceWorker.controller.postMessage({
type: 'unload',
id: sessionId
});
});
setInterval(heartbeat, HEARTBEAT_INTERVAL);
heartbeat();
}
- **sessionId **本次頁面會(huì)話的唯一 id邓萨;
- postMessage 附帶一些信息,用于上報(bào) crash 需要的數(shù)據(jù),比如當(dāng)前頁面的地址等等缔恳。
const CHECK_CRASH_INTERVAL = 10 * 1000; // 每 10s 檢查一次
const CRASH_THRESHOLD = 15 * 1000; // 15s 超過15s沒有心跳則認(rèn)為已經(jīng) crash
const pages = {}
let timer
function checkCrash() {
const now = Date.now()
for (var id in pages) {
let page = pages[id]
if ((now - page.t) > CRASH_THRESHOLD) {
// 上報(bào) crash
delete pages[id]
}
}
if (Object.keys(pages).length == 0) {
clearInterval(timer)
timer = null
}
}
worker.addEventListener('message', (e) => {
const data = e.data;
if (data.type === 'heartbeat') {
pages[data.id] = {
t: Date.now()
}
if (!timer) {
timer = setInterval(function () {
checkCrash()
}, CHECK_CRASH_INTERVAL)
}
} else if (data.type === 'unload') {
delete pages[data.id]
}
})
都挺簡(jiǎn)單的代碼宝剖,不細(xì)說了歉甚。
方案的可行性
兼容性:
Service Worker 的普及率已經(jīng)相當(dāng)高了纸泄,鑒于國內(nèi)各種瀏覽器都是 Chrome 內(nèi)核,而且版本已經(jīng)在 Chrome 45 以上聘裁,已經(jīng)覆蓋了相當(dāng)一部分用戶雪营。作為監(jiān)控衡便,數(shù)據(jù)覆蓋大部分就好。
可靠性:
這應(yīng)該是我目前已知可以相對(duì)準(zhǔn)確判斷出網(wǎng)頁崩潰的方式了征唬。不過我們的方案還在測(cè)試環(huán)境总寒,上線一段時(shí)間后再給大家共享數(shù)據(jù)摄闸。
對(duì)瀏覽器廠商的建議
題圖的 Crash 列表年枕,可以在 Chrome 中訪問 chrome://crashes/ 看到乎完,如果廠商可以提供一個(gè) API树姨,在頁面打開時(shí),可以獲知用戶上一次崩潰的信息就很棒了硝清!