近期在熟悉怎样处理前端异常,在客户端跑的h5代码,如果遇到体量大的客户群(几百w,几千w),对前端js进行异常监控就变得很重要了,因为测试并不能完整的捕捉到某些情况和场景下的异常,其中包括了接口返回信息缺失、固定操作下的js报错等,我的思路大概是这样:

1、前端对异常进行收集

2、上报到node server

3、通过 log4js进行日志记录

当然,如果想体验更好,也可以通过node server 将异常入库(结合mongodb),在后台通过数据结合类

chart 组件通过图形化展示异常信息更为直观。


一、前端异常收集。

前端的异常收集常用的两种方式:

1try catch

try,catch能够知道出错的信息,并且也有堆栈信息可以知道在哪个文件第几行第几列发生错误。

但是try,catch的方案有2个缺点:

  1. 没法捕捉try,catch块,当前代码块有语法错误,JS解释器压根都不会执行当前这个代码块,所以也就没办法被catch住;

  2. 没法捕捉到全局的错误事件,也即是只有try,catch的块里边运行出错才会被你捕捉到,这里的块你要理解成一个函数块。


2window.onerror

  1. 可以捕捉语法错误,也可以捕捉运行时错误;

  2. 可以拿到出错的信息,堆栈,出错的文件、行号、列号;

  3. 只要在当前页面执行的js脚本出错都会捕捉到,例如:浏览器插件的javascript、或者flash抛出的异常等。

  4. 跨域的资源需要特殊头部支持。


需要注意的是:

1、window.onerror能捕捉到语法错误,但是语法出错的代码块不能跟window.onerror在同一个块

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

window.onerror = (msg, url, line, col, error) => {
            //没有URL不上报!上报也不知道错误
            if (msg != "Script error." && !url) {
                return true;
            }
            setTimeout(() => {
                var data = {};
                //不一定所有浏览器都支持col参数
                col = col || (window.event && window.event.errorCharacter) || 0;

                data.url = url;
                data.line = line;
                data.col = col;
                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上报到后台!
               //这里可以做日志上报
               $.ajax({})
               
            }, 0);
            return false;
        };


这里我们在后面返回false,让控制台也能把错误打印出来,至此,前端异常收集完成!


二、上报到node server

服务端要做的,就是提供上传数据的接口,让错误数据能保存下来,很简单,我们加到express路由中就可以了:


/* 写入前端日志 */

router.post('/restapi/reportErrInfo', (req, res, next) => {

    let logParams = req.query;
    let logUrl = logParams.url;
    let sendState = false;
    if (autoUrl(logUrl)) {
        logUtil.logh5Error(req, logParams);
        sendState = true;
    }

    return res.json({
        data: {
            responseCode: '0',
            responseMsg: sendState ? 'success!' : 'failed!'
        }
    })

});


这里主要是通过接口写入日志的操作, 这是我通过log4js封装的方法,也就是下面这段:

logUtil.logh5Error(req, logParams);


另外需要重点说明两点:

1、日志上报使用post方式,更安全

2、为了防止恶意请求(csrc等),需要在接口处对信息进行鉴权处理,方法很多,常用的比如通过对比cookie,或者前端传token的方式


三、通过 log4js进行日志记录

nodeJS自带的console.log已经可以打印出日志了,为了让日志看起来没那么糟,我打算对日子进行改造(将常规日志response和错误日志error分开),具体实现如下:


1、新建log配置文件 logConfig.js

let path = require('path');

//日志根目录

let isDevEnv = (process.env.NODE_ENV == 'development' || process.env.NODE_ENV == 'FAT') ? true : false;
let baseLogPath = isDevEnv ? path.resolve(__dirname, '../logs') : '/home/reslogs’;
//错误日志目录
let errorPath = isDevEnv ? "/error" : ‘/home/errlogs';
//错误日志文件名
let errorFileName = "error";
//错误日志输出完整路径
let errorLogPath = baseLogPath + errorPath + "/" + errorFileName;


//响应日志目录
let responsePath = isDevEnv ? "/response" : '';
//响应日志文件名
let responseFileName = "response";
//响应日志输出完整路径
let responseLogPath = baseLogPath + responsePath + "/" + responseFileName;

module.exports = {
    "appenders":
    [
        //错误日志 默认按小时数记录
        {
            "category": "errorLogger",             //logger名称
            "type": "dateFile",                   //日志类型
            "filename": errorLogPath,             //日志输出位置
            "alwaysIncludePattern": true,          //是否总是有后缀名
            "pattern": "-yyyy-MM-dd.log",      //后缀,每天创建一个新的日志文件
            "path": errorPath                     //自定义属性,错误日志的根目录
        },
        //响应日志 响应日志默认按天记录
        {
            "category": "resLogger",
            "type": "dateFile",
            "filename": responseLogPath,
            "alwaysIncludePattern": true,
            "pattern": "-yyyy-MM-dd.log",   //后缀,每天创建一个新的日志文件
            "path": responsePath
        }
    ],
    "levels":                                   //设置logger名称对应的的日志等级
    {
        "errorLogger": "ERROR",
        "resLogger": "ALL"
    },
    "baseLogPath": baseLogPath                  //logs根目录
}


这里是log4js的配置文件,记录日志类型,保存文件格式以及路径等信息


2、增加 logUtil.js ,代码如下:

let log4js = require('log4js');
let fs = require('fs');
import logConfig from 'config/logConfig';
import _ from 'lodash';
//加载配置文件
log4js.configure(logConfig);

let errorLogger = log4js.getLogger('errorLogger');
let resLogger = log4js.getLogger('resLogger');

let logUtil = {

    initPath() {
        if (logConfig.baseLogPath) {
            confirmPath(logConfig.baseLogPath)
            //根据不同的logType创建不同的文件目录
            for (let i = 0; i < logConfig.appenders.length; i++) {
                if (logConfig.appenders[i].path) {
                    confirmPath(logConfig.baseLogPath + logConfig.appenders[i].path);
                }
            }
        }
    },

    logh5Error(req, error, resTime) {
        if (req) {
            errorLogger.error(formatError(req, error, 'h5', resTime));
        }
    },

    logError(error, req, resTime) {
        if (error) {
            if (typeof (error) == "string") {
                errorLogger.error('***** node server error *****', error);
            } else {
                errorLogger.error(formatError(req, error, 'node', resTime));
            }
        }
    },

    logResponse(ctx, resTime) {
        if (ctx) {
            resLogger.info(formatRes(ctx, resTime));
        }
    },

    info(key, info) {
        if (key) {
            resLogger.info(key, info);
        }
    }

};

let confirmPath = function (pathStr) {
    if (!fs.existsSync(pathStr)) {
        fs.mkdirSync(pathStr);
        // console.log('createPath: ' + pathStr);
    }
}


//格式化响应日志
let formatRes = function (req, resTime) {
    let logText = new String();

    //响应日志开始
    logText += "\n" + "*************** response log start ***************" + "\n";

    //添加请求日志
    logText += formatReqLog(req, resTime);

    //响应状态码
    logText += "response status: " + req.status + "\n";

    //响应内容
    logText += "response body: " + "\n" + JSON.stringify(req.body) + "\n";

    //响应日志结束
    logText += "*************** response log end ***************" + "\n";

    return logText;

}

//格式化错误日志
let formatError = function (req = {}, error = {}, type = 'node server', resTime = 0) {
    let logText = new String();
    let err = type === 'h5' ? req.query : error;
    //错误信息开始
    logText += "\n" + "***************  " + type + " error log start ***************" + "\n";
    //添加请求日志
    if (!_.isEmpty(req)) {
        logText += formatReqLog(req);
    }
    if (type === 'h5') {
        //用户信息
        if (err.userInfo) {
            logText += "request user info:  " + err.userInfo + "\n";
        }
        // 客户端渠道信息
        if (err.pageParams) {
            logText += "request client channel info:  " + err.pageParams + "\n";
        }
        // 客户端设备信息
        if (err.clientInfo) {
            logText += "request mobile info:  " + err.clientInfo + "\n";
        }
        //报错位置
        logText += "err line: " + err.line + ", col: " + err.col + "\n";
        //错误信息
        logText += "err message: " + err.msg + "\n";
        //错误页面
        logText += "err url: " + err.url + "\n";

    } else { // node server
        //错误名称
        logText += "err name: " + error.name + "\n";
        //错误信息
        logText += "err message: " + error.message + "\n";
        //错误详情
        logText += "err stack: " + error.stack + "\n";
    }
    //错误信息结束
    logText += "***************  " + type + "  error log end ***************" + "\n";
    return logText;
};

//格式化请求日志
let formatReqLog = function (req) {

    let logText = new String();
    let method = req.method;
    // 访问路径
    logText += "request url: " + req.url + "\n";
    //访问方法
    logText += "request method: " + method + "\n";
    //客户端ip
    logText += "request client ip:  " + req.ip + "\n";

    return logText;
}


module.exports = logUtil;


此处主要是创建日志输出目录,对日志格式进行重新编辑,并约定了前端和node端错误的格式

前端错误调用:logh5Error    node端错误调用:logError

这里回到node接口调用时执行的方法,就能看明白了,

logUtil.logh5Error(req, logParams);


app.js中对log4js进行初始化,主要代码:

//加载中间件
app.use(log4js.connectLogger(logger, {
    level: 'auto',
    format: ':method :url HTTP/:http-version :status [:res[content-length]]bytes :remote-addr :referrer :user-agent'
}));
// 初始化日志目录
logUtil.initPath();

会在服务启动时在项目生成文件夹

WechatIMG2.jpg

来看看报错的效果吧:

WechatIMG3.jpeg


参考文档:

1、前端代码异常监控

2、「新手向」koa2从起步到填坑