关于错误上报

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

  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顺序执行,我们可以将相关代码放在最前头,





    
    
    







但是,要注意,放在最前面的是处理错误的逻辑,上报的计时器不能立即开启,因为,此时jquery 还没加载,
计时器开启放在至少jquery加载完成之后。

2.一定要做好处理错误部分代码的容错处理,不然业务逻辑代码还没报错,处理错误的部分反而报错就不好了。

3.当你直接双击html,在浏览器打开时,错误收集机制可能不会正确工作,例如没有行号,列号,文件名,错误信息仅仅是Script Error,这是因为onerror MDN

当加载自 不同域的脚本中发生语法错误时,为避免信息泄露(参见 bug 363897),语法错误的细节将不会报告,而代之简单的 **"Script error."**。在某些浏览器中,通过在

你可能感兴趣的:(javascript,错误日志提交,cocos)