有一天一個測試同事的一個移動端頁面白屏了,看樣子是頁面哪裏報錯了。 我本身打開頁面並無報錯,最後發現報錯只存在於他的手機,移動端項目又是在微信環境下,調試起來會比較麻煩,最後用他手機調試才發現問題: 是他帳戶下面有個對話的消息數據有問題致使頁面報錯了。 通常遇到這種狀況只有用他的手機或者帳戶調試能很快查到問題,若是是外部的用戶怎麼辦,我無法拿他的手機去測試。javascript
其實這個問題很常見,可是此次我以爲這個問題若是不是咱們本身同事發現的,那就很恐怖,可能廢很大精力才能查出問題,甚至會致使很嚴重的線上bug,細思極恐,恰好前不久成都FCC的大前端交流會上葉小釵談到了監控這塊,也讓我有所啓發,這些公共服務纔是公司的核心財富,目前公司業務發展處在上升階段,將來用戶確定會愈來愈多,對系統的穩定性要求也會愈來愈高,那既然咱們還缺少這塊的服務,如今作正合適。php
從提出這個想法的一開始就知道,落地纔是關鍵,不然一切空談。 恰好半個多月之後,咱們前端組須要在公司作一次分享,我如今作個題材就挺適合分享的,其餘後端和測試同事也容易聽進去一點。 最開始我考慮了後端存儲和可視化的狀況,想找個現成後端集成工具幫我處理後端的工做。 就找後端同事問了一下,同事推薦了 Elasticsearch+Fluentd+Kibana 。 而後稍微研究了一下,總以爲哪裏不對,反正研究了以後發現可能仍是須要作一些定製開發才能解決需求,後端同事聽了個人需求也是這麼說的。 一人之力有限,而且公司業務上的事情也多,找一個後端同事配合極好,利用各自的優點能夠更快落地,這樣我也能夠專一前端的工做和把控整個項目落地。 就這樣,我和後端同事商量了一下,他也答應抽空和我一塊兒搞了。 拋開後端的事情,我開始思考前端的工做,去調研一下別人的方案和這塊的知識。 有一些三方庫或者開源項目提供相似的功能的,作了很簡單的瞭解。 最後想着本身開發更容易去適應自身的業務,而且目前初版的需求功能也並無那麼大的開發量,那就本身作吧。 前期碰見了一些須要解決和實現的功能點: 生成sourcemap,監聽js報錯和信息上報,壓縮的js代碼上報後sourcemap解析問題,如何更平滑的應用在業務項目中,數據存儲優化等。html
前端前端
後端java
經過onerror咱們能監聽和拿到js的報錯信息, 能夠拿到以下代碼的五個參數。 columnNo, error這兩個參數在一些老版本的IE8-9瀏覽器和opera低版本等瀏覽器上可能拿不到,可是沒有關係,咱們在代碼上兼容拿不到參數的狀況,若是缺乏後兩個參數,傳空值就好了。 也能夠經過其餘方式拿到這些老版本瀏覽器的columnNo和error參數,目前監控主要是針對移動端,也沒太大必要去兼容老版本的瀏覽器。node
window.onerror = function (msg, fileUrl, lineNo, columnNo, error) {}
onerror方法大體實現以下:nginx
可能存在跨域問題,不一樣域下的js須要配置script屬性 crossorigin="anonymous" 和後端配置 Access-Control-Allow-Origin,可是目前咱們的項目不存在js跨域問題。chrome
提示一下onerror並不能拿到全部報錯信息,好比網絡報錯等。如今咱們能經過onerror拿到報錯信息了,但是線上的代碼是通過壓縮的,報錯的時候咱們能拿到的的行列數和變量命都不能告訴咱們源代碼哪裏出錯了。這裏咱們須要用到sourcemap,下面來說講它。數據庫
sourcemap就是一個信息文件,裏面儲存着位置信息。 也就是說,sourcemap文件記錄了代碼轉換前的位置和轉換後對應的位置(www.ruanyifeng.com/blog/2013/0…)。 下面圖1是login.js的壓縮版本,第二行的註釋指定了map文件的相對路徑,瀏覽器根據註釋會找到map文件而後自動解析出來,在調試器裏就能夠看到源碼了; 圖2是map文件(json格式); 圖3圖4介紹sourcemap文件。 圖2咱們生成的map文件sourcesContent字段直接引入了源文件代碼(構建工具能夠配置是否給map文件引入源文件),這樣能夠方便後端解析,若是沒有源文件對應的話後端是解析不出正確結果的。json
(圖1)
(圖2)
(圖3)
(圖4)
咱們的移動端項目構建工具比較老了,統一用的grunt做爲打包工具。 以前沒有在壓縮代碼時使用sourceMap,由於開發和測試環境沒有壓縮,因此也不須要在瀏覽器用sourceMap調試。 而後我就再去修改gruntfile文件(以前不是我寫的),sourceMap配置感受和官方文檔對不上,總是報錯,最後才發現以前的打包工具的依賴版本是13年的了,也暫時不必去折騰版本問題了,把老版本的文檔翻出來再配置了一下sourcemap文件就成功的生成在源文件的同級目錄下了,好比源文件叫xx.js,map文件就是xx.js.map。 咱們給js文件加上了md5版本號,因此實際的文件是xx.md5.js和xx.md5.js.map(md5是根據內容變化的)。
思考的時候發現最大的難點應該在sourcemap解析。 最開始後端同事覺得sourcemap是nodejs生成的文件,他們後端用的go或者php彷佛不能解析吧,若是知道了sourcemap原理就應該知道,它只是一種數據格式和開發語言不要緊。 我把map文件和報錯信息交給後端同事,他們用go語言的一個工具成功解析出了答案,實現了本地文件的解析。 可是咱們須要的是自動化解析,不可能每次都去把存儲的報錯信息手動的拿出來再去找對應的map文件作人工解析。 因此須要咱們後端程序本身去找到map文件,並解析報錯信息。
如此一來,後端解析存在兩個關鍵問題:
這裏只說咱們的方案,map文件和源js文件打包到同級目錄下,一塊兒上傳到服務器(好比js的路徑是www.xxx.com/dist/index.md5.js,那map文件的地址就是www.xxx.com/dist/index.md5.js.map),服務端就能夠根據報錯的js路徑再加上.map後綴找到map文件。 壓縮文件有一段註釋描述sourceMappongURL指定了map文件的位置,打開瀏覽器以後調試器會找到這個map文件,在瀏覽器裏就能看到源代碼,爲了不這種狀況,須要服務器配置 .js.map 後綴的文件不可訪問。 若是這樣的話,服務器解析的時候不能直接去下載靜態資源.map文件,而是須要去找到服務器本地對應的map文件,這樣要單獨配置路徑和寫邏輯很麻煩,並且文件夾結構有變更的話也不靈活。 因此咱們的方案是作token權限校驗,map文件必須加正確的token參數,服務器纔會返回資源(xxx.js.map?token=xxxx),不然nginx會屏蔽沒有token或者token錯誤的請求。
兩種方法,一種是後端接口收到報錯信息以後,立刻找到map文件,並解析存儲到數據庫。 一種是先保留上報信息,經過接口查詢的時候再去解析。 咱們選擇了前者,接口收到數據以後,後端根據當前報錯文件的url,去查查本地是否已經下載過當前文件,若是已經存在這個文件,就直接用本地的文件解析,若是本地沒有,路徑加上.map和token參數,下載對應的map文件到本地,而後再去讀取當前本地文件並解析,解析的數據和上報的數據就存爲一條記錄。 若是是後者的方法,存在不少麻煩的問題,這裏很少說了。
一張圖詳細描述咱們的解析流程:
有一種狀況可能發生: 當前項目已經更新到1.1版本了,1.0版本的一個報錯之前沒被觸發,這個時候有個用戶緩存了1.0版本的代碼,而且觸發了一個新的報錯,這個時候服務器本地存儲的map文件裏沒有這個文件,就會帶上token去下載map文件,由於當前已是1.1版本了,原js文件發生過變更,md5的版本已經對應不上了,這個時候就無法找到map文件了,沒法解析,因此這種特殊狀況只能存儲上報的errorInfo信息。
目前js的onerror方法只有代碼量不大,後期還會有疊加。如今的想法是儘可能不和業務代碼作過多接觸,只須要直接引入當前js到各個業務項目中去,每一個項目不用對它太多任何配置,讓它儘可能單純一點。
後期是會作管理後臺來查詢和統計這些異常日誌的,同一個錯誤可能上傳報錯數據到服務端,後端查詢出來是一條條獨立的記錄,咱們不能區分這條記錄的報錯是否是有重複數據,也不該該讓後端去作字段對比。 後來想到給 報錯的文件路徑+行+列 信息拼在一塊兒字段作md5生成,根據這個惟一值生成md5,最後查詢的時候只須要查詢當前md5字段就能知道這一條報錯一個有多少條記錄。 不過我想的太天真了,不一樣的瀏覽器報錯行列信息有點不同,同一報錯就可能生成不一樣的md5字符串,即使這裏有點問題,我仍是繼續用這個方案保存了md5(由於內核緣由,移動端的差別仍是比較小,當前字段也能有必定的區分性)。
咱們初版存儲的主要數據(還有一些常規的就不說) :
{ "businessInfo": "{}",//業務項目自定義的數據 "errorMd5": "80bb86b86da0607c0dc5c3a77e16eab6",//根據報錯部分信息生成的md5 "manualSendError": "{}",//手動上傳的報錯信息 "pageUrl": "http://www.xxx.com/xxx.html",//放生報錯的頁面url "parseError": true,//解釋是否失敗 "parsed": '{"col":0,"errKey":"list","file":"xxx.js","line":105}',//解析後的行列、文件路徑和變量 "raw": '{"msg":'', "fileUrl":'', "lineNo":'', "columnNo":'', "error":''}',//onerror的五個參數 "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Redmi 4X Build/MMB29M; wv)..." //navigator.userAgent }
郵件提醒是頗有必要的一個功能,目前已經實現實時郵件提醒功能。 公司企業郵箱建個單獨的郵箱就叫frontendmonitor@吧,當後端接口收到報錯後,把解析數據經過這個郵箱發送給前端,達到提醒效果。 若是是用QQ郵箱或者我的郵箱應該須要在帳戶裏開啓smtp服務,QQ企業郵箱是默認開啓此功能的。 郵件功能要注意性能和優化問題,不能由於前端報錯太多致使服務器掛掉。
這種非業務服務,來源於我的興趣和思考,並無上層壓力須要你作或者何時作完。 從最開始有個想法、去調研、去找後端同事求助、 開幹到最終落地。 這個過程須要本身堅持作下去,由於懼怕本身不能最終落地,因此抓緊時間,一步步去實現每一個細節的想法,讓事情儘快落地和上線,以避免本身對這個事情越拖越久。 做爲需求方,更好的把握整個項目,加上本身的興趣,因此此次本身也學習了一點go語言,保證能看懂後端代碼和了解後端邏輯,最好能作一點開發,此次在後端同事代碼的基礎上,實現了發郵件的小功能,我稱之爲淺入淺出,裝完逼就跑路~ 如今初版已經上線,而且在剛上線不到兩個小時,就收到了報錯郵件,嚇得我急忙查找bug,很快查出來了問題來,這個bug應該存在好久了,可是由於沒有阻塞性,而且沒有影響到業務,也一直沒被發現,結論是咱們這個前端異常監控功能仍是很成功! 後期還有不少功能須要開發,統計、數據可視化、智能報警等等。 初版落地,就爲之後的迭代和進化打下了良好基礎。
在作這個事情的過程當中,我是想盡快把事情落地,時間也很緊張,也並無作很是充分的調研,好比現成的一些開源項目是怎麼作的。 後來從同事那裏瞭解到 sentry 這些三方開源項目以後,也有一點失落過,雖然我也解決了個人需求,可是三方的開源項目是一個很是完善的系統,提供了不少功能,比我這個強大多了,那我作這個到底有什麼意義, 感受徹底和別人比拼不上,將來我這個項目會繼續迭代嗎,有繼續迭代的必要嗎? 之後有特殊定製化的需求的時候,也許本身開發的才容易更適應業務,但是有那個機會嗎? 這一次落地已經達到我最初的要求了,也能幫我解決目前問題,將來還有不少挑戰和迭代等待着,我會帶着它一路過關斬將,仍是半路死掉? 我想說:
最後大力地感謝我司後端同事的大力支持!!~
關注大詩人公衆號,第一時間獲取最新文章。