构建可靠的前端异常监控服务-采集篇
http://jdc.jd.com/archives/2175
在 Web 應用異常復雜的今天,一個頁面不單單只包含文字、圖片和超鏈接,還可能包含復雜表單、大量動畫、海量交互。很多 Web 應用完全單頁化,操作體驗、復雜程度堪比原生應用。這對開發者們來說,是巨大的挑戰,縱然有 unit test、code review、各種黑白盒測試保價護航,也無法保證代碼上線后,面對成千上萬用戶、在各類瀏覽器下、遇到未知數據時不出問題。所以,對一個擁有大量用戶的互聯網產品而言,一個可靠的前端異常數據采集、上報、處理、監控、報警平臺是非常有必要的。今天,我想來談談如何采集異常數據。
注:本文中的“錯誤”和“異常”,都是腳本執行中的報錯信息,只是表述不同,原則上可以劃等號。
可采集的異常
頁面的異常有很多種,HTML 標簽異常,CSS 展現異常,樣式、圖片、腳本文件的請求異常、腳本執行異常。有些涉及用戶自身的網絡環境,如網速很慢、被運營商強行注入標簽或腳本,我們很難規避;有些僅僅是展示上的問題,如文本和按鈕沒有對齊、文本折行,用戶自己就能覺察和規避,不影響正常使用;但有些異常,如交互邏輯錯誤、獲取填充數據提交導致的腳本錯誤,會立刻終止用戶的下一步操作,這類異常危害最大。用戶不是開發者,不知道是什么問題導致腳本異常,甚至不知道已經發生了異常。我們主要抓取的,就是此類異常。那么,哪些具體的數據需要采集呢?
全局錯誤
打開瀏覽器自帶的開發者工具,當一個錯誤發生時,我們可以立刻得到提示,并且知道錯誤發生的位置以及調用的堆棧信息。我們可以通過?window.onerror?來捕獲頁面上的各種腳本執行異常,它能幫助我們獲取有用的信息。這個方法存在兼容性問題,在不同的瀏覽器上提供的數據不完全一致,部分過時的瀏覽器只能提供部分數據。它的標準函數簽名是這樣的:
?
| 1 2 | window.onerror = function (message, url, lineNo, columnNo, error) |
?
一共5個參數:
- message {String} 錯誤信息。直觀的錯誤描述信息,不過有時候你確實無法從這里面看出端倪,特別是壓縮后腳本的報錯信息,可能讓你更加疑惑。
- url {String} 發生錯誤對應的腳本路徑。
- lineNo {Number} 錯誤發生的行號。
- columnNo {Number} 錯誤發生的列號。
- error {Object} 具體的 error 對象,繼承自 window.Error 的某一類,部分屬性和前面幾項有重疊,但是包含更加詳細的錯誤調用堆棧信息,這對于定位錯誤非常有幫助。
這些信息如果可以完整提供的話,相信我們能很快定位錯誤。但是不同瀏覽器對同一個錯誤的?message?是不一樣的。IE10- 瀏覽器只能獲取到?message,url?和?lineNo,columnNo?以及具體的?error?是獲取不到的。但是?window.event?對象卻實時提供了?errorLine?和?errorCharacter,以此來對應相應的行列號信息。調用堆棧雖然獲取不到,但在 onerror 中,arguments.callee.caller?可以遞歸出調用堆棧。在不同 IE 版本下面,獲取的調用堆棧也不相同。這里?你可以看到例子。這一類信息是最直接的錯誤信息信息,是必須要捕獲并上報的。下表可以對比同源策略下不同瀏覽器默認可獲取的參數值:
| Chrome | ? | ? | ? | ? |
| Firefox | ? | ? | ? | ? |
| Edge | ? | ? | ? | ? |
| IE11 | ? | ? | ? | ? |
| IE10 | ? | ? | ? | ? |
| IE 9 | ? | ? | ? | ? |
| IE 8 | ? | ? | ? | ? |
| Safari 6+ | ? | ? | ? | ? |
| iOS Safari 6+ | ? | ? | ? | ? |
| Opera 15+ | ? | ? | ? | ? |
| Android Browser 4.4 | ? | ? | ? | ? |
| Android Browser 4 – 4.3 | ? | ? | ? | ? |
?
Ajax 上下文
回想下,有那么幾次,程序在測試和開發時一點問題都沒有,但上線請求到各種五花八門的數據以后,問題才出現。有時明明定位了異常的位置,但是在已知數據的回歸測試下,仍然無法復現。如果這時有發生錯誤時的數據上下文,就很容易排錯了。所以,Ajax 的請求上下文對于排錯會有一定幫助。你要是用過 unit test 的一些輔助工具(如?sinon?)里面的各種 fake 方法,就一定不陌生如何去fake XMLHttpRequest.prototype 上的各類方法。這樣,我們可以 hook 住 XMLHttpRequest 對象?open、send?時的數據,也可以獲取返回數據的?statusCode、statusText,甚至是?responseText(不建議獲取這種可能會是大容量數據的信息)。
操作上下文
異常的造成,除了直接請求到的,可能產生于于交互中。設想一個存在表單的場景,對表單的每個字段進行判斷和處理,或者一個控件的 onchange 觸發一系列的邏輯,也有可能造成異常。如果可以提供部分表單控件的信息,對于錯誤的定位將會更加便利。表單控件大體上分為兩類,click型和?input?型,也就是?點擊類?和?輸入類。
- 點擊類:a,?button,?input[button],?input[submit],?input[radio],?input[checkbox]
- 輸入類:input[text],?input[password],?textarea,?select
統一需要記錄的如 tagName、標簽中的 attribute。不同的對象需要記錄不同的值,checkbox 要知道是否被勾選,select 最好帶著選擇項的 value 和 text,textarea 記錄全部 value 不現實,只記錄有沒有值或者字符串的長度就可以了,這些輔助信息都有可能幫助我們定位錯誤。
頁面依賴
現在的系統,幾乎都是構建在一些流行的庫之上的。jQuery、angular、react.js、vue.js、backbone、underscore、knockout,這些常用的類庫,發布時大都會帶版本信息,如
?
| 1 2 3 4 5 6 7 8 9 | `jQuery`, `jQuery.fn.jquery`, `jQuery ui`, `jQuery.ui.version`, `lodash(underscore)`,`_.VERSION`, `Backbone`, `Backbone.VERSION`, `knockout`, `ko.version`, `Angular`, `angular.version.full`, `React`, `React.version`, `Vue`, `Vue.version`, |
?
有些異常,通常是伴隨著類庫的升級而發生的。如果你的很多頁面中用到了 jQuery,但你升級后無法回歸測試到所有頁面,那些頁面中用到了已經修改或者廢棄的方法時,就可能會異常。這時候,一個頁面依賴庫的信息可能就會徹底幫到你。
除了上述的類庫以外,大多數類庫會直接暴露一個引用到 window 對象中,以供開發者調用。你只要簡單循環一下 window 對象,看看哪些屬性包含?Version,version,VERSION?就可以了。雖然會有些類庫逃過你的檢測,不過可以作為一個補充。
自定義數據
除了默認采集的數據,提供自定義數據接口也是一個必要的功能。因為不同產品的業務需求是千差萬別的,你永遠不知道哪些數據對開發者有用。如果有了自定義數據,開發者就能通過自定義數據來進行異常類型的區分。比如國際化的站點,可能簡單通過一個?lang?字段就可以區分不同語言環境的異常。
瀏覽器數據
你應該有這種經歷,代碼開發、測試各種主流瀏覽器都 bingo 了,上線后用戶投訴說他的瀏覽器下有問題。這屬于特定瀏覽器下面的異常,上線之前如果測試覆蓋度不夠就很難發現。比如,IE8 里面?catch、default?是作為保留關鍵字的,但是在 Chrome 下就不是。你可以在?JScript Reserved Words?和?Reserved keywords as of ECMAScript 6?對比 JScript 和 ECMAScript 6 中的關鍵字。如果監控系統顯示,這種錯誤只發生在 IE8 的頁面時,你就可以快速定位這個錯誤。簡單的說,瀏覽器數據只需要?osType、browserType、browserVersion?就可以了,這些通過 userAgent 就能獲得。
其他數據
除了上述幾種比較重要的數據,屏幕的分辨率、錯誤發生的客戶端時間信息有時也會成為我們定位錯誤的有力參考。在流量允許的范圍內,盡可能多的提供環境數據是必要的。
理想美好,現實殘酷,困難重重
我們盡可能多的獲取到了錯誤信息和相關的環境信息,應該能非常迅速的定位錯誤,但實際情況要復雜的多。瀏覽器的兼容性、安全設置,靜態服務器的配置等有時也是不可控的。很多差異性和不確定性導致我們很難獲取到理想的數據。
同源策略 & ‘Script error.’
首當其沖就是跨域問題。現在的站點,靜態文件大多都是放在一個獨立的域名下面。既可以減少瀏覽器并發的域名限制,又能通過 CDN 提高資源的訪問速度。默認情況下,在本域名下捕獲到一個跨域腳本的錯誤信息時,只能獲取到一條信息?Script error.,沒有文件信息,沒有行列號數據,更沒有詳細的錯誤對象,這使得其他額外的信息變得很雞肋。解決這個問題不但需要在服務器端增加?Access-Control-Allow-Origin?的配置,客戶端引用腳本時也需要給腳本增加?crossorigin=”anonymous”?的屬性,只有這兩個同時設置時,瀏覽器才會把詳細的錯誤數據都吐出來。
注:crossorigin?存在兼容性問題,使用上也有要注意的地方。如果只設置了?crossorigin,而不在服務器設置?Access-Control-Allow-Origin,部分瀏覽器連這個腳本文件都不加載。在 IE edge 之前,是不支持?crossorigin?的,即使設置了以上兩項,你仍然只能獲取到?Script error.。下圖以 Chrome 為例,對比不同情況下,錯誤的返回情況。
| ? | ? | ? | ‘Script error’ |
| ? | ? | ?(見下圖) | — |
| ? | ? | ? | 包含所有信息 |
| ? | ? | ? | ‘Script error’ |
?
(uglifyjs + combo) vs sourcemap
目前大多數站點的靜態腳本文件,上線時都要壓縮混淆的。所以發生錯誤時,獲取到的行號就是第 1 行,列號會是一個巨大無比的數。這時你只能依賴錯誤信息和文件路徑來定位錯誤。好在我們有?sourcemap,有了它,我們可以定位到源代碼的位置。關于 sourcemap, 阮大這篇?詳解?你可以去了解 sourcemap 的原理,mozilla 開源了一個?sourcemap的工具,可以靠它來生成 sourcemap 或者根據 sourcemap 反算出變量名稱和行列號。
通過這種方式,把生成 sourcemap 保存在線下,發生錯誤時,通過 sourcemap,既可以保護代碼安全,又能快速定位問題
也有很多站點,采用 combo 的方式一次性請求多個腳本文件,這時報錯信息變得更加復雜。如果 combo 服務器把所有文件合并到一行的話,行列號信息就變成了無用的信息,你就只剩下 message 一項可以參考了,可以設置 combo 時的策略,比如每個文件另起 n 行,中間可以間隔幾個空行,這樣我們在打包時給每個獨立的文件加上 banner 信息,即使 combo 后,我們也知道發生錯誤的文件是哪個了。下圖是京東 combo 服務器返回的合并后的文件:
節流
根據上面我們需要提交的數據類型計算,一個錯誤發生時,上報的數據量還是蠻大的。如果一個異常一直重復觸發,連續不斷的向服務器轟炸,既是數據冗余,也造成流量浪費。所以,對于異常信息的上報,從上報內容和上報頻率上,應該加以限制。
上報內容的限制比較簡單,通過可插拔的配置,靈活的上報需要的數據類型。可以根據錯誤信息關鍵字過濾不需要上報的錯誤或者哪個頁面的報錯不上報。
上報頻率的限制上,大致上有以下方案是可行的。
數據差異化
差異化主要表現在錯誤信息不一致。相同的異常,在不同瀏覽器下,message?不一致、columnNo?不一致、error?不一致(有的沒有這個對象)。這就需要一個 normalize 函數將差異化的信息盡量抹平。這篇文章?有些一致化的實現方案可以參考。
其他資源
對 img、link、script 資源的動態加載,可以通過給標簽添加 onerror 回調函數,獲取到這類資源是否加載成功。但沒有方案可以全局控制,因為各種類庫可能都有自己加載外部資源的實現方式,如果不侵入,我們是無法給每一個動態資源添加上 onerror 回調的。
運營商注入
國內大多數站點依然是 http 的,很容易被運營商注入腳本或者頁面。此類腳本也有可能導致錯誤,但并不屬于我們可維護范圍,在瀏覽器端能做的工作也十分有限。但是在上報的服務端可以建立黑名單或者白名單的方式,對于注入產生異常的腳本域名進行封堵。當然,最好的方案還是直接支持 https,可大大杜絕這類錯誤的發生。
跨域上報
拿到異常信息后,下一步就是如何上報。由于我們獲取的數據類型比較多,數據量不算少,單純的get方式無法滿足需求。可以通過 ajax 的 post 方法提交數據,這需要在上報服務端設置 Access-Control-Allow-Origin 允許跨域的提交。對于低端的瀏覽器(ie6-7),由于不支持跨域 ajax 提交,ifamre+post?是一個比較完美的解決方案。
由于同源策略的限制,從不同 protocol 發送數據都認為是不安全的,所以上報服務器還要提供 http/https 的雙向入口以適應不同的協議類型。
設計原則
盡可能多的提供錯誤信息和上下文對于定位錯誤確實有很大的幫助,但是受限于實際情況,我們無法保證遠程靜態服務器可控,用戶瀏覽器可控,所以在設計采集模塊時,有幾方面需要重點關注。
配置可插拔
可插拔體現在
- 上報內容可配置。可以做到頁面級的數據配置,按需發送數據。前面講到了很多類型的數據:異常數據、 ajax 上下文、交互上下文、依賴庫信息、自定義數據、瀏覽器信息,并不一定是所有的頁面的都需要上報這些信息。一個門戶頁面,pv 很高,只對異常數據感興趣,不希望造成額外的流量,其他的數據都不需要發送;一個企業內部的 erp,需要保證數據的完整性,每個錯誤都不放過,不太在乎流量,所有異常數據都需要上報。
- 上報頻率可配置。是 one by one 的連續上報還是組合起來隨機上報或者延時上報,完全根據頁面的需求來定制。
兼容多平臺、多瀏覽器
正因為很多異常具有特定性、平臺單一性,才不容易在測試時被發現。所以采集異常時要盡量多的兼容各種平臺瀏覽器,保證在不同瀏覽器上都能上報錯誤信息。桌面端特別要符合國情,至少保證 ie 瀏覽器上面數據的完整性。目前,瀏覽器住戰場已經從桌面端轉移到了移動端,移動端帶來的流量在突飛猛進,而移動端瀏覽器又在重現幾年前桌面端兼容性的亂象。所以,采集系統也需要考慮移動端的性能、兼容性和數據種類。
支持自定義數據
即使內置了很多類型的數據上報,但總無法面面俱到。這時提供一個自定義數據接口很有必要。使用者可以根據不同頁面上報不同的自定義數據來進行異常分析。比如一個正在運行的系統,由于歷史原因,有兩個線上版本的腳本在部署,我可以給不同的系統添加對應的版本號數據,通過監測,對比錯誤發生在不同版本系統中的比重。
最后
「Talk is cheap. Show you the code」?flextracker?是我基于上面所講內容的一個簡單實現,你可以根據自己的需求來重新封裝。
參考
- how-to-catch-javascript-errors-with-window-onerror-even-on-chrome-and-firefox
- Cross-domain Script Errors
- JS stacktraces. The good, the bad, and the ugly
- ‘Script Error’ and get the most data possible from cross-domain JS errors
- blink start window.onerror extra new params support after stable 28
- lz-string: JavaScript compression, fast!
- 如何做前端異常監控?
- 前端代碼異常日志收集與監控
轉載于:https://www.cnblogs.com/davidwang456/articles/9285942.html
總結
以上是生活随笔為你收集整理的构建可靠的前端异常监控服务-采集篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微服务实践分享(9)文档中心
- 下一篇: 使用mspaint查看图片像素