前端监控的范围
- 性能监控
- 基础数据(首次渲染时间,白屏等)
- 可交互时间
- 资源加载
- 异常监控
- js 异常
- ajax异常
- 异常文件
前端监控系统组成
- 数据采集
- 数据上报
- 数据可视化
- 监控告警
数据采集
- 根据 performance.timing 的时间,计算出各个阶段的时间
getPerData(timing) {
return {
// 网络建连
pervPage: filterTime(timing.fetchStart, timing.navigationStart), // 上一个页面
redirect: filterTime(timing.responseEnd, timing.redirectStart), // 页面重定向时间
dns: filterTime(timing.domainLookupEnd, timing.domainLookupStart), // DNS查找时间
connect: filterTime(timing.connectEnd, timing.connectStart), // TCP建连时间
network: filterTime(timing.connectEnd, timing.navigationStart), // 网络总耗时
// 网络接收
send: filterTime(timing.responseStart, timing.requestStart), // 前端从发送到接收到后端第一个返回
receive: filterTime(timing.responseEnd, timing.responseStart), // 接受页面时间
request: filterTime(timing.responseEnd, timing.requestStart), // 请求页面总时间
// 前端渲染
dom: filterTime(timing.domComplete, timing.domLoading), // dom解析时间
loadEvent: filterTime(timing.loadEventEnd, timing.loadEventStart), // loadEvent时间
frontend: filterTime(timing.loadEventEnd, timing.domLoading), // 前端总时间
// 关键阶段
load: filterTime(timing.loadEventEnd, timing.navigationStart), // 页面完全加载总时间
domReady: filterTime(
timing.domContentLoadedEventStart,
timing.navigationStart
), // domready时间
interactive: filterTime(
timing.domInteractive,
timing.navigationStart
), // 可操作时间
ttfb: filterTime(timing.responseStart, timing.navigationStart) // 首字节时间
};
},
- 计算关键时间任务,
const perfObserver = new PerformanceObserver()
perfObserver.observe('paint')
观察 paint 时,返回格式
/*
* [{
* duration: 0
entryType: "paint"
name: "first-paint"
startTime: 151.10000002384186
first-paint: 223.69999998807907
* },{
* duration: 0
entryType: "paint"
name: "first-contentful-paint"
startTime: 151.10000002384186
*
* }]
*/
getPaintTime() {
const data = {}
getObserver('paint', entries => {
entries.forEach(entry => {
data[entry.name] = entry.startTime
})
})
return data
}
- 资源加载的时间控制
let entries = performance.getEntriesByType("resource");
let entriesData = resolveEntries(entries)
cb(entriesData)
- ajax的时间上报
- 重写 xhr 的 open 和 send 方法
- 监听 load error abort 方法
- 对 fetch 方法的重写
let xhr = window.XMLHttpRequest;
let _originOpen = xhr.prototype.open;
xhr.prototype.open = function(method, url, async, user, password) {
// 记录在里面
this._eagle_xhr_info = {
url: url,
method: method,
status: null
};
return _originOpen.apply(this, arguments); // 执行原有的 open 方法
};
let _originSend = xhr.prototype.send;
xhr.prototype.send = function(value) {
let _self = this;
this._eagle_start_time = Date.now(); // 记录发送开始时间
let ajaxEnd = event => () => {
if (_self.response) {
let responseSize = null;
switch (_self.responseType) {
case "json":
responseSize = JSON && JSON.stringify(_this.response).length;
break;
case "blob":
case "moz-blob":
responseSize = _self.response.size;
break;
case "arraybuffer":
responseSize = _self.response.byteLength;
break;
case "document":
responseSize =
_self.response.documentElement &&
_self.response.documentElement.innerHTML &&
_self.response.documentElement.innerHTML.length + 28;
break;
default:
responseSize = _self.response.length;
}
_self._eagle_xhr_info.event = event;
_self._eagle_xhr_info.status = _self.status;
_self._eagle_xhr_info.success =
(_self.status >= 200 && _self.status <= 206) ||
_self.status === 304;
_self._eagle_xhr_info.duration = Date.now() - _self._eagle_start_time; // 统计回来的时间
_self._eagle_xhr_info.responseSize = responseSize;
_self._eagle_xhr_info.requestSize = value ? value.length : 0;
_self._eagle_xhr_info.type = "xhr";
cb(this._eagle_xhr_info);
}
};
if (this.addEventListener) {
this.addEventListener("load", ajaxEnd("load"), false); // 当 load 时执行
this.addEventListener("error", ajaxEnd("error"), false);
this.addEventListener("abort", ajaxEnd("abort"), false); // 当 load 时执行
} else {
let _origin_onreadystatechange = this.onreadystatechange;
this.onreadystatechange = function(event) {
if (_origin_onreadystatechange) {
_originOpen.apply(this, arguments);
}
if (this.readyState === 4) {
ajaxEnd("end")();
}
};
}
return _originSend.apply(this, arguments);
};
// fetch 兼容
if (window.fetch) {
let _origin_fetch = window.fetch;
window.fetch = function() {
let startTime = Date.now();
let args = [].slice.call(arguments); // 将 arguments 转化为数组
let fetchInput = args[0];
let method = "GET";
let url;
if (typeof fetchInput === "string") {
url = fetchInput;
} else if (
"Request" in window &&
fetchInput instanceof window.Request
) {
url = fetchInput.url;
if (fetchInput.method) {
method = fetchInput.method;
}
} else {
url = "" + fetchInput;
}
if (args[1] && args[1].method) {
method = args[1].method;
}
let fetchData = {
method: method,
url: url,
status: null
};
return _origin_fetch.apply(this, args).then(function(response) {
fetchData.status = response.status;
fetchData.type = "fetch";
fetchData.duration = Date.now() - startTime;
cb(fetchData);
return response;
});
};
}
- 捕获js错误
- window.onerror
- window.addEventListener('unhandleRejection') // promise错误
- 还需要对编译后的源码,用source-map反解析到编译前端源码中,错误位置
window.onerror = function(message, source, lineno, colno, error){
console.log("-> error", error);
let errorInfo = formatError(error)
errorInfo._message = message
errorInfo._source = source
errorInfo._lineno = lineno
errorInfo._colno = colno
cb(errorInfo)
errorInfo.type = 'error'
_origin_error && _origin_error.apply(window,arguments)
}
- 白屏统计
- 可以根据具体的业务形态, elementsFromPoint 方法可以获取到当前视口内指定坐标处,由里到外排列的所有元素
function getSelector(element) {
var selector;
if (element.id) {
selector = `#${element.id}`;
} else if (element.className && typeof element.className === "string") {
selector =
"." +
element.className
.split(" ")
.filter(function (item) {
return !!item;
})
.join(".");
} else {
selector = element.nodeName.toLowerCase();
}
return selector;
}
export function blankScreen() {
const wrapperSelectors = ["body", "html", "#container", ".content"];
let emptyPoints = 0;
function isWrapper(element) {
let selector = getSelector(element);
if (wrapperSelectors.indexOf(selector) >= 0) {
emptyPoints++;
}
}
onload(function () {
let xElements, yElements;
debugger;
for (let i = 1; i <= 9; i++) {
xElements = document.elementsFromPoint(
(window.innerWidth * i) / 10,
window.innerHeight / 2
);
yElements = document.elementsFromPoint(
window.innerWidth / 2,
(window.innerHeight * i) / 10
);
isWrapper(xElements[0]);
isWrapper(yElements[0]);
}
if (emptyPoints >= 0) {
let centerElements = document.elementsFromPoint(
window.innerWidth / 2,
window.innerHeight / 2
);
tracker.send({
kind: "stability",
type: "blank",
emptyPoints: "" + emptyPoints,
screen: window.screen.width + "x" + window.screen.height,
viewPoint: window.innerWidth + "x" + window.innerHeight,
selector: getSelector(centerElements[0]),
});
}
});
}
数据上报和处理
请求时机
- 1像素的gif图,src后面带数据
- navigation.sendBean 在浏览器关闭的时候也可以发送
- ajax请求
请求兜底
- 如果没有 node 中间层,前端缓存一个数组,设置阈值,定时或超量时发送
- 如果有,可以先发送到node中去,放在缓存中,再过滤处理下
- 不合理的值
- 外部大规模的流量攻击
数据处理
- 登录身份等,ip信息等
- 接入是以sdk形式,每个业务部门有自己唯一的编号
1. type 日志类型,有 performance 性能统计,abnormal 异常统计,或者其他扩展
2. module 业务方部门
3. group 分组,落地页分组
4. dim 维度 , 简单说就是将每个数据都分为多个模块来统计
5. info 性能或异常的具体信息
数据可视化
- 落库可以用 ELK 一体的系统
- 视图层面,可以从es中查询取,自己再针对个性化用户展示
监控告警
-
根据用户设定的一系列阈值,提供电话,邮件,短信等告知情况