導火索
有一天一個測試同事的一個移動端頁面白屏了,看樣子是頁面哪里報錯了。? 我自己打開頁面并沒有報錯,最后發現報錯只存在于他的手機,移動端項目又是在微信環境下,調試起來會比較麻煩,最后用他手機調試才發現問題: 是他賬戶下面有個對話的消息數據有問題導致頁面報錯了。? 一般遇到這種情況只有用他的手機或者賬戶調試能很快查到問題,如果是外部的用戶怎么辦,我沒法拿他的手機去測試。
其實這個問題很常見,但是這次我覺得這個問題如果不是我們自己同事發現的,那就很恐怖,可能廢很大精力才能查出問題,甚至會導致很嚴重的線上bug,細思極恐,剛好前不久成都FCC的大前端交流會上葉小釵談到了監控這塊,也讓我有所啟發,這些公共服務才是公司的核心財富,目前公司業務發展處在上升階段,未來用戶肯定會越來越多,對系統的穩定性要求也會越來越高,那既然我們還缺乏這塊的服務,現在做正合適。
前期準備
從提出這個想法的一開始就知道,落地才是關鍵,否則一切空談。? 剛好半個多月以后,我們前端組需要在公司做一次分享,我現在做個題材就挺適合分享的,其他后端和測試同事也容易聽進去一點。??最開始我考慮了后端存儲和可視化的情況,想找個現成后端集成工具幫我處理后端的工作。? 就找后端同事問了一下,同事推薦了Elasticsearch+Fluentd+Kibana 。然后稍微研究了一下,總覺得哪里不對,反正研究了之后發現可能還是需要做一些定制開發才能解決需求,后端同事聽了我的需求也是這么說的。一人之力有限,并且公司業務上的事情也多,找一個后端同事配合極好,利用各自的優勢可以更快落地,這樣我也可以專注前端的工作和把控整個項目落地。? 就這樣,我和后端同事商量了一下,他也答應抽空和我一起搞了。? ?拋開后端的事情,我開始思考前端的工作,去調研一下別人的方案和這塊的知識。? 有一些三方庫或者開源項目提供類似的功能的,做了很簡單的了解。? 最后想著自己開發更容易去適應自身的業務,并且目前第一版的需求功能也并沒有那么大的開發量,那就自己做吧。? 前期遇見了一些需要解決和實現的功能點: 生成sourcemap,監聽js報錯和信息上報,壓縮的js代碼上報后sourcemap解析問題,如何更平滑的應用在業務項目中,數據存儲優化等。
基本實現
前端
????● js報錯事件監聽+處理上報
????● 構建工具生成sourcemap文件
????● sourcemap文件上傳
后端
????● 提供接口收集報錯
????● 讀取sourcemap文件,解析上傳的報錯(解析發生時間:接口收集到后馬上處理,后期提取的時候處理)
????● 存儲數據
監聽js報錯和信息上報
通過onerror我們能監聽和拿到js的報錯信息, 可以拿到如下代碼的五個參數。? columnNo, error這兩個參數在一些老版本的IE8-9瀏覽器和opera低版本等瀏覽器上可能拿不到,但是沒有關系,我們在代碼上兼容拿不到參數的情況,如果缺少后兩個參數,傳空值就行了。? 也可以通過其他方式拿到這些老版本瀏覽器的columnNo和error參數,目前監控主要是針對移動端,也沒太大必要去兼容老版本的瀏覽器。
onerror方法大致實現如下:
可能存在跨域問題,不同域下的js需要配置script屬性crossorigin="anonymous"和后端配置Access-Control-Allow-Origin,但是目前我們的項目不存在js跨域問題。
現在我們能通過onerror拿到報錯信息了,可是線上的代碼是經過壓縮的,報錯的時候我們能拿到的的行列數和變量命都不能告訴我們源代碼哪里出錯了。這里我們需要用到sourcemap,下面來講講它。
sourcemap
sourcemap就是一個信息文件,里面儲存著位置信息。? 也就是說,sourcemap文件記錄了代碼轉換前的位置和轉換后對應的位置(http://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html 阮一峰詳解)。? 下面圖1是login.js的壓縮版本,第二行的注釋指定了map文件的相對路徑,瀏覽器根據注釋會找到map文件然后自動解析出來,在調試器里就可以看到源碼了;? 圖2是map文件(json格式);? 圖3圖4介紹sourcemap文件。? 圖2我們生成的map文件sourcesContent字段直接引入了源文件代碼(構建工具可以配置是否給map文件引入源文件),這樣可以方便后端解析,如果沒有源文件對應的話后端是解析不出正確結果的。
(圖1)
(圖2)
(圖3)
(圖4)
grunt生成sourcemap:
我們的移動端項目構建工具比較老了,統一用的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解析。? 最開始后端同事以為sourcemap是nodejs生成的文件,他們后端用的go或者php似乎不能解析吧,如果知道了sourcemap原理就應該知道,它只是一種數據格式和開發語言沒關系。? 我把map文件和報錯信息交給后端同事,他們用go語言的一個工具成功解析出了答案,實現了本地文件的解析。? 但是我們需要的是自動化解析,不可能每次都去把存儲的報錯信息手動的拿出來再去找對應的map文件做人工解析。? 所以需要我們后端程序自己去找到map文件,并解析報錯信息。
如此一來,后端解析存在兩個關鍵問題:
? ? ● 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(因為內核原因,移動端的差異還是比較小,當前字段也能有一定的區分性)。
我們第一版存儲的主要數據(還有一些常規的就不說) :
發送郵件
郵件提醒是很有必要的一個功能,目前已經實現實時郵件提醒功能。 公司企業郵箱建個單獨的郵箱就叫frontendmonitor@吧,當后端接口收到報錯后,把解析數據通過這個郵箱發送給前端,達到提醒效果。? ?如果是用QQ郵箱或者個人郵箱應該需要在賬戶里開啟smtp服務,QQ企業郵箱是默認開啟此功能的。? 郵件功能要注意性能和優化問題,不能因為前端報錯太多導致服務器掛掉。
實際使用后的優化
我們發現不同的瀏覽器報錯的變量可能不一樣,同一個報錯在chrome瀏覽器和firefox上columnNo參數一點偏差。? 用兩種報錯解析了一下,如下圖,報錯的代碼都是18行,是沒問題的,Firefox報錯是下圖第一個:console 18 0 true,chrome是testBase 18 0 true,行數沒問題,偏差不影響我們最終查錯,我的18行源代碼是:console.log(testBase)。? testBase是故意沒有申明,testBase是undefined,出問題的應該是testBase這個變量,過從報錯情況上看,確實是谷歌瀏覽器更精準一點。? 雖然不在意IE,不過IE11報錯列數和firefox一致。
頁面觸發事件報錯,用戶一直觸發按鈕,這時就會不停上報錯誤信息。解決:存儲上一個報錯信息和時間,進行比對,同一個報錯,短時間內避免一直重復發送。
框架模板報錯,被框架本身捕獲,不會觸發window.onerror,需要使用框架本身的全局監聽捕獲信息后手動上傳,這里需要加手動上傳錯誤信息的方法。
引入監控的項目,由于業務原因可能需要上傳一些業務信息方便分析,所以預留一個配置字段,上傳錯誤的時候請求會帶上業務相關信息。
總結
這種非業務服務,來源于個人興趣和思考,并沒有上層壓力需要你做或者什么時候做完。? 從最開始有個想法、去調研、去找后端同事求助、 開干到最終落地。? 這個過程需要自己堅持做下去,因為害怕自己不能最終落地,所以抓緊時間,一步步去實現每個細節的想法,讓事情盡快落地和上線,以免自己對這個事情越拖越久。? 作為需求方,更好的把握整個項目,加上自己的興趣,所以這次自己也學習了一點go語言,保證能看懂后端代碼和了解后端邏輯,最好能做一點開發,這次在后端同事代碼的基礎上,實現了發郵件的小功能,我稱之為淺入淺出,裝完逼就跑路~? 現在第一版已經上線,并且在剛上線不到兩個小時,就收到了報錯郵件,嚇得我急忙查找bug,很快查出來了問題來,這個bug應該存在很久了,但是因為沒有阻塞性,并且沒有影響到業務,也一直沒被發現,結論是我們這個前端異常監控功能還是很成功!? 后期還有很多功能需要開發,統計、數據可視化、智能報警等等。? 第一版落地,就為以后的迭代和進化打下了良好基礎。
在做這個事情的過程中,我是想盡快把事情落地,時間也很緊張,也并沒有做非常充分的調研,比如現成的一些開源項目是怎么做的。? 后來從同事那里了解到sentry這些三方開源項目之后,也有一點失落過,雖然我也解決了我的需求,但是三方的開源項目是一個非常完善的系統,提供了很多功能,比我這個強大多了,那我做這個到底有什么意義, 感覺完全和別人比拼不上,未來我這個項目會繼續迭代嗎,有繼續迭代的必要嗎?? ?以后有特殊定制化的需求的時候,也許自己開發的才容易更適應業務,可是有那個機會嗎?? 這一次落地已經達到我最初的要求了,也能幫我解決目前問題,未來還有很多挑戰和迭代等待著,我會帶著它一路過關斬將,還是半路死掉?? ?我想說:
最后大力地感謝我司后端同事的大力支持!!~