導火索
有一天一個測試同事的一個移動端頁面白屏了,看樣子是頁面哪裡報錯了。 我自己開啟頁面並沒有報錯,最後發現報錯只存在於他的手機,移動端專案又是在微信環境下,除錯起來會比較麻煩,最後用他手機除錯才發現問題: 是他賬戶下面有個對話的訊息資料有問題導致頁面報錯了。 一般遇到這種情況只有用他的手機或者賬戶除錯能很快查到問題,如果是外部的使用者怎麼辦,我沒法拿他的手機去測試。
其實這個問題很常見,但是這次我覺得這個問題如果不是我們自己同事發現的,那就很恐怖,可能廢很大精力才能查出問題,甚至會導致很嚴重的線上bug,細思極恐,剛好前不久成都FCC的大前端交流會上葉小釵談到了監控這塊,也讓我有所啟發,這些公共服務才是公司的核心財富,目前公司業務發展處在上升階段,未來使用者肯定會越來越多,對系統的穩定性要求也會越來越高,那既然我們還缺乏這塊的服務,現在做正合適。
前期準備
從提出這個想法的一開始就知道,落地才是關鍵,否則一切空談。 剛好半個多月以後,我們前端組需要在公司做一次分享,我現在做個題材就挺適合分享的,其他後端和測試同事也容易聽進去一點。 最開始我考慮了後端儲存和視覺化的情況,想找個現成後端整合工具幫我處理後端的工作。 就找後端同事問了一下,同事推薦了 Elasticsearch+Fluentd+Kibana 。 然後稍微研究了一下,總覺得哪裡不對,反正研究了之後發現可能還是需要做一些定製開發才能解決需求,後端同事聽了我的需求也是這麼說的。 一人之力有限,並且公司業務上的事情也多,找一個後端同事配合極好,利用各自的優勢可以更快落地,這樣我也可以專注前端的工作和把控整個專案落地。 就這樣,我和後端同事商量了一下,他也答應抽空和我一起搞了。 拋開後端的事情,我開始思考前端的工作,去調研一下別人的方案和這塊的知識。 有一些三方庫或者開源專案提供類似的功能的,做了很簡單的瞭解。 最後想著自己開發更容易去適應自身的業務,並且目前第一版的需求功能也並沒有那麼大的開發量,那就自己做吧。 前期遇見了一些需要解決和實現的功能點: 生成sourcemap,監聽js報錯和資訊上報,壓縮的js程式碼上報後sourcemap解析問題,如何更平滑的應用在業務專案中,資料儲存優化等。
基本實現
前端
- js報錯事件監聽+處理上報
- 構建工具生成sourcemap檔案
- sourcemap檔案上傳
後端
- 提供介面收集報錯
- 讀取sourcemap檔案,解析上傳的報錯(解析發生時間:介面收集到後馬上處理,後期提取的時候處理)
- 儲存資料
監聽js報錯和資訊上報
通過onerror我們能監聽和拿到js的報錯資訊, 可以拿到如下程式碼的五個引數。 columnNo, error這兩個引數在一些老版本的IE8-9瀏覽器和opera低版本等瀏覽器上可能拿不到,但是沒有關係,我們在程式碼上相容拿不到引數的情況,如果缺少後兩個引數,傳空值就行了。 也可以通過其他方式拿到這些老版本瀏覽器的columnNo和error引數,目前監控主要是針對移動端,也沒太大必要去相容老版本的瀏覽器。
window.onerror = function (msg, fileUrl, lineNo, columnNo, error) {}
onerror方法大致實現如下:
可能存在跨域問題,不同域下的js需要配置script屬性 crossorigin="anonymous" 和後端配置 Access-Control-Allow-Origin,但是目前我們的專案不存在js跨域問題。
提示一下onerror並不能拿到所有報錯資訊,比如網路報錯等。現在我們能通過onerror拿到報錯資訊了,可是線上的程式碼是經過壓縮的,報錯的時候我們能拿到的的行列數和變數命都不能告訴我們原始碼哪裡出錯了。這裡我們需要用到sourcemap,下面來講講它。
sourcemap
sourcemap就是一個資訊檔案,裡面儲存著位置資訊。 也就是說,sourcemap檔案記錄了程式碼轉換前的位置和轉換後對應的位置(www.ruanyifeng.com/blog/2013/0…)。 下面圖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(因為核心原因,移動端的差異還是比較小,當前欄位也能有一定的區分性)。
我們第一版儲存的主要資料(還有一些常規的就不說) :
{ "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企業郵箱是預設開啟此功能的。 郵件功能要注意效能和優化問題,不能因為前端報錯太多導致伺服器掛掉。
實際使用後的優化
- 我們發現不同的瀏覽器報錯的變數可能不一樣,同一個報錯在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 這些三方開源專案之後,也有一點失落過,雖然我也解決了我的需求,但是三方的開源專案是一個非常完善的系統,提供了很多功能,比我這個強大多了,那我做這個到底有什麼意義, 感覺完全和別人比拼不上,未來我這個專案會繼續迭代嗎,有繼續迭代的必要嗎? 以後有特殊定製化的需求的時候,也許自己開發的才容易更適應業務,可是有那個機會嗎? 這一次落地已經達到我最初的要求了,也能幫我解決目前問題,未來還有很多挑戰和迭代等待著,我會帶著它一路過關斬將,還是半路死掉? 我想說:
最後大力地感謝我司後端同事的大力支援!!~
關注大詩人公眾號,第一時間獲取最新文章。