# 前端监控与异常处理
# 目录
# 引言
前端监控是提升用户体验和应用质量的关键环节。一个完善的前端监控系统能够帮助开发团队及时发现问题、分析根因、优化应用,从而提高用户满意度和业务转化率。本文将深入探讨前端监控系统的设计与实现,帮助开发者构建强大而高效的监控解决方案。
# 为什么需要前端监控
- 用户体验透明化:了解真实用户的操作流程和体验痛点
- 问题快速定位:缩短从问题发生到发现和解决的时间
- 性能持续优化:基于真实数据进行有针对性的性能改进
- 业务决策支持:为产品和运营决策提供数据支持
# 监控系统架构
一个完整的前端监控系统通常包含以下组件:
# 整体架构
+----------------+ +----------------+ +----------------+
| | | | | |
| 数据采集层 | --> | 数据处理层 | --> | 数据应用层 |
| | | | | |
+----------------+ +----------------+ +----------------+
| | |
v v v
客户端SDK 数据清洗/聚合 可视化展示
采集插件 实时/离线计算 告警系统
埋点管理 数据存储 数据分析
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
# 核心模块
- 数据采集SDK:在客户端收集各类监控数据
- 数据上报服务:将数据安全高效地传输到服务端
- 数据处理引擎:清洗、聚合和分析监控数据
- 数据存储中心:高效存储海量监控数据
- 告警系统:根据预设规则触发告警
- 可视化平台:直观展示监控数据
# 系统设计原则
- 低侵入性:监控代码不应影响业务逻辑和用户体验
- 可扩展性:支持自定义监控指标和灵活的数据维度
- 容错性:监控系统本身的故障不应影响主应用功能
- 实时性:关键异常需要实时检测和报告
- 数据安全:确保用户隐私和数据合规
# 性能监控
# Web Vitals监控
// 监控核心Web指标
import {onCLS, onFID, onLCP, onTTFB, onINP} from 'web-vitals';
function sendToAnalytics({name, delta, id}) {
const body = JSON.stringify({name, delta, id});
// 使用Beacon API进行数据上报
if (navigator.sendBeacon) {
navigator.sendBeacon('/analytics', body);
} else {
fetch('/analytics', {
body,
method: 'POST',
keepalive: true
});
}
}
// 注册监控
onCLS(sendToAnalytics);
onFID(sendToAnalytics);
onLCP(sendToAnalytics);
onTTFB(sendToAnalytics);
onINP(sendToAnalytics);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 自定义性能指标
// 监控自定义性能指标
class PerformanceMonitor {
constructor() {
this.marks = {};
}
startMark(name) {
this.marks[name] = {
startTime: performance.now()
};
}
endMark(name) {
if (this.marks[name]) {
const duration = performance.now() - this.marks[name].startTime;
this.sendMetric(name, duration);
delete this.marks[name];
}
}
sendMetric(name, value) {
// 上报自定义指标
this.sendToAnalytics({
type: 'custom-performance',
name,
value
});
}
sendToAnalytics(data) {
// 数据上报逻辑
}
}
// 使用示例
const monitor = new PerformanceMonitor();
monitor.startMark('data-loading');
fetchData().then(() => {
monitor.endMark('data-loading');
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 资源加载监控
// 监控资源加载性能
function monitorResourceLoading() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach(entry => {
if (entry.initiatorType === 'script' ||
entry.initiatorType === 'css' ||
entry.initiatorType === 'img') {
sendToAnalytics({
type: 'resource',
name: entry.name,
duration: entry.duration,
size: entry.transferSize,
initiatorType: entry.initiatorType
});
}
});
});
observer.observe({entryTypes: ['resource']});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 错误监控
# JavaScript错误捕获
// 全局错误捕获
window.addEventListener('error', function(event) {
sendToAnalytics({
type: 'js-error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error ? event.error.stack : '',
timestamp: Date.now()
});
// 不阻止默认行为
return false;
}, true);
// Promise未捕获异常
window.addEventListener('unhandledrejection', function(event) {
sendToAnalytics({
type: 'promise-error',
message: event.reason.message || String(event.reason),
stack: event.reason.stack || '',
timestamp: Date.now()
});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 框架特定错误处理
# React错误边界
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
// 更新状态,下一次渲染将显示回退UI
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// 上报错误
sendToAnalytics({
type: 'react-error',
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: Date.now()
});
}
render() {
if (this.state.hasError) {
// 自定义回退UI
return <h1>出错了,请稍后再试</h1>;
}
return this.props.children;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Vue错误处理
// Vue 3 全局错误处理
const app = createApp(App)
app.config.errorHandler = (err, instance, info) => {
sendToAnalytics({
type: 'vue-error',
message: err.message,
stack: err.stack,
info: info,
componentName: instance?.type?.name,
timestamp: Date.now()
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# 请求异常监控
// 使用Fetch API拦截器
const originalFetch = window.fetch;
window.fetch = async function(resource, init) {
const startTime = Date.now();
let response;
try {
response = await originalFetch(resource, init);
const endTime = Date.now();
// 上报请求成功信息
sendToAnalytics({
type: 'api-success',
url: typeof resource === 'string' ? resource : resource.url,
method: init?.method || 'GET',
status: response.status,
duration: endTime - startTime
});
if (!response.ok) {
// 处理HTTP错误
sendToAnalytics({
type: 'api-http-error',
url: typeof resource === 'string' ? resource : resource.url,
method: init?.method || 'GET',
status: response.status,
statusText: response.statusText,
duration: endTime - startTime
});
}
return response;
} catch (error) {
// 处理网络错误
sendToAnalytics({
type: 'api-network-error',
url: typeof resource === 'string' ? resource : resource.url,
method: init?.method || 'GET',
message: error.message,
stack: error.stack,
duration: Date.now() - startTime
});
throw error;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 用户行为监控
# 页面访问追踪
// 页面浏览监控
function trackPageView() {
// 采集页面加载信息
window.addEventListener('load', () => {
const navigationTiming = performance.getEntriesByType('navigation')[0];
sendToAnalytics({
type: 'page-view',
url: window.location.href,
title: document.title,
referrer: document.referrer,
loadTime: navigationTiming ? navigationTiming.loadEventEnd : performance.now(),
timestamp: Date.now()
});
});
// 监听路由变化 (SPA)
let lastUrl = window.location.href;
// 创建观察器实例
const observer = new MutationObserver(() => {
if (window.location.href !== lastUrl) {
const oldUrl = lastUrl;
lastUrl = window.location.href;
// 上报页面变化
sendToAnalytics({
type: 'spa-view',
url: lastUrl,
title: document.title,
from: oldUrl,
timestamp: Date.now()
});
}
});
// 开始观察文档变化
observer.observe(document, { subtree: true, childList: true });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 用户交互追踪
// 用户交互监控
function trackUserInteractions() {
// 点击行为追踪
document.addEventListener('click', (event) => {
// 获取事件目标
const target = event.target;
// 提取有用信息
const data = {
type: 'user-interaction',
action: 'click',
tag: target.tagName.toLowerCase(),
id: target.id,
class: target.className,
text: target.innerText?.substring(0, 50) || '',
path: getElementPath(target),
timestamp: Date.now()
};
// 添加自定义数据属性
if (target.dataset.trackid) {
data.trackId = target.dataset.trackid;
}
sendToAnalytics(data);
});
// 表单提交追踪
document.addEventListener('submit', (event) => {
const form = event.target;
sendToAnalytics({
type: 'user-interaction',
action: 'form-submit',
formId: form.id,
formName: form.name,
fields: Array.from(form.elements)
.filter(el => el.name)
.map(el => el.name),
timestamp: Date.now()
});
});
}
// 获取DOM元素路径
function getElementPath(element) {
const path = [];
let currentElement = element;
while (currentElement && currentElement !== document.body) {
let selector = currentElement.tagName.toLowerCase();
if (currentElement.id) {
selector += `#${currentElement.id}`;
} else {
const siblings = Array.from(currentElement.parentNode.children)
.filter(e => e.tagName === currentElement.tagName);
if (siblings.length > 1) {
const index = siblings.indexOf(currentElement);
selector += `:nth-child(${index + 1})`;
}
}
path.unshift(selector);
currentElement = currentElement.parentNode;
}
return path.join(' > ');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# 自定义事件追踪
// 自定义事件记录器
class EventTracker {
trackEvent(category, action, label, value) {
sendToAnalytics({
type: 'custom-event',
category,
action,
label,
value,
timestamp: Date.now()
});
}
trackSearch(keyword, resultsCount, source) {
this.trackEvent('search', 'perform', keyword, resultsCount);
// 额外记录搜索细节
sendToAnalytics({
type: 'search',
keyword,
resultsCount,
source,
timestamp: Date.now()
});
}
trackConversion(name, value, currency, products) {
this.trackEvent('conversion', name, '', value);
// 记录转化详情
sendToAnalytics({
type: 'conversion',
name,
value,
currency,
products,
timestamp: Date.now()
});
}
}
// 使用示例
const tracker = new EventTracker();
// 搜索行为
searchButton.addEventListener('click', () => {
const keyword = searchInput.value;
performSearch(keyword).then(results => {
tracker.trackSearch(keyword, results.length, 'main-search');
});
});
// 购买转化
checkoutButton.addEventListener('click', () => {
tracker.trackConversion('purchase', cart.total, 'USD', cart.products);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 监控数据采集
# SDK初始化配置
// 前端监控SDK
class MonitoringSDK {
constructor(config) {
this.config = {
// 默认配置
endpoint: '/api/monitoring',
appId: '',
userId: '',
sessionId: '',
version: '',
enablePerformance: true,
enableError: true,
enableBehavior: true,
sampleRate: 1.0, // 采样率
maxBatchSize: 10, // 批量上报数量
maxQueueSize: 100, // 最大队列长度
reportInterval: 5000, // 定时上报间隔
...config
};
// 初始化数据缓冲区
this.dataQueue = [];
// 生成或获取会话ID
if (!this.config.sessionId) {
this.config.sessionId = this.generateSessionId();
}
// 初始化各模块
this.initModules();
// 设置定时上报
this.setupReporting();
// 页面卸载前发送所有数据
window.addEventListener('beforeunload', () => this.flush());
}
// 初始化监控模块
initModules() {
// 启用性能监控
if (this.config.enablePerformance) {
this.initPerformanceMonitoring();
}
// 启用错误监控
if (this.config.enableError) {
this.initErrorMonitoring();
}
// 启用行为监控
if (this.config.enableBehavior) {
this.initBehaviorMonitoring();
}
}
// 生成会话ID
generateSessionId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
// 采样决策
shouldSample() {
return Math.random() <= this.config.sampleRate;
}
// 添加数据到队列
addToQueue(data) {
// 应用采样
if (!this.shouldSample()) {
return;
}
// 添加公共字段
const enrichedData = {
...data,
appId: this.config.appId,
userId: this.config.userId,
sessionId: this.config.sessionId,
version: this.config.version,
userAgent: navigator.userAgent,
timestamp: data.timestamp || Date.now()
};
// 添加到队列
this.dataQueue.push(enrichedData);
// 检查是否需要立即上报
if (this.dataQueue.length >= this.config.maxBatchSize) {
this.flush();
}
}
// 设置定时上报
setupReporting() {
setInterval(() => {
if (this.dataQueue.length > 0) {
this.flush();
}
}, this.config.reportInterval);
}
// 发送数据
flush() {
if (this.dataQueue.length === 0) {
return;
}
// 提取队列数据
const dataToSend = this.dataQueue.splice(0, Math.min(this.dataQueue.length, this.config.maxBatchSize));
// 上报数据
this.sendData(dataToSend);
}
// 发送数据到服务器
sendData(data) {
// 使用Beacon API (不阻塞页面卸载)
if (navigator.sendBeacon) {
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
const success = navigator.sendBeacon(this.config.endpoint, blob);
// 如果发送失败,使用备选方法
if (!success) {
this.sendWithFetch(data);
}
} else {
this.sendWithFetch(data);
}
}
// 使用Fetch API发送数据
sendWithFetch(data) {
fetch(this.config.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data),
keepalive: true
}).catch(error => {
console.error('Failed to send monitoring data:', error);
});
}
// 其他初始化方法...
}
// 使用示例
const monitor = new MonitoringSDK({
endpoint: 'https://monitoring.example.com/collect',
appId: 'shop-web-app',
version: '1.2.3',
userId: currentUser?.id || '',
sampleRate: 0.5 // 采样50%的用户
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# 监控数据结构设计
// 监控数据结构示例
const monitoringDataSchema = {
// 公共字段
common: {
appId: 'String', // 应用标识
sessionId: 'String', // 会话ID
userId: 'String', // 用户ID
timestamp: 'Number', // 时间戳
userAgent: 'String', // 用户代理
url: 'String', // 页面URL
version: 'String' // 应用版本
},
// 性能数据
performance: {
type: 'performance',
name: 'String', // 指标名称 (如 LCP, FID, TTI)
value: 'Number', // 指标值
rating: 'String' // 评级 (good, needs-improvement, poor)
},
// 资源加载
resource: {
type: 'resource',
name: 'String', // 资源URL
initiatorType: 'String', // 资源类型 (img, script, css等)
duration: 'Number', // 加载时间
size: 'Number', // 资源大小
protocol: 'String' // 协议
},
// 错误数据
error: {
type: 'error',
subtype: 'String', // js-error, promise-error, api-error等
message: 'String', // 错误信息
stack: 'String', // 错误堆栈
filename: 'String', // 文件名
lineno: 'Number', // 行号
colno: 'Number' // 列号
},
// 用户行为
behavior: {
type: 'behavior',
action: 'String', // 行为类型 (click, input, scroll等)
target: 'Object', // 交互目标信息
path: 'String' // 元素路径
},
// 自定义事件
customEvent: {
type: 'custom',
category: 'String',
action: 'String',
label: 'String',
value: 'Number',
extraData: 'Object' // 自定义额外数据
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# 数据上报策略
# 高效的上报方式
class ReportingStrategy {
constructor() {
this.queue = [];
this.sending = false;
this.maxRetries = 3;
}
// 添加到上报队列
addToQueue(data) {
this.queue.push({
data,
attempts: 0
});
this.processQueue();
}
// 处理队列
async processQueue() {
if (this.sending || this.queue.length === 0) {
return;
}
this.sending = true;
// 获取队首数据
const item = this.queue.shift();
try {
await this.sendData(item.data);
} catch (error) {
// 重试逻辑
if (item.attempts < this.maxRetries) {
item.attempts++;
// 重新放入队列,指数退避
setTimeout(() => {
this.queue.unshift(item);
this.sending = false;
this.processQueue();
}, Math.pow(2, item.attempts) * 1000);
return;
} else {
console.error('Failed to send data after retries', error);
}
}
this.sending = false;
this.processQueue();
}
// 根据情况选择最佳上报方式
async sendData(data) {
// 页面卸载时优先使用 sendBeacon
if (document.visibilityState === 'hidden' || navigator.onLine === false) {
return this.sendWithBeacon(data);
}
// 正常情况使用fetch API
return this.sendWithFetch(data);
}
// 使用Beacon API发送
sendWithBeacon(data) {
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
const success = navigator.sendBeacon('/analytics', blob);
if (!success) {
// 如果sendBeacon失败,保存到localStorage
this.saveToStorage(data);
return Promise.reject(new Error('Beacon failed'));
}
return Promise.resolve();
}
// 使用Fetch API发送
sendWithFetch(data) {
return fetch('/analytics', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data),
keepalive: true
});
}
// 降级存储
saveToStorage(data) {
try {
const storedItems = JSON.parse(localStorage.getItem('monitoring_queue') || '[]');
storedItems.push({
data,
timestamp: Date.now()
});
// 只保留最近的100条
if (storedItems.length > 100) {
storedItems.splice(0, storedItems.length - 100);
}
localStorage.setItem('monitoring_queue', JSON.stringify(storedItems));
} catch (e) {
console.error('Failed to save to localStorage', e);
}
}
// 页面加载时检查是否有存储的数据需要发送
checkStoredData() {
try {
const storedItems = JSON.parse(localStorage.getItem('monitoring_queue') || '[]');
if (storedItems.length > 0) {
// 清空存储
localStorage.removeItem('monitoring_queue');
// 批量发送
this.sendBatch(storedItems.map(item => item.data));
}
} catch (e) {
console.error('Failed to process stored data', e);
}
}
// 批量发送
async sendBatch(dataArray) {
try {
await fetch('/analytics/batch', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(dataArray)
});
} catch (error) {
console.error('Failed to send batch', error);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# 采样与优先级
class SamplingStrategy {
constructor(config) {
this.config = {
// 不同类型数据的采样率
performance: 0.5, // 50%的性能数据
error: 1.0, // 100%的错误数据
behavior: 0.1, // 10%的行为数据
custom: 0.8, // 80%的自定义事件
// 优先采样特定条件的数据
highPriorityUsers: [], // 高优先级用户ID列表
newUserRate: 1.0, // 新用户的采样率
mobileRate: 0.3, // 移动设备的采样率
...config
};
// 用户会话信息
this.userInfo = {
userId: '',
isNewUser: false,
isMobile: /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
};
}
// 设置用户信息
setUserInfo(userId, isNewUser) {
this.userInfo.userId = userId;
this.userInfo.isNewUser = isNewUser;
}
// 根据数据类型和上下文决定是否采样
shouldSample(dataType, data) {
// 高优先级用户总是采样
if (this.userInfo.userId &&
this.config.highPriorityUsers.includes(this.userInfo.userId)) {
return true;
}
// 新用户使用特定采样率
if (this.userInfo.isNewUser) {
return Math.random() < this.config.newUserRate;
}
// 移动设备使用特定采样率
if (this.userInfo.isMobile) {
return Math.random() < this.config.mobileRate;
}
// 错误数据中特定类型总是采样
if (dataType === 'error') {
// 严重错误总是上报
if (data.level === 'fatal' || data.level === 'critical') {
return true;
}
}
// 其他情况按类型采样
const typeRate = this.config[dataType] || 0.1; // 默认10%
return Math.random() < typeRate;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 后端存储与分析
# 数据处理流水线
// 后端数据处理流水线 (Node.js伪代码)
const { MongoClient } = require('mongodb');
const { createClient } = require('redis');
const { Kafka } = require('kafkajs');
// 数据处理类
class MonitoringPipeline {
constructor() {
// 初始化存储客户端
this.initStorageClients();
// 初始化Kafka消费者
this.initKafkaConsumer();
}
async initStorageClients() {
// MongoDB连接
this.mongoClient = new MongoClient('mongodb://localhost:27017');
await this.mongoClient.connect();
this.db = this.mongoClient.db('monitoring');
// Redis连接
this.redisClient = createClient();
await this.redisClient.connect();
}
async initKafkaConsumer() {
// Kafka配置
const kafka = new Kafka({
clientId: 'monitoring-service',
brokers: ['kafka-broker:9092']
});
this.consumer = kafka.consumer({ groupId: 'monitoring-processors' });
await this.consumer.connect();
await this.consumer.subscribe({ topic: 'monitoring-events', fromBeginning: false });
// 消费消息
await this.consumer.run({
eachMessage: async ({ topic, partition, message }) => {
const data = JSON.parse(message.value.toString());
await this.processData(data);
},
});
}
async processData(data) {
// 数据清洗和转换
const processedData = this.cleanAndTransform(data);
// 按类型存储数据
switch (processedData.type) {
case 'performance':
await this.storePerformanceData(processedData);
break;
case 'error':
await this.storeErrorData(processedData);
break;
case 'behavior':
await this.storeBehaviorData(processedData);
break;
case 'custom':
await this.storeCustomEventData(processedData);
break;
default:
console.log(`Unknown data type: ${processedData.type}`);
}
}
cleanAndTransform(data) {
// 数据清洗
const cleaned = { ...data };
// 移除无效字段
Object.keys(cleaned).forEach(key => {
if (cleaned[key] === undefined || cleaned[key] === null) {
delete cleaned[key];
}
});
// 添加处理时间戳
cleaned.processedAt = Date.now();
return cleaned;
}
async storePerformanceData(data) {
// 存储到MongoDB
await this.db.collection('performance').insertOne(data);
// 更新Redis中的实时统计
const metricKey = `perf:${data.name}:${this.getDateKey()}`;
await this.redisClient.rPush(metricKey, data.value.toString());
// 限制列表长度
await this.redisClient.lTrim(metricKey, -1000, -1);
// 更新最新的性能分数
if (data.rating) {
const scoreKey = `score:${data.name}:${this.getDateKey()}`;
await this.redisClient.hIncrBy(scoreKey, data.rating, 1);
}
}
async storeErrorData(data) {
// 存储到MongoDB
await this.db.collection('errors').insertOne(data);
// 错误计数
const errorKey = `error:count:${this.getDateKey()}`;
await this.redisClient.hIncrBy(errorKey, data.subtype || 'unknown', 1);
// 如果是严重错误,添加到实时告警队列
if (data.level === 'critical' || data.level === 'fatal') {
await this.redisClient.lPush('alerts:errors', JSON.stringify(data));
}
}
async storeBehaviorData(data) {
// 存储到MongoDB
await this.db.collection('behavior').insertOne(data);
// 更新行为统计
const behaviorKey = `behavior:${data.action}:${this.getDateKey()}`;
await this.redisClient.hIncrBy(behaviorKey, data.page || 'unknown', 1);
}
async storeCustomEventData(data) {
// 存储到MongoDB
await this.db.collection('custom_events').insertOne(data);
// 更新自定义事件统计
const eventKey = `event:${data.category}:${data.action}:${this.getDateKey()}`;
await this.redisClient.incr(eventKey);
}
// 生成日期键 (yyyy-mm-dd)
getDateKey() {
const now = new Date();
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
}
}
// 启动处理流水线
const pipeline = new MonitoringPipeline();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# 告警系统
# 告警规则设置
// 告警规则引擎
class AlertRuleEngine {
constructor() {
this.rules = [];
}
// 添加规则
addRule(rule) {
this.rules.push(rule);
}
// 评估数据是否触发告警
evaluateData(data) {
const triggeredRules = [];
for (const rule of this.rules) {
if (this.matchesConditions(data, rule.conditions)) {
triggeredRules.push({
ruleId: rule.id,
ruleName: rule.name,
priority: rule.priority,
message: this.formatMessage(rule.messageTemplate, data),
data
});
}
}
return triggeredRules;
}
// 检查数据是否满足条件
matchesConditions(data, conditions) {
// 如果是AND条件组
if (conditions.operator === 'AND') {
return conditions.conditions.every(condition =>
this.matchesCondition(data, condition));
}
// 如果是OR条件组
else if (conditions.operator === 'OR') {
return conditions.conditions.some(condition =>
this.matchesCondition(data, condition));
}
// 单个条件
else {
return this.matchesCondition(data, conditions);
}
}
// 检查单个条件
matchesCondition(data, condition) {
// 嵌套条件组
if (condition.operator && condition.conditions) {
return this.matchesConditions(data, condition);
}
// 获取字段值
const fieldPath = condition.field.split('.');
let value = data;
for (const key of fieldPath) {
if (value === undefined || value === null) {
return false;
}
value = value[key];
}
// 根据条件类型评估
switch (condition.comparison) {
case 'equals':
return value === condition.value;
case 'notEquals':
return value !== condition.value;
case 'contains':
return String(value).includes(condition.value);
case 'greaterThan':
return Number(value) > Number(condition.value);
case 'lessThan':
return Number(value) < Number(condition.value);
case 'in':
return Array.isArray(condition.value) && condition.value.includes(value);
case 'regex':
return new RegExp(condition.value).test(String(value));
default:
return false;
}
}
// 格式化告警消息
formatMessage(template, data) {
return template.replace(/\${([^}]+)}/g, (match, path) => {
const keys = path.split('.');
let value = data;
for (const key of keys) {
if (value === undefined || value === null) {
return match;
}
value = value[key];
}
return value !== undefined ? value : match;
});
}
}
// 使用示例
const alertEngine = new AlertRuleEngine();
// 添加规则
alertEngine.addRule({
id: 'error-rate-high',
name: '错误率过高',
priority: 'high',
conditions: {
operator: 'AND',
conditions: [
{
field: 'type',
comparison: 'equals',
value: 'error-rate'
},
{
field: 'value',
comparison: 'greaterThan',
value: 5
},
{
field: 'pageUrl',
comparison: 'contains',
value: '/checkout'
}
]
},
messageTemplate: '${pageUrl} 页面错误率达到 ${value}%,超过阈值 5%'
});
// 处理告警数据
const alertData = {
type: 'error-rate',
value: 7.5,
pageUrl: '/checkout/payment',
timestamp: Date.now()
};
const alerts = alertEngine.evaluateData(alertData);
if (alerts.length > 0) {
// 触发告警通知
alerts.forEach(alert => notifyTeam(alert));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# 告警通知渠道
// 告警通知系统
class AlertNotifier {
constructor() {
this.channels = {
email: new EmailNotifier(),
slack: new SlackNotifier(),
sms: new SMSNotifier(),
webhook: new WebhookNotifier()
};
// 告警通知配置
this.config = {
default: ['email', 'slack'],
critical: ['email', 'slack', 'sms'],
throttle: {
// 默认每小时最多发送10条同类告警
default: { maxCount: 10, timeWindow: 60 * 60 * 1000 },
// 严重告警每10分钟最多发送5条
critical: { maxCount: 5, timeWindow: 10 * 60 * 1000 }
}
};
// 告警历史记录
this.alertHistory = {};
}
// 发送告警通知
async notify(alert) {
// 确定告警级别
const level = alert.priority === 'high' || alert.priority === 'critical'
? 'critical' : 'default';
// 检查告警频率限制
if (this.isThrottled(alert, level)) {
return;
}
// 获取通知渠道
const channels = this.config[level] || this.config.default;
// 记录告警历史
this.recordAlert(alert);
// 并行发送到所有渠道
await Promise.allSettled(
channels.map(channelName => {
const channel = this.channels[channelName];
if (channel) {
return channel.send(alert).catch(err => {
console.error(`Failed to send alert via ${channelName}:`, err);
});
}
return Promise.resolve();
})
);
}
// 检查是否需要限制告警频率
isThrottled(alert, level) {
const alertKey = `${alert.ruleId}:${JSON.stringify(alert.data).slice(0, 100)}`;
const now = Date.now();
if (!this.alertHistory[alertKey]) {
this.alertHistory[alertKey] = [];
return false;
}
const throttleConfig = this.config.throttle[level] || this.config.throttle.default;
const { maxCount, timeWindow } = throttleConfig;
// 清理过期记录
this.alertHistory[alertKey] = this.alertHistory[alertKey].filter(
timestamp => now - timestamp < timeWindow
);
// 检查是否超过频率限制
if (this.alertHistory[alertKey].length >= maxCount) {
return true;
}
return false;
}
// 记录告警历史
recordAlert(alert) {
const alertKey = `${alert.ruleId}:${JSON.stringify(alert.data).slice(0, 100)}`;
if (!this.alertHistory[alertKey]) {
this.alertHistory[alertKey] = [];
}
this.alertHistory[alertKey].push(Date.now());
}
}
// 邮件通知实现
class EmailNotifier {
async send(alert) {
// 实际邮件发送逻辑
console.log(`[EMAIL] 发送告警: ${alert.message}`);
return {
channel: 'email',
success: true,
timestamp: Date.now()
};
}
}
// Slack通知实现
class SlackNotifier {
async send(alert) {
// 构建Slack消息
const message = {
channel: '#monitoring-alerts',
username: 'MonitoringBot',
icon_emoji: ':warning:',
attachments: [
{
color: alert.priority === 'high' ? 'danger' : 'warning',
title: alert.ruleName,
text: alert.message,
fields: [
{
title: '优先级',
value: alert.priority,
short: true
},
{
title: '时间',
value: new Date().toISOString(),
short: true
}
],
footer: 'Monitoring System'
}
]
};
// 实际Slack API调用
console.log(`[SLACK] 发送告警: ${alert.message}`);
return {
channel: 'slack',
success: true,
timestamp: Date.now()
};
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# 监控可视化
# 数据可视化仪表板
// 前端监控可视化仪表板组件
class MonitoringDashboard {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.charts = {};
// 时间范围选择
this.timeRange = '24h';
// 初始化仪表板
this.init();
}
async init() {
// 创建仪表板布局
this.createLayout();
// 初始化图表
await this.initCharts();
// 设置数据刷新计时器
this.startDataRefresh();
// 注册事件监听
this.registerEventListeners();
}
createLayout() {
this.container.innerHTML = `
<div class="dashboard-header">
<h1>前端监控仪表板</h1>
<div class="time-range-selector">
<button data-range="1h">1小时</button>
<button data-range="24h" class="active">24小时</button>
<button data-range="7d">7天</button>
<button data-range="30d">30天</button>
<button data-range="custom">自定义</button>
</div>
</div>
<div class="dashboard-grid">
<div class="card" id="performance-overview">
<h2>性能概览</h2>
<div class="chart-container" id="web-vitals-chart"></div>
</div>
<div class="card" id="error-overview">
<h2>错误概览</h2>
<div class="chart-container" id="error-chart"></div>
</div>
<div class="card" id="user-behavior">
<h2>用户行为</h2>
<div class="chart-container" id="behavior-chart"></div>
</div>
<div class="card" id="resource-loading">
<h2>资源加载</h2>
<div class="chart-container" id="resource-chart"></div>
</div>
<div class="card full-width" id="real-time-events">
<h2>实时事件流</h2>
<div class="events-container" id="events-stream"></div>
</div>
</div>
`;
}
async initCharts() {
// 初始化核心指标图表
this.charts.webVitals = new PerformanceChart('web-vitals-chart');
await this.charts.webVitals.init();
// 初始化错误统计图表
this.charts.errors = new ErrorChart('error-chart');
await this.charts.errors.init();
// 初始化用户行为图表
this.charts.behavior = new BehaviorChart('behavior-chart');
await this.charts.behavior.init();
// 初始化资源加载图表
this.charts.resources = new ResourceChart('resource-chart');
await this.charts.resources.init();
// 初始化实时事件流
this.eventStream = new EventStream('events-stream');
this.eventStream.init();
}
startDataRefresh() {
// 每分钟刷新一次数据
this.refreshTimer = setInterval(() => {
this.refreshData();
}, 60000);
// 立即加载数据
this.refreshData();
}
async refreshData() {
// 获取时间范围
const timeRange = this.getTimeRangeParams();
// 并行加载各类数据
await Promise.all([
this.charts.webVitals.loadData(timeRange),
this.charts.errors.loadData(timeRange),
this.charts.behavior.loadData(timeRange),
this.charts.resources.loadData(timeRange)
]);
}
getTimeRangeParams() {
const now = Date.now();
let startTime;
switch(this.timeRange) {
case '1h':
startTime = now - 60 * 60 * 1000;
break;
case '24h':
startTime = now - 24 * 60 * 60 * 1000;
break;
case '7d':
startTime = now - 7 * 24 * 60 * 60 * 1000;
break;
case '30d':
startTime = now - 30 * 24 * 60 * 60 * 1000;
break;
case 'custom':
// 从自定义日期选择器获取
startTime = this.customDateRange.start;
now = this.customDateRange.end;
break;
default:
startTime = now - 24 * 60 * 60 * 1000;
}
return {
start: startTime,
end: now
};
}
registerEventListeners() {
// 注册时间范围选择事件
const buttons = this.container.querySelectorAll('.time-range-selector button');
buttons.forEach(button => {
button.addEventListener('click', () => {
// 移除之前的激活状态
buttons.forEach(b => b.classList.remove('active'));
// 设置新的激活状态
button.classList.add('active');
// 更新时间范围
this.timeRange = button.dataset.range;
if (this.timeRange === 'custom') {
this.showCustomDatePicker();
} else {
this.refreshData();
}
});
});
}
showCustomDatePicker() {
// 显示自定义日期选择器
// ...
}
}
// 性能指标图表
class PerformanceChart {
constructor(containerId) {
this.container = document.getElementById(containerId);
}
async init() {
// 初始化图表库 (如 Chart.js)
this.chart = new Chart(
this.container.getContext('2d'),
{
type: 'line',
data: {
labels: [],
datasets: [
{
label: 'LCP',
borderColor: '#FF6384',
data: []
},
{
label: 'FID',
borderColor: '#36A2EB',
data: []
},
{
label: 'CLS',
borderColor: '#FFCE56',
data: []
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: 'time',
time: {
unit: 'hour'
}
},
y: {
beginAtZero: true
}
}
}
}
);
}
async loadData(timeRange) {
try {
// 从API加载数据
const response = await fetch(`/api/performance?start=${timeRange.start}&end=${timeRange.end}`);
const data = await response.json();
// 更新图表数据
this.updateChart(data);
} catch (error) {
console.error('Failed to load performance data:', error);
}
}
updateChart(data) {
// 转换数据格式
const timestamps = data.timestamps;
// 更新LCP数据
this.chart.data.labels = timestamps.map(ts => new Date(ts));
this.chart.data.datasets[0].data = data.metrics.lcp;
this.chart.data.datasets[1].data = data.metrics.fid;
this.chart.data.datasets[2].data = data.metrics.cls;
// 更新图表
this.chart.update();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# 实时错误监控
// 实时错误监控组件
class ErrorDetailsView {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.currentError = null;
// 初始化视图
this.init();
}
init() {
this.container.innerHTML = `
<div class="error-details-header">
<h2>错误详情</h2>
<div class="actions">
<button id="prev-error">上一个</button>
<button id="next-error">下一个</button>
<button id="close-details">关闭</button>
</div>
</div>
<div class="error-info">
<div class="error-message"></div>
<div class="error-metadata">
<div class="metadata-item">
<span class="label">时间:</span>
<span class="value" id="error-time"></span>
</div>
<div class="metadata-item">
<span class="label">浏览器:</span>
<span class="value" id="error-browser"></span>
</div>
<div class="metadata-item">
<span class="label">操作系统:</span>
<span class="value" id="error-os"></span>
</div>
<div class="metadata-item">
<span class="label">URL:</span>
<span class="value" id="error-url"></span>
</div>
<div class="metadata-item">
<span class="label">用户ID:</span>
<span class="value" id="error-user"></span>
</div>
</div>
<div class="error-stack">
<h3>堆栈信息</h3>
<pre id="error-stack-trace"></pre>
</div>
<div class="error-context">
<h3>上下文信息</h3>
<pre id="error-context-data"></pre>
</div>
</div>
`;
// 注册事件处理
this.registerEventHandlers();
}
registerEventHandlers() {
document.getElementById('close-details').addEventListener('click', () => {
this.hide();
});
document.getElementById('prev-error').addEventListener('click', () => {
// 显示上一个错误
if (this.onNavigatePrev) {
this.onNavigatePrev();
}
});
document.getElementById('next-error').addEventListener('click', () => {
// 显示下一个错误
if (this.onNavigateNext) {
this.onNavigateNext();
}
});
}
// 显示错误详情
showError(error) {
this.currentError = error;
// 填充错误信息
document.querySelector('.error-message').textContent = error.message;
document.getElementById('error-time').textContent = new Date(error.timestamp).toLocaleString();
document.getElementById('error-browser').textContent = `${error.browser.name} ${error.browser.version}`;
document.getElementById('error-os').textContent = `${error.os.name} ${error.os.version}`;
document.getElementById('error-url').textContent = error.url;
document.getElementById('error-user').textContent = error.userId || '匿名用户';
document.getElementById('error-stack-trace').textContent = error.stack || '无堆栈信息';
document.getElementById('error-context-data').textContent = JSON.stringify(error.context, null, 2);
// 显示详情面板
this.container.style.display = 'block';
}
// 隐藏详情面板
hide() {
this.container.style.display = 'none';
}
// 设置导航回调
setNavigationHandlers(onPrev, onNext) {
this.onNavigatePrev = onPrev;
this.onNavigateNext = onNext;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# 实践案例
# 电商网站监控案例
在我们为一家大型电商网站实施的前端监控系统中,我们面临几个关键挑战:
- 大规模数据处理:每天处理超过5000万次前端事件
- 高峰期性能:促销活动期间流量暴增10倍
- 跨平台一致性:需要覆盖Web、移动网页和App
- 复杂业务流程:完整购物流程包含超过20个步骤
# 监控架构设计
我们采用了分层架构来解决这些挑战:
[前端] [边缘节点] [数据处理] [存储]
+-------------+ +------------+ +----------------+ +------------+
| Web SDK | | | | | | |
| Mobile SDK | ---> | Edge | ---> | Stream | ---> | 实时数据库 |
| App SDK | | Collectors | | Processors | | |
+-------------+ +------------+ +----------------+ +------------+
|
v
+----------------+ +------------+
| Batch | ---> | 数据仓库 |
| Processors | | |
+----------------+ +------------+
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 关键业务监控设计
我们特别关注购物车到支付完成的关键业务路径,实施了以下监控策略:
漏斗分析:定义8个关键步骤形成转化漏斗
- 商品详情页浏览
- 添加购物车
- 查看购物车
- 开始结账
- 填写配送信息
- 选择支付方式
- 提交订单
- 完成支付
性能基线:为每个步骤设置性能基线和监控阈值
- 商品详情页:LCP < 1.5s
- 购物车加载:LCP < 1.0s
- 结账流程:每步交互响应 < 200ms
错误影响分析:根据业务流程位置评估错误严重性
- 支付流程错误:立即触发高优先级告警
- 非关键流程错误:聚合后定期报告
# 实施成效
该监控系统实施后取得了显著成效:
性能提升:
- 结账页面LCP减少40%
- 首次可交互时间减少35%
- 整体页面加载时间减少28%
错误率降低:
- 关键业务流程错误率降低70%
- 支付流程相关JS错误减少85%
转化率提升:
- 购物车到订单转化率提升15%
- 移动端完成支付比例提升22%
运营效益:
- 问题发现到解决时间从均值4小时缩短至30分钟
- 每月节省约200工时的人工故障排查时间
# SaaS平台监控案例
在为一家SaaS平台开发监控系统时,我们面临的主要挑战是:
- 多租户环境:数百个客户有不同的配置和使用模式
- 差异化SLA:不同级别客户有不同的服务等级协议
- 复杂前端应用:单页应用包含超过100个视图组件
# 定制化监控解决方案
针对这些挑战,我们设计了以下解决方案:
租户隔离的监控:
- 为每个租户创建独立的监控空间
- 支持租户级别的自定义配置和阈值
- 实现租户级别的数据访问控制
SLA驱动的告警:
- 基于客户等级自动调整监控频率和灵敏度
- 企业级客户问题自动升级为高优先级
- 告警通知根据客户等级选择不同渠道
组件级性能跟踪:
- 实现细粒度组件渲染性能监控
- 跟踪状态管理性能和组件重渲染
- 记录组件初始化和交互响应时间
# 技术亮点
该项目的技术亮点包括:
自适应采样:
- 动态调整数据采集频率
- 在问题发生时自动提高采样率
- 流量高峰期智能降低非关键数据采样
上下文感知错误分组:
- 使用机器学习算法对相似错误进行分组
- 自动关联错误与代码变更和部署
- 识别周期性出现的问题模式
预测性监控:
- 检测性能退化趋势并提前预警
- 预测可能的内存泄漏和资源耗尽
- 根据历史数据预测高流量时段并自动调整监控参数
# 最佳实践
# 监控系统设计原则
根据多年实践经验,以下是设计前端监控系统的关键原则:
以用户体验为中心:
- 始终将真实用户体验作为首要监控目标
- 不仅监控技术指标,也关注业务指标和用户行为
- 区分关键业务流程和非关键流程的监控优先级
低开销高价值:
- 最小化监控代码对应用性能的影响
- 实施智能采样策略,减少数据传输量
- 优先收集有直接分析价值的数据
可操作性:
- 每项监控指标都应对应明确的行动计划
- 告警消息应包含问题诊断和可能解决方案的信息
- 建立清晰的错误分类和处理流程
隐私与合规:
- 严格遵循数据保护法规(GDPR, CCPA等)
- 实施数据最小化和匿名化策略
- 提供透明的监控策略和用户选择机制
# 监控实施路线图
下面是一个阶段性的监控系统实施路线图,适合大多数Web应用:
# 第一阶段:基础监控
- 错误捕获:实现基本的JavaScript错误和Promise异常捕获
- 性能基线:收集核心Web指标(CWV)数据
- 健康检查:监控应用关键功能的可用性
# 第二阶段:增强监控
- 用户行为跟踪:实现关键业务流程的用户行为分析
- 资源监控:添加API请求和资源加载性能监控
- 自定义事件:实现业务相关的自定义事件跟踪
# 第三阶段:高级监控
- 会话回放:选择性记录用户会话以重现问题
- 预测性分析:实施趋势分析和异常检测
- A/B测试集成:将监控数据与实验框架集成
# 性能优化与监控系统自身性能
监控系统本身也需要优化以确保不会对应用性能产生负面影响:
轻量级SDK设计:
- 核心SDK应小于5KB(gzip后)
- 使用模块化设计,按需加载功能
- 避免使用重量级第三方依赖
批处理与缓冲:
- 批量发送事件而非单独发送
- 使用内存缓冲区暂存事件数据
- 实现指数退避的重试策略
资源消耗限制:
- 限制监控代码的CPU使用率(<1%)
- 控制内存使用,防止内存泄漏
- 使用浏览器空闲时间处理非关键任务
# 跨团队协作最佳实践
有效的前端监控需要开发、运维、产品和管理层的紧密协作:
共同确定KPI:
- 与业务团队共同确定关键监控指标
- 建立技术指标与业务指标的关联关系
- 定期回顾和调整监控策略
监控驱动开发:
- 在开发新功能前先设计监控策略
- 将可观测性作为验收标准的一部分
- 鼓励开发人员主动添加业务相关的自定义事件
数据驱动决策:
- 使用监控数据指导产品和技术决策
- 建立定期的监控数据回顾会议
- 将监控结果纳入开发团队的KPI
# 故障应对流程
建立系统化的故障应对流程,确保问题能够快速有效地解决:
分级响应策略:
- P0 (关键业务中断):立即响应,全团队参与
- P1 (重要功能受损):1小时内响应,专人负责
- P2 (非关键问题):工作日内响应,计划修复
- P3 (轻微问题):下一迭代周期处理
标准化事件处理:
- 问题确认与分类
- 初步评估与升级决策
- 根因分析与修复
- 验证与复盘
- 知识库更新
持续改进循环:
- 每次重大故障后进行复盘会议
- 更新监控规则和告警阈值
- 完善自动化修复和预防措施
通过遵循这些最佳实践,团队可以构建出一个强大、可靠且具有成本效益的前端监控系统,不仅能够及时发现并解决问题,还能持续优化应用性能和用户体验。