跳到主要内容

5.4-网站监控

Create by fall on 06 Feb 2025 Recently revised in 28 Feb 2025

网站监控

  • 主动发现问题和解决问题
  • 做产品的决策依据
  • 为业务扩展提供了更多可能性
  • 提升前端工程师的技术深度和广度(简历亮点)

监控方向

系统稳定性

  • 脚本执行错误
  • 资源错误(加载异常
  • 请求异常

用户体验

  • FID:first input delay(首次输入延迟)
  • FCP:首屏渲染时间,一般需要在一秒内
  • LCP:Largest ContentFul Paint (最大内容绘制)
  • TTI:Time to Interreactive(可以使用页面中的内容的时间)
  • INP:Interaction to Next Paint(从输入到下一个画面的时间)

业务

  • 页面浏览和点击量(pageview)
  • 访问页面不同 ip 的人数(unique visitor)
  • 停留时间

监控流程

前端埋点、数据上报、加工汇总、可视化展示、监控预警

监控实现

捕获 js 异常

// 一般 JS 运行时错误使用 window.onerror 捕获处理
window.addEventListener(
"error",
function (event) {
let lastEvent = getLastEvent();
// 有 e.target.src(href) 的认定为资源加载错误
if (event.target && (event.target.src || event.target.href)) {
tracker.send({
// 资源加载错误
type: "error", // resource
subType:'resource-error'
// ...
});
} else {
tracker.send({
type: "error", //error
errorType: "js-error",
});
}
},
true
); // true代表在捕获阶段调用,false代表在冒泡阶段捕获,使用true或false都可以

捕获 promise 异常

//当 Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件
window.addEventListener("unhandledrejection",
function (event) {
let lastEvent = getLastEvent();
let message = "";
let line = 0;
let column = 0;
let file = "";
let stack = "";
if (typeof event.reason === "string") {
message = event.reason;
} else if (typeof event.reason === "object") {
message = event.reason.message;
}
let reason = event.reason;
if (typeof reason === "object") {
if (reason.stack) {
var matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/);
if (matchResult) {
file = matchResult[1];
line = matchResult[2];
column = matchResult[3];
}
stack = getLines(reason.stack);
}
}
tracker.send({
//未捕获的promise错误
kind: "stability", //稳定性指标
type: "error", //jsError
errorType: "promiseError", //unhandledrejection
message: message, //标签名
filename: file,
position: line + ":" + column, //行列
stack,
selector: lastEvent
? getSelector(lastEvent.path || lastEvent.target)
: "",
});
},
true
); // true代表在捕获阶段调用,false代表在冒泡阶段捕获,使用true或false都可以

页面是否白屏

import tracker from "../util/tracker";
import onload from "../util/onload";
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]),
});
}
});
}
// window.innerWidth 去除工具条与滚动条的窗口宽度
// window.innerHeight 去除工具条与滚动条的窗口高度

加载时间

阶段含义

image.png

字段含义
navigationStart初始化页面,在同一个浏览器上下文中前一个页面unload的时间戳,如果没有前一个页面的unload,则与fetchStart值相等
redirectStart第一个HTTP重定向发生的时间,有跳转且是同域的重定向,否则为0
redirectEnd最后一个重定向完成时的时间,否则为0
fetchStart浏览器准备好使用http请求获取文档的时间,这发生在检查缓存之前
domainLookupStartDNS域名开始查询的时间,如果有本地的缓存或keep-alive则时间为0
domainLookupEndDNS域名结束查询的时间
connectStartTCP开始建立连接的时间,如果是持久连接,则与fetchStart值相等
secureConnectionStarthttps 连接开始的时间,如果不是安全连接则为0
connectEndTCP完成握手的时间,如果是持久连接则与fetchStart值相等
requestStartHTTP请求读取真实文档开始的时间,包括从本地缓存读取
requestEndHTTP请求读取真实文档结束的时间,包括从本地缓存读取
responseStart返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的Unix毫秒时间戳
responseEnd返回浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最后一个字节时的 Unix 毫秒时间戳
unloadEventStart前一个页面的 unload 的时间戳 如果没有则为 0
unloadEventEndunloadEventStart相对应,返回的是unload函数执行完成的时间戳
domLoading返回当前网页DOM结构开始解析时的时间戳,此时document.readyState变成loading,并将抛出readyStateChange事件
domInteractive返回当前网页DOM结构结束解析、开始加载内嵌资源时时间戳,document.readyState 变成interactive,并将抛出readyStateChange事件(注意只是DOM树解析完成,这时候并没有开始加载网页内的资源)
domContentLoadedEventStart网页 domContentLoaded 事件发生的时间
domContentLoadedEventEnd网页 domContentLoaded 事件脚本执行完毕的时间,domReady 的时间
domCompleteDOM 树解析完成,且资源也准备就绪的时间,document.readyState变成complete.并将抛出 readystatechange 事件
loadEventStartload 事件发送给文档,也即load回调函数开始执行的时间
loadEventEndload 回调函数执行完成的时间

阶段计算

字段描述计算方式意义
unload前一个页面卸载耗时unloadEventEnd – unloadEventStart-
redirect重定向耗时redirectEnd – redirectStart重定向的时间
appCache缓存耗时domainLookupStart – fetchStart读取缓存的时间
dnsDNS 解析耗时domainLookupEnd – domainLookupStart可观察域名解析服务是否正常
tcpTCP 连接耗时connectEnd – connectStart建立连接的耗时
sslSSL 安全连接耗时connectEnd – secureConnectionStart反映数据安全连接建立耗时
ttfbTime to First Byte (TTFB)网络请求耗时responseStart – requestStartTTFB 是发出页面请求到接收到应答数据第一个字节所花费的毫秒数
response响应数据传输耗时responseEnd – responseStart观察网络是否正常
domDOM解析耗时domInteractive – responseEnd观察DOM结构是否合理,是否有JS阻塞页面解析
dclDOMContentLoaded 事件耗时domContentLoadedEventEnd – domContentLoadedEventStart当 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,无需等待样式表、图像和子框架的完成加载
resources资源加载耗时domComplete – domContentLoadedEventEnd可观察文档流是否过大
domReadyDOM阶段渲染耗时domContentLoadedEventEnd – fetchStartDOM 树和页面资源加载完成时间,会触发domContentLoaded事件
首次渲染耗时首次渲染耗时responseEnd-fetchStart加载文档到看到第一帧非空图像的时间,也叫白屏时间
首次可交互时间首次可交互时间domInteractive-fetchStartDOM 树解析完成时间,此时document.readyState 为 interactive
首包时间耗时首包时间responseStart-domainLookupStartDNS解析到响应返回给浏览器第一个字节的时间
页面完全加载时间页面完全加载时间loadEventStart - fetchStart-
onLoadonLoad事件耗时loadEventEnd – loadEventStart

image.png

数据结构

{
"programName": "前端监控系统",
"type": "performance",
"subType": "FCP",
"message": "someVar is not defined",
"userAgent": "Chrome", // 用户浏览器类型
"deviceInfo":{
"CPU":"",
"GPU":"",
},
}

实现

import onload from "../util/onload";
import tracker from "../util/tracker";
import formatTime from "../util/formatTime";
import getLastEvent from "../util/getLastEvent";
import getSelector from "../util/getSelector";
export function timing() {
onload(function () {
setTimeout(() => {
const {
fetchStart,
connectStart,
connectEnd,
requestStart,
responseStart,
responseEnd,
domLoading,
domInteractive,
domContentLoadedEventStart,
domContentLoadedEventEnd,
loadEventStart,
} = performance.timing;
tracker.send({
kind: "experience",
type: "timing",
connectTime: connectEnd - connectStart, //TCP连接耗时
ttfbTime: responseStart - requestStart, //ttfb
responseTime: responseEnd - responseStart, //Response响应耗时
parseDOMTime: loadEventStart - domLoading, //DOM解析渲染耗时
domContentLoadedTime:
domContentLoadedEventEnd - domContentLoadedEventStart, //DOMContentLoaded事件回调耗时
timeToInteractive: domInteractive - fetchStart, //首次可交互时间
loadTime: loadEventStart - fetchStart, //完整的加载时间
});
}, 3000);
});
}

主要性能指标

image.png

image.png

数据设计

paint

{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590828364186",
"userAgent": "chrome",
"kind": "experience",
"type": "paint",
"firstPaint": "102",
"firstContentPaint": "2130",
"firstMeaningfulPaint": "2130",
"largestContentfulPaint": "2130"
}

firstInputDelay

{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590828477284",
"userAgent": "chrome",
"kind": "experience",
"type": "firstInputDelay",
"inputDelay": "3",
"duration": "8",
"startTime": "4812.344999983907",
"selector": "HTML BODY #container .content H1"
}

实现

关键时间节点通过 window.performance.timing 获取

image.png

import tracker from "../utils/tracker";
import onload from "../utils/onload";
import getLastEvent from "../utils/getLastEvent";
import getSelector from "../utils/getSelector";

export function timing() {
let FMP, LCP;
// 增加一个性能条目的观察者
new PerformanceObserver((entryList, observer) => {
const perfEntries = entryList.getEntries();
FMP = perfEntries[0];
observer.disconnect(); // 不再观察了
}).observe({ entryTypes: ["element"] }); // 观察页面中有意义的元素
// 增加一个性能条目的观察者
new PerformanceObserver((entryList, observer) => {
const perfEntries = entryList.getEntries();
const lastEntry = perfEntries[perfEntries.length - 1];
LCP = lastEntry;
observer.disconnect(); // 不再观察了
}).observe({ entryTypes: ["largest-contentful-paint"] }); // 观察页面中最大的元素
// 增加一个性能条目的观察者
new PerformanceObserver((entryList, observer) => {
const lastEvent = getLastEvent();
const firstInput = entryList.getEntries()[0];
if (firstInput) {
// 开始处理的时间 - 开始点击的时间,差值就是处理的延迟
let inputDelay = firstInput.processingStart - firstInput.startTime;
let duration = firstInput.duration; // 处理的耗时
if (inputDelay > 0 || duration > 0) {
tracker.send({
kind: "experience", // 用户体验指标
type: "firstInputDelay", // 首次输入延迟
inputDelay: inputDelay ? formatTime(inputDelay) : 0, // 延迟的时间
duration: duration ? formatTime(duration) : 0,
startTime: firstInput.startTime, // 开始处理的时间
selector: lastEvent
? getSelector(lastEvent.path || lastEvent.target)
: "",
});
}
}
observer.disconnect(); // 不再观察了
}).observe({ type: "first-input", buffered: true }); // 第一次交互

// 刚开始页面内容为空,等页面渲染完成,再去做判断
onload(function () {
setTimeout(() => {
const {
fetchStart,
connectStart,
connectEnd,
requestStart,
responseStart,
responseEnd,
domLoading,
domInteractive,
domContentLoadedEventStart,
domContentLoadedEventEnd,
loadEventStart,
} = window.performance.timing;
// 发送时间指标
tracker.send({
kind: "experience", // 用户体验指标
type: "timing", // 统计每个阶段的时间
connectTime: connectEnd - connectStart, // TCP连接耗时
ttfbTime: responseStart - requestStart, // 首字节到达时间
responseTime: responseEnd - responseStart, // response响应耗时
parseDOMTime: loadEventStart - domLoading, // DOM解析渲染的时间
domContentLoadedTime:
domContentLoadedEventEnd - domContentLoadedEventStart, // DOMContentLoaded事件回调耗时
timeToInteractive: domInteractive - fetchStart, // 首次可交互时间
loadTime: loadEventStart - fetchStart, // 完整的加载时间
});
// 发送性能指标
let FP = performance.getEntriesByName("first-paint")[0];
let FCP = performance.getEntriesByName("first-contentful-paint")[0];
console.log("FP", FP);
console.log("FCP", FCP);
console.log("FMP", FMP);
console.log("LCP", LCP);
tracker.send({
kind: "experience",
type: "paint",
firstPaint: FP ? formatTime(FP.startTime) : 0,
firstContentPaint: FCP ? formatTime(FCP.startTime) : 0,
firstMeaningfulPaint: FMP ? formatTime(FMP.startTime) : 0,
largestContentfulPaint: LCP
? formatTime(LCP.renderTime || LCP.loadTime)
: 0,
});
}, 3000);
});
}

卡顿

响应用户交互的响应时间如果大于 100ms,用户就会感觉卡顿

数据设计

{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590828656781",
"userAgent": "chrome",
"kind": "experience",
"type": "longTask",
"eventType": "mouseover",
"startTime": "9331",
"duration": "200",
"selector": "HTML BODY #container .content"
}

实现

  • new PerformanceObserver
  • entry.duration > 100 判断大于 100ms,即可认定为长任务
  • 使用 requestIdleCallback上报数据
import tracker from "../util/tracker";
import formatTime from "../util/formatTime";
import getLastEvent from "../util/getLastEvent";
import getSelector from "../util/getSelector";
export function longTask() {
new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.duration > 100) {
let lastEvent = getLastEvent();
requestIdleCallback(() => {
tracker.send({
kind: "experience",
type: "longTask",
eventType: lastEvent.type,
startTime: formatTime(entry.startTime), // 开始时间
duration: formatTime(entry.duration), // 持续时间
selector: lastEvent
? getSelector(lastEvent.path || lastEvent.target)
: "",
});
});
}
});
}).observe({ entryTypes: ["longtask"] });
}

PV、UV、用户停留时间

PV(page view)是页面浏览量,UV(Unique visitor)用户访问量。PV 只要访问一次页面就算一次,UV 同一天内多次访问只算一次。

对于前端来说,只要每次进入页面上报一次 PV 就行,UV 的统计放在服务端来做,主要是分析上报的数据来统计得出 UV。

数据设计

{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590829304423",
"userAgent": "chrome",
"kind": "business",
"type": "pv",
"effectiveType": "4g",
"rtt": "50",
"screen": "2049x1152"
}

实现

import tracker from "../util/tracker";
export function pv() {
tracker.send({
kind: "business",
type: "pv",
startTime: performance.now(),
pageURL: getPageURL(),
referrer: document.referrer,
uuid: getUUID(),
});
let startTime = Date.now();
window.addEventListener(
"beforeunload",
() => {
let stayTime = Date.now() - startTime;
tracker.send({
kind: "business",
type: "stayTime",
stayTime,
pageURL: getPageURL(),
uuid: getUUID(),
});
},
false
);
}

参考文章

作者链接
miracle90https://github.com/miracle90/monitor