隨着前端技術突飛猛進迅猛發展,爲了實現更好的前端性能,最大程度提升用戶體驗,支持單頁應用的框架逐漸佔領市場,如衆所周知的React,Vue等等。可是在單頁應用的趨勢下,快速定位並解決JS錯誤卻成爲一大難題。在當下的互聯網行業,對前端性能要求愈來愈高,前端性能監控的產品層出不窮,javascript錯誤診斷更是其中舉足輕重的一個環節。幫助開發者排查線上bug,實現快速定位問題,高效解決問題,是咱們努力的方向。javascript
目前已經有了許多諸如Arms,Sentry等前端性能監控框架,都在必定程度上對JS錯誤診斷提供了相應的 支持,整體來講,你們的思路比較類似,能夠總結爲如下幾個步驟:html
每當上線一個新的產品或新的業務功能,每每伴隨着一些不可避免的線上bug,這些bug發生的頻率有多高、發生在什麼頁面、影響了多少用戶等等,對於判斷解決問題的優先級、幫助排查問題和優化性能來講是很是關鍵的。前端
圖1是Arms前端性能監控的JS錯誤診斷診斷頁面,能夠清晰地看到錯誤發生的次數、影響用戶數以及錯誤分佈狀況等信息,開發者能夠根據這些統計數據更好地決策問題的輕重緩急,使性能優化有條不紊進行。java
前端報錯的緣由有不少,網絡因素、瀏覽器兼容性、用戶操做邏輯、業務代碼自己的問題等等均可能致使故障發生,在JS錯誤統計中咱們已經知道了錯誤發生的頁面,這在必定程度上縮小了排查範圍,但這樣仍是不夠的,咱們還想要知道更多錯誤詳情,好比:web
包括報錯的設備、操做系統、瀏覽器等等,這些信息無疑能夠幫助開發者更好地復現問題,進而修復bug。ajax
利用error stack和source map來精準定位發生錯誤的代碼位置。後端
a. 上傳source map文件api
b. 在代碼中定位錯誤跨域
還原錯誤發生時用戶行爲上下文是JS錯誤診斷的最後一步,也是最關鍵和最困難的一步。瀏覽器
假設如下場景:咱們收到用戶反饋說點了某按鈕後沒有反應……
面對用戶反饋,咱們不禁得會想,沒反應是什麼緣由呢,是點擊事件的handler出錯了致使接口請求沒發出?仍是接口掛了?或者是接口返回數據渲染失敗了?
相似以上的困惑應該不少開發者都會時常遇到,咱們不可能去指揮用戶打開開發者工具再一步步debug給咱們看,這就須要咱們的前端監控平臺具有還原報錯現場的能力,幫助開發者瞭解錯誤發生時候的用戶行爲上下文,進而能夠預想一下剛纔的場景——
根據用戶的uid等信息,咱們能夠回溯到該用戶報錯先後的一系列操做以及前端行爲:
進入頁面->點擊按鈕->發出api請求成功-> js error……
因而咱們能夠知道報錯是發生在api請求成功後的數據處理環節,再依據步驟2中提供的錯誤詳情快速解決問題。
已經有一些前端性能監控平臺接入了用戶行爲監控,實現的方式也各有千秋,主要流程能夠分爲三個步驟:行爲採集、行爲上報、行爲回溯。
1)哪些行爲須要採集?
咱們站在用戶的立場去考慮一個單頁應用的瀏覽週期內的可能流程:進入應用首頁——加載頁面內容——瀏覽頁面內容——用戶交互(鼠標交互/鍵盤交互等)——跳轉到新頁面……
要將用戶行爲串聯成完整的行爲鏈來爲js error提供上下文,咱們須要知道什麼時間,什麼位置,發生了什麼事情。由此,以上用戶瀏覽過程當中的全部的頁面行爲(包括但不只限於用戶交互)能夠用如下幾類來大體歸納:
Api請求,鼠標事件,鍵盤事件,路由跳轉,error 等。
2)如何採集?
在肯定了哪些行爲須要上報之後,咱們再在來看如何完成行爲打點。
在過往的時代,咱們有傳統的手動埋點方法,它的缺點也是不言而喻的:手動埋點是容易混亂的,有時可能會出現錯埋、漏埋等狀況,往往上線一個新功能,須要開發團隊和數據團隊進行埋點溝通,徒增了時間和人力成本;其次時常由於產品排期緊張,功能急於上線,就不得不先砍掉了埋點的需求,在後續的版本更新中再補上,這使得新上線的功能得不到驗證,而新上線的功能對業務和產品性能的影響都很關鍵。
現現在,一個好的前端監控產品須要實現「無埋點」監控,充當一雙眼睛,時時刻刻監控着產品的運做狀況,全量採集頁面事件和用戶行爲,爲業務分析和錯誤診斷都提供充足的信息。
以上流程圖爲Arms作行爲採集的大體步驟,首先須要在正常的html頁面中插入一小段js代碼,即引入咱們具備行爲日誌採集功能的SDK,以下代碼所示,經過createElement(「script」)在Dom節點添加script的元素,並將SDK的js文件引入進來,用於收集用戶行爲,並在適當的時候上報到後端,具體方法以下代碼所示,其中bl.js爲SDK文件。
<script> !(function(c,b,d,a){c[a]||(c[a]={});c[a].config={pid:"xxxxxxx",appType:"web",imgUrl:"https://arms-retcode.aliyuncs.com/r.png?"}; with(b)with(body)with(insertBefore(createElement("script"),firstChild))setAttribute("crossorigin","",src=d) })(window,document,"https://retcode.alicdn.com/retcode/bl.js","__bl"); </script>
3)SDK中行爲採集的實現
reWrite(XMLHttpRequest.send, originalSend => function(...args){ const xhr = this; function onreadystatechangeHandler(){ if (xhr.readyState === 4) { //記錄API行爲 } } if ('onreadystatechange' in xhr && typeof xhr.onreadystatechange === 'function') { reWrite(xhr.onreadystatechange, original=> function(...innerargs){ //記錄API行爲 original.apply(this, innerargs); }); } else { xhr.onreadystatechange = onreadystatechangeHandler; } return originalSend.apply(this, args); });
var consoleType = ['debug', 'info', 'warn', 'log', 'error', 'assert']; for (var i = 0; consoleType.length; i++) { var type = consoleType[i]; reWrite(console.type, function (orig) { var thisType = type; return function (...args) { // 添加console行爲 if (orig) { Function.prototype.apply.call(orig, window.console, args); } }; }); }
注意,經過重寫 console 對象監控瀏覽器控制檯的打印信息,這樣會致使在控制檯下打印的日誌沒法正確看到原代碼文件中的位置,console的位置會定位到SDK的代碼中,能夠經過配置瀏覽器 Blackboxing來解決。
var bhEventHandler = function(){ //記錄用戶行爲 } document.addEventListener('click', bhEventHandler, false); var types = ['EventTarget', 'Node']; for (var i = 0; i < types.length; i++) { var type = types[i]; var proto = window[type] && window[type].prototype; reWrite(proto.addEventListener, function (orig) { //重寫addEventListener,記錄用戶行爲 }); }
var origPopstate = window.onpopstate; window.onpopstate = function () { //記錄路由行爲 if (origPopstate) { return origPopstate.apply(this, args); } }; var dosomething = function (orig) { return function (...args) { //記錄路由行爲 return orig.apply(this, args); }; }; reWrite(window.history.pushState, dosomething); reWrite(window.history.replaceState, dosomething);
window.addEventListener('error', function(event){...})
監聽全局未處理的rejection:
window.addEventListener('unhandledrejection', function(event){...})
SDK採集到用戶行爲後,以必定的格式進行信息拼接,而後假裝成圖片發送給後端。爲何要使用圖片來發送日誌信息而不是直接使用ajax呢?這是由於SDK的script文件和後端分析的代碼可能不在相同的域內,而將image對象的src屬性指向後端腳本並攜帶參數,能夠輕鬆實現跨域請求。不一樣平臺對於前端監控行爲的類別都比較相似,但上報的方式仍是不盡相同的,服務於不一樣的業務需求。如下是兩種比較典型的行爲上報形式:
1)持續全量上報
圖6是截取的New relic的行爲日誌上報信息,爲何稱之爲持續全量上報呢?「持續」是指它的行爲日誌是定時上報的,每隔幾秒便會上報一次,上報請求很是密集。「全量」則是指它採集的行爲類型覆蓋面很廣,能夠從上報的Request Payload中看到,它採集的不只限於咱們上面提到的一些主要的頁面行爲,它幾乎覆蓋了全部的頁面事件,包括鼠標移動等等。而這種上報方式也是利弊參半:
優勢:它能夠更真實地還原用戶在整個頁面週期內的行爲,保真度很高,對於還原現場來講是有必定優點的。
缺陷:首先是無差異的行爲採集,這顯然增大了行爲日誌的數據體量,在上報中對流量的消耗很大,日誌存儲成本也很高,而這巨大的損耗所帶來的信息價值呢?幾乎大部分都是鼠標移動或者滑輪滾動的大量重複信息,不管是對業務分析仍是錯誤診斷都沒有太大貢獻,甚至增長了分析問題的複雜性。
其次是行爲日誌的頻繁上報,這對業務方來講可能也是不太友好的,性能監控請求發的比自己的業務請求都多。
2)場景觸發上報
場景觸發上報便是指,當符合必定條件或者場景時才上報行爲日誌,對於JS錯誤診斷中的用戶行爲回溯場景而言,固然就是錯誤觸發上報,當頁面監聽到error時,便把當前採集到的行爲列表伴隨error詳情一併上報,做爲錯誤診斷的輔助信息。
行爲日誌伴隨錯誤一塊兒上報的方式須要維護一個行爲隊列,隊列應設有最大長度,當捕獲到js error的時候,將行爲隊列做爲js錯誤日誌的一個補充信息一塊兒上報,圖7所示爲sentry錯誤日誌上報的request payload,其用戶行爲隊列包含在錯誤日誌的「breadcrumbs」字段中。
這種用戶行爲伴隨錯誤上報的方式就相對輕量不少,首先錯誤和行爲一塊兒上報減小了請求數量;其次對用戶行爲選擇性採集,避免了大量冗餘信息對流量和存儲的消耗,就js錯誤診斷而言會使得錯誤現場變得更清晰,對分析錯誤有利。
固然這種行爲上報的方式也是有不可忽視的問題,即行爲和js error的強耦合,使得用戶行爲信息失去了業務分析的擴展性,僅適用於JS錯誤診斷。
圖8展現了Arms前端性能監控的JS錯誤診斷中用戶行爲回溯的狀況,在錯誤詳情中復原出錯現場,輔助排查。下圖展現了用戶在進行了「接口請求-控制檯輸出-點擊按鈕-控制檯輸出」的一系列行爲後發生了JS錯誤,便可從按鈕的點擊事件的處理函數中進行錯誤排查。
本文爲雲棲社區原創內容,未經容許不得轉載。