为什么要做前端监控
- 更快发现问题和解决问题;
- 做产品的決策依据;
- 提升前端工程师的技术深度和广度,打造简历亮点;
- 为业务扩展提供了更多可能性;
前端监控目标
稳定性
-
JS 错误:JS 执行错误或者 promise 异常
-
资源异常:script、link 等资源加载异常
-
接口错误:ajax 或 fetch 请求接口异常
-
白屏:页面空白
用户体验
-
加载时间:各个阶段的加载时间
-
TTFB (time to first bytey首字节时间): 是指浏览器发起第—个请求到数据返回第—个字节所消耗的时间,这个时间包含了网络请求时间、后端处理时间
-
FP(First Paint)(首次绘制):首次绘制包括了任何用户自定义的背景绘制,它是将第一个像素点绘制到屏幕的时刻
-
FCP(First Content Paint)(首次内容绘制):首次内容绘制是浏览器将第一个DOM渲染到屏幕的时间,可以是任何文本、图像、SVG等的时间
-
FMP(First Meaningful paintX首次有意义绘制):首次有意义绘制是页面可用性的量度标准
-
FID(First Input Delayr首次输入延迟):用户首次和页面交互到页面响应交互的时间
-
卡顿:超过50ms的长任务
业务
-
PV:page view 即页面浏览量或点击量
-
UV:指访问某个站点的不同 IP 地址的人数
-
页面的停留时间:用户在每一个页面的停留时间
前端监控流程
前端监控流程
-
前端埋点;
-
数据上报;
-
分析和计算,将采集到的数据进行加工汇总;
-
可视化展示,将数据按各种维度进行展示;
-
监控报警,发现问题后按一定的条件触发报警;
常见的埋点方案
-
代码埋点:
- 代码埋点,就是以嵌入代码的形式进行埋点,比如需要监控用户的点击事件,会选择在用户点击时,插入一段代码,保存这个监听行为或者直接将监听行为以某一种数据格式直接传递给服务器;
- 优点是可以在任意时刻,精确的发送或保存所需要的数据信息;
- 缺点是工作量比较大;
-
可视化埋点:
- 通过可视化交互的手段,代替代码埋点;
- 将业务代码和埋点代码分离,提供一个可视化交互的页面,输入为业务代码,通过这个可视化系统,可以在业务代码中自定义的增加埋点事件等等,最后输出的代码耦合了业务代码和埋点代码;
- 可视化埋点其实就是用系统来代替手工插入埋点代码;
-
无痕埋点:
- 前端任意的一个事件都被绑定一个标识,所有的事件都要记录下来;
- 通过定期上传记录文件,配合文件解析,解析出来想要的数据,并生成可视化报告供专业人员分析;
- 无痕埋点的优点是采集全量数据,不会出现漏埋和误埋等现象;
- 缺点是给数据传输和服务器增加压力,也无法灵活定制数据结构;
编写监控采集脚本
监控 js 错误 + 监控资源加载错误
window.addEventListener('error', function (event) { //错误事件对象
let lastEvent = getLastEvent(); //最后一个交互事件
// 脚本资源加载错误
if (event.target && (event.target.src || event.target.href)) {
console.log({
kind: 'stability', //监控指标的大类
type: 'error', //小类型 这是一个错误
errorType: 'resourceError', //js或css资源加载错误
filename: event.target.src || event.target.href, //哪个文件报错了
tagName: event.target.tagName, //SCRIPT
//body div#container div.content input
selector: getSelector(event.target) //代表最后一个操作的元素
});
} else {
// js 错误
console.log({
kind: 'stability', // 监控指标的大类
type: 'error', // 小类型 这是一个错误
errorType: 'jsError', // JS执行错误
message: event.message, // 报错信息
filename: event.filename, // 哪个文件报错了
position: `${event.lineno}:${event.colno}`,
stack: event.error.stack,
selector: lastEvent ? getSelector(lastEvent.path) : '' //代表最后一个操作的元素
});
}
}, true);
监控 promise 错误
window.addEventListener('unhandledrejection', (event) => {
let lastEvent = getLastEvent(); //最后一个交互事件
let message;
let filename;
let line = 0;
let column = 0;
let stack = '';
let reason = event.reason;
if (typeof reason === 'string') {
message = reason;
} else if (typeof reason === 'object') { //说明是一个错误对象
message = reason.message;
if (reason.stack) {
let matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/);
filename = matchResult[1];
line = matchResult[2];
column = matchResult[3];
}
stack = reason.stack;
}
tracker.send({
kind: 'stability', // 监控指标的大类
type: 'error', // 小类型 这是一个错误
errorType: 'promiseError', // JS 执行错误
message, // 报错信息
filename, // 哪个文件报错了
position: `${line}:${column}`,
stack,
selector: lastEvent ? getSelector(lastEvent.path) : '' // 代表最后一个操作的元素
});
}, true);
监控请求错误
let XMLHttpRequest = window.XMLHttpRequest;
let oldOpen = XMLHttpRequest.prototype.open; // 缓存以前老的 open 方法,重写 open
XMLHttpRequest.prototype.open = function (method, url, async) {
if (!url.match(/logstores/) && !url.match(/sockjs/)) { // 上报接口不需要监控
// 扩展一个 logData,根据这个字段拦截请求
this.logData = { method, url, async };
}
return oldOpen.apply(this, arguments);
}
let oldSend = XMLHttpRequest.prototype.send; // 缓存以前老的 send 方法,重写 send
XMLHttpRequest.prototype.send = function (body) {
if (this.logData) {
let startTime = Date.now(); // 在发送之前记录一下开始的时间
let handler = (type) => (event) => {
let duration = Date.now() - startTime;
let status = this.status;
let statusText = this.statusText;
tracker.send({
kind: 'stability',
type: 'xhr',
eventType: type, // load error abort
pathname: this.logData.url, // 请求路径
status: status + '-' + statusText, // 状态码
duration, // 持续时间
response: this.response ? JSON.stringify(this.response) : '', // 响应体
params: body || ''
});
}
this.addEventListener('load', handler('load'), false);
this.addEventListener('error', handler('error'), false);
this.addEventListener('abort', handler('abort'), false);
}
return oldSend.apply(this, arguments);
}
监控白屏
let wrapperElements = ['html', 'body', '#container', '.content'];
let emptyPoints = 0;
// 获取元素选择器
function getSelector(element) {
if (element.id) {
return "#" + element.id;
} else if (element.className) {// a b c => .a.b.c
return "." + element.className.split(' ').filter(item => !!item).join('.');
} else {
return element.nodeName.toLowerCase();
}
}
// 判断当前坐标元素是否是空白点
function isWrapper(element) {
let selector = getSelector(element);
// 如果当前坐标的元素 是 wrapperElements中的一个,说明是空白点
if (wrapperElements.indexOf(selector) != -1) {
emptyPoints++;
}
}
// 页面 load 之后再判断是否是白屏
window.addEventListener('load', function () {
// 根据业务来判断屏幕的空白点设计
// 将屏幕的横向和纵向中心位置,分别设置 9 个点的元素
for (let i = 1; i <= 9; i++) {
let xElements = document.elementsFromPoint(
window.innerWidth * i / 10, window.innerHeight / 2);
let yElements = document.elementsFromPoint(
window.innerWidth / 2, window.innerHeight * i / 10);
isWrapper(xElements[0]);
isWrapper(yElements[0]);
}
// 空白点 大于等于 18,说明是白屏
if (emptyPoints >= 18) {
let centerElements = document.elementsFromPoint(
window.innerWidth / 2, window.innerHeight / 2
);
console.log({
kind: 'stability',
type: 'blank',
emptyPoints,
screen: window.screen.width + "X" + window.screen.height,
viewPoint: window.innerWidth + "X" + window.innerHeight,
selector: getSelector(centerElements[0])
});
}
});
加载时间

案例
(function () {
/**
* 打印日志
* @param msg
* @constructor
*/
function Mlog(msg) {
var p = document.createElement('p');
if (typeof msg === 'object') {
msg = JSON.stringify(msg);
}
p.style.backgroundColor = '#272822';
p.style.color = '#f8f8f2';
p.style.padding = '15px';
p.style.wordWrap = 'break-word';
p.innerHTML = msg;
document.body.appendChild(p);
}
/**
* 继承
* @param origin
* @param target
* @returns {*}
*/
function extend(origin, target) {
for (var i in target) {
if (target.hasOwnProperty(i)) {
origin[i] = target[i];
}
}
return origin;
}
// 性能数据
var performanceData = {
// webview 初始化所需时间
wbInit: 0,
// webView初始化加载url到第一次webView start回调时间
wbResponse: 0,
// webview加载H5页面所有时间
wbTotal: 0,
// 网络类型
netType: 'wifi',
// 页面白屏时间
invisibleTime: 0,
// 首包时间
ttfb: 0,
// 请求h5页面文档所需的时间
pageRequest: 0,
// 解析 DOM 树结构的时间
domReady: 0,
// 页面基础渲染完成时间
renderPage: 0,
// 页面所有资源加载完成时间
loadPageComplete: 100
};
// 存储APP传过来的数据
var webData = null;
var monitor = {
init: function () {
var self = this;
// 获取web
self.getWebviewData();
// 获取性能数据
var per = self.getPerformanceData();
// 覆盖默认数据
setTimeout(function () {
extend(performanceData, per);
// 将网络类型转化为小写
performanceData.netType = performanceData.netType.toLowerCase();
// 获取APP数据
self.uploadData('H5_monitor_performance', performanceData);
}, 0)
},
getWebviewData: function () {
// 进来获取APP传过来webview数据
if (typeof jsRegisterHandler === 'function') { // 第二套
// Mlog('进入到第二三套:');
jsRegisterHandler('getPfmData', function (response) {
webData = typeof response === 'string' ? JSON.parse(response) : response;
// Mlog('APP返回的数据:'+ JSON.stringify(webData));
extend(performanceData, webData);
return false;
})
}
},
/**
* 上报数据方法
* @param funcName 大数据埋点方法名称
* @param data 存储的数据
* @param callback
*/
uploadData: function (funcName, data) {
},
/**
* 获取性能数据
* @returns {*}
*/
getPerformanceData: function () {
// 当不支持performance API时
if (!window.performance || !window.performance.timing) {
return false;
}
var timing = window.performance.timing;
// 性能数据
return {
// 白屏时间
invisibleTime: timing.domLoading - timing.navigationStart,
// 首包时间
ttfb: timing.responseStart - timing.navigationStart,
// 首屏时间
renderPage: timing.domComplete - timing.navigationStart,
// 整页加载时间
loadPageComplete: timing.loadEventEnd - timing.navigationStart,
// 基础页时间=请求h5页面文档下载时间
pageRequest: timing.responseEnd - timing.fetchStart,
// 解析 DOM 树结构的时间
domReady: timing.domContentLoadedEventEnd - timing.responseEnd
};
},
/**
* 工具函数
*/
util: {
getModuleName: function () {
var path = location.pathname,
pathList = path.split('/');
return pathList[pathList.length - 2];
}
}
};
setTimeout(function () {
monitor.init();
}, 1000);
// 记录错误
window.onerror = function (msg, url, line) {
var error = {
msg: msg,
url: url,
line: line
};
monitor.uploadData('H5_monitor_syntaxError', error);
}
})();
取消未完成的请求
上一篇