【序】:前端異常監(jiān)控是很重要的一部分,今天剛好看到一篇文章竞穷,就在此總結(jié)學(xué)習(xí)唐责,文章地址
【疑惑】:什么是前端監(jiān)控?
【解惑】:前端監(jiān)控包括行為監(jiān)控瘾带、異常監(jiān)控鼠哥、性能監(jiān)控等,本文主要討論異常監(jiān)控看政。對(duì)于前端而言朴恳,和后端處于同一個(gè)監(jiān)控系統(tǒng)中,前端有自己的監(jiān)控方案允蚣,后端也有自己等監(jiān)控方案于颖,但兩者并不分離,因?yàn)橐粋€(gè)用戶在操作應(yīng)用過(guò)程中如果出現(xiàn)異常嚷兔,有可能是前端引起森渐,也有可能是后端引起做入,需要有一個(gè)機(jī)制,將前后端串聯(lián)起來(lái)同衣,使監(jiān)控本身統(tǒng)一于監(jiān)控系統(tǒng)竟块。因此,即使只討論前端異常監(jiān)控耐齐,其實(shí)也不能?chē)?yán)格區(qū)分前后端界限浪秘,而要根據(jù)實(shí)際系統(tǒng)的設(shè)計(jì),在最終的報(bào)表中體現(xiàn)出監(jiān)控對(duì)開(kāi)發(fā)和業(yè)務(wù)的幫助蚪缀。
【疑惑】:異常監(jiān)控有哪些部分組成秫逝?
【解惑】:一般而言,一個(gè)監(jiān)控系統(tǒng)询枚,大致可以分為四個(gè)階段:日志采集违帆、日志存儲(chǔ)、統(tǒng)計(jì)與分析金蜀、報(bào)告和警告刷后。
- 收集階段:收集異常日志,先在本地做一定的處理渊抄,采取一定的方案上報(bào)到服務(wù)器尝胆。
- 存儲(chǔ)階段:后端接收前端上報(bào)的異常日志,經(jīng)過(guò)一定處理护桦,按照一定的存儲(chǔ)方案存儲(chǔ)含衔。
- 分析階段:分為機(jī)器自動(dòng)分析和人工分析。機(jī)器自動(dòng)分析二庵,通過(guò)預(yù)設(shè)的條件和算法贪染,對(duì)存儲(chǔ)的日志信息進(jìn)行統(tǒng)計(jì)和篩選,發(fā)現(xiàn)問(wèn)題催享,觸發(fā)報(bào)警杭隙。人工分析,通過(guò)提供一個(gè)可視化的數(shù)據(jù)面板因妙,讓系統(tǒng)用戶可以看到具體的日志數(shù)據(jù)痰憎,根據(jù)信息,發(fā)現(xiàn)異常問(wèn)題根源攀涵。
- 報(bào)警階段:分為告警和預(yù)警铣耘。告警按照一定的級(jí)別自動(dòng)報(bào)警,通過(guò)設(shè)定的渠道以故,按照一定的觸發(fā)規(guī)則進(jìn)行涡拘。預(yù)警則在異常發(fā)生前,提前預(yù)判据德,給出警告鳄乏。
【疑惑】:什么是前端異常跷车?
【解惑】:前端異常是指在用戶使用Web應(yīng)用時(shí)無(wú)法快速得到符合預(yù)期結(jié)果的情況,不同的異常帶來(lái)的后果程度不同橱野,輕則引起用戶使用不悅朽缴,重則導(dǎo)致產(chǎn)品無(wú)法使用,使用戶喪失對(duì)產(chǎn)品的認(rèn)可水援。
前端異趁芮浚可分為幾類:
a. 出錯(cuò)
界面呈現(xiàn)的內(nèi)容與用戶預(yù)期的內(nèi)容不符,例如點(diǎn)擊進(jìn)入非目標(biāo)界面蜗元,數(shù)據(jù)不準(zhǔn)確或渤,出現(xiàn)的錯(cuò)誤提示不可理解,界面錯(cuò)位奕扣,提交后跳轉(zhuǎn)到錯(cuò)誤界面等情況薪鹦。這類異常出現(xiàn)時(shí),雖然產(chǎn)品本身功能還能正常使用惯豆,但用戶無(wú)法達(dá)成自己目標(biāo)池磁。
b. 呆滯
界面出現(xiàn)操作后沒(méi)有反應(yīng)的現(xiàn)象,例如點(diǎn)擊按鈕無(wú)法提交楷兽,提示成功后無(wú)法繼續(xù)操作地熄。這類異常出現(xiàn)時(shí),產(chǎn)品已經(jīng)存在界面級(jí)局部不可用現(xiàn)象芯杀。
c. 損壞
界面出現(xiàn)無(wú)法實(shí)現(xiàn)操作目的的現(xiàn)象端考,例如點(diǎn)擊無(wú)法進(jìn)入目標(biāo)界面,點(diǎn)擊無(wú)法查看詳情內(nèi)容等揭厚。這類異常出現(xiàn)時(shí)却特,應(yīng)用部分功能無(wú)法被正常使用。
d. 假死
界面出現(xiàn)卡頓棋弥,無(wú)法對(duì)任何功能進(jìn)行使用的現(xiàn)象。例如用戶無(wú)法登陸導(dǎo)致無(wú)法使用應(yīng)用內(nèi)功能诚欠,由于某個(gè)遮罩層阻擋且不可關(guān)閉導(dǎo)致無(wú)法進(jìn)行任何后續(xù)操作顽染。這類異常出現(xiàn)時(shí),用戶很可能殺死應(yīng)用轰绵。
e. 崩潰
應(yīng)用出現(xiàn)經(jīng)常性自動(dòng)退出或無(wú)法操作的現(xiàn)象粉寞。例如間歇性crash,網(wǎng)頁(yè)無(wú)法正常加載或加載后無(wú)法進(jìn)行任何操作左腔。這類異常持續(xù)出現(xiàn)唧垦,將直接導(dǎo)致用戶流失,影響產(chǎn)品生命力液样。
【疑惑】: 產(chǎn)生異常的原因有哪些振亮?
【解惑】:
【疑惑】: 如何采集異常巧还?
【解惑】:
當(dāng)異常出現(xiàn)的時(shí)候,我們需要知道異常的具體信息坊秸,根據(jù)異常的具體信息來(lái)決定采用什么樣的解決方案麸祷。在采集異常信息時(shí),可以遵循4W原則:
"WHO" "did WHAT" and "get WHICH exception" in "WHICH environment?"
話句話說(shuō)就是:是誰(shuí)做了什么和報(bào)了什么錯(cuò)褒搔,在那個(gè)環(huán)境阶牍?
第一點(diǎn):是誰(shuí)發(fā)現(xiàn)的錯(cuò)誤?
a. 用戶信息
出現(xiàn)異常時(shí)該用戶的信息星瘾,例如該用戶在當(dāng)前時(shí)刻的狀態(tài)走孽、權(quán)限等,以及需要區(qū)分用戶可多終端登錄時(shí)琳状,異常對(duì)應(yīng)的是哪一個(gè)終端磕瓷。
第二點(diǎn):當(dāng)時(shí)做了什么操作?
b. 行為信息
用戶進(jìn)行什么操作時(shí)產(chǎn)生了異常:所在的界面路徑算撮;執(zhí)行了什么操作生宛;操作時(shí)使用了哪些數(shù)據(jù);當(dāng)時(shí)的API吐了什么數(shù)據(jù)給客戶端肮柜;如果是提交操作陷舅,提交了什么數(shù)據(jù);上一個(gè)路徑审洞;上一個(gè)行為日志記錄ID等莱睁。
第三點(diǎn):獲取哪些異常信息?
c. 異常信息
產(chǎn)生異常的代碼信息:用戶操作的DOM元素節(jié)點(diǎn)芒澜;異常級(jí)別仰剿;異常類型;異常描述痴晦;代碼stack信息等南吮。
第四點(diǎn):是在什么環(huán)境發(fā)現(xiàn)的?
d. 環(huán)境信息
網(wǎng)絡(luò)環(huán)境誊酌;設(shè)備型號(hào)和標(biāo)識(shí)碼部凑;操作系統(tǒng)版本;客戶端版本碧浊;API接口版本等涂邀。
【疑惑】:如何捕獲異常呢?
【解惑】:分為兩種捕獲方式箱锐,單點(diǎn)捕獲與全局捕獲
1. 全局捕獲
- window.addEventListener(‘error’) / window.addEventListener(“unhandledrejection”) / document.addEventListener(‘click’) 等
- 框架級(jí)別的全局監(jiān)聽(tīng)比勉,例如aixos中使用interceptor進(jìn)行攔截,vue、react都有自己的錯(cuò)誤采集接口
- 通過(guò)對(duì)全局函數(shù)進(jìn)行封裝包裹浩聋,實(shí)現(xiàn)在在調(diào)用該函數(shù)時(shí)自動(dòng)捕獲異常
- 對(duì)實(shí)例方法重寫(xiě)(Patch)观蜗,在原有功能基礎(chǔ)上包裹一層,例如對(duì)console.error進(jìn)行重寫(xiě)赡勘,在使用方法不變的情況下也可以異常捕獲
2. 單點(diǎn)捕獲
- try…catch
- 專門(mén)寫(xiě)一個(gè)函數(shù)來(lái)收集異常信息嫂便,在異常發(fā)生時(shí),調(diào)用該函數(shù)
- 專門(mén)寫(xiě)一個(gè)函數(shù)來(lái)包裹其他函數(shù)闸与,得到一個(gè)新函數(shù)毙替,該新函數(shù)運(yùn)行結(jié)果和原函數(shù)一模一樣,只是在發(fā)生異常時(shí)可以捕獲異常
跨域腳本異常
產(chǎn)生的場(chǎng)景:由于瀏覽器安全策略限制践樱,跨域腳本報(bào)錯(cuò)時(shí)厂画,無(wú)法直接獲取錯(cuò)誤的詳細(xì)信息,只能得到一個(gè)Script Error拷邢。例如袱院,我們會(huì)引入第三方依賴,或者將自己的腳本放在CDN時(shí)瞭稼。
方案一:
- 將js內(nèi)聯(lián)到HTML中
- 將js文件與HTML放在同域下
方案二:
- 為頁(yè)面上script標(biāo)簽添加crossorigin屬性
- 被引入腳本所在服務(wù)端響應(yīng)頭中忽洛,增加 Access-Control-Allow-Origin 來(lái)支持跨域資源共享
問(wèn)題解決神器
異常錄制
git上有一個(gè)項(xiàng)目主要用于這個(gè)
異常級(jí)別劃分
不同級(jí)別的告警使用不同級(jí)別處理方式,特別緊急和非常重要的影響用戶的問(wèn)題要及時(shí)上報(bào)环肘。
異常上報(bào)
-
首先要存儲(chǔ)日志欲虚,這里推薦indexeddb,配合hello-indexeddb工具異步調(diào)用,高性能
image.png -
合理使用indexedDB
image
上圖展示了前端存儲(chǔ)日志的流程和數(shù)據(jù)庫(kù)布局悔雹。當(dāng)一個(gè)事件复哆、變動(dòng)、異常被捕獲之后腌零,形成一條初始日志梯找,被立即放入暫存區(qū)(indexedDB的一個(gè)store),之后主程序就結(jié)束了收集過(guò)程益涧,后續(xù)的事只在webworker中發(fā)生锈锤。在一個(gè)webworker中,一個(gè)循環(huán)任務(wù)不斷從暫存區(qū)中取出日志闲询,對(duì)日志進(jìn)行分類久免,將分類結(jié)果存儲(chǔ)到索引區(qū)中,并對(duì)日志記錄的信息進(jìn)行豐富嘹裂,將最終將會(huì)上報(bào)到服務(wù)端的日志記錄轉(zhuǎn)存到歸檔區(qū)妄壶。而當(dāng)一條日志在歸檔區(qū)中存在的時(shí)間超過(guò)一定天數(shù)之后摔握,它就已經(jīng)沒(méi)有價(jià)值了寄狼,但是為了防止特殊情況,它被轉(zhuǎn)存到回收區(qū),再經(jīng)歷一段時(shí)間后泊愧,就會(huì)被從回收區(qū)中清除伊磺。
3.前端整理日志
上文講到,在一個(gè)webworker中對(duì)日志進(jìn)行整理后存到索引區(qū)和歸檔區(qū)删咱,那么這個(gè)整理過(guò)程是怎樣的呢屑埋?
由于我們下文要講的上報(bào),是按照索引進(jìn)行的痰滋,因此摘能,我們?cè)谇岸说娜罩菊砉ぷ鳎饕褪歉鶕?jù)日志特征敲街,整理出不同的索引团搞。我們?cè)谑占罩緯r(shí),會(huì)給每一條日志打上一個(gè)type多艇,以此進(jìn)行分類逻恐,并創(chuàng)建索引,同時(shí)通過(guò)object-hashcode計(jì)算每個(gè)log對(duì)象的hash值峻黍,作為這個(gè)log的唯一標(biāo)志复隆。
- 將所有日志記錄按時(shí)序存放在歸檔區(qū),并將新入庫(kù)的日志加入索引
- BatchIndexes:批量上報(bào)索引(包含性能等其他日志)姆涩,可一次批量上報(bào)100條
- MomentIndexes:即時(shí)上報(bào)索引挽拂,一次全部上報(bào)
- FeedbackIndexes:用戶反饋索引,一次上報(bào)一條
- BlockIndexes:區(qū)塊上報(bào)索引阵面,按異常/錯(cuò)誤(traceId轻局,requestId)分塊,一次上報(bào)一塊
上報(bào)完成后样刷,被上報(bào)過(guò)的日志對(duì)應(yīng)的索引刪除 - 3天以上日志進(jìn)入回收區(qū)
- 7天以上的日志從回收區(qū)清除
- rquestId:同時(shí)追蹤前后端日志仑扑。由于后端也會(huì)記錄自己的日志,因此置鼻,在前端請(qǐng)求api的時(shí)候镇饮,默認(rèn)帶上requestId,后端記錄的日志就可以和前端日志對(duì)應(yīng)起來(lái)箕母。
-
traceId:追蹤一個(gè)異常發(fā)生前后的相關(guān)日志注竿。當(dāng)應(yīng)用啟動(dòng)時(shí)栏赴,創(chuàng)建一個(gè)traceId,直到一個(gè)異常發(fā)生時(shí),刷新* traceId眉踱。把一個(gè)traceId相關(guān)的requestId收集起來(lái),把這些requestId相關(guān)的日志組合起來(lái)舟奠,就是最終這個(gè)異常相關(guān)的所有日志儒洛,用來(lái)對(duì)異常進(jìn)行復(fù)盤(pán)蔚携。
image
圖舉例展示了如何利用traceId和requestId找出和一個(gè)異常相關(guān)的所有日志。在上圖中克饶,hash4是一條異常日志酝蜒,我們找到hash4對(duì)應(yīng)的traceId為traceId2,在日志列表中矾湃,有兩條記錄具有該traceId亡脑,但是hash3這條記錄并不是一個(gè)動(dòng)作的開(kāi)始,因?yàn)閔ash3對(duì)應(yīng)的requestId為reqId2邀跃,而reqId2開(kāi)始于hash2霉咨,因此,我們實(shí)際上要把hash2也加入到該異常發(fā)生的整個(gè)復(fù)盤(pán)備選記錄中拍屑∏ぃ總結(jié)起來(lái)就是,我們要找出同一個(gè)traceId對(duì)應(yīng)的所有requestId對(duì)應(yīng)的日志記錄丽涩,雖然有點(diǎn)繞棺滞,但稍理解就可以明白其中的道理。
我們把這些和一個(gè)異常相關(guān)的所有日志集合起來(lái)矢渊,稱為一個(gè)block继准,再利用日志的hash集合,得出這個(gè)block的hash矮男,并在索引區(qū)中建立索引移必,等待上報(bào)。
日志上報(bào)
上報(bào)日志也在webworker中進(jìn)行毡鉴,為了和整理區(qū)分崔泵,可以分兩個(gè)worker。上報(bào)的流程大致為:在每一個(gè)循環(huán)中猪瞬,從索引區(qū)取出對(duì)應(yīng)條數(shù)的索引憎瘸,通過(guò)索引中的hash,到歸檔區(qū)取出完整的日志記錄陈瘦,再上傳到服務(wù)器幌甘。
按照上報(bào)的頻率(重要緊急度)可將上報(bào)分為四種:
a. 即時(shí)上報(bào)
收集到日志后,立即觸發(fā)上報(bào)函數(shù)痊项。僅用于A類異常锅风。而且由于受到網(wǎng)絡(luò)不確定因素影響,A類日志上報(bào)需要有一個(gè)確認(rèn)機(jī)制鞍泉,只有確認(rèn)服務(wù)端已經(jīng)成功接收到該上報(bào)信息之后皱埠,才算完成。否則需要有一個(gè)循環(huán)機(jī)制咖驮,確保上報(bào)成功边器。
b. 批量上報(bào)
將收集到的日志存儲(chǔ)在本地泪姨,當(dāng)收集到一定數(shù)量之后再打包一次性上報(bào),或者按照一定的頻率(時(shí)間間隔)打包上傳饰抒。這相當(dāng)于把多次合并為一次上報(bào),以降低對(duì)服務(wù)器的壓力诀黍。
c. 區(qū)塊上報(bào)
將一次異常的場(chǎng)景打包為一個(gè)區(qū)塊后進(jìn)行上報(bào)袋坑。它和批量上報(bào)不同,批量上報(bào)保證了日志的完整性眯勾,全面性枣宫,但會(huì)有無(wú)用信息。而區(qū)塊上報(bào)則是針對(duì)異常本身的吃环,確保單個(gè)異常相關(guān)的日志被全部上報(bào)也颤。
d. 用戶主動(dòng)提交
在界面上提供一個(gè)按鈕,用戶主動(dòng)反饋bug郁轻。這有利于加強(qiáng)與用戶的互動(dòng)翅娶。
或者當(dāng)異常發(fā)生時(shí),雖然對(duì)用戶沒(méi)有任何影響好唯,但是應(yīng)用監(jiān)控到了竭沫,彈出一個(gè)提示框,讓用戶選擇是否愿意上傳日志骑篙。這種方案適合涉及用戶隱私數(shù)據(jù)時(shí)蜕提。
即時(shí)上報(bào)雖然叫即時(shí),但是其實(shí)也是通過(guò)類似隊(duì)列的循環(huán)任務(wù)去完成的靶端,它主要是盡快把一些重要的異常提交給監(jiān)控系統(tǒng)谎势,好讓運(yùn)維人員發(fā)現(xiàn)問(wèn)題,因此杨名,它對(duì)應(yīng)的緊急程度比較高脏榆。
批量上報(bào)和區(qū)塊上報(bào)的區(qū)別:批量上報(bào)是一次上報(bào)一定條數(shù),比如每2分鐘上報(bào)1000條台谍,直到上報(bào)完成姐霍。而區(qū)塊上報(bào)是在異常發(fā)生之后,馬上收集和異常相關(guān)的所有日志典唇,查詢出哪些日志已經(jīng)由批量上報(bào)上報(bào)過(guò)了镊折,剔除掉,把其他相關(guān)日志上傳介衔,和異常相關(guān)的這些日志相對(duì)而言更重要一些恨胚,它們可以幫助盡快復(fù)原異常現(xiàn)場(chǎng)炎咖,找出發(fā)生異常的根源赃泡。
用戶提交的反饋信息寒波,則可以慢悠悠上報(bào)上去。
為了確保上報(bào)是成功的升熊,在上報(bào)時(shí)需要有一個(gè)確認(rèn)機(jī)制俄烁,由于在服務(wù)端接收到上報(bào)日志之后,并不會(huì)立即存入數(shù)據(jù)庫(kù)级野,而是放到一個(gè)隊(duì)列中页屠,因此,前后端在確保日志確實(shí)已經(jīng)記錄進(jìn)數(shù)據(jù)庫(kù)這一點(diǎn)上需要再做一些處理蓖柔。
圖展示了上報(bào)的一個(gè)大致流程辰企,在上報(bào)時(shí),先通過(guò)hash查詢况鸣,讓客戶端知道準(zhǔn)備要上報(bào)的日志集合中牢贸,是否存在已經(jīng)被服務(wù)端保存好的日志,如果已經(jīng)存在镐捧,就將這些日志去除潜索,避免重復(fù)上報(bào),浪費(fèi)流量懂酱。
壓縮上報(bào)數(shù)據(jù)
一次性上傳批量數(shù)據(jù)時(shí)帮辟,必然遇到數(shù)據(jù)量大,浪費(fèi)流量玩焰,或者傳輸慢等情況由驹,網(wǎng)絡(luò)不好的狀態(tài)下,可能導(dǎo)致上報(bào)失敗昔园。因此蔓榄,在上報(bào)之前進(jìn)行數(shù)據(jù)壓縮也是一種方案。
對(duì)于合并上報(bào)這種情況默刚,一次的數(shù)據(jù)量可能要十幾k甥郑,對(duì)于日 pv 大的站點(diǎn)來(lái)說(shuō),產(chǎn)生的流量還是很可觀的荤西。所以有必要對(duì)數(shù)據(jù)進(jìn)行壓縮上報(bào)澜搅。lz-string是一個(gè)非常優(yōu)秀的字符串壓縮類庫(kù),兼容性好邪锌,代碼量少勉躺,壓縮比高,壓縮時(shí)間短觅丰,壓縮率達(dá)到驚人的60%饵溅。但它基于LZ78壓縮,如果后端不支持解壓妇萄,可選擇gzip壓縮蜕企,一般而言后端會(huì)默認(rèn)預(yù)裝gzip咬荷,因此,選擇gzip壓縮數(shù)據(jù)也可以轻掩,工具包pako中自帶了gzip壓縮幸乒,可以嘗試使用。