Skip to content

前端监控与性能分析

在现代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')

🎯 最佳实践总结

监控策略

  1. 分层监控

    • 基础设施监控:服务器、网络、CDN
    • 应用性能监控:响应时间、吞吐量、错误率
    • 用户体验监控:Core Web Vitals、用户行为
    • 业务指标监控:转化率、用户留存、收入
  2. 数据采集原则

    • 采样策略:避免性能影响,合理控制数据量
    • 隐私保护:遵循数据保护法规,匿名化敏感信息
    • 数据质量:确保数据准确性和完整性
    • 实时性:关键指标需要实时监控和告警
  3. 告警机制

    • 分级告警:根据严重程度设置不同级别
    • 智能降噪:避免告警风暴,合并相似告警
    • 自动恢复:问题解决后自动关闭告警
    • 升级机制:长时间未处理的告警自动升级

性能优化建议

  1. 监控代码优化

    • 异步处理:避免阻塞主线程
    • 批量发送:减少网络请求次数
    • 本地缓存:临时存储监控数据
    • 错误处理:监控代码本身不应影响业务
  2. 数据存储优化

    • 时序数据库:适合存储监控指标
    • 数据压缩:减少存储空间占用
    • 数据分层:热数据和冷数据分别存储
    • 自动清理:定期清理过期数据

前端监控是保障用户体验的重要手段,通过系统性的监控体系,我们可以及时发现问题、分析原因并持续优化,为用户提供更好的Web体验。


持续监控,持续改进,让每一次用户访问都是完美体验!

vitepress开发指南