1

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

  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>

风云小小生
15 声望0 粉丝