前端监控与性能分析
在现代Web应用开发中,监控和性能分析是确保用户体验的关键环节。本文将深入探讨如何构建完整的前端监控系统,从性能指标收集到数据分析和优化策略。
🎯 监控体系概览
监控维度
1. 性能监控
- 页面加载性能
- 运行时性能
- 资源加载监控
- 网络性能分析
2. 错误监控
- JavaScript错误
- 资源加载错误
- 接口请求错误
- 用户操作错误
3. 用户体验监控
- 用户行为追踪
- 页面交互分析
- 转化漏斗监控
- 用户满意度评估
4. 业务监控
- 核心功能可用性
- 业务流程完成率
- 关键指标变化
- A/B测试效果
📊 核心性能指标
Web Vitals 指标
javascript
// Core Web Vitals 监控
class WebVitalsMonitor {
constructor() {
this.metrics = {}
this.init()
}
init() {
// 监控 LCP (Largest Contentful Paint)
this.observeLCP()
// 监控 FID (First Input Delay)
this.observeFID()
// 监控 CLS (Cumulative Layout Shift)
this.observeCLS()
// 监控 FCP (First Contentful Paint)
this.observeFCP()
// 监控 TTFB (Time to First Byte)
this.observeTTFB()
}
// 监控 LCP
observeLCP() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
this.metrics.lcp = {
value: lastEntry.startTime,
element: lastEntry.element,
timestamp: Date.now()
}
this.reportMetric('lcp', this.metrics.lcp)
})
observer.observe({ entryTypes: ['largest-contentful-paint'] })
}
// 监控 FID
observeFID() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
entries.forEach(entry => {
this.metrics.fid = {
value: entry.processingStart - entry.startTime,
timestamp: Date.now()
}
this.reportMetric('fid', this.metrics.fid)
})
})
observer.observe({ entryTypes: ['first-input'] })
}
// 监控 CLS
observeCLS() {
let clsValue = 0
let sessionValue = 0
let sessionEntries = []
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
entries.forEach(entry => {
if (!entry.hadRecentInput) {
const firstSessionEntry = sessionEntries[0]
const lastSessionEntry = sessionEntries[sessionEntries.length - 1]
if (sessionValue &&
entry.startTime - lastSessionEntry.startTime < 1000 &&
entry.startTime - firstSessionEntry.startTime < 5000) {
sessionValue += entry.value
sessionEntries.push(entry)
} else {
sessionValue = entry.value
sessionEntries = [entry]
}
if (sessionValue > clsValue) {
clsValue = sessionValue
this.metrics.cls = {
value: clsValue,
entries: [...sessionEntries],
timestamp: Date.now()
}
this.reportMetric('cls', this.metrics.cls)
}
}
})
})
observer.observe({ entryTypes: ['layout-shift'] })
}
// 监控 FCP
observeFCP() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
entries.forEach(entry => {
if (entry.name === 'first-contentful-paint') {
this.metrics.fcp = {
value: entry.startTime,
timestamp: Date.now()
}
this.reportMetric('fcp', this.metrics.fcp)
}
})
})
observer.observe({ entryTypes: ['paint'] })
}
// 监控 TTFB
observeTTFB() {
const navigationEntry = performance.getEntriesByType('navigation')[0]
if (navigationEntry) {
this.metrics.ttfb = {
value: navigationEntry.responseStart - navigationEntry.requestStart,
timestamp: Date.now()
}
this.reportMetric('ttfb', this.metrics.ttfb)
}
}
// 上报指标
reportMetric(name, data) {
// 发送到监控服务
fetch('/api/metrics', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
metric: name,
data: data,
url: location.href,
userAgent: navigator.userAgent,
timestamp: Date.now()
})
}).catch(error => {
console.error('Failed to report metric:', error)
})
}
// 获取所有指标
getAllMetrics() {
return this.metrics
}
}
// 初始化监控
const vitalsMonitor = new WebVitalsMonitor()
自定义性能指标
javascript
// 自定义性能监控
class CustomPerformanceMonitor {
constructor() {
this.marks = new Map()
this.measures = new Map()
this.init()
}
init() {
// 监控资源加载
this.observeResources()
// 监控长任务
this.observeLongTasks()
// 监控内存使用
this.observeMemory()
// 监控网络状态
this.observeNetwork()
}
// 标记时间点
mark(name) {
const timestamp = performance.now()
this.marks.set(name, timestamp)
performance.mark(name)
return timestamp
}
// 测量时间间隔
measure(name, startMark, endMark) {
const startTime = this.marks.get(startMark)
const endTime = this.marks.get(endMark) || performance.now()
const duration = endTime - startTime
this.measures.set(name, {
duration,
startTime,
endTime,
timestamp: Date.now()
})
performance.measure(name, startMark, endMark)
// 上报自定义指标
this.reportCustomMetric(name, {
duration,
startTime,
endTime
})
return duration
}
// 监控资源加载
observeResources() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
entries.forEach(entry => {
const resourceData = {
name: entry.name,
type: entry.initiatorType,
size: entry.transferSize,
duration: entry.duration,
startTime: entry.startTime,
dns: entry.domainLookupEnd - entry.domainLookupStart,
tcp: entry.connectEnd - entry.connectStart,
ssl: entry.secureConnectionStart > 0 ?
entry.connectEnd - entry.secureConnectionStart : 0,
ttfb: entry.responseStart - entry.requestStart,
download: entry.responseEnd - entry.responseStart
}
this.reportResourceMetric(resourceData)
})
})
observer.observe({ entryTypes: ['resource'] })
}
// 监控长任务
observeLongTasks() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
entries.forEach(entry => {
const longTaskData = {
duration: entry.duration,
startTime: entry.startTime,
attribution: entry.attribution?.map(attr => ({
name: attr.name,
type: attr.containerType,
src: attr.containerSrc,
id: attr.containerId
}))
}
this.reportLongTask(longTaskData)
})
})
observer.observe({ entryTypes: ['longtask'] })
}
// 监控内存使用
observeMemory() {
if ('memory' in performance) {
const memoryInfo = {
used: performance.memory.usedJSHeapSize,
total: performance.memory.totalJSHeapSize,
limit: performance.memory.jsHeapSizeLimit,
timestamp: Date.now()
}
this.reportMemoryUsage(memoryInfo)
// 定期检查内存使用
setInterval(() => {
const currentMemory = {
used: performance.memory.usedJSHeapSize,
total: performance.memory.totalJSHeapSize,
limit: performance.memory.jsHeapSizeLimit,
timestamp: Date.now()
}
this.reportMemoryUsage(currentMemory)
}, 30000) // 每30秒检查一次
}
}
// 监控网络状态
observeNetwork() {
if ('connection' in navigator) {
const connection = navigator.connection
const networkInfo = {
effectiveType: connection.effectiveType,
downlink: connection.downlink,
rtt: connection.rtt,
saveData: connection.saveData,
timestamp: Date.now()
}
this.reportNetworkInfo(networkInfo)
// 监听网络变化
connection.addEventListener('change', () => {
const updatedInfo = {
effectiveType: connection.effectiveType,
downlink: connection.downlink,
rtt: connection.rtt,
saveData: connection.saveData,
timestamp: Date.now()
}
this.reportNetworkInfo(updatedInfo)
})
}
}
// 上报自定义指标
reportCustomMetric(name, data) {
this.sendToMonitoring('custom-metric', { name, ...data })
}
// 上报资源指标
reportResourceMetric(data) {
this.sendToMonitoring('resource-metric', data)
}
// 上报长任务
reportLongTask(data) {
this.sendToMonitoring('long-task', data)
}
// 上报内存使用
reportMemoryUsage(data) {
this.sendToMonitoring('memory-usage', data)
}
// 上报网络信息
reportNetworkInfo(data) {
this.sendToMonitoring('network-info', data)
}
// 发送监控数据
sendToMonitoring(type, data) {
// 使用 sendBeacon 确保数据发送
if (navigator.sendBeacon) {
const payload = JSON.stringify({
type,
data,
url: location.href,
timestamp: Date.now()
})
navigator.sendBeacon('/api/monitoring', payload)
} else {
// 降级到 fetch
fetch('/api/monitoring', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
type,
data,
url: location.href,
timestamp: Date.now()
})
}).catch(error => {
console.error('Failed to send monitoring data:', error)
})
}
}
}
// 使用示例
const perfMonitor = new CustomPerformanceMonitor()
// 标记关键时间点
perfMonitor.mark('api-request-start')
fetch('/api/data')
.then(response => {
perfMonitor.mark('api-request-end')
perfMonitor.measure('api-request-duration', 'api-request-start', 'api-request-end')
return response.json()
})
.then(data => {
perfMonitor.mark('data-processing-end')
perfMonitor.measure('total-request-time', 'api-request-start', 'data-processing-end')
})
🚨 错误监控系统
JavaScript 错误捕获
javascript
// 错误监控系统
class ErrorMonitor {
constructor(options = {}) {
this.options = {
maxErrors: 50,
sampleRate: 1.0,
enableConsoleCapture: true,
enableUnhandledRejection: true,
enableResourceError: true,
...options
}
this.errorQueue = []
this.init()
}
init() {
// 捕获 JavaScript 错误
this.captureJSErrors()
// 捕获未处理的 Promise 拒绝
if (this.options.enableUnhandledRejection) {
this.captureUnhandledRejections()
}
// 捕获资源加载错误
if (this.options.enableResourceError) {
this.captureResourceErrors()
}
// 捕获控制台错误
if (this.options.enableConsoleCapture) {
this.captureConsoleErrors()
}
// 监听页面卸载,发送剩余错误
this.setupBeforeUnload()
}
// 捕获 JavaScript 错误
captureJSErrors() {
window.addEventListener('error', (event) => {
const errorInfo = {
type: 'javascript',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
timestamp: Date.now(),
url: location.href,
userAgent: navigator.userAgent
}
this.addError(errorInfo)
})
}
// 捕获未处理的 Promise 拒绝
captureUnhandledRejections() {
window.addEventListener('unhandledrejection', (event) => {
const errorInfo = {
type: 'unhandled-promise',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
timestamp: Date.now(),
url: location.href,
userAgent: navigator.userAgent
}
this.addError(errorInfo)
})
}
// 捕获资源加载错误
captureResourceErrors() {
window.addEventListener('error', (event) => {
if (event.target !== window) {
const errorInfo = {
type: 'resource',
message: `Failed to load resource: ${event.target.src || event.target.href}`,
element: event.target.tagName,
source: event.target.src || event.target.href,
timestamp: Date.now(),
url: location.href,
userAgent: navigator.userAgent
}
this.addError(errorInfo)
}
}, true)
}
// 捕获控制台错误
captureConsoleErrors() {
const originalError = console.error
const originalWarn = console.warn
console.error = (...args) => {
const errorInfo = {
type: 'console-error',
message: args.join(' '),
timestamp: Date.now(),
url: location.href,
userAgent: navigator.userAgent
}
this.addError(errorInfo)
originalError.apply(console, args)
}
console.warn = (...args) => {
const errorInfo = {
type: 'console-warn',
message: args.join(' '),
timestamp: Date.now(),
url: location.href,
userAgent: navigator.userAgent
}
this.addError(errorInfo)
originalWarn.apply(console, args)
}
}
// 添加错误到队列
addError(errorInfo) {
// 采样控制
if (Math.random() > this.options.sampleRate) {
return
}
// 添加用户信息和环境信息
errorInfo.userId = this.getUserId()
errorInfo.sessionId = this.getSessionId()
errorInfo.buildVersion = this.getBuildVersion()
errorInfo.environment = this.getEnvironment()
// 添加到队列
this.errorQueue.push(errorInfo)
// 限制队列大小
if (this.errorQueue.length > this.options.maxErrors) {
this.errorQueue.shift()
}
// 立即发送严重错误
if (this.isCriticalError(errorInfo)) {
this.sendErrors([errorInfo])
} else {
// 批量发送
this.debouncedSend()
}
}
// 判断是否为严重错误
isCriticalError(errorInfo) {
const criticalPatterns = [
/network error/i,
/script error/i,
/uncaught/i,
/fatal/i
]
return criticalPatterns.some(pattern =>
pattern.test(errorInfo.message)
)
}
// 防抖发送
debouncedSend = this.debounce(() => {
if (this.errorQueue.length > 0) {
this.sendErrors([...this.errorQueue])
this.errorQueue = []
}
}, 2000)
// 发送错误数据
sendErrors(errors) {
const payload = {
errors,
metadata: {
timestamp: Date.now(),
url: location.href,
referrer: document.referrer,
viewport: {
width: window.innerWidth,
height: window.innerHeight
},
screen: {
width: screen.width,
height: screen.height
}
}
}
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/errors', JSON.stringify(payload))
} else {
fetch('/api/errors', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
}).catch(error => {
console.error('Failed to send error data:', error)
})
}
}
// 页面卸载前发送剩余错误
setupBeforeUnload() {
window.addEventListener('beforeunload', () => {
if (this.errorQueue.length > 0) {
this.sendErrors([...this.errorQueue])
}
})
}
// 工具方法
getUserId() {
return localStorage.getItem('userId') || 'anonymous'
}
getSessionId() {
return sessionStorage.getItem('sessionId') || 'unknown'
}
getBuildVersion() {
return process.env.BUILD_VERSION || 'unknown'
}
getEnvironment() {
return process.env.NODE_ENV || 'production'
}
debounce(func, wait) {
let timeout
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout)
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}
// 手动报告错误
reportError(error, context = {}) {
const errorInfo = {
type: 'manual',
message: error.message || String(error),
stack: error.stack,
context,
timestamp: Date.now(),
url: location.href,
userAgent: navigator.userAgent
}
this.addError(errorInfo)
}
}
// 初始化错误监控
const errorMonitor = new ErrorMonitor({
sampleRate: 0.1, // 10% 采样率
maxErrors: 100
})
// 使用示例
try {
// 可能出错的代码
riskyOperation()
} catch (error) {
errorMonitor.reportError(error, {
operation: 'riskyOperation',
userId: getCurrentUserId(),
additionalData: { /* ... */ }
})
}
📈 用户行为分析
用户交互追踪
javascript
// 用户行为追踪系统
class UserBehaviorTracker {
constructor(options = {}) {
this.options = {
trackClicks: true,
trackScrolls: true,
trackPageViews: true,
trackFormInteractions: true,
trackHovers: false,
batchSize: 20,
flushInterval: 5000,
...options
}
this.eventQueue = []
this.sessionData = this.initSession()
this.init()
}
init() {
if (this.options.trackClicks) {
this.trackClicks()
}
if (this.options.trackScrolls) {
this.trackScrolls()
}
if (this.options.trackPageViews) {
this.trackPageViews()
}
if (this.options.trackFormInteractions) {
this.trackFormInteractions()
}
if (this.options.trackHovers) {
this.trackHovers()
}
// 定期发送数据
this.startBatchSending()
// 页面卸载时发送剩余数据
this.setupBeforeUnload()
}
// 初始化会话数据
initSession() {
const sessionId = this.generateSessionId()
const startTime = Date.now()
return {
sessionId,
startTime,
pageViews: 0,
interactions: 0,
scrollDepth: 0,
timeOnPage: 0
}
}
// 追踪点击事件
trackClicks() {
document.addEventListener('click', (event) => {
const element = event.target
const elementInfo = this.getElementInfo(element)
const clickEvent = {
type: 'click',
timestamp: Date.now(),
element: elementInfo,
coordinates: {
x: event.clientX,
y: event.clientY
},
viewport: {
width: window.innerWidth,
height: window.innerHeight
}
}
this.addEvent(clickEvent)
this.sessionData.interactions++
})
}
// 追踪滚动事件
trackScrolls() {
let scrollTimeout
let maxScrollDepth = 0
window.addEventListener('scroll', () => {
clearTimeout(scrollTimeout)
scrollTimeout = setTimeout(() => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop
const windowHeight = window.innerHeight
const documentHeight = document.documentElement.scrollHeight
const scrollPercentage = Math.round(
((scrollTop + windowHeight) / documentHeight) * 100
)
if (scrollPercentage > maxScrollDepth) {
maxScrollDepth = scrollPercentage
this.sessionData.scrollDepth = maxScrollDepth
const scrollEvent = {
type: 'scroll',
timestamp: Date.now(),
scrollTop,
scrollPercentage,
maxDepth: maxScrollDepth
}
this.addEvent(scrollEvent)
}
}, 100)
})
}
// 追踪页面浏览
trackPageViews() {
const pageViewEvent = {
type: 'pageview',
timestamp: Date.now(),
url: location.href,
title: document.title,
referrer: document.referrer,
userAgent: navigator.userAgent,
language: navigator.language,
screen: {
width: screen.width,
height: screen.height
},
viewport: {
width: window.innerWidth,
height: window.innerHeight
}
}
this.addEvent(pageViewEvent)
this.sessionData.pageViews++
// 追踪页面停留时间
this.trackTimeOnPage()
}
// 追踪表单交互
trackFormInteractions() {
// 表单聚焦
document.addEventListener('focusin', (event) => {
if (this.isFormElement(event.target)) {
const focusEvent = {
type: 'form-focus',
timestamp: Date.now(),
element: this.getElementInfo(event.target),
formId: event.target.form?.id
}
this.addEvent(focusEvent)
}
})
// 表单提交
document.addEventListener('submit', (event) => {
const form = event.target
const formData = new FormData(form)
const fields = {}
for (let [key, value] of formData.entries()) {
fields[key] = typeof value === 'string' ? value.length : 'file'
}
const submitEvent = {
type: 'form-submit',
timestamp: Date.now(),
formId: form.id,
action: form.action,
method: form.method,
fieldCount: Object.keys(fields).length,
fields: fields
}
this.addEvent(submitEvent)
})
}
// 追踪悬停事件
trackHovers() {
let hoverTimeout
document.addEventListener('mouseover', (event) => {
const element = event.target
if (this.isTrackableElement(element)) {
hoverTimeout = setTimeout(() => {
const hoverEvent = {
type: 'hover',
timestamp: Date.now(),
element: this.getElementInfo(element),
duration: 1000 // 悬停超过1秒才记录
}
this.addEvent(hoverEvent)
}, 1000)
}
})
document.addEventListener('mouseout', () => {
clearTimeout(hoverTimeout)
})
}
// 追踪页面停留时间
trackTimeOnPage() {
const startTime = Date.now()
const updateTimeOnPage = () => {
this.sessionData.timeOnPage = Date.now() - startTime
}
// 定期更新停留时间
const timeInterval = setInterval(updateTimeOnPage, 1000)
// 页面隐藏时停止计时
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
clearInterval(timeInterval)
updateTimeOnPage()
}
})
// 页面卸载时记录最终时间
window.addEventListener('beforeunload', updateTimeOnPage)
}
// 获取元素信息
getElementInfo(element) {
return {
tagName: element.tagName.toLowerCase(),
id: element.id,
className: element.className,
text: element.textContent?.slice(0, 100),
href: element.href,
src: element.src,
type: element.type,
name: element.name,
xpath: this.getXPath(element)
}
}
// 获取元素的 XPath
getXPath(element) {
if (element.id) {
return `//*[@id="${element.id}"]`
}
if (element === document.body) {
return '/html/body'
}
let ix = 0
const siblings = element.parentNode?.childNodes || []
for (let i = 0; i < siblings.length; i++) {
const sibling = siblings[i]
if (sibling === element) {
return this.getXPath(element.parentNode) +
`/${element.tagName.toLowerCase()}[${ix + 1}]`
}
if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
ix++
}
}
return ''
}
// 判断是否为表单元素
isFormElement(element) {
const formElements = ['input', 'select', 'textarea', 'button']
return formElements.includes(element.tagName.toLowerCase())
}
// 判断是否为可追踪元素
isTrackableElement(element) {
const trackableElements = ['a', 'button', 'input', 'select', 'textarea']
return trackableElements.includes(element.tagName.toLowerCase()) ||
element.onclick !== null ||
element.getAttribute('data-track') !== null
}
// 添加事件到队列
addEvent(event) {
// 添加会话信息
event.sessionId = this.sessionData.sessionId
event.userId = this.getUserId()
event.url = location.href
this.eventQueue.push(event)
// 达到批量大小时立即发送
if (this.eventQueue.length >= this.options.batchSize) {
this.flushEvents()
}
}
// 开始批量发送
startBatchSending() {
setInterval(() => {
if (this.eventQueue.length > 0) {
this.flushEvents()
}
}, this.options.flushInterval)
}
// 发送事件数据
flushEvents() {
if (this.eventQueue.length === 0) return
const payload = {
events: [...this.eventQueue],
session: this.sessionData,
timestamp: Date.now()
}
// 清空队列
this.eventQueue = []
// 发送数据
this.sendData('/api/behavior', payload)
}
// 发送数据
sendData(endpoint, data) {
if (navigator.sendBeacon) {
navigator.sendBeacon(endpoint, JSON.stringify(data))
} else {
fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
}).catch(error => {
console.error('Failed to send behavior data:', error)
})
}
}
// 页面卸载处理
setupBeforeUnload() {
window.addEventListener('beforeunload', () => {
this.flushEvents()
})
}
// 生成会话ID
generateSessionId() {
return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
}
// 获取用户ID
getUserId() {
return localStorage.getItem('userId') || 'anonymous'
}
}
// 初始化用户行为追踪
const behaviorTracker = new UserBehaviorTracker({
trackClicks: true,
trackScrolls: true,
trackFormInteractions: true,
batchSize: 10,
flushInterval: 3000
})
🔧 监控数据处理
数据聚合与分析
javascript
// 监控数据分析器
class MonitoringAnalyzer {
constructor() {
this.metrics = new Map()
this.alerts = []
this.thresholds = {
lcp: 2500, // LCP 阈值 2.5s
fid: 100, // FID 阈值 100ms
cls: 0.1, // CLS 阈值 0.1
errorRate: 0.05, // 错误率阈值 5%
memoryUsage: 0.8 // 内存使用率阈值 80%
}
}
// 分析性能数据
analyzePerformance(data) {
const analysis = {
timestamp: Date.now(),
url: data.url,
metrics: {},
issues: [],
recommendations: []
}
// 分析 Core Web Vitals
if (data.lcp) {
analysis.metrics.lcp = data.lcp.value
if (data.lcp.value > this.thresholds.lcp) {
analysis.issues.push({
type: 'performance',
metric: 'lcp',
value: data.lcp.value,
threshold: this.thresholds.lcp,
severity: 'high'
})
analysis.recommendations.push('优化最大内容绘制时间:压缩图片、使用CDN、优化服务器响应时间')
}
}
if (data.fid) {
analysis.metrics.fid = data.fid.value
if (data.fid.value > this.thresholds.fid) {
analysis.issues.push({
type: 'performance',
metric: 'fid',
value: data.fid.value,
threshold: this.thresholds.fid,
severity: 'medium'
})
analysis.recommendations.push('减少首次输入延迟:优化JavaScript执行、减少主线程阻塞')
}
}
if (data.cls) {
analysis.metrics.cls = data.cls.value
if (data.cls.value > this.thresholds.cls) {
analysis.issues.push({
type: 'performance',
metric: 'cls',
value: data.cls.value,
threshold: this.thresholds.cls,
severity: 'high'
})
analysis.recommendations.push('减少累积布局偏移:为图片设置尺寸、避免动态插入内容')
}
}
return analysis
}
// 分析错误数据
analyzeErrors(errors) {
const errorAnalysis = {
timestamp: Date.now(),
totalErrors: errors.length,
errorTypes: {},
topErrors: [],
affectedUsers: new Set(),
recommendations: []
}
// 统计错误类型
errors.forEach(error => {
const type = error.type
if (!errorAnalysis.errorTypes[type]) {
errorAnalysis.errorTypes[type] = 0
}
errorAnalysis.errorTypes[type]++
if (error.userId) {
errorAnalysis.affectedUsers.add(error.userId)
}
})
// 找出最频繁的错误
const errorCounts = {}
errors.forEach(error => {
const key = `${error.message}_${error.filename}_${error.lineno}`
if (!errorCounts[key]) {
errorCounts[key] = { count: 0, error: error }
}
errorCounts[key].count++
})
errorAnalysis.topErrors = Object.values(errorCounts)
.sort((a, b) => b.count - a.count)
.slice(0, 10)
.map(item => ({
...item.error,
count: item.count
}))
// 生成建议
if (errorAnalysis.errorTypes['javascript'] > 0) {
errorAnalysis.recommendations.push('检查JavaScript代码质量,使用ESLint进行静态分析')
}
if (errorAnalysis.errorTypes['resource'] > 0) {
errorAnalysis.recommendations.push('检查资源加载路径,确保CDN和服务器稳定性')
}
errorAnalysis.affectedUsers = errorAnalysis.affectedUsers.size
return errorAnalysis
}
// 生成性能报告
generatePerformanceReport(timeRange = '24h') {
const report = {
timeRange,
timestamp: Date.now(),
summary: {
totalPageViews: 0,
averageLoadTime: 0,
errorRate: 0,
topPages: [],
performanceScore: 0
},
webVitals: {
lcp: { average: 0, p95: 0, samples: 0 },
fid: { average: 0, p95: 0, samples: 0 },
cls: { average: 0, p95: 0, samples: 0 }
},
trends: {
hourly: [],
daily: []
},
issues: [],
recommendations: []
}
// 这里应该从数据库或缓存中获取实际数据
// 示例数据处理逻辑
return report
}
// 实时告警检查
checkAlerts(data) {
const alerts = []
// 性能告警
if (data.lcp && data.lcp.value > this.thresholds.lcp * 1.5) {
alerts.push({
type: 'performance',
severity: 'critical',
metric: 'lcp',
value: data.lcp.value,
message: `LCP严重超标: ${data.lcp.value}ms > ${this.thresholds.lcp * 1.5}ms`,
url: data.url,
timestamp: Date.now()
})
}
// 错误率告警
const errorRate = this.calculateErrorRate(data.url)
if (errorRate > this.thresholds.errorRate) {
alerts.push({
type: 'error',
severity: 'high',
metric: 'error_rate',
value: errorRate,
message: `错误率过高: ${(errorRate * 100).toFixed(2)}% > ${(this.thresholds.errorRate * 100).toFixed(2)}%`,
url: data.url,
timestamp: Date.now()
})
}
// 发送告警
alerts.forEach(alert => this.sendAlert(alert))
return alerts
}
// 计算错误率
calculateErrorRate(url) {
// 这里应该从实际数据中计算
// 示例返回
return 0.02
}
// 发送告警
sendAlert(alert) {
// 发送到告警系统
fetch('/api/alerts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(alert)
}).catch(error => {
console.error('Failed to send alert:', error)
})
}
}
可视化仪表板
javascript
// 监控仪表板
class MonitoringDashboard {
constructor(containerId) {
this.container = document.getElementById(containerId)
this.charts = {}
this.init()
}
init() {
this.createLayout()
this.initCharts()
this.startRealTimeUpdates()
}
// 创建布局
createLayout() {
this.container.innerHTML = `
<div class="dashboard-header">
<h1>前端监控仪表板</h1>
<div class="time-range-selector">
<select id="timeRange">
<option value="1h">最近1小时</option>
<option value="24h" selected>最近24小时</option>
<option value="7d">最近7天</option>
<option value="30d">最近30天</option>
</select>
</div>
</div>
<div class="dashboard-grid">
<div class="metric-cards">
<div class="metric-card" id="pageViews">
<h3>页面浏览量</h3>
<div class="metric-value">-</div>
<div class="metric-change">-</div>
</div>
<div class="metric-card" id="errorRate">
<h3>错误率</h3>
<div class="metric-value">-</div>
<div class="metric-change">-</div>
</div>
<div class="metric-card" id="avgLoadTime">
<h3>平均加载时间</h3>
<div class="metric-value">-</div>
<div class="metric-change">-</div>
</div>
<div class="metric-card" id="performanceScore">
<h3>性能评分</h3>
<div class="metric-value">-</div>
<div class="metric-change">-</div>
</div>
</div>
<div class="chart-container">
<div class="chart-panel">
<h3>Core Web Vitals</h3>
<canvas id="webVitalsChart"></canvas>
</div>
<div class="chart-panel">
<h3>页面加载时间趋势</h3>
<canvas id="loadTimeChart"></canvas>
</div>
</div>
<div class="chart-container">
<div class="chart-panel">
<h3>错误分布</h3>
<canvas id="errorChart"></canvas>
</div>
<div class="chart-panel">
<h3>用户行为热力图</h3>
<div id="heatmapContainer"></div>
</div>
</div>
<div class="alerts-panel">
<h3>实时告警</h3>
<div id="alertsList"></div>
</div>
<div class="top-pages-panel">
<h3>热门页面</h3>
<div id="topPagesList"></div>
</div>
</div>
`
}
// 初始化图表
initCharts() {
// Web Vitals 图表
const webVitalsCtx = document.getElementById('webVitalsChart').getContext('2d')
this.charts.webVitals = new Chart(webVitalsCtx, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: 'LCP (ms)',
data: [],
borderColor: '#ff6b6b',
backgroundColor: 'rgba(255, 107, 107, 0.1)'
},
{
label: 'FID (ms)',
data: [],
borderColor: '#4ecdc4',
backgroundColor: 'rgba(78, 205, 196, 0.1)'
},
{
label: 'CLS (×100)',
data: [],
borderColor: '#45b7d1',
backgroundColor: 'rgba(69, 183, 209, 0.1)'
}
]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true
}
}
}
})
// 加载时间趋势图表
const loadTimeCtx = document.getElementById('loadTimeChart').getContext('2d')
this.charts.loadTime = new Chart(loadTimeCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: '平均加载时间 (ms)',
data: [],
borderColor: '#96ceb4',
backgroundColor: 'rgba(150, 206, 180, 0.1)',
fill: true
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true
}
}
}
})
// 错误分布图表
const errorCtx = document.getElementById('errorChart').getContext('2d')
this.charts.error = new Chart(errorCtx, {
type: 'doughnut',
data: {
labels: ['JavaScript错误', '资源加载错误', '网络错误', '其他错误'],
datasets: [{
data: [0, 0, 0, 0],
backgroundColor: [
'#ff6b6b',
'#feca57',
'#48dbfb',
'#ff9ff3'
]
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom'
}
}
}
})
}
// 更新指标卡片
updateMetricCards(data) {
const cards = {
pageViews: {
value: data.pageViews?.toLocaleString() || '-',
change: data.pageViewsChange || 0
},
errorRate: {
value: data.errorRate ? `${(data.errorRate * 100).toFixed(2)}%` : '-',
change: data.errorRateChange || 0
},
avgLoadTime: {
value: data.avgLoadTime ? `${data.avgLoadTime.toFixed(0)}ms` : '-',
change: data.loadTimeChange || 0
},
performanceScore: {
value: data.performanceScore || '-',
change: data.scoreChange || 0
}
}
Object.entries(cards).forEach(([key, card]) => {
const element = document.getElementById(key)
if (element) {
element.querySelector('.metric-value').textContent = card.value
const changeElement = element.querySelector('.metric-change')
const changeValue = card.change
const changeText = changeValue > 0 ? `+${changeValue.toFixed(1)}%` : `${changeValue.toFixed(1)}%`
const changeClass = changeValue > 0 ? 'positive' : changeValue < 0 ? 'negative' : 'neutral'
changeElement.textContent = changeText
changeElement.className = `metric-change ${changeClass}`
}
})
}
// 更新图表数据
updateCharts(data) {
// 更新 Web Vitals 图表
if (data.webVitals) {
const chart = this.charts.webVitals
chart.data.labels = data.webVitals.labels
chart.data.datasets[0].data = data.webVitals.lcp
chart.data.datasets[1].data = data.webVitals.fid
chart.data.datasets[2].data = data.webVitals.cls.map(v => v * 100) // CLS 乘以100便于显示
chart.update()
}
// 更新加载时间图表
if (data.loadTime) {
const chart = this.charts.loadTime
chart.data.labels = data.loadTime.labels
chart.data.datasets[0].data = data.loadTime.values
chart.update()
}
// 更新错误分布图表
if (data.errors) {
const chart = this.charts.error
chart.data.datasets[0].data = [
data.errors.javascript || 0,
data.errors.resource || 0,
data.errors.network || 0,
data.errors.other || 0
]
chart.update()
}
}
// 更新告警列表
updateAlerts(alerts) {
const alertsList = document.getElementById('alertsList')
if (alerts.length === 0) {
alertsList.innerHTML = '<div class="no-alerts">暂无告警</div>'
return
}
const alertsHTML = alerts.map(alert => `
<div class="alert-item ${alert.severity}">
<div class="alert-icon">⚠️</div>
<div class="alert-content">
<div class="alert-message">${alert.message}</div>
<div class="alert-meta">
<span class="alert-time">${new Date(alert.timestamp).toLocaleTimeString()}</span>
<span class="alert-url">${alert.url}</span>
</div>
</div>
</div>
`).join('')
alertsList.innerHTML = alertsHTML
}
// 开始实时更新
startRealTimeUpdates() {
// 每30秒更新一次数据
setInterval(() => {
this.fetchDashboardData()
}, 30000)
// 初始加载
this.fetchDashboardData()
}
// 获取仪表板数据
async fetchDashboardData() {
try {
const timeRange = document.getElementById('timeRange').value
const response = await fetch(`/api/dashboard?range=${timeRange}`)
const data = await response.json()
this.updateMetricCards(data.metrics)
this.updateCharts(data.charts)
this.updateAlerts(data.alerts)
} catch (error) {
console.error('Failed to fetch dashboard data:', error)
}
}
}
// 初始化仪表板
const dashboard = new MonitoringDashboard('dashboard-container')
🎯 最佳实践总结
监控策略
分层监控
- 基础设施监控:服务器、网络、CDN
- 应用性能监控:响应时间、吞吐量、错误率
- 用户体验监控:Core Web Vitals、用户行为
- 业务指标监控:转化率、用户留存、收入
数据采集原则
- 采样策略:避免性能影响,合理控制数据量
- 隐私保护:遵循数据保护法规,匿名化敏感信息
- 数据质量:确保数据准确性和完整性
- 实时性:关键指标需要实时监控和告警
告警机制
- 分级告警:根据严重程度设置不同级别
- 智能降噪:避免告警风暴,合并相似告警
- 自动恢复:问题解决后自动关闭告警
- 升级机制:长时间未处理的告警自动升级
性能优化建议
监控代码优化
- 异步处理:避免阻塞主线程
- 批量发送:减少网络请求次数
- 本地缓存:临时存储监控数据
- 错误处理:监控代码本身不应影响业务
数据存储优化
- 时序数据库:适合存储监控指标
- 数据压缩:减少存储空间占用
- 数据分层:热数据和冷数据分别存储
- 自动清理:定期清理过期数据
前端监控是保障用户体验的重要手段,通过系统性的监控体系,我们可以及时发现问题、分析原因并持续优化,为用户提供更好的Web体验。
持续监控,持续改进,让每一次用户访问都是完美体验!