https://juejin.cn/post/7046756775252459528
https://juejin.cn/post/6936562262480158728#heading-2
https://cloud.tencent.com/developer/article/1983779
https://zhuanlan.zhihu.com/p/495649475
為什么要?jiǎng)?chuàng)建前端監(jiān)控系統(tǒng)
主要是為了解決兩種問(wèn)題:
- 如何及時(shí)發(fā)現(xiàn)問(wèn)題
- 如何快速定位并解決問(wèn)題
前端監(jiān)控體系主要需要做的事情
- 頁(yè)面整體訪問(wèn)情況,pv、uv、用戶(hù)行為上報(bào)
- 頁(yè)面的性能情況,包括:加載耗時(shí)、接口耗時(shí)統(tǒng)計(jì)
- 灰度發(fā)布和有效的監(jiān)控能力,方便及時(shí)發(fā)現(xiàn)問(wèn)題
- 用戶(hù)反饋問(wèn)題,需要足夠的日志定位
總結(jié)起來(lái)就是兩點(diǎn):數(shù)據(jù)收集、數(shù)據(jù)上報(bào)
數(shù)據(jù)收集
一般是三個(gè)方面
- 穩(wěn)定性(頁(yè)面穩(wěn)定性/異常):js錯(cuò)誤(js執(zhí)行錯(cuò)誤、promise異常)、資源錯(cuò)誤(js、css加載異常)、接口錯(cuò)誤(ajax、fetch請(qǐng)求錯(cuò)誤)、白屏
- 流暢性(頁(yè)面訪問(wèn)速度)
- 用戶(hù)行為回放(外部服務(wù)調(diào)用情況):pv、uv、用戶(hù)在某個(gè)頁(yè)面的停留時(shí)間
1、穩(wěn)定性(頁(yè)面穩(wěn)定性/異常)
1)腳本錯(cuò)誤
有兩類(lèi),分別為【語(yǔ)法錯(cuò)誤】、【運(yùn)行時(shí)錯(cuò)誤】,下面是主要監(jiān)控方式:
- try catch:可以用在預(yù)知情況下監(jiān)控特定錯(cuò)誤,但發(fā)生語(yǔ)法錯(cuò)誤或者異步錯(cuò)誤時(shí),無(wú)法捕捉,常與window.onerror結(jié)合使用
- window.onerror:捕獲JS運(yùn)行時(shí)錯(cuò)誤
- window.addEventListener('unhandledrejection'):捕獲promise未處理reject的錯(cuò)誤,即promise被reject了,但是沒(méi)有reject處理器,則會(huì)走到這一步(https://developer.mozilla.org/zh-CN/docs/Web/API/Window/unhandledrejection_event)
- window.addEventListener('error'):資源加載錯(cuò)誤(
js、css加載異常),但是他也能上報(bào)js運(yùn)行時(shí)錯(cuò)誤, 所以判斷只有是script、link、image時(shí)才去拿數(shù)據(jù)
為避免重復(fù)上報(bào)js運(yùn)行時(shí)錯(cuò)誤,此時(shí)只有event.srcElement inatanceof HTMLScriptElement或HTMLLinkElement或HTMLImageElement時(shí)才進(jìn)行數(shù)據(jù)采集。
// try catch 語(yǔ)法錯(cuò)誤
try {
function empty() // <- throw error 語(yǔ)法錯(cuò)誤
} catch(e){
console.log('語(yǔ)法錯(cuò)誤信息 ↙');
console.log(e);
}
// try catch 異步錯(cuò)誤
try {
setTimeout(function() {
test // <- throw error 異步錯(cuò)誤
},0)
} catch(e){
console.log('異步錯(cuò)誤信息 ↙');
console.log(e);
}
2)跨域資源的腳本報(bào)錯(cuò)Script error
常見(jiàn)的形式就是,我本來(lái)的網(wǎng)站域名是a.com,然后,里面使用script引入了一個(gè)cdn連接,cdn連接域名是b.com,這個(gè)時(shí)候在調(diào)用cdn里的方法時(shí)就會(huì)報(bào)Script error,但是獲取不到完整的信息,只能獲取到類(lèi)似于:"Script error.", "", 0, 0, undefined的信息
解決方式:
- 為頁(yè)面上script標(biāo)簽添加crossorigin屬性
// 這一步告訴瀏覽器,目標(biāo)腳本通過(guò)匿名方式獲取。這意味著請(qǐng)求腳本時(shí)沒(méi)有潛在的用戶(hù)身份信息(如cookies、HTTP 證書(shū)等)發(fā)送到服務(wù)端
<script src="http://another-domain.com/app.js" crossorigin="anonymous"></script>
- 響應(yīng)頭中增加 Access-Control-Allow-Origin 來(lái)支持跨域資源共享,使用*或者上面本來(lái)的網(wǎng)站即http://a.com
- 當(dāng) Access-Control-Allow-Origin的值為http://a.com時(shí),響應(yīng)頭中需帶上Vary:Origin。避免引緩存導(dǎo)致的權(quán)限問(wèn)題。(ps:為*的時(shí)候無(wú)所謂,因?yàn)樗械挠蛎甲屗フ?qǐng)求了)
通過(guò)以上方式可以使用window.onerro獲取到具體的報(bào)錯(cuò)信息
3)接口異常
通過(guò)重寫(xiě)XMLHttpRequest和fetch的原生方法來(lái)實(shí)現(xiàn)
- 重寫(xiě)XMLHttpRequest的open、send方法
- 監(jiān)聽(tīng)load、error、abort事件
window.addEventListener("error", handler("error"), false);
4)資源加載異常
頁(yè)面內(nèi)的圖片、css、JS等Assets資源加載失敗,會(huì)在error事件里可以拿到資源加載失敗回調(diào):
window.addEventListener( 'error ', function(e){
//排除JSError
if( ! (e instanceof ErrorEvent)){
//資源路徑
e.target.src || e.target.href
//資源類(lèi)型
e.target.tagName
}
}, true) // 為true則是捕獲階段處理函數(shù),否則為冒泡階段處理函數(shù)
5)白屏問(wèn)題
頁(yè)面加載過(guò)程中因?yàn)槿我鈫?wèn)題導(dǎo)致渲染沒(méi)有完成,呈現(xiàn)空白頁(yè)面的異常
一般基于不同方案會(huì)有不同的處理方式
1、基于mutationObserver:頁(yè)面加載完成后3s,頁(yè)面沒(méi)有節(jié)點(diǎn)變化(節(jié)點(diǎn)不變不代表不白屏)
2、基于native容器:頁(yè)面加載完成后3s,頁(yè)面還是全屏白色像素(依賴(lài)容器)
3、基于pointTiming:頁(yè)面加載完成后3s,頁(yè)面沒(méi)有fisrt-point,下方為實(shí)現(xiàn)(兼容性不好)
- document.elementsFromPoint方法可以獲取到當(dāng)前視口內(nèi)指定坐標(biāo)處,由里到外排列的所有元素
- 根據(jù) elementsFromPoint api,獲取屏幕水平中線和豎直中線所在的元素(9點(diǎn)監(jiān)測(cè))

for (let i = 1; i <= 9; i++) {
xElements = document.elementsFromPoint(
(window.innerWidth * i) / 10,
window.innerHeight / 2
);
yElements = document.elementsFromPoint(
window.innerWidth / 2,
(window.innerHeight * i) / 10
);
isWrapper(xElements[0]);
isWrapper(yElements[0]);
}
6)crash
頁(yè)面因?yàn)閮?nèi)存溢出、死循環(huán)等原因?qū)е卤罎⒌漠惓?/p>
1、基于LocalStroage里的頁(yè)面離開(kāi)狀態(tài)
- 在頁(yè)面加載時(shí),標(biāo)記開(kāi)始加載
- 在pagehide或者beforunload時(shí)標(biāo)記離開(kāi),二次進(jìn)入頁(yè)面時(shí)判斷是否正常離開(kāi)
- 埋點(diǎn)發(fā)送滯后,起不到監(jiān)控告警作用
2、基于native
- 監(jiān)控webview進(jìn)程狀態(tài),發(fā)送crash日志
- 依賴(lài)容器
3、基于service worker
- html請(qǐng)求進(jìn)入service worker時(shí),標(biāo)記頁(yè)面開(kāi)始加載
- 每隔一段時(shí)間像sw發(fā)送一次心跳,當(dāng)一段時(shí)間沒(méi)有收到心跳則表示crash了
- 兼容性差,一個(gè)頁(yè)面只能有一個(gè)sw,如果頁(yè)面也需要使用sw,則需進(jìn)行嵌入,切換頁(yè)面又可能pause
2、流暢性(頁(yè)面訪問(wèn)速度)

1)卡頓
響應(yīng)用戶(hù)交互的響應(yīng)時(shí)間如果大于100ms,用戶(hù)就會(huì)感覺(jué)卡頓
卡頓:頁(yè)面整個(gè)生命周期中,主線程持續(xù)執(zhí)行某一個(gè)任務(wù)的耗時(shí)大于50ms
瀏覽器的事件隊(duì)列機(jī)制決定,要實(shí)現(xiàn)小于100毫秒的響應(yīng),應(yīng)用必須在每50毫秒內(nèi)將控制返回給主線程
- new PerformanceObserver
- entry.duration(持續(xù)時(shí)間) > 100 判斷大于100ms,即可認(rèn)定為長(zhǎng)任務(wù)(entry是參數(shù),詳見(jiàn)第三個(gè)鏈接)
- 使用 requestIdleCallback上報(bào)數(shù)據(jù)
2)PV、UV、用戶(hù)停留時(shí)間
PV(page view) 是頁(yè)面瀏覽量,UV(Unique visitor)用戶(hù)訪問(wèn)量。PV 只要訪問(wèn)一次頁(yè)面就算一次,UV 同一天內(nèi)多次訪問(wèn)只算一次。
對(duì)于前端來(lái)講,只要每次進(jìn)入頁(yè)面上報(bào)一次 PV 就行,UV 的統(tǒng)計(jì)放在服務(wù)端來(lái)做
window.addEventListener(
"beforeunload",
() => {},
false// 冒泡的時(shí)候做回調(diào)
}
3)加載時(shí)間(onload時(shí)監(jiān)聽(tīng))
3、用戶(hù)行為回放(外部服務(wù)調(diào)用情況)【可以先不說(shuō)】
數(shù)據(jù)上報(bào)
主要有三種方式處理信息上報(bào)
丟點(diǎn):在瀏覽器點(diǎn)擊跳轉(zhuǎn)時(shí),跳轉(zhuǎn)前的點(diǎn)擊上報(bào)請(qǐng)求都會(huì)進(jìn)行一個(gè)三次握手,如果此時(shí),網(wǎng)絡(luò)較慢、服務(wù)器運(yùn)行緩慢或者上報(bào)請(qǐng)求還在處理階段,這時(shí),如果頁(yè)面被卸載了,瀏覽器都會(huì)自動(dòng)對(duì)當(dāng)前的請(qǐng)求進(jìn)行終止。這樣,這個(gè)http的請(qǐng)求就沒(méi)有建立,導(dǎo)致上報(bào)沒(méi)有真正發(fā)出。
延遲卸載:在卸載事件處理器中嘗試通過(guò)一個(gè)同步的 XMLHttpRequest 向服務(wù)器發(fā)送數(shù)據(jù)。這導(dǎo)致了頁(yè)面卸載被延遲。
img請(qǐng)求
- 兼容性好
- 部分瀏覽器可能造成丟點(diǎn)、get請(qǐng)求長(zhǎng)度限制,延遲頁(yè)面卸載
- 就是動(dòng)態(tài)js創(chuàng)建img標(biāo)簽,把上報(bào)的url拼接,然后放在src上
Fetch/XHR(XMLHttpRequest)
- 兼容性好
- fetch丟點(diǎn),XHR不丟點(diǎn),但是頁(yè)面延遲卸載
Navigator.sendBeacon(推薦使用)
sendBeacon()會(huì)使用戶(hù)代理在有機(jī)會(huì)時(shí)異步地向服務(wù)器發(fā)送數(shù)據(jù),同時(shí)不會(huì)延遲頁(yè)面的卸載或影響下一導(dǎo)航的載入性能
- 不丟點(diǎn),不會(huì)頁(yè)面延遲卸載
- 兼容性不好