前端代碼異常監控

前言

以前在對公司的前端代碼腳本錯誤進行排查,試圖下降 JS Error 的錯誤量,結合本身以前的經驗對這方面內容進行了實踐並總結,下面就此談談我對前端代碼異常監控的一些看法。html

本文大體圍繞下面幾點展開討論:前端

  1. JS 處理異常的方式
  2. 上報方式
  3. 異常監控上報常見問題

JS 異常處理

對於 Javascript 而言,咱們面對的僅僅只是異常,異常的出現不會直接致使 JS 引擎崩潰,最多隻會使當前執行的任務終止。webpack

  1. 當前代碼塊將做爲一個任務壓入任務隊列中,JS 線程會不斷地從任務隊列中提取任務執行。ios

  2. 當任務執行過程當中出現異常,且異常沒有捕獲處理,則會一直沿着調用棧一層層向外拋出,最終終止當前任務的執行。git

  3. JS 線程會繼續從任務隊列中提取下一個任務繼續執行。github

在對腳本錯誤進行上報以前,咱們須要對異常進行處理,程序須要先感知到腳本錯誤的發生,而後再談異常上報。

腳本錯誤通常分爲兩種:語法錯誤,運行時錯誤。web

下面就談談幾種異常監控的處理方式:ajax

try-catch 異常處理npm

try-catch 在咱們的代碼中常常見到,經過給代碼塊進行 try-catch 進行包裝後,當代碼塊發生出錯時 catch 將能捕捉到錯誤的信息,頁面也將能夠繼續執行。編程

可是 try-catch 處理異常的能力有限,只能捕獲捉到運行時非異步錯誤,對於語法錯誤和異步錯誤就顯得無能爲力,捕捉不到。

示例:運行時錯誤

try{error// 未定義變量 }catch(e){console.log('我知道錯誤了');console.log(e);}
複製代碼

然而對於語法錯誤和異步錯誤就捕捉不到了。

示例:語法錯誤

try{varerror='error';// 大寫分號}catch(e){console.log('我感知不到錯誤');console.log(e);}
複製代碼

通常語法錯誤在編輯器就會體現出來,常表現的錯誤信息爲: Uncaught SyntaxError: Invalid or unexpected token xxx 這樣。可是這種錯誤會直接拋出異常,常使程序崩潰,通常在編碼時候容易觀察獲得。

示例:異步錯誤

try{setTimeout(()=>{error// 異步錯誤})}catch(e){console.log('我感知不到錯誤');console.log(e);}
複製代碼

除非你在 setTimeout 函數中再套上一層 try-catch,不然就沒法感知到其錯誤,但這樣代碼寫起來比較囉嗦。

window.onerror 異常處理

window.onerror 捕獲異常能力比 try-catch 稍微強點,不管是異步仍是非異步錯誤,onerror 都能捕獲到運行時錯誤。

示例:運行時同步錯誤

/** * @param {String}  msg    錯誤信息 * @param {String}  url    出錯文件 * @param {Number}  row    行號 * @param {Number}  col    列號 * @param {Object}  error  錯誤詳細信息 */window.onerror=function(msg,url,row,col,error){console.log('我知道錯誤了');console.log({msg,url,row,col,error})returntrue;};error;
複製代碼

示例:異步錯誤

window.onerror=function(msg,url,row,col,error){console.log('我知道異步錯誤了');console.log({msg,url,row,col,error})returntrue;};setTimeout(()=>{error;});
複製代碼

然而 window.onerror 對於語法錯誤仍是無能爲力,因此咱們在寫代碼的時候要儘量避免語法錯誤的,不過通常這樣的錯誤會使得整個頁面崩潰,仍是比較容易可以察覺到的。

在實際的使用過程當中,onerror 主要是來捕獲預料以外的錯誤,而 try-catch 則是用來在可預見狀況下監控特定的錯誤,二者結合使用更加高效。

須要注意的是,window.onerror 函數只有在返回 true 的時候,異常纔不會向上拋出,不然即便是知道異常的發生控制檯仍是會顯示 Uncaught Error: xxxxx。

關於 window.onerror 還有兩點須要值得注意

  1. 對於 onerror 這種全局捕獲,最好寫在全部 JS 腳本的前面,由於你沒法保證你寫的代碼是否出錯,若是寫在後面,一旦發生錯誤的話是不會被 onerror 捕獲到的。
  2. 另外 onerror 是沒法捕獲到網絡異常的錯誤。

當咱們遇到 報 404 網絡請求異常的時候,onerror 是沒法幫助咱們捕獲到異常的。

<script>window.onerror=function(msg,url,row,col,error){console.log('我知道異步錯誤了');console.log({msg,url,row,col,error})returntrue;};</script><imgsrc="./404.png">
複製代碼

因爲網絡請求異常不會事件冒泡,所以必須在捕獲階段將其捕捉到才行,可是這種方式雖然能夠捕捉到網絡請求的異常,可是沒法判斷 HTTP 的狀態是 404 仍是其餘好比 500 等等,因此還須要配合服務端日誌才進行排查分析才能夠。

<script>window.addEventListener('error',(msg,url,row,col,error)=>{console.log('我知道 404 錯誤了');console.log(msg,url,row,col,error);returntrue;},true);</script><imgsrc="./404.png"alt="">
複製代碼

這點知識仍是須要知道,要否則用戶訪問網站,圖片 CDN 沒法服務,圖片加載不出來而開發人員沒有察覺就尷尬了。

Promise 錯誤

經過 Promise 能夠幫助咱們解決異步回調地獄的問題,可是一旦 Promise 實例拋出異常而你沒有用 catch 去捕獲的話,onerror 或 try-catch 也無能爲力,沒法捕捉到錯誤。

window.addEventListener('error',(msg,url,row,col,error)=>{console.log('我感知不到 promise 錯誤');console.log(msg,url,row,col,error);},true);Promise.reject('promise error');newPromise((resolve,reject)=>{reject('promise error');});newPromise((resolve)=>{resolve();}).then(()=>{throw'promise error'});
複製代碼

雖然在寫 Promise 實例的時候養成最後寫上 catch 函數是個好習慣,可是代碼寫多了就容易糊塗,忘記寫 catch。

因此若是你的應用用到不少的 Promise 實例的話,特別是你在一些基於 promise 的異步庫好比 axios 等必定要當心,由於你不知道何時這些異步請求會拋出異常而你並無處理它,因此你最好添加一個 Promise 全局異常捕獲事件 unhandledrejection。

window.addEventListener("unhandledrejection",function(e){e.preventDefault()console.log('我知道 promise 的錯誤了');console.log(e.reason);returntrue;});Promise.reject('promise error');newPromise((resolve,reject)=>{reject('promise error');});newPromise((resolve)=>{resolve();}).then(()=>{throw'promise error'});
複製代碼

固然,若是你的應用沒有作 Promise 全局異常處理的話,那極可能就像某乎首頁這樣:

異常上報方式

監控拿到報錯信息以後,接下來就須要將捕捉到的錯誤信息發送到信息收集平臺上,經常使用的發送形式主要有兩種:

  1. 經過 Ajax 發送數據
  2. 動態建立 img 標籤的形式

實例 - 動態建立 img 標籤進行上報

functionreport(error){varreportUrl='http://xxxx/report';newImage().src=reportUrl+'error='+error;}
複製代碼

監控上報常見問題

下述例子我所有放在個人 github 上,讀者能夠自行查閱,後面再也不贅述。

git clone https://github.com/happylindz/blog.git
cd blog/code/jserror/
npm install
複製代碼

Script error 腳本錯誤是什麼

由於咱們在線上的版本,常常作靜態資源 CDN 化,這就會致使咱們常訪問的頁面跟腳本文件來自不一樣的域名,這時候若是沒有進行額外的配置,就會容易產生 Script error。

可經過 npm run nocors 查看效果。

Script error 是瀏覽器在同源策略限制下產生的,瀏覽器處於對安全性上的考慮,當頁面引用非同域名外部腳本文件時中拋出異常的話,此時本頁面是沒有權利知道這個報錯信息的,取而代之的是輸出 Script error 這樣的信息。

這樣作的目的是避免數據泄露到不安全的域中,舉個簡單的例子,

<scriptsrc="xxxx.com/login.html"></script>
複製代碼

上面咱們並無引入一個 js 文件,而是一個 html,這個 html 是銀行的登陸頁面,若是你已經登陸了,那 login 頁面就會自動跳轉到 Welcome xxx...,若是未登陸則跳轉到 Please Login...,那麼報錯也會是 Welcome xxx... is not defined,Please Login... is not defined,經過這些信息能夠判斷一個用戶是否登陸他的賬號,給入侵者提供了十分便利的判斷渠道,這是至關不安全的。

介紹完背景後,那麼咱們應該去解決這個問題?

首先能夠想到的方案確定是同源化策略,將 JS 文件內聯到 html 或者放到同域下,雖然能簡單有效地解決 script error 問題,可是這樣沒法利用好文件緩存和 CDN 的優點,不推薦使用。正確的方法應該是從根本上解決 script error 的錯誤。

跨源資源共享機制( CORS )

首先爲頁面上的 script 標籤添加 crossOrigin 屬性

// http://localhost:8080/index.html
<script>window.onerror=function(msg,url,row,col,error){console.log('我知道錯誤了,也知道錯誤信息');console.log({msg,url,row,col,error})returntrue;};</script><scriptsrc="http://localhost:8081/test.js"crossorigin></script>

// http://localhost:8081/test.js
setTimeout(() => {
  console.log(error);
});
複製代碼

當你修改完前端代碼後,你還須要額外給後端在響應頭裏加上 Access-Control-Allow-Origin: localhost:8080,這裏我以 Koa 爲例。

constKoa=require('koa');constpath=require('path');constcors=require('koa-cors');constapp=newKoa();app.use(cors());app.use(require('koa-static')(path.resolve(__dirname,'./public')));app.listen(8081,()=>{console.log('koa app listening at 8081')});
複製代碼
讀者可經過 npm run cors 詳細的跨域知識我就不展開了,有興趣能夠看看我以前寫的文章:[跨域,你須要知道的全在這裏](https://link.zhihu.com/?target=https%3A//github.com/happylindz/blog/issues/3)

你覺得這樣就完了嗎?並無,下面就說一些 Script error 你不常碰見的點:

咱們都知道 JSONP 是用來跨域獲取數據的,而且兼容性良好,在一些應用中仍然會使用到,因此你的項目中可能會用這樣的代碼:

// http://localhost:8080/index.htmlwindow.onerror=function(msg,url,row,col,error){console.log('我知道錯誤了,但不知道錯誤信息');console.log({msg,url,row,col,error})returntrue;};functionjsonpCallback(data){console.log(data);}consturl='http://localhost:8081/data?callback=jsonpCallback';constscript=document.createElement('script');script.src=url;document.body.appendChild(script);
複製代碼

由於返回的信息會當作腳本文件來執行,一旦返回的腳本內容出錯了,也是沒法捕捉到錯誤的信息。 解決辦法也不難,跟以前同樣,在添加動態添加腳本的時候加上 crossOrigin,而且在後端配上相應的 CORS 字段便可.

constscript=document.createElement('script');script.crossOrigin='anonymous';script.src=url;document.body.appendChild(script);
複製代碼

讀者能夠經過 npm run jsonp 查看效果 知道原理以後你可能會以爲沒什麼,不就是給每一個動態生成的腳本添加 crossOrigin 字段嘛,可是在實際工程中,你多是面向不少庫來編程,好比使用 jQuery,Seajs 或者 webpack 來異步加載腳本,許多庫封裝了異步加載腳本的能力,以 jQeury 爲例你多是這樣來觸發異步腳本。

$.ajax({url:'http://localhost:8081/data',dataType:'jsonp',success:(data)=>{console.log(data);}})
複製代碼

假如這些庫中沒有提供 crossOrigin 的能力的話(jQuery jsonp 可能有,僞裝你不知道),那你只能去修改人家寫的源代碼了,因此我這裏提供一個思路,就是去劫持 document.createElement,從根源上去爲每一個動態生成的腳本添加 crossOrigin 字段。

document.createElement=(function(){constfn=document.createElement.bind(document);returnfunction(type){constresult=fn(type);if(type==='script'){result.crossOrigin='anonymous';}returnresult;}})();window.onerror=function(msg,url,row,col,error){console.log('我知道錯誤了,也知道錯誤信息');console.log({msg,url,row,col,error})returntrue;};$.ajax({url:'http://localhost:8081/data',dataType:'jsonp',success:(data)=>{console.log(data);}})
複製代碼

效果也是同樣的,讀者能夠經過 npm run jsonpjq 來查看效果: 這樣重寫 createElement 理論上沒什麼問題,可是入侵了本來的代碼,不保證必定不會出錯,在工程上仍是須要多嘗試下看看再使用,可能存在兼容性上問題,若是你以爲會出現什麼問題的話也歡迎留言討論下。

關於 Script error 的問題就寫到這裏,若是你理解了上面的內容,基本上絕大部分的 Script error 都能迎刃而解。

window.onerror 可否捕獲 iframe 的錯誤

當你的頁面有使用 iframe 的時候,你須要對你引入的 iframe 作異常監控的處理,不然一旦你引入的 iframe 頁面出現了問題,你的主站顯示不出來,而你卻渾然不知。

首先須要強調,父窗口直接使用 window.onerror 是沒法直接捕獲,若是你想要捕獲 iframe 的異常的話,有分好幾種狀況。

若是你的 iframe 頁面和你的主站是同域名的話,直接給 iframe 添加 onerror 事件便可。

<iframesrc="./iframe.html"frameborder="0"></iframe><script>window.frames[0].onerror=function(msg,url,row,col,error){console.log('我知道 iframe 的錯誤了,也知道錯誤信息');console.log({msg,url,row,col,error})returntrue;};</script>
複製代碼

讀者能夠經過 npm run iframe 查看效果: 若是你嵌入的 iframe 頁面和你的主站不是同個域名的,可是 iframe 內容不屬於第三方,是你能夠控制的,那麼能夠經過與 iframe 通訊的方式將異常信息拋給主站接收。與 iframe 通訊的方式有不少,經常使用的如:postMessage,hash 或者 name 字段跨域等等,這裏就不展開了,感興趣的話能夠看:跨域,你須要知道的全在這裏

若是是非同域且網站不受本身控制的話,除了經過控制檯看到詳細的錯誤信息外,沒辦法捕獲,這是出於安全性的考慮,你引入了一個百度首頁,人家頁面報出的錯誤憑啥讓你去監控呢,這會引出不少安全性的問題。

壓縮代碼如何定位到腳本異常位置

線上的代碼幾乎都通過了壓縮處理,幾十個文件打包成了一個並醜化代碼,當咱們收到 a is not defined 的時候,咱們根本不知道這個變量 a 到底是什麼含義,此時報錯的錯誤日誌顯然是無效的。

第一想到的辦法是利用 sourcemap 定位到錯誤代碼的具體位置,詳細內容能夠參考:Sourcemap 定位腳本錯誤

另外也能夠經過在打包的時候,在每一個合併的文件之間添加幾行空格,並相應加上一些註釋,這樣在定位問題的時候很容易能夠知道是哪一個文件報的錯誤,而後再經過一些關鍵詞的搜索,能夠快速地定位到問題的所在位置。

收集異常信息量太多,怎麼辦

若是你的網站訪問量很大,假如網頁的 PV 有 1kw,那麼一個必然的錯誤發送的信息就有 1kw 條,咱們能夠給網站設置一個採集率:

Reporter.send=function(data){// 只採集 30%if(Math.random()<0.3){send(data)// 上報錯誤信息}}
複製代碼

這個採集率能夠經過具體實際的狀況來設定,方法多樣化,可使用一個隨機數,也能夠具體根據用戶的某些特徵來進行斷定。

上面差很少是我對前端代碼監控的一些理解,提及來容易,可是一旦在工程化運用,不免須要考慮到兼容性等種種問題,讀者能夠經過本身的具體狀況進行調整,前端代碼異常監控對於咱們的網站的穩定性起着相當重要的做用。如若文中全部不對的地方,還望指正。

參考文章

相關文章
相關標籤/搜索