前端监控体系搭建实战
Created on
前言
“线上怎么又白屏了?”、“为什么加载这么慢?”—— 如果你经常听到这些问题却无从下手,说明你需要一套完善的前端监控系统。本文将带你从零搭建生产可用的监控平台。
监控体系架构
┌─────────────────────────────────────────┐
│ 前端应用 (SDK) │
│ ┌─────────┬─────────┬──────────────┐ │
│ │性能监控│错误监控│ 行为追踪 │ │
│ └────┬────┴────┬────┴──────┬───────┘ │
│ │ │ │ │
│ └─────────┴───────────┘ │
│ │ │
│ 数据收集 & 上报 │
└─────────────────┼───────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 数据接收服务 (API) │
│ ┌──────────────────────────────────┐ │
│ │ 数据验证 → 清洗 → 聚合 → 存储 │ │
│ └──────────────────────────────────┘ │
└─────────────────┼───────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 数据分析 & 可视化 │
│ ┌────────┬─────────┬──────────────┐ │
│ │Dashboard│告警系统 │数据分析报表 │ │
│ └────────┴─────────┴──────────────┘ │
└─────────────────────────────────────────┘
性能监控
Web Vitals 核心指标
// monitor/performance.js
import { onCLS, onFID, onLCP, onFCP, onTTFB } from "web-vitals";
class PerformanceMonitor {
constructor() {
this.metrics = {};
this.init();
}
init() {
// Largest Contentful Paint - 最大内容绘制
onLCP((metric) => {
this.reportMetric("LCP", metric);
});
// First Input Delay - 首次输入延迟
onFID((metric) => {
this.reportMetric("FID", metric);
});
// Cumulative Layout Shift - 累积布局偏移
onCLS((metric) => {
this.reportMetric("CLS", metric);
});
// First Contentful Paint - 首次内容绘制
onFCP((metric) => {
this.reportMetric("FCP", metric);
});
// Time to First Byte - 首字节时间
onTTFB((metric) => {
this.reportMetric("TTFB", metric);
});
}
reportMetric(name, metric) {
const data = {
name,
value: metric.value,
rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
delta: metric.delta,
id: metric.id,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
};
this.metrics[name] = data;
this.send(data);
}
send(data) {
// 数据上报
navigator.sendBeacon("/api/metrics", JSON.stringify(data));
}
}
export default new PerformanceMonitor();
Navigation Timing 详细性能数据
// 获取页面加载性能数据
function getNavigationTiming() {
const timing = performance.getEntriesByType("navigation")[0];
if (!timing) return null;
return {
// DNS 查询耗时
dnsTime: timing.domainLookupEnd - timing.domainLookupStart,
// TCP 连接耗时
tcpTime: timing.connectEnd - timing.connectStart,
// SSL 安全连接耗时
sslTime: timing.secureConnectionStart
? timing.connectEnd - timing.secureConnectionStart
: 0,
// 请求响应耗时
requestTime: timing.responseEnd - timing.requestStart,
// DOM 解析耗时
domParseTime: timing.domInteractive - timing.responseEnd,
// 资源加载耗时
resourceTime: timing.loadEventStart - timing.domContentLoadedEventEnd,
// 首字节时间
ttfb: timing.responseStart - timing.requestStart,
// DOM Ready 时间
domReady: timing.domContentLoadedEventEnd - timing.fetchStart,
// 页面完全加载时间
loadTime: timing.loadEventEnd - timing.fetchStart,
// 重定向次数
redirectCount: timing.redirectCount,
// 传输类型 (navigate/reload/back_forward/prerender)
type: timing.type,
};
}
// 在页面加载完成后上报
window.addEventListener("load", () => {
setTimeout(() => {
const perfData = getNavigationTiming();
navigator.sendBeacon("/api/perf", JSON.stringify(perfData));
}, 0);
});
Resource Timing 资源加载性能
// 监控资源加载性能
function getResourceTiming() {
const resources = performance.getEntriesByType("resource");
return resources.map((resource) => ({
name: resource.name,
type: resource.initiatorType,
duration: resource.duration,
size: resource.transferSize,
cached: resource.transferSize === 0,
startTime: resource.startTime,
// 慢资源标记 (超过 1s)
slow: resource.duration > 1000,
}));
}
// 监听新资源加载
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.duration > 1000) {
// 上报慢资源
reportSlowResource({
name: entry.name,
duration: entry.duration,
size: entry.transferSize,
});
}
});
});
observer.observe({ entryTypes: ["resource"] });
错误监控
JavaScript 错误捕获
// monitor/error.js
class ErrorMonitor {
constructor() {
this.errors = [];
this.init();
}
init() {
// 全局错误监听
window.addEventListener(
"error",
(event) => {
this.handleError({
type: "jsError",
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
timestamp: Date.now(),
});
},
true
);
// Promise 未捕获错误
window.addEventListener("unhandledrejection", (event) => {
this.handleError({
type: "promiseError",
message: event.reason?.message || event.reason,
stack: event.reason?.stack,
timestamp: Date.now(),
});
});
// 资源加载错误
window.addEventListener(
"error",
(event) => {
const target = event.target;
if (target !== window) {
this.handleError({
type: "resourceError",
tagName: target.tagName,
src: target.src || target.href,
timestamp: Date.now(),
});
}
},
true
);
}
handleError(error) {
// 添加上下文信息
const enrichedError = {
...error,
url: window.location.href,
userAgent: navigator.userAgent,
viewport: `${window.innerWidth}x${window.innerHeight}`,
// 用户信息 (如果有)
userId: window.__USER_ID__,
// 环境信息
env: process.env.NODE_ENV,
};
this.errors.push(enrichedError);
this.report(enrichedError);
}
report(error) {
// 错误去重
const errorKey = `${error.type}_${error.message}_${error.filename}_${error.lineno}`;
if (this.hasReported(errorKey)) {
return;
}
// 采样上报 (生产环境可能需要)
if (Math.random() > 0.1) {
// 10% 采样率
return;
}
navigator.sendBeacon("/api/error", JSON.stringify(error));
this.markReported(errorKey);
}
hasReported(key) {
return sessionStorage.getItem(`error_${key}`) === "1";
}
markReported(key) {
sessionStorage.setItem(`error_${key}`, "1");
}
}
export default new ErrorMonitor();
Source Map 还原
// 后端 Source Map 解析
const sourceMap = require("source-map");
const fs = require("fs");
async function parseError(error) {
try {
// 读取 Source Map 文件
const mapContent = fs.readFileSync(`./dist/${error.filename}.map`, "utf8");
const consumer = await new sourceMap.SourceMapConsumer(mapContent);
// 获取原始位置
const originalPosition = consumer.originalPositionFor({
line: error.lineno,
column: error.colno,
});
return {
...error,
originalSource: originalPosition.source,
originalLine: originalPosition.line,
originalColumn: originalPosition.column,
originalName: originalPosition.name,
};
} catch (e) {
return error;
}
}
用户行为追踪
页面访问追踪
// monitor/pageview.js
class PageViewTracker {
constructor() {
this.startTime = Date.now();
this.init();
}
init() {
// 页面进入
this.trackPageEnter();
// 页面离开
window.addEventListener("beforeunload", () => {
this.trackPageLeave();
});
// 页面可见性变化
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
this.trackPageHide();
} else {
this.trackPageShow();
}
});
// SPA 路由变化
this.trackRouteChange();
}
trackPageEnter() {
const data = {
type: "pageEnter",
url: window.location.href,
referrer: document.referrer,
timestamp: Date.now(),
};
this.send(data);
}
trackPageLeave() {
const data = {
type: "pageLeave",
url: window.location.href,
duration: Date.now() - this.startTime,
timestamp: Date.now(),
};
this.send(data);
}
trackPageHide() {
this.send({ type: "pageHide", timestamp: Date.now() });
}
trackPageShow() {
this.send({ type: "pageShow", timestamp: Date.now() });
}
trackRouteChange() {
// History API
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function (...args) {
originalPushState.apply(this, args);
this.trackPageEnter();
}.bind(this);
history.replaceState = function (...args) {
originalReplaceState.apply(this, args);
this.trackPageEnter();
}.bind(this);
// Hash 变化
window.addEventListener("hashchange", () => {
this.trackPageEnter();
});
}
send(data) {
navigator.sendBeacon("/api/pageview", JSON.stringify(data));
}
}
export default new PageViewTracker();
用户点击追踪
// monitor/click.js
class ClickTracker {
init() {
document.addEventListener(
"click",
(event) => {
const target = event.target;
// 获取元素信息
const data = {
type: "click",
tagName: target.tagName,
id: target.id,
className: target.className,
text: target.innerText?.slice(0, 50), // 限制长度
xpath: this.getXPath(target),
timestamp: Date.now(),
};
// 只追踪重要元素
if (this.shouldTrack(target)) {
this.send(data);
}
},
true
);
}
shouldTrack(element) {
// 追踪按钮、链接、表单元素
const trackedTags = ["BUTTON", "A", "INPUT", "SELECT"];
return (
trackedTags.includes(element.tagName) ||
element.hasAttribute("data-track")
);
}
getXPath(element) {
if (element.id) {
return `//*[@id="${element.id}"]`;
}
const parts = [];
while (element && element.nodeType === Node.ELEMENT_NODE) {
let index = 0;
let sibling = element.previousSibling;
while (sibling) {
if (
sibling.nodeType === Node.ELEMENT_NODE &&
sibling.tagName === element.tagName
) {
index++;
}
sibling = sibling.previousSibling;
}
const tagName = element.tagName.toLowerCase();
const pathIndex = index > 0 ? `[${index + 1}]` : "";
parts.unshift(`${tagName}${pathIndex}`);
element = element.parentNode;
}
return "/" + parts.join("/");
}
send(data) {
navigator.sendBeacon("/api/click", JSON.stringify(data));
}
}
export default new ClickTracker();
数据上报策略
批量上报
class Reporter {
constructor() {
this.queue = [];
this.maxSize = 10; // 队列最大长度
this.maxWait = 5000; // 最大等待时间 (ms)
this.timer = null;
}
add(data) {
this.queue.push(data);
if (this.queue.length >= this.maxSize) {
this.flush();
} else if (!this.timer) {
this.timer = setTimeout(() => this.flush(), this.maxWait);
}
}
flush() {
if (this.queue.length === 0) return;
const data = this.queue.splice(0);
this.send(data);
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
send(data) {
// 优先使用 sendBeacon
if (navigator.sendBeacon) {
navigator.sendBeacon("/api/batch", JSON.stringify(data));
} else {
// 降级到 fetch
fetch("/api/batch", {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
keepalive: true,
}).catch(console.error);
}
}
}
export default new Reporter();
请求失败重试
class RetryReporter {
constructor() {
this.maxRetries = 3;
this.retryDelay = 1000;
}
async send(data, retries = 0) {
try {
const response = await fetch("/api/report", {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
if (!response.ok) {
throw new Error("Request failed");
}
} catch (error) {
if (retries < this.maxRetries) {
// 指数退避重试
const delay = this.retryDelay * Math.pow(2, retries);
setTimeout(() => {
this.send(data, retries + 1);
}, delay);
} else {
// 重试失败,存储到本地
this.saveToLocal(data);
}
}
}
saveToLocal(data) {
try {
const stored = JSON.parse(localStorage.getItem("failed_reports") || "[]");
stored.push(data);
localStorage.setItem("failed_reports", JSON.stringify(stored));
} catch (e) {
console.error("Failed to save report", e);
}
}
// 页面加载时重新发送失败的数据
retrySavedReports() {
try {
const stored = JSON.parse(localStorage.getItem("failed_reports") || "[]");
if (stored.length > 0) {
stored.forEach((data) => this.send(data));
localStorage.removeItem("failed_reports");
}
} catch (e) {
console.error("Failed to retry reports", e);
}
}
}
监控 SDK 封装
// monitor/index.js
class Monitor {
constructor(options = {}) {
this.options = {
appId: options.appId,
userId: options.userId,
apiUrl: options.apiUrl || "/api/monitor",
enablePerformance: options.enablePerformance !== false,
enableError: options.enableError !== false,
enableBehavior: options.enableBehavior !== false,
sampleRate: options.sampleRate || 1,
};
this.init();
}
init() {
// 采样控制
if (Math.random() > this.options.sampleRate) {
return;
}
if (this.options.enablePerformance) {
import("./performance").then((module) => {
this.performance = module.default;
});
}
if (this.options.enableError) {
import("./error").then((module) => {
this.error = module.default;
});
}
if (this.options.enableBehavior) {
Promise.all([import("./pageview"), import("./click")]).then(
([pageview, click]) => {
this.pageview = pageview.default;
this.click = click.default;
}
);
}
}
// 自定义事件追踪
track(eventName, properties = {}) {
const data = {
type: "customEvent",
eventName,
properties,
timestamp: Date.now(),
url: window.location.href,
};
navigator.sendBeacon(this.options.apiUrl, JSON.stringify(data));
}
// 设置用户信息
setUser(userId, userInfo = {}) {
this.options.userId = userId;
this.options.userInfo = userInfo;
}
}
// 使用示例
const monitor = new Monitor({
appId: "my-app",
userId: "user-123",
sampleRate: 0.5, // 50% 采样
enablePerformance: true,
enableError: true,
enableBehavior: true,
});
// 自定义事件
monitor.track("purchase", {
productId: "12345",
amount: 99.99,
});
export default Monitor;
后端接收服务
// server/monitor.js
const express = require("express");
const router = express.Router();
// 数据验证
function validateData(data) {
return data && typeof data === "object" && data.timestamp;
}
// 接收错误数据
router.post("/api/error", async (req, res) => {
const error = req.body;
if (!validateData(error)) {
return res.status(400).json({ error: "Invalid data" });
}
try {
// 存储到数据库
await db.errors.insert({
...error,
receivedAt: new Date(),
});
// 严重错误实时告警
if (error.type === "jsError") {
await sendAlert(error);
}
res.json({ success: true });
} catch (e) {
res.status(500).json({ error: "Server error" });
}
});
// 接收性能数据
router.post("/api/perf", async (req, res) => {
const perfData = req.body;
try {
await db.performance.insert({
...perfData,
receivedAt: new Date(),
});
res.json({ success: true });
} catch (e) {
res.status(500).json({ error: "Server error" });
}
});
// 批量接收
router.post("/api/batch", async (req, res) => {
const batch = req.body;
if (!Array.isArray(batch)) {
return res.status(400).json({ error: "Invalid batch data" });
}
try {
// 按类型分组
const grouped = batch.reduce((acc, item) => {
acc[item.type] = acc[item.type] || [];
acc[item.type].push(item);
return acc;
}, {});
// 批量插入
for (const [type, items] of Object.entries(grouped)) {
await db[type].insertMany(items);
}
res.json({ success: true });
} catch (e) {
res.status(500).json({ error: "Server error" });
}
});
module.exports = router;
告警系统
// server/alert.js
async function sendAlert(error) {
// 告警规则
const rules = {
// 错误率超过阈值
errorRate: {
threshold: 0.01, // 1%
window: 5 * 60 * 1000, // 5分钟
},
// 慢请求比例
slowRequest: {
threshold: 0.1, // 10%
duration: 1000, // 1秒
},
};
// 检查错误率
const recentErrors = await db.errors.count({
timestamp: { $gte: Date.now() - rules.errorRate.window },
});
const totalRequests = await db.pageviews.count({
timestamp: { $gte: Date.now() - rules.errorRate.window },
});
const errorRate = recentErrors / totalRequests;
if (errorRate > rules.errorRate.threshold) {
// 发送告警
await notify({
level: "critical",
message: `Error rate is ${(errorRate * 100).toFixed(2)}%`,
errors: recentErrors,
});
}
}
async function notify(alert) {
// 邮件通知
await sendEmail({
to: "[email protected]",
subject: `[${alert.level.toUpperCase()}] Frontend Alert`,
body: alert.message,
});
// 企业微信/钉钉通知
await sendWebhook({
url: process.env.WEBHOOK_URL,
data: alert,
});
// Sentry
if (process.env.SENTRY_DSN) {
Sentry.captureMessage(alert.message, {
level: alert.level,
extra: alert,
});
}
}
总结
完整的前端监控体系包括:
- 性能监控: Web Vitals、资源加载、页面渲染
- 错误监控: JS 错误、Promise 错误、资源错误
- 行为监控: 页面访问、用户交互、自定义事件
- 数据上报: 批量上报、失败重试、采样控制
- 数据分析: 实时大盘、趋势分析、告警通知
记住:监控不是目的,提升用户体验才是最终目标。