关于错误上报

最近在写错误上报,记录一下,若是对你有所帮助,荣幸之至;
第一次写,有点啰嗦,见谅!
大概分为三个部分: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>
相关文章
相关标签/搜索