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 去除工具条与滚动条的窗口高度
加载时间
阶段含义
字段 | 含义 |
---|---|
navigationStart | 初始化页面,在同一个浏览器上下文中前一个页面unload的时间戳,如果没有前一个页面的unload,则与fetchStart值相等 |
redirectStart | 第一个HTTP重定向发生的时间,有跳转且是同域的重定向,否则为0 |
redirectEnd | 最后一个重定向完成时的时间,否则为0 |
fetchStart | 浏览器准备好使用http请求获取文档的时间,这发生在检查缓存之前 |
domainLookupStart | DNS域名开始查询的时间,如果有本地的缓存或keep-alive则时间为0 |
domainLookupEnd | DNS域名结束查询的时间 |
connectStart | TCP开始建立连接的时间,如果是持久连接,则与fetchStart 值相等 |
secureConnectionStart | https 连接开始的时间,如果不是安全连接则为0 |
connectEnd | TCP完成握手的时间,如果是持久连接则与fetchStart 值相等 |
requestStart | HTTP请求读取真实文档开始的时间,包括从本地缓存读取 |
requestEnd | HTTP请求读取真实文档结束的时间,包括从本地缓存读取 |
responseStart | 返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的Unix毫秒时间戳 |
responseEnd | 返回浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最后一个字节时的 Unix 毫秒时间戳 |
unloadEventStart | 前一个页面的 unload 的时间戳 如果没有则为 0 |
unloadEventEnd | 与unloadEventStart 相对应,返回的是unload 函数执行完成的时间戳 |
domLoading | 返回当前网页DOM结构开始解析时的时间戳,此时document.readyState 变成loading,并将抛出readyStateChange 事件 |
domInteractive | 返回当前网页DOM结构结束解析、开始加载内嵌资源时时间戳,document.readyState 变成interactive ,并将抛出readyStateChange 事件(注意只是DOM树解析完成,这时候并没有开始加载网页内的资源) |
domContentLoadedEventStart | 网页 domContentLoaded 事件发生的时间 |
domContentLoadedEventEnd | 网页 domContentLoaded 事件脚本执行完毕的时间,domReady 的时间 |
domComplete | DOM 树解析完成,且资源也准备就绪的时间,document.readyState 变成complete .并将抛出 readystatechange 事件 |
loadEventStart | load 事件发送给文档,也即load回调函数开始执行的时间 |
loadEventEnd | load 回调函数执行完成的时间 |
阶段计算
字段 | 描述 | 计算方式 | 意义 |
---|---|---|---|
unload | 前一个页面卸载耗时 | unloadEventEnd – unloadEventStart | - |
redirect | 重定向耗时 | redirectEnd – redirectStart | 重定向的时间 |
appCache | 缓存耗时 | domainLookupStart – fetchStart | 读取缓存的时间 |
dns | DNS 解析耗时 | domainLookupEnd – domainLookupStart | 可观察域名解析服务是否正常 |
tcp | TCP 连接耗时 | connectEnd – connectStart | 建立连接的耗时 |
ssl | SSL 安全连接耗时 | connectEnd – secureConnectionStart | 反映数据安全连接建立耗时 |
ttfb | Time to First Byte (TTFB)网络请求耗时 | responseStart – requestStart | TTFB 是发出页面请求到接收到应答数据第一个字节所花费的毫秒数 |
response | 响应数据传输耗时 | responseEnd – responseStart | 观察网络是否正常 |
dom | DOM解析耗时 | domInteractive – responseEnd | 观察DOM结构是否合理,是否有JS阻塞页面解析 |
dcl | DOMContentLoaded 事件耗时 | domContentLoadedEventEnd – domContentLoadedEventStart | 当 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,无需等待样式表、图像和子框架的完成加载 |
resources | 资源加载耗时 | domComplete – domContentLoadedEventEnd | 可观察文档流是否过大 |
domReady | DOM阶段渲染耗时 | domContentLoadedEventEnd – fetchStart | DOM 树和页面资源加载完成时间,会触发domContentLoaded 事件 |
首次渲染耗时 | 首次渲染耗时 | responseEnd-fetchStart | 加载文档到看到第一帧非空图像的时间,也叫白屏时间 |
首次可交互时间 | 首次可交互时间 | domInteractive-fetchStart | DOM 树解析完成时间,此时document.readyState 为 interactive |
首包时间耗时 | 首包时间 | responseStart-domainLookupStart | DNS解析到响应返回给浏览器第一个字节的时间 |
页面完全加载时间 | 页面完全加载时间 | loadEventStart - fetchStart | - |
onLoad | onLoad事件耗时 | loadEventEnd – loadEventStart |
数据结构
{
"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);
});
}
主要性能指标
- PerformanceObserver.observe 方法用于观察传入的参数中指定的性能条目类型的集合。当记录一个指定类型的性能条目时,性能监测对象的回调函数将会被调用
- entryType
- paint-timing
- event-timing
数据设计
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 获取
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
);
}
参考文章
作者 | 链接 |
---|---|
miracle90 | https://github.com/miracle90/monitor |