關於錯誤上報

最近在寫錯誤上報,記錄一下,若是對你有所幫助,榮幸之至;
第一次寫,有點囉嗦,見諒!
大概分爲三個部分:javascript

  1. 錯誤收集
  2. 錯誤篩選
  3. 錯誤上報
  4. 注意事項
  5. 完整示例

1、錯誤收集
js的錯誤通常分爲:運行時錯誤、資源加載錯誤、網絡請求錯誤;
對於語法錯誤、資源加載錯誤,供咱們選擇的錯誤收集方式通常是:css

window.addEventListener('error', e => {}, true);
window.onerror = function (msg, url, line, col, error) {}

**劃重點:**html

  • 二者得到的參數不同;
  • window.addEventListener能監測到資源(css,img,script)加載失敗;
  • window.addEventListener能捕捉到window.onerror能捕捉到的錯誤;
  • 兩者都不能捕捉到console.error的錯誤信息;
  • 兩者都不能捕捉到:當promise被reject而且錯誤信息沒有被處理時的錯誤信息;

所以咱們能夠這樣收集錯誤:java

window.addEventListener("error", e => {
        if (!e) {
            return;
        }

        console.log(e.message);//錯誤信息
        conosle.log(e.filename);//發生錯誤的文件名
        console.log(e.lineno);//發生錯誤的行號(代碼壓縮合並對值有影響)
        console.log(e.colno);//發生錯誤的列號(代碼壓縮合並對值有影響)

        const _target = e.target || e.srcElement;
        if (!_target) {
            return;
        }
        if (_target === window) {
            //語法錯誤
            let _error = e.error;
            if (_error) {
                console.log(_error.stack);//錯誤的堆棧信息
            }
        } else {
            // 元素錯誤,好比引用資源報錯
            let _src = _target.src;
            console.log(_src);//_src: 錯誤的資源路徑
        }
    }, true);

當是運行時的語法錯誤時,咱們能夠拿到報錯的行號,列號,錯誤信息,錯誤堆棧,以及發生錯誤的腳本的路徑及名字。
當是資源路徑錯誤時,咱們能夠拿到錯誤資源的路徑及名字。jquery

至此,咱們就拿到了想要的資源加載錯誤、運行時語法錯誤的信息,那ajax網絡請求錯誤怎麼辦呢?ajax

此時:有兩個方式能夠選擇後端

throw new Error('拋出的一個錯誤');
console.error('打印一個錯誤');//下面會講

咱們前面定義的方法能夠收集到throw new Error拋出的錯誤,可是要注意,拋出錯誤一樣也會阻斷後續的程序,使用的時候要當心;若是你的項目中也封裝了http請求的話,可參照下面代碼:跨域

//基於jquery
    function ajaxFun (params) {
        var _d = {
            type: params.type || 'POST',
            url: params.url || '',
            data: params.data || null,
            dataType: params.dataType || 'JSON',
            contentType: params.contentType || 'application/x-www-form-urlencoded',
            beforeSend: function (request) {
               
            },
            success: function (data, status, xhr) {

            },
            error: function (xhr, type, error) {
                throw new Error(params.url + '請求失敗');
            }
        }
        $.ajax(_d);
    }

上面的代碼是用jquery封裝的請求,我在error方法裏面拋出了這個ajax請求的錯誤,由於拋出錯誤後面沒有其餘業務邏輯,不會有什麼問題,這裏我只要求收集ajax的error方法錯誤,若是你的項目要求處理全部異常錯誤,好比token失效致使的登錄失敗,就須要在success函數裏面也作處理了。可是,要注意throw new Error('拋出的一個錯誤')console.error('打印一個錯誤')的區別。數組

當使用console.error打印錯誤時,前面的window.addEventListener方式無法收集到,可是咱們能夠經過其餘方式收集到錯誤,下面是一個更特殊的例子;promise

**特例:**

js運用範圍很廣,有些狀況,這樣是不可以收集到咱們想要的錯誤的;

打個比方,咱們用 cocos creator 引擎寫遊戲時,加載資源是使用引擎的方法,當發生資源不存在的錯誤時,咱們是不知道的,可是,咱們發現 cocos creator 引擎會將錯誤打印到控制檯,那也是引擎作的操做,咱們一番順藤摸瓜,會發現,cocos creator 引擎在底層報錯都是用cc.error,翻看cc.error的源碼,咱們就看見了咱們想看見的東西了console.error(),這樣一來,知道錯誤是怎麼來的,就好辦了。(具體狀況,具體對待,這裏只是恰巧cocos是這麼處理的,其餘引擎可能不太同樣)

let _windowError = window.console.error;
window.console.error = function () {
    let _str = JSON.stringify(arguments);
    console.log(_str);
    _windowError && _windowError.apply(window, arguments);
}

複寫console.error後,不管和人在何處使用這個函數,咱們均可以保證這個打印被咱們處理過,
記住,必定要先將原來的console.error接收一下,而且在實現咱們須要的業務後,執行原來console.error,
保證不會影響到其餘的邏輯。


2、錯誤篩選

也許你會疑惑?不是全部的錯誤都上報麼,爲何要篩選呢?
大多數狀況,咱們收集到錯誤,而後上報便可,
可是,有時候,會有循環報錯資源加載失敗一直重試,一直失敗 等種種特殊狀況,若是按照正常的上報流程,那麼可能會發生在短短几秒的時間內,收集到了上千、上萬條數據,致使程序卡頓,甚至是崩潰。

所以,咱們須要對錯誤進行篩選。

let _errorMap = {};//用於錯誤篩選的對象;
let _errorArg = [];//存放錯誤信息的數組;

全局維護一個_errorMap,用於錯誤篩選的對象,每當有錯誤時,咱們按照約定好的規則,組成一個key,和_errorMap已經存在的key進行比對,若是不存在,證實是新的錯誤,須要上報,若是是已經上報的錯誤,就再也不處理。
固然,爲了防止_errorMap無限大、以及錯誤漏報,當_errorMap的key的數量大於必定數量時,咱們須要將_errorMap的key清空,這時候可能出現前面已經上報的錯誤再次上報,可是沒關係,這個重複能夠接受。

這個臨界值能夠根據實際狀況定,我項目中最大值爲100。

對於上面這個約定好的規則,其實就是根據咱們上面收集到的有關錯誤的信息,組成的一個惟一key值,
能實現惟一性且越短越好便可

//上面的代碼,複製下來,方便看
window.addEventListener("error", e => {
        if (!e) {
            return;
        }

        console.log(e.message);//錯誤信息
        conosle.log(e.filename);//發生錯誤的文件名
        console.log(e.lineno);//發生錯誤的行號(代碼壓縮合並對值有影響)
        console.log(e.colno);//發生錯誤的列號(代碼壓縮合並對值有影響)

        const _target = e.target || e.srcElement;
        if (!_target) {
            return;
        }
        if (_target === window) {
            //語法錯誤
            let _error = e.error;
            if (_error) {
                console.log(_error.stack);//錯誤的堆棧信息
            }
        } else {
            // 元素錯誤,好比引用資源報錯
            let _src = _target.src;
            console.log(_src);//_src: 錯誤的資源路徑
        }
}, true);
對於語法錯誤,能夠根據報錯的文件名,行號,列號,組成key
let _key = `${e.filename}_${e.lineno}_${e.colno}`;
對於資源加載錯誤,能夠根據錯誤資源的路徑做爲key:
let _key = e.src;

拿到key以後,咱們就能夠存貯錯誤了,

下面是存儲的完整代碼:

function _sendErr(key, errType, errMsg) {
        //篩選
        if (_ErrorMap.hasOwnProperty(key)) {
            //篩選到相同的錯誤,可將值加一,能夠判斷錯誤出現的次數
            _ErrorMap[key] += 1;
            return;
        }
        //閾值
        if (_ErrorArg.length >= 100) {
            return;
        }
        //存儲錯誤
        //對於要發給後端的數據,可根據需求組織,數據結構
        _ErrorArg.push({
            errType: errType,//錯誤類型
            errMsg: errMsg || '',//錯誤信息
            ver: _ver || '',//版本號
            timestamp: new Date().getTime(),//時間戳
        });
        //存放錯誤信息的數組的閾值
        if (Object.keys(_ErrorMap).length >= 100) {
            //達到閾值以後,清空去重對象
            _ErrorMap = {};
        }
        _ErrorMap[key] = 1;
    }

存儲錯誤的數組也須要閾值,實際運用中,咱們能夠控制每次上報的錯誤條數,可是,必定得記得已經上報的錯誤必定要從數組中移出。此外,上報的數據結構根據需求能夠調整,通常包含錯誤信息、堆棧信息、加載失敗資源的路徑。


3、錯誤上報

難道不是一收集到錯誤就上報?
同時出現一個兩個錯誤,固然能夠當即上報,
可是若是千百個錯誤在短短的幾秒鐘出現,就會出現網絡擁堵,甚至是程序崩潰。
所以,通常都會全局維護一個計時器,延遲上報;

let _ErrorTimer = null;
timerError();
function timerError() {
    clearTimeout(_ErrorTimer);
    let _ErrorArg = g.IndexGlobal.ErrorArg;//前面提到的全局錯誤存貯數組
    let _ErrorArgLength = _ErrorArg.length;
    if (_ErrorArgLength > 0) {
        let _data = [];//要發送的錯誤信息,由於是一次性發5條,放零時數組中。
        //組織要發送的錯誤信息
        for (let i = 0; i < _ErrorArgLength; i++) {
            if (_data.length >= 5) {
                break;
            }
            _data.push(_ErrorArg.shift());
        }
        
        if (_data.length) {
            //發送錯誤信息
            //jq ajax
            g.IndexGlobal.errorSend(_data, function (p) {
                //失敗
                //若是發送失敗,將未發送的數據,從新放入存儲錯誤信息的數組中
                if (p && p.data && p.data.data) {
                    if (_ErrorArg.length >= 100) {
                        return;
                    }
                    let _ag = p.data.data;
                    try {
                        g.IndexGlobal.ErrorArg.push(...JSON.parse(_ag));
                    } catch (error) {

                    }
                }
            });
        }
    }
    //計時器間隔,當數組長度大於20時,一秒執行一次,默認2秒一次
    let _ti = _ErrorArgLength >= 20 ? 1000 : 2000;
    _ErrorTimer = setTimeout(timerError, _ti);
}

咱們能夠根據錯誤的數量,調整錯誤上報的頻率。可是這個間隔通常不要過小,否則容易出問題。


4、注意事項

1.不管是window.addEventLister仍是console.error,在咱們定義這些方法以前報的全部錯誤,咱們是收集不到的,
怎麼處理呢,很簡單,js順序執行,咱們能夠將相關代碼放在最前頭,

<!DOCTYPE html>
<html>
<script>
    //處理錯誤的代碼
    window.addEventLister;
    console.error = function(){}
</script>
<head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="">
    <link rel="stylesheet" href="">
</head>

<body>
</body>
<script src="js/zepto.min.js"></script>
<script src="js/a.js"></script>
<script>
    //開始錯誤上報計數器
</script>
</html>

可是,要注意,放在最前面的是處理錯誤的邏輯,上報的計時器不能當即開啓,由於,此時jquery 還沒加載,
計時器開啓放在至少jquery加載完成以後。

2.必定要作好處理錯誤部分代碼的容錯處理,否則業務邏輯代碼還沒報錯,處理錯誤的部分反而報錯就很差了。

3.當你直接雙擊html,在瀏覽器打開時,錯誤收集機制可能不會正確工做,例如沒有行號,列號,文件名,錯誤信息僅僅是Script Error,這是由於onerror MDN

當加載自 不一樣域的腳本中發生語法錯誤時,爲避免信息泄露(參見 bug 363897),語法錯誤的細節將不會報告,而代之簡單的 **"Script error."**。在某些瀏覽器中,經過在 <script>使用 `[crossorigin](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/script#attr-crossorigin)` 屬性並要求服務器發送適當的 CORS HTTP 響應頭,該行爲可被覆蓋。一個變通方案是單獨處理"Script error.",告知錯誤詳情僅能經過瀏覽器控制檯查看,沒法經過JavaScript訪問。

處理方式爲:服務端添加Access-Control-Allow-Origin,頁面在script標籤中配置 crossorigin="anonymous"。這樣,便解決了由於跨域而帶來的問題。

5、完整代碼

<!DOCTYPE html>
<html>
<script>
    //處理錯誤的命名空間
    window['errorSpace'] = {
        ErrorTimer: null, //全局錯誤上報計時器
        ErrorArg: [], //全局錯誤存儲數組
        ErrorMap: {}, //用於錯誤篩選的對象
        //存儲錯誤信息
        PushError: function (key, errMsg) {
            let _ErrorMap = window.errorSpace.ErrorMap;
            let _ErrorArg = window.errorSpace.ErrorArg;
            //篩選
            if (_ErrorMap.hasOwnProperty(key)) {
                //篩選到相同的錯誤,可將值加一,能夠判斷錯誤出現的次數
                _ErrorMap[key] += 1;
                return;
            }
            //閾值
            if (_ErrorArg.length >= 100) {
                return;
            }
            //存儲錯誤
            //對於要發給後端的數據,可根據需求組織,數據結構
            _ErrorArg.push({
                errMsg: errMsg || '', //錯誤信息
                ver: '', //版本號
                timestamp: new Date().getTime(), //時間戳
            });
            //存放錯誤信息的數組的閾值
            if (Object.keys(_ErrorMap).length >= 100) {
                //達到閾值以後,清空去重對象
                _ErrorMap = {};
            }
            _ErrorMap[key] = 1;
        },
        //錯誤上報函數
        ErrorSend: function () {
            clearTimeout(window.errorSpace.ErrorTimer);
            let _ErrorArg = window.errorSpace.ErrorArg; //前面提到的全局錯誤存貯數組
            let _ErrorArgLength = _ErrorArg.length;
            if (_ErrorArgLength > 0) {
                let _data = []; //要發送的錯誤信息,由於是一次性發5條,放零時數組中。
                //組織要發送的錯誤信息
                for (let i = 0; i < _ErrorArgLength; i++) {
                    if (_data.length >= 5) {
                        break;
                    }
                    _data.push(_ErrorArg.shift());
                }

                if (_data.length) {
                    //發送錯誤信息
                    //jq ajax
                    var _d = {
                        type: 'POST',
                        url: '',
                        data: _data || null,
                        dataType: 'JSON',
                        contentType: 'application/x-www-form-urlencoded',
                        success: function (data, status, xhr) {

                            //上報失敗,將錯誤從新存儲

                            //這是假設服務端返回的數據結構是{status: 200}
                            if (data.status !== 200) {
                                //失敗
                                try {
                                    //直接存入
                                    //此處沒有對_ErrorArg的長度進行判斷,因此會溢出一次,使得錯誤錯誤儘量的保留,問題不大,也能夠不讓溢出
                                    _ErrorArg.push(..._data);
                                } catch (error) {
                                    console.log(error);
                                }
                            }
                        },
                        error: function (xhr, type, error) {
                            //上報失敗,將錯誤從新存儲
                            try {
                                //直接存入
                                //此處沒有對_ErrorArg的長度進行判斷,因此會溢出一次,使得錯誤錯誤儘量的保留,問題不大,也能夠不讓溢出
                                _ErrorArg.push(..._data);
                            } catch (error) {
                                console.log(error);
                            }
                        }

                    }
                    $.ajax(_d);
                }
            }
            //計時器間隔,當數組長度大於20時,一秒執行一次,默認2秒一次
            let _ti = _ErrorArgLength >= 20 ? 1000 : 2000;
            window.errorSpace.ErrorTimer = setTimeout(window.errorSpace.ErrorSend, _ti);
        },
    };
    //錯誤收集
    window.addEventListener("error", e => {
        if (!e) {
            return;
        }
        let _err_msg = ''; //要上報的錯誤信息

        let _r = 0; //發生錯誤的行號
        let _l = 0; //發生錯誤的列號
        let _fileName = ''; //發生錯誤的文件名

        const srcElement = e.target || e.srcElement;
        if (!srcElement) {
            return;
        }
        if (srcElement === window) {
            //語法錯誤
            let _error = e.error;
            if (_error) {
                _err_msg = _error.message + _error.stack;
                _r = e.lineno || 0;
                _l = e.colno || 0;
                _fileName = e.filename || '';
            }
        } else {
            // 元素錯誤,好比引用資源報錯
            if (srcElement.src) {
                _err_msg = srcElement.src;
                _fileName = srcElement.src;
            }
        }
        let _key = `${_fileName}_${_r}_${_l}`;
        window.errorSpace.PushError(_key, _err_msg);
    }, true);
    //處理console.error;
    let _windowError = window.console.error;
    window.console.error = function () {
        let _str = JSON.stringify(arguments);
        window.errorSpace.PushError(_str, _str);
        _windowError && _windowError.apply(window, arguments);
    }
</script>

<head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="">
    <link rel="stylesheet" href="">
</head>

<body>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script>
    //開始錯誤上報計數器
    window.errorSpace && window.errorSpace.ErrorSend && window.errorSpace.ErrorSend();
    
</script>

</html>
相關文章
相關標籤/搜索