这是我参与「第五届青训营」伴学笔记创作活动的第 13 天
前端监控
什么是前端监控?
一般来讲一个成熟的产品,运营与产品团队需要关注用户在产品内的行为记录,通过用户的行为记录来优化产品,研发与测试团队则需要关注产品的性能以及异常,确保产品的性能体验以及安全迭代。
前端监控的重要性
性能监控(监控页面性能)
- 不同用户,不同机型和不同系统下的首屏加载时间
- 白屏时间
- http 等请求的响应时间
- 静态资源整体下载时间
- 页面渲染时间
- 页面交互动画完成时间,等...
这些性能监控的结果,可以展示前端性能的好坏,根据性能监测的结果可以进一步的去优化前端性能,尽可能的提高用户体验。
当我们没有前端监控时,我们面对的问题的解决方案:
当我们利用前端监控时:
以用户为中心的性能指标
创建以用户为中心的性能指标,可以让我们专注于用户视角下的浏览体验。
当用户打开一个网页时,会经历三个重要结点。
- FP: 首次渲染结点
- FCP: 首次有内容渲染结点
- FMP:绘制有意义的时间点。
- TTI: 测量用户能开始操作页面的时间
- FID:测量用户第一次页面交互直到浏览器做出反应的时间
- LCP: 最大内容在可视区域内变得可见的时间点
常见的监控异常
静态资源异常
静态资源:加载页面所需的html、css 和js等文件,以及其他各类多媒体文件,如 图片、音频和视频等。
静态资源错误:在拉取和加载静态资源的过程中发生了预期之外的错误,如网络异常 等,导致静态资源无法正常渲染到页面上。
请求异常
请求异常和我们的状态码相关,一般当我们的状态码>=400就可以归类于请求异常。
- 2xx:成功类型,例:200 OK。
- 3xx: 重定向类型,例:301 永久重定向,302 暂时重定向
- 4xx:客户端错误类型:例:400 错误请求,401 授权错误,403 服务器理解请求但拒绝请求
- 5xx:服务端错误类型,例:500 内部错误,501 服务不可用
状态码为0的情况,即意味着xhr无法请求执行。
JS异常
在页面运行时发生JS错误会严重影响页面的正常交互和渲染。
白屏异常
白屏异常没有标准的监听方法,所以更考验前端开发者的功底。
通常我们可以用DOM树来粗略判断是否发生。
监听到白屏发生后,我们还需要对白屏的发生进行归因。
通常导致白屏发生的原因可能有如下几点:
- 发生Js错误导致关键资源渲染失败。
- 请求异常或静态资源加载失败。
- 长时间的Js线程繁忙阻塞渲染任务。
监控
前端性能指标监控
我们可以用 pefformance 和 pefformanceObserver 来监控
**
* 列举出性能指标对应的 entry type
* fp,fcp --> paint
* lcp --> largest-contentful-paint
* fip --> first-input
*/
const entryTypes = ['paint', 'largest-contentful-paint', 'first-input']
// 1. 通过 PerformanceObserver 监听
const p = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
console.log(entry);
}
})
p.observe({ entryTypes });
// 2. 也可以通过 window.performance 对象拿到 fp fcp 和 fip。
// 注意如果同步打印他们是取不到值的,想想为什么?
window.performance.getEntriesByType('paint');
window.performance.getEntriesByType('first-input');
// 3. 封装成一个 monitor
function createPerfMonitor(report: ({ name: string, data: any }) => void) {
const name = 'performance';
const entryTypes = ['paint', 'largest-contentful-paint', 'first-input']
function start() {
const p = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
report({ name, data: entry });
}
})
p.observe({ entryTypes });
}
return { name, start }
}
静态资源错误监控
利用window.addEventListener的error事件监听.
// 1. 监控静态资源错误,注意需要在捕获阶段才能监听到
window.addEventListener('error', e => {
// 注意区分 js error
const target = e.target || e.srcElement;
if (!target) {
return
}
if (target instanceof HTMLElement) {
let url;
// 区分 link 标签,获取静态资源地址
if (target.tagName.toLowerCase() === 'link') {
url = target.getAttribute('href');
} else {
url = target.getAttribute('src');
}
console.log('异常的资源', url);
}
}, true)
const link = document.createElement("link");
link.href = "1.css";
link.rel = "stylesheet";
document.head.append(link);
const script = document.createElement("script");
script.src = "2.js";
document.head.append(script);
// 2. 封装成一个 monitor
function createResourceErrorMonitor(report: ({ name: string, data: any }) => void) {
const name = "resource-error";
function start() {
window.addEventListener('error', e => {
// 注意区分 js error
const target = e.target || e.srcElement;
if (!target) {
return
}
if (target instanceof HTMLElement) {
let url;
// 区分 link 标签,获取静态资源地址
if (target.tagName.toLowerCase() === 'link') {
url = target.getAttribute('href');
} else {
url = target.getAttribute('src');
}
report({ name, data: { url } });
}
}, true)
}
return { name, start }
}
请求异常监控
通过用钩子的方式,来监听xhr发生的错误.
// 1. 写一个简易的 hook 函数
function hookMethod(
obj: any,
key: string,
hookFunc: Function,
) {
return (...params: any[]) => {
obj[key] = hookFunc(obj[key], ...params)
}
}
// 2. hook xhr 对象的 open 方法拿到请求地址和方法
hookMethod(XMLHttpRequest.prototype, 'open', (origin: Function) =>
function (method: string, url: string) {
this.payload = {
method,
url,
};
// 执行原函数
origin.apply(this, [method, url]);
}
)();
// 3. hook xhr 对象的 send 方法监听到错误的请求
hookMethod(XMLHttpRequest.prototype, 'send', (origin: Function) =>
function (...params: any[]) {
this.addEventListener("readystatechange", function () {
if (this.readyState === 4 && this.status >= 400) {
this.payload.status = this.status;
console.log(this.payload);
}
});
origin.apply(this, params);
}
)();
const xhr = new XMLHttpRequest();
xhr.open("post", "111.cc");
xhr.send();
// 4, 封装成一个 monitor
function createXhrMonitor(report: ({ name: string, data: any }) => void) {
const name = "xhr-error";
function hookMethod(
obj: any,
key: string,
hookFunc: Function,
) {
return (...params: any[]) => {
obj[key] = hookFunc(obj[key], ...params)
}
}
function start() {
hookMethod(XMLHttpRequest.prototype, 'open', (origin: Function) =>
function (this, method: string, url: string) {
this.payload = {
method,
url,
};
origin.apply(this, [method, url]);
}
)();
hookMethod(XMLHttpRequest.prototype, 'send', (origin: Function) =>
function (this, ...params: any[]) {
this.addEventListener("readystatechange", function () {
if (this.readyState === 4 && this.status >= 400) {
this.payload.status = this.status;
report({ name, data: this.payload });
}
});
origin.apply(this, ...params);
}
)();
}
return { name, start }
}
按需加载前端监控
function createSdk(url: string) {
const monitors: Array<{ name: string, start: Function }> = [];
const sdk = {
url,
report,
loadMonitor,
monitors,
start,
}
function report({ name: string, data: any }) {
// 注意:数据发送前需要先序列化为字符串
navigator.sendBeacon(url, JSON.stringify({ name: string, data: any }));
}
function loadMonitor({ name: string, start: Function }) {
monitors.push({ name: string, start: Function });
// 实现链式调用
return sdk;
}
function start() {
monitors.forEach(m => m.start());
}
return sdk;
}
const sdk = createSdk("111.com");
const jsMonitor = createJsErrorMonitor(sdk.report);
sdk.loadMonitor(jsMonitor).loadMonitor(createPerfMonitor(sdk.report));
sdk.start();
throw (new Error('test'));
function createJsErrorMonitor(report: ({ name: string, data: any }) => void) {
const name = "js-error";
function start() {
window.addEventListener("error", (e) => {
// 只有 error 属性不为空的 ErrorEvent 才是一个合法的 js 错误
if (e.error) {
report({ name, data: { type: e.type, message: e.message } });
}
});
window.addEventListener("unhandledrejection", (e) => {
report({ name, data: { type: e.type, reason: e.reason } });
});
}
return { name, start }
}
function createPerfMonitor(report: ({ name: string, data: any }) => void) {
const name = 'performance';
const entryTypes = ['paint', 'largest-contentful-paint', 'first-input']
function start() {
const p = new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
report({ name, data: entry });
}
})
p.observe({ entryTypes });
}
return { name, start }
}