最近在研究前端监控方案, 由于工作需要研究了下鹅厂的 badjs 源码, 主要是看了前端上报这一块, 也就是 badjs-report. 关于 badjs 的使用可以看下官方文档 https://github.com/iv-web/badjs2
前端监控痛点
了解一个框架或者库之前要先思考它想解决的是什么问题. 前端异常监控系统的落地 https://zhuanlan.zhihu.com/p/26085642 这篇文章比较详细地总结了前端监控所需要解决的问题, 总结了下有:
错误拦截
上报错误
离线错误日志存储
错误路径回放
日志可视化管理后台
压缩单行文件的源码定位
邮箱 (短信) 提醒
上面的功能除了第四点和第六点, 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 给我~~
来源: https://juejin.im/post/5b434402f265da0f7d4edf77