乐趣区

关于错误上报

最近在写谬误上报,记录一下,如果对你有所帮忙,荣幸之至;
第一次写,有点啰嗦,见谅!
大略分为三个局部:

  1. 谬误收集
  2. 谬误筛选
  3. 谬误上报
  4. 注意事项
  5. 残缺示例

一、谬误收集
js 的谬误个别分为:运行时谬误、资源加载谬误、网络申请谬误;
对于语法错误、资源加载谬误,供咱们抉择的谬误收集形式个别是:

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

** 划重点:**

  • 两者取得的参数不一样;
  • window.addEventListener 能监测到资源(css,img,script)加载失败;
  • window.addEventListener 能捕捉到 window.onerror 能捕捉到的谬误;
  • 二者都不能捕捉到 console.error 的错误信息;
  • 二者都不能捕捉到:当 promise 被 reject 并且错误信息没有被解决时的错误信息;

因而咱们能够这样收集谬误:

 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);

当是运行时的语法错误时,咱们能够拿到报错的行号,列号,错误信息,谬误堆栈,以及产生谬误的脚本的门路及名字。
当是资源门路谬误时,咱们能够拿到谬误资源的门路及名字。

至此,咱们就拿到了想要的 资源加载谬误、运行时语法错误 的信息,那 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 形式没法收集到,然而咱们能够通过其余形式收集到谬误,上面是一个更非凡的例子;

** 特例:**

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,
保障不会影响到其余的逻辑。


二、谬误筛选

兴许你会纳闷?不是所有的谬误都上报么,为什么要筛选呢?
大多数状况,咱们收集到谬误,而后上报即可,
然而,有时候,会有 循环报错 资源加载失败始终重试,始终失败 等种种非凡状况,如果依照失常的上报流程,那么可能会产生在短短几秒的工夫内,收集到了上千、上万条数据,导致程序卡顿,甚至是解体。

因而,咱们须要对谬误进行筛选。

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;
    }

存储谬误的数组也须要阈值,理论使用中,咱们能够管制每次上报的谬误条数,然而,肯定得记得曾经上报的谬误肯定要从数组中移出。此外,上报的数据结构依据需要能够调整,个别蕴含错误信息、堆栈信息、加载失败资源的门路。


三、谬误上报

难道不是一收集到谬误就上报?
同时呈现一个两个谬误,当然能够立刻上报,
然而如果千百个谬误在短短的几秒钟呈现,就会呈现网络拥挤,甚至是程序解体。
因而,个别都会全局保护一个计时器,提早上报;

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);
}

咱们能够依据谬误的数量,调整谬误上报的频率。然而这个距离个别不要太小,不然容易出问题。


四、注意事项

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"。这样,便解决了因为跨域而带来的问题。

五、残缺代码

<!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>
退出移动版