/ avascript

构建前端错误日志系统

为什么要做前端代码异常监控?

如何检测前端异常

try,catch

我们可以通过对代码块加入一个 try,catch 块来获得出错信息。try,catch 能够知道出错的信息,并且也有堆栈信息可以知道在哪个文件第几行第几列发生错误。

但是 try,catch 有 2 个缺点:

  • 无法捕捉 try,catch 块,当前代码块有语法错误,JS解释器不会执行当前这个代码块,所以无法被 catch。
  • 无法捕捉到全局的错误事件,只有 try,catch 里边代码运行出错才会被它捕捉到。

第一个缺点,没有任何解决办法,只能在开发阶段/或者用工具检测到的。

第二个缺点,可以利用 uglifyjs 的词法语法分析,在 uglifyjs 最后输出压缩文件的时候往文件块以及 function 块加入了 try,catch。为了方便出错时在日志里能定位具体模块,可以给每个块加编号。

window.onerror

window.onerror 也可以获得出错的信息以及文件名、行号、列号,还可以让 window.onerror 最后 return true 使得浏览器不输出错误信息到控制台。

window.onerror 能捕捉到语法错误,前提是语法出错的代码块不能跟 window.onerror 在同一个块(语法都没过,window.onerror 不会被执行)

只要把 window.onerror 这个代码块分离出去,并且比其他脚本先执行,例如放在 head (注意这个前提!),即可捕捉到语法错误。

对于跨域的 JS 资源,window.onerror 拿不到详细的信息,需要往资源的请求添加额外的头部。

静态资源请求需要加多一个 Access-Control-Allow-Origin 头部,同时 script 引入外链的标签需要加多一个 crossorigin 的属性。这样折腾后就能获取到准确的出错信息。

总结

try,catch 的方案有如下特点:

  • 无法捕捉到语法错误,只能捕捉运行时错误;
  • 可以拿到出错的信息,堆栈,出错的文件、行号、列号;
  • 需要借助工具把所有的 function 块以及文件块加入 try,catch,可以在这个阶段打入更多的静态信息。

window.onerror 的方案有如下特点:

  • 可以捕捉语法错误,也可以捕捉运行时错误;
  • 可以拿到出错的信息,堆栈,出错的文件、行号、列号;
  • 只要在当前页面执行的js脚本出错都会捕捉到,例如:浏览器插件的 javascript、或者 flash 抛出的异常等。
  • 跨域的资源需要特殊头部支持。

window.onerror 的方法要比 try,catch 方法更加完善,少量由于插件带来的脚本错误是被允许的,最后 window.onerror 实现的方式是:

window.onerror = function(msg,url,line,col,error){
    //没有URL不上报!上报也不知道错误
    if (msg != "Script error." && !url){
        return true;
    }
    //采用异步的方式
    //我遇到过在window.onunload进行ajax的堵塞上报
    //由于客户端强制关闭webview导致这次堵塞上报有Network Error
    //我猜测这里window.onerror的执行流在关闭前是必然执行的
    //而离开文章之后的上报对于业务来说是可丢失的
    //所以我把这里的执行流放到异步事件去执行
    //脚本的异常数降低了10倍
    setTimeout(function(){
        var data = {};
        //不一定所有浏览器都支持col参数
        col = col || (window.event && window.event.errorCharacter) || 0;
 
        data.url = url;
        data.line = line;
        data.col = col;
        data.ua = navigator.userAgent
        if (!!error && !!error.stack){
            //如果浏览器有堆栈信息
            //直接使用
            data.msg = error.stack.toString();
        }else if (!!arguments.callee){
            //尝试通过callee拿堆栈信息
            var ext = [];
            var f = arguments.callee.caller, c = 3;
            //这里只拿三层堆栈信息
            while (f && (--c>0)) {
               ext.push(f.toString());
               if (f  === f.caller) {
                    break;//如果有环
               }
               f = f.caller;
            }
            ext = ext.join(",");
            data.msg = ext;
        }
        //把data上报到后台!
    },0);
 
    return true;
};