導火索
有一天一個測試同事的一個移動端頁面白屏了,看樣子是頁面哪里報錯了目溉。? 我自己打開頁面并沒有報錯明肮,最后發(fā)現(xiàn)報錯只存在于他的手機,移動端項目又是在微信環(huán)境下缭付,調(diào)試起來會比較麻煩柿估,最后用他手機調(diào)試才發(fā)現(xiàn)問題: 是他賬戶下面有個對話的消息數(shù)據(jù)有問題導致頁面報錯了。? 一般遇到這種情況只有用他的手機或者賬戶調(diào)試能很快查到問題陷猫,如果是外部的用戶怎么辦秫舌,我沒法拿他的手機去測試。
其實這個問題很常見绣檬,但是這次我覺得這個問題如果不是我們自己同事發(fā)現(xiàn)的足陨,那就很恐怖,可能廢很大精力才能查出問題娇未,甚至會導致很嚴重的線上bug墨缘,細思極恐,剛好前不久成都FCC的大前端交流會上葉小釵談到了監(jiān)控這塊零抬,也讓我有所啟發(fā)镊讼,這些公共服務才是公司的核心財富,目前公司業(yè)務發(fā)展處在上升階段平夜,未來用戶肯定會越來越多蝶棋,對系統(tǒng)的穩(wěn)定性要求也會越來越高,那既然我們還缺乏這塊的服務忽妒,現(xiàn)在做正合適玩裙。
前期準備
從提出這個想法的一開始就知道兼贸,落地才是關鍵,否則一切空談献酗。? 剛好半個多月以后寝受,我們前端組需要在公司做一次分享,我現(xiàn)在做個題材就挺適合分享的罕偎,其他后端和測試同事也容易聽進去一點很澄。??最開始我考慮了后端存儲和可視化的情況,想找個現(xiàn)成后端集成工具幫我處理后端的工作颜及。? 就找后端同事問了一下甩苛,同事推薦了Elasticsearch+Fluentd+Kibana 。然后稍微研究了一下俏站,總覺得哪里不對讯蒲,反正研究了之后發(fā)現(xiàn)可能還是需要做一些定制開發(fā)才能解決需求,后端同事聽了我的需求也是這么說的肄扎。一人之力有限墨林,并且公司業(yè)務上的事情也多,找一個后端同事配合極好犯祠,利用各自的優(yōu)勢可以更快落地旭等,這樣我也可以專注前端的工作和把控整個項目落地。? 就這樣衡载,我和后端同事商量了一下搔耕,他也答應抽空和我一起搞了。? ?拋開后端的事情痰娱,我開始思考前端的工作弃榨,去調(diào)研一下別人的方案和這塊的知識。? 有一些三方庫或者開源項目提供類似的功能的梨睁,做了很簡單的了解鲸睛。? 最后想著自己開發(fā)更容易去適應自身的業(yè)務,并且目前第一版的需求功能也并沒有那么大的開發(fā)量坡贺,那就自己做吧腊凶。? 前期遇見了一些需要解決和實現(xiàn)的功能點: 生成sourcemap,監(jiān)聽js報錯和信息上報拴念,壓縮的js代碼上報后sourcemap解析問題,如何更平滑的應用在業(yè)務項目中褐缠,數(shù)據(jù)存儲優(yōu)化等政鼠。
基本實現(xiàn)
前端
????● js報錯事件監(jiān)聽+處理上報
????● 構建工具生成sourcemap文件
????● sourcemap文件上傳
后端
????● 提供接口收集報錯
????● 讀取sourcemap文件,解析上傳的報錯(解析發(fā)生時間:接口收集到后馬上處理队魏,后期提取的時候處理)
????● 存儲數(shù)據(jù)
監(jiān)聽js報錯和信息上報
通過onerror我們能監(jiān)聽和拿到js的報錯信息公般, 可以拿到如下代碼的五個參數(shù)万搔。? columnNo, error這兩個參數(shù)在一些老版本的IE8-9瀏覽器和opera低版本等瀏覽器上可能拿不到,但是沒有關系官帘,我們在代碼上兼容拿不到參數(shù)的情況瞬雹,如果缺少后兩個參數(shù),傳空值就行了刽虹。? 也可以通過其他方式拿到這些老版本瀏覽器的columnNo和error參數(shù)酗捌,目前監(jiān)控主要是針對移動端,也沒太大必要去兼容老版本的瀏覽器涌哲。
onerror方法大致實現(xiàn)如下:
可能存在跨域問題胖缤,不同域下的js需要配置script屬性crossorigin="anonymous"和后端配置Access-Control-Allow-Origin,但是目前我們的項目不存在js跨域問題阀圾。
現(xiàn)在我們能通過onerror拿到報錯信息了哪廓,可是線上的代碼是經(jīng)過壓縮的,報錯的時候我們能拿到的的行列數(shù)和變量命都不能告訴我們源代碼哪里出錯了初烘。這里我們需要用到sourcemap涡真,下面來講講它。
sourcemap
sourcemap就是一個信息文件肾筐,里面儲存著位置信息哆料。? 也就是說,sourcemap文件記錄了代碼轉換前的位置和轉換后對應的位置(http://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html 阮一峰詳解)局齿。? 下面圖1是login.js的壓縮版本剧劝,第二行的注釋指定了map文件的相對路徑,瀏覽器根據(jù)注釋會找到map文件然后自動解析出來抓歼,在調(diào)試器里就可以看到源碼了讥此;? 圖2是map文件(json格式);? 圖3圖4介紹sourcemap文件谣妻。? 圖2我們生成的map文件sourcesContent字段直接引入了源文件代碼(構建工具可以配置是否給map文件引入源文件)萄喳,這樣可以方便后端解析,如果沒有源文件對應的話后端是解析不出正確結果的蹋半。
(圖1)
(圖2)
(圖3)
(圖4)
grunt生成sourcemap:
我們的移動端項目構建工具比較老了他巨,統(tǒng)一用的grunt作為打包工具。? 之前沒有在壓縮代碼時使用sourceMap减江,因為開發(fā)和測試環(huán)境沒有壓縮染突,所以也不需要在瀏覽器用sourceMap調(diào)試。? 然后我就再去修改gruntfile文件(之前不是我寫的)辈灼,sourceMap配置感覺和官方文檔對不上份企,老是報錯,最后才發(fā)現(xiàn)之前的打包工具的依賴版本是13年的了巡莹,也暫時沒必要去折騰版本問題了司志,把老版本的文檔翻出來再配置了一下sourcemap文件就成功的生成在源文件的同級目錄下了甜紫,比如源文件叫xx.js,map文件就是xx.js.map骂远。? 我們給js文件加上了md5版本號囚霸,所以實際的文件是xx.md5.js和xx.md5.js.map(md5是根據(jù)內(nèi)容變化的)。
sourcemap解析問題
思考的時候發(fā)現(xiàn)最大的難點應該在sourcemap解析激才。? 最開始后端同事以為sourcemap是nodejs生成的文件拓型,他們后端用的go或者php似乎不能解析吧,如果知道了sourcemap原理就應該知道贸营,它只是一種數(shù)據(jù)格式和開發(fā)語言沒關系吨述。? 我把map文件和報錯信息交給后端同事,他們用go語言的一個工具成功解析出了答案钞脂,實現(xiàn)了本地文件的解析揣云。? 但是我們需要的是自動化解析,不可能每次都去把存儲的報錯信息手動的拿出來再去找對應的map文件做人工解析冰啃。? 所以需要我們后端程序自己去找到map文件邓夕,并解析報錯信息。
如此一來阎毅,后端解析存在兩個關鍵問題:
? ? ● map文件存儲在哪里
????● 什么時候解析
①map文件存儲在哪里
這里只說我們的方案焚刚,map文件和源js文件打包到同級目錄下,一起上傳到服務器(比如js的路徑是www.xxx.com/dist/index.md5.js扇调,那map文件的地址就是www.xxx.com/dist/index.md5.js.map)矿咕,服務端就可以根據(jù)報錯的js路徑再加上.map后綴找到map文件。? 壓縮文件有一段注釋描述sourceMappongURL指定了map文件的位置狼钮,打開瀏覽器之后調(diào)試器會找到這個map文件碳柱,在瀏覽器里就能看到源代碼,為了避免這種情況熬芜,需要服務器配置.js.map后綴的文件不可訪問莲镣。? 如果這樣的話,服務器解析的時候不能直接去下載靜態(tài)資源.map文件涎拉,而是需要去找到服務器本地對應的map文件瑞侮,這樣要單獨配置路徑和寫邏輯很麻煩,而且文件夾結構有變動的話也不靈活鼓拧。? 所以我們的方案是做token權限校驗半火,map文件必須加正確的token參數(shù),服務器才會返回資源(xxx.js.map?token=xxxx)季俩,否則nginx會屏蔽沒有token或者token錯誤的請求慈缔。
②什么時候解析
兩種方法,一種是后端接口收到報錯信息之后种玛,馬上找到map文件藐鹤,并解析存儲到數(shù)據(jù)庫。? 一種是先保留上報信息赂韵,通過接口查詢的時候再去解析娱节。? 我們選擇了前者,接口收到數(shù)據(jù)之后祭示,后端根據(jù)當前報錯文件的url肄满,去查查本地是否已經(jīng)下載過當前文件,如果已經(jīng)存在這個文件质涛,就直接用本地的文件解析稠歉,如果本地沒有,路徑加上.map和token參數(shù)汇陆,下載對應的map文件到本地怒炸,然后再去讀取當前本地文件并解析,解析的數(shù)據(jù)和上報的數(shù)據(jù)就存為一條記錄毡代。? 如果是后者的方法阅羹,存在很多麻煩的問題,這里不多說了教寂。
一張圖詳細描述我們的解析流程:
有一種情況可能發(fā)生: 當前項目已經(jīng)更新到1.1版本了捏鱼,1.0版本的一個報錯以前沒被觸發(fā),這個時候有個用戶緩存了1.0版本的代碼酪耕,并且觸發(fā)了一個新的報錯导梆,這個時候服務器本地存儲的map文件里沒有這個文件,就會帶上token去下載map文件迂烁,因為當前已經(jīng)是1.1版本了看尼,原js文件發(fā)生過變動,md5的版本已經(jīng)對應不上了婚被,這個時候就沒法找到map文件了狡忙,無法解析,所以這種特殊情況只能存儲上報的errorInfo信息址芯。
如何更平滑的應用在業(yè)務項目中
目前js的onerror方法只有代碼量不大灾茁,后期還會有疊加。現(xiàn)在的想法是盡量不和業(yè)務代碼做過多接觸谷炸,只需要直接引入當前js到各個業(yè)務項目中去北专,每個項目不用對它太多任何配置,讓它盡量單純一點旬陡。
存儲優(yōu)化
后期是會做管理后臺來查詢和統(tǒng)計這些異常日志的拓颓,同一個錯誤可能上傳報錯數(shù)據(jù)到服務端,后端查詢出來是一條條獨立的記錄描孟,我們不能區(qū)分這條記錄的報錯是不是有重復數(shù)據(jù)驶睦,也不應該讓后端去做字段對比砰左。? 后來想到給報錯的文件路徑+行+列信息拼在一起字段做md5生成,根據(jù)這個唯一值生成md5场航,最后查詢的時候只需要查詢當前md5字段就能知道這一條報錯一個有多少條記錄缠导。? 不過我想的太天真了,不同的瀏覽器報錯行列信息有點不一樣溉痢,同一報錯就可能生成不同的md5字符串僻造,即便這里有點問題,我還是繼續(xù)用這個方案保存了md5(因為內(nèi)核原因孩饼,移動端的差異還是比較小髓削,當前字段也能有一定的區(qū)分性)。
我們第一版存儲的主要數(shù)據(jù)(還有一些常規(guī)的就不說) :
發(fā)送郵件
郵件提醒是很有必要的一個功能镀娶,目前已經(jīng)實現(xiàn)實時郵件提醒功能立膛。 公司企業(yè)郵箱建個單獨的郵箱就叫frontendmonitor@吧,當后端接口收到報錯后汽畴,把解析數(shù)據(jù)通過這個郵箱發(fā)送給前端旧巾,達到提醒效果。? ?如果是用QQ郵箱或者個人郵箱應該需要在賬戶里開啟smtp服務忍些,QQ企業(yè)郵箱是默認開啟此功能的鲁猩。? 郵件功能要注意性能和優(yōu)化問題,不能因為前端報錯太多導致服務器掛掉罢坝。
實際使用后的優(yōu)化
我們發(fā)現(xiàn)不同的瀏覽器報錯的變量可能不一樣廓握,同一個報錯在chrome瀏覽器和firefox上columnNo參數(shù)一點偏差。? 用兩種報錯解析了一下嘁酿,如下圖隙券,報錯的代碼都是18行,是沒問題的闹司,F(xiàn)irefox報錯是下圖第一個:console 18 0 true娱仔,chrome是testBase 18 0 true,行數(shù)沒問題游桩,偏差不影響我們最終查錯牲迫,我的18行源代碼是:console.log(testBase)。? testBase是故意沒有申明借卧,testBase是undefined盹憎,出問題的應該是testBase這個變量,過從報錯情況上看铐刘,確實是谷歌瀏覽器更精準一點陪每。? 雖然不在意IE,不過IE11報錯列數(shù)和firefox一致。
頁面觸發(fā)事件報錯檩禾,用戶一直觸發(fā)按鈕挂签,這時就會不停上報錯誤信息。解決:存儲上一個報錯信息和時間盼产,進行比對竹握,同一個報錯,短時間內(nèi)避免一直重復發(fā)送辆飘。
框架模板報錯,被框架本身捕獲谓传,不會觸發(fā)window.onerror蜈项,需要使用框架本身的全局監(jiān)聽捕獲信息后手動上傳,這里需要加手動上傳錯誤信息的方法续挟。
引入監(jiān)控的項目紧卒,由于業(yè)務原因可能需要上傳一些業(yè)務信息方便分析,所以預留一個配置字段诗祸,上傳錯誤的時候請求會帶上業(yè)務相關信息跑芳。
總結
這種非業(yè)務服務,來源于個人興趣和思考直颅,并沒有上層壓力需要你做或者什么時候做完博个。? 從最開始有個想法、去調(diào)研功偿、去找后端同事求助盆佣、 開干到最終落地。? 這個過程需要自己堅持做下去械荷,因為害怕自己不能最終落地共耍,所以抓緊時間,一步步去實現(xiàn)每個細節(jié)的想法吨瞎,讓事情盡快落地和上線痹兜,以免自己對這個事情越拖越久。? 作為需求方颤诀,更好的把握整個項目字旭,加上自己的興趣,所以這次自己也學習了一點go語言着绊,保證能看懂后端代碼和了解后端邏輯谐算,最好能做一點開發(fā),這次在后端同事代碼的基礎上归露,實現(xiàn)了發(fā)郵件的小功能洲脂,我稱之為淺入淺出,裝完逼就跑路~? 現(xiàn)在第一版已經(jīng)上線,并且在剛上線不到兩個小時恐锦,就收到了報錯郵件往果,嚇得我急忙查找bug,很快查出來了問題來一铅,這個bug應該存在很久了陕贮,但是因為沒有阻塞性,并且沒有影響到業(yè)務潘飘,也一直沒被發(fā)現(xiàn)肮之,結論是我們這個前端異常監(jiān)控功能還是很成功!? 后期還有很多功能需要開發(fā)卜录,統(tǒng)計戈擒、數(shù)據(jù)可視化、智能報警等等艰毒。? 第一版落地筐高,就為以后的迭代和進化打下了良好基礎。
在做這個事情的過程中丑瞧,我是想盡快把事情落地柑土,時間也很緊張,也并沒有做非常充分的調(diào)研绊汹,比如現(xiàn)成的一些開源項目是怎么做的稽屏。? 后來從同事那里了解到sentry這些三方開源項目之后,也有一點失落過灸促,雖然我也解決了我的需求诫欠,但是三方的開源項目是一個非常完善的系統(tǒng)术陶,提供了很多功能喘落,比我這個強大多了,那我做這個到底有什么意義广凸, 感覺完全和別人比拼不上典鸡,未來我這個項目會繼續(xù)迭代嗎被廓,有繼續(xù)迭代的必要嗎?? ?以后有特殊定制化的需求的時候萝玷,也許自己開發(fā)的才容易更適應業(yè)務嫁乘,可是有那個機會嗎?? 這一次落地已經(jīng)達到我最初的要求了球碉,也能幫我解決目前問題蜓斧,未來還有很多挑戰(zhàn)和迭代等待著,我會帶著它一路過關斬將睁冬,還是半路死掉挎春?? ?我想說:
最后大力地感謝我司后端同事的大力支持!!~