浅析badjs源码(前端监控方案)

4,407 阅读5分钟

最近在研究前端监控方案,由于工作需要研究了下鹅厂的badjs源码,主要是看了前端上报这一块,也就是badjs-report。关于badjs的使用可以看下官方文档

前端监控痛点

了解一个框架或者库之前要先思考它想解决的是什么问题。前端异常监控系统的落地这篇文章比较详细地总结了前端监控所需要解决的问题,总结了下有:

  1. 错误拦截
  2. 上报错误
  3. 离线错误日志存储
  4. 错误路径回放
  5. 日志可视化管理后台
  6. 压缩单行文件的源码定位
  7. 邮箱(短信)提醒

上面的功能除了第四点和第六点,badjs2都已经实现到。其中错误拦截、上报错误和离线错误日志存储是由前端组件badjs-report来实现的。而badjs-report的代码主要有三大入口:init初始化、onerror改写和reportOfflinelog上报离线日志。下面将一一介绍这三大入口如何调用其他函数并实现功能(限于篇幅限制,下面贴的代码有所删减,可结合源码理解)。

BJ_REPORT.init初始化

badjs-report是在全局对象中插入BJ_REPORT对象,它提供了init()来进行初始化,该函数方法接受一个对象作为配置参数。

首先是将传入的配置参数对象的值覆盖私有_config对象的值。

init: function(config) {
	if (T.isOBJ(config)) {
		// 遍历覆盖
        for (var key in config) {
            _config[key] = config[key];
        }
    }
}

接着拼接上报url和清空错误缓存。

// 没有设置id将不上报
var id = parseInt(_config.id, 10);
if (id) {
    _config._reportUrl = (_config.url || "/badjs") +
        "?id=" + id +
        "&uin=" + _config.uin +
        "&";
}
// 清空错误列表,_process_log函数会在下面讲到
if (_log_list.length) {
	_process_log();
}

接着初始化indexedDB数据库。badjs是将离线日志信息存储于indexedDB数据库中,然后通过调用reportOfflineLog()方法来上传离线日志。

if (!Offline_DB._initing) {
    Offline_DB._initing = true;
    Offline_DB.ready(function(err, DB) {
        if (DB) {
            setTimeout(function() {
		        // 清除过期日志
                DB.clearDB(_config.offlineLogExp);
                setTimeout(function() {
                    _config.offlineLogAuto && _autoReportOffline();
                }, 5000);
            }, 1000);
        }

    });
}

Offline_DB.ready()的主要工作是打开数据库并设置success和upgradeneeded监听事件

// 打开数据库
var request = window.indexedDB.open("badjs", version);

// 打开成功
request.onsuccess = function(e) {
    self.db = e.target.result;
    // 打开成功后执行回调
    setTimeout(function() {
        callback(null, self);
    }, 500);
};
// 版本升级(初始化时会先触发upgradeneeded,再触发success)
request.onupgradeneeded = function(e) {
   var db = e.target.result;
   if (!db.objectStoreNames.contains('logs')) {
       db.createObjectStore('logs', { autoIncrement: true });
   }
};

改写onerror

在BJreport初始化后就需要来改写window.onerror,以便捕获到程序发生的错误。重写后的onerror主要是格式化错误信息,并把错误push进错误队列中,同时push()方法也会触发_process_log()。

var orgError = global.onerror;
global.onerror = function(msg, url, line, col, error) {
    var newMsg = msg;
	// 格式化错误信息
    if (error && error.stack) {
        newMsg = T.processStackMsg(error);
    }
    if (T.isOBJByType(newMsg, "Event")) {
        newMsg += newMsg.type ?
            ("--" + newMsg.type + "--" + (newMsg.target ?
                (newMsg.target.tagName + "::" + newMsg.target.src) : "")) : "";
    }
    // 将错误信息对象推入错误队列中,执行_process_log方法进行上报
    report.push({
        msg: newMsg,
        target: url,
        rowNum: line,
        colNum: col,
        _orgMsg: msg
    });

    _process_log();
    // 调用原有的全局onerror事件
    orgError && orgError.apply(global, arguments);
};

badjs上报的功能主要通过_process_log()来实现,有随机上报、忽略上报、离线日志存储和延迟上报。首先在push的时候会把错误对象push进_log_list,然后_process_log()会循环清空_log_list。

先根据config的random来决定是否忽略该次上报

// 取随机数,来决定是否忽略该次上报
var randomIgnore = Math.random() >= _config.random;

每次循环时先判断是否超过重复上报数

// 重复上报
if (T.isRepeat(report_log)) continue;

然后按照用户定义的ignore规则进行筛选

// 格式化log信息
var log_str = _report_log_tostring(report_log, submit_log_list.length);
// 若用户自定义了ignore规则,则按照规则进行筛选
if (T.isOBJByType(_config.ignore, "Array")) {
    for (var i = 0, l = _config.ignore.length; i < l; i++) {
        var rule = _config.ignore[i];
        if ((T.isOBJByType(rule, "RegExp") && rule.test(log_str[1])) ||
            (T.isOBJByType(rule, "Function") && rule(report_log, log_str[1]))) {
            isIgnore = true;
            break;
        }
    }
}

接着将离线日志存入数据库,将需要上报的日志push进submit_log_list

// 通过了ignore规则
if (!isIgnore) {
    // 若离线日志功能已开启,则将日志存入数据库
    _config.offlineLog && _save2Offline("badjs_" + _config.id + _config.uin, report_log);
    // level为20表示是offlineLog方法push进来的,只存入离线日志而不上报
    if (!randomIgnore && report_log.level != 20) {
        // 若可以上报,则推入submit_log_list,稍后由_submit_log方法来清空该队列并上报
        submit_log_list.push(log_str[0]);
        // 执行上报回调函数
        _config.onReport && (_config.onReport(_config.id, report_log));
    }

}

循环结束后根据需要进行上报或者延迟上报

if (isReportNow) {
  _submit_log(); // 立即上报
} else if (!comboTimeout) {
    comboTimeout = setTimeout(_submit_log, _config.delay); // 延迟上报
}

在_submit_log()方法中,采用的是new一个img标签来进行上报

var _submit_log = function() {
    // 若用户自定义了上报方法,则使用自定义方法
    if (_config.submit) {
        _config.submit(url, submit_log_list);
    } else {
        // 否则使用img标签上报
        var _img = new Image();
        _img.src = url;
    }
    submit_log_list = [];
};

上传离线日志

badjs需要用户主动调用BJ_REPORT.reportOfflineLog()方法来上传数据库中的离线日志。

reportOfflineLog()方法首先是调用Offline_DB.ready打开数据库,然后在回调中通过DB.getLogs()来获取到数据库中的日志,最后通过form表单提交来上传数据。

reportOfflineLog: function() {
    Offline_DB.ready(function(err, DB) {
        // 日期要求是startDate ~ endDate
        var startDate = new Date - 0 - _config.offlineLogExp * 24 * 3600 * 1000;
        var endDate = new Date - 0;
        DB.getLogs({
            start: startDate,
            end: endDate,
            id: _config.id,
            uin: _config.uin
        }, function(err, result) {
            var iframe = document.createElement("iframe");
            iframe.name = "badjs_offline_" + (new Date - 0);
            iframe.frameborder = 0;
            iframe.height = 0;
            iframe.width = 0;
            iframe.src = "javascript:false;";

            iframe.onload = function() {
                var form = document.createElement("form");
                form.style.display = "none";
                form.target = iframe.name;
                form.method = "POST";
                form.action = _config.offline_url || _config.url.replace(/badjs$/, "offlineLog");
                form.enctype.method = 'multipart/form-data';

                var input = document.createElement("input");
                input.style.display = "none";
                input.type = "hidden";
                input.name = "offline_log";
                input.value = JSON.stringify({ logs: result, userAgent: navigator.userAgent, startDate: startDate, endDate: endDate, id: _config.id, uin: _config.uin });
                iframe.contentDocument.body.appendChild(form);
                form.appendChild(input);
                // 通过form表单提交来上报离线日志
                form.submit();

                setTimeout(function() {
                    document.body.removeChild(iframe);
                }, 10000);

                iframe.onload = null;
            };
            document.body.appendChild(iframe);
        });
    });
}

结语

为了防止篇幅过长,上述源码我做了一些删减,如果想看完整源码可以看下我自己加了中文注释的版本https://github.com/Q-Zhan/badjs-report-annotated,有任何问题都可以提issue给我~~