日志监控

最近项目上有对于网站监控的需求,主要功能如下

  1. 白屏时间
  2. ajax请求响应时间
  3. js报错信息
    这些都需要通过request请求将对应的数据提交到后端,虽然从功能上来说,后端运维的log日志完全可以看到大多数内容,但是既然要求做了那就写呗

白屏时间

这个就是使用浏览器自带的 performance 对象来实现了
performance.timing则是包含浏览器在各个状态下的时间戳以及其他属性值
部分js时间计算的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Logger.prototype.getTiming = function () {
var _return = {
// DNS查询耗时
dnsT: this.timing.domainLookupEnd - this.timing.domainLookupStart,
// 白屏时间
loadT: this.timing.domLoading - this.timing.navigationStart,
// request请求耗时
requestT: this.timing.responseEnd - this.timing.responseStart,
// TCP链接耗时
tcpT: this.timing.connectEnd - this.timing.connectStart,
// 解析dom树耗时
renderDomT: this.timing.domComplete - this.timing.domInteractive,
// domready时间(用户可操作时间节点)
readyDomT: this.timing.domContentLoadedEventEnd - this.timing.navigationStart,
// onload时间(总下载时间)
onLoadT: this.timing.loadEventEnd - this.timing.navigationStart
};
return _return;
};

ajax请求响应时间

由于公司网址都是用的$.ajax的请求方法,新老官网以及落地页,关于ajax请求的代码就有好几百个,不可能一个个在ajax里写starttime然后成功或者失败定义endtime拿到差值再减,看起来很难维护
想了很久,最后决定重写$.ajax,也就是包装这个方法,专业术语叫 AOP 装饰者模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
(function ($, w) {
// window上初始一个方法
w._ajax = $.ajax;
$.ajax = function (arg) {
// 请求之前的时间
var start_t = new Date().getTime()
// 拿到$.ajax的执行的成功回调,记录下来
var success = arg.success
// 重写succes
arg.success = function () {
// 拿到$.ajax传入的参数
var data = arguments
// 成功执行后定义的方法
var time = new Date().getTime() - start_t
// 此时执行默认的ajax,因为默认的ajax不会捕获success之后的回调函数
_ajax({
url: 'url',
data: {
time: time
}
success: success.call(this, data)
});
}
return _ajax.call($, arg)
};
})(jQuery, window);

js报错信息

js报错主要就是用window的onerror事件去监听js的报错信息
这里需要注意一下几点

1.不能使用 window.onerror = function () {…} ,而是使用 window.addEventListener (‘error’, funciton (e) {})

原因就是 window.onerror 会覆盖或者被覆盖,因为他是表达式,所以在其他地方执行这个方法的时候,会被覆盖,addEventListener则是添加一个error的监听事件,不会影响之前error的监听代码

2.执行顺序

error的监听代码尽量写在js最前面,因为涉及到调用 $.ajax , 因此放在jquery引入之后,其余的代码则放在后面, 先初始化监听, 后执行后续代码

3.crossorigin

在script引入日志监控代码标签没有用到crossorigin的时候, 由于跨域调用的windows脚本, onerror的事件不会打印详细的报错信息, 为了打印详细信息, 需配置crossorigin属性, 这需要后端的 Access-Control-Allow-Origin 支持

4.不是所有js报错代码都可以监听

  • console.error 不会被执行
  • try {} catch () {} 不会被监听
  • 静态资源加载错误不会被监听

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Logger.prototype.initErrorEvent = function () {
/**
* @param {String} errorMessage 错误信息
* @param {String} scriptURL 出错文件的URL
* @param {Number} lineNumber 出错代码的行号
* @param {Number} columnNumber 出错代码的列号
* @param {Object} errorObj 错误信息Object
*/
var _this = this;
window.addEventListener('error', function (e) {
setTimeout(function () {
_this.staticError({
errorMessage: e.message,
scriptURL: e.filename,
lineNumber: e.lineno,
columnNumber: e.colno,
errorObj: e.error
});
}, 0);
});
};

Logger.prototype.staticError = function (data) {
$.ajax({
url: 'url...',
type: 'POST',
dataType: 'json',
data: {
url: window.location.href,
current_time: new Date().getTime(),
js_url: data.scriptURL,
error_info: data.errorMessage,
error_line: data.lineNumber,
error_column: data.columnNumber
}
});
};

完整的logger代码,不包含包装的ajax方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
var Logger = /** @class */ (function () {
function Logger() {
this.timing = window.performance.timing;
this.initErrorEvent();
}
Logger.prototype.getTiming = function () {
var _return = {
// DNS查询耗时
dnsT: this.timing.domainLookupEnd - this.timing.domainLookupStart,
// 白屏时间
loadT: this.timing.domLoading - this.timing.navigationStart,
// request请求耗时
requestT: this.timing.responseEnd - this.timing.responseStart,
// TCP链接耗时
tcpT: this.timing.connectEnd - this.timing.connectStart,
// 解析dom树耗时
renderDomT: this.timing.domComplete - this.timing.domInteractive,
// domready时间(用户可操作时间节点)
readyDomT: this.timing.domContentLoadedEventEnd - this.timing.navigationStart,
// onload时间(总下载时间)
onLoadT: this.timing.loadEventEnd - this.timing.navigationStart
};
return _return;
};
/**
* 初始化error监听事件
*/
Logger.prototype.initErrorEvent = function () {
/**
* @param {String} errorMessage 错误信息
* @param {String} scriptURL 出错文件的URL
* @param {Number} lineNumber 出错代码的行号
* @param {Number} columnNumber 出错代码的列号
* @param {Object} errorObj 错误信息Object
*/
var _this = this;
window.addEventListener('error', function (e) {
setTimeout(function () {
_this.staticError({
errorMessage: e.message,
scriptURL: e.filename,
lineNumber: e.lineno,
columnNumber: e.colno,
errorObj: e.error
});
}, 0);
});
};
/**
* 事件错误的回调事件
*/
Logger.prototype.staticError = function (data) {
$.ajax({
url: '',
type: 'POST',
dataType: 'json',
data: {
url: window.location.href,
current_time: new Date().getTime(),
js_url: data.scriptURL,
error_info: data.errorMessage,
error_line: data.lineNumber,
error_column: data.columnNumber
}
});
};
/**
* 页面加载时长的数据信息
*/
Logger.prototype.fetchPageLoadInfo = function () {
var timingInfo = this.getTiming();
$.ajax({
url: '',
type: 'POST',
dataType: 'json',
data: {
url: window.location.href,
current_time: new Date().getTime(),
response_time: timingInfo.loadT
}
});
};
return Logger;
}());
window.logger = new Logger();
window.addEventListener('load', function () {
setTimeout(function () {
logger.fetchPageLoadInfo();
}, 1000);
}, false);