Skip to content

监控上报方案

概述

前端监控上报是指收集用户行为、性能数据、错误信息等,并上报到服务器进行分析,帮助开发者了解应用运行状态和用户体验。

监控类型

1. 性能监控

javascript
class PerformanceMonitor {
  constructor() {
    this.data = {}
  }

  // 页面加载性能
  getPageLoadTiming() {
    const timing = performance.timing
    return {
      dns: timing.domainLookupEnd - timing.domainLookupStart,
      tcp: timing.connectEnd - timing.connectStart,
      request: timing.responseStart - timing.requestStart,
      response: timing.responseEnd - timing.responseStart,
      dom: timing.domComplete - timing.domInteractive,
      load: timing.loadEventEnd - timing.navigationStart,
      total: timing.loadEventEnd - timing.navigationStart
    }
  }

  // 资源加载性能
  getResourceTiming() {
    const resources = performance.getEntriesByType('resource')
    return resources.map(resource => ({
      name: resource.name,
      type: resource.initiatorType,
      duration: resource.duration,
      size: resource.transferSize
    }))
  }

  // Web Vitals
  async getWebVitals() {
    const vitals = {}
    
    // LCP (Largest Contentful Paint)
    const lcpEntries = performance.getEntriesByType('largest-contentful-paint')
    if (lcpEntries.length > 0) {
      vitals.lcp = lcpEntries[lcpEntries.length - 1].renderTime
    }

    // FID (First Input Delay)
    const fidEntries = performance.getEntriesByType('first-input')
    if (fidEntries.length > 0) {
      vitals.fid = fidEntries[0].processingStart - fidEntries[0].startTime
    }

    // CLS (Cumulative Layout Shift)
    let clsValue = 0
    const clsEntries = performance.getEntriesByType('layout-shift')
    clsEntries.forEach(entry => {
      if (!entry.hadRecentInput) {
        clsValue += entry.value
      }
    })
    vitals.cls = clsValue

    return vitals
  }

  // 收集所有性能数据
  async collect() {
    return {
      pageLoad: this.getPageLoadTiming(),
      resources: this.getResourceTiming(),
      webVitals: await this.getWebVitals(),
      timestamp: Date.now()
    }
  }
}

2. 错误监控

javascript
class ErrorMonitor {
  constructor() {
    this.errors = []
  }

  // 监听 JavaScript 错误
  listenJSErrors() {
    window.onerror = (message, source, lineno, colno, error) => {
      this.report({
        type: 'js-error',
        message,
        source,
        lineno,
        colno,
        stack: error?.stack,
        timestamp: Date.now()
      })
    }
  }

  // 监听 Promise 错误
  listenPromiseErrors() {
    window.addEventListener('unhandledrejection', (event) => {
      this.report({
        type: 'promise-error',
        message: event.reason?.message || String(event.reason),
        stack: event.reason?.stack,
        timestamp: Date.now()
      })
    })
  }

  // 监听资源加载错误
  listenResourceErrors() {
    window.addEventListener('error', (event) => {
      if (event.target !== window) {
        this.report({
          type: 'resource-error',
          tagName: event.target.tagName,
          src: event.target.src || event.target.href,
          timestamp: Date.now()
        })
      }
    }, true)
  }

  // 监听 Vue 错误
  listenVueErrors(Vue) {
    Vue.config.errorHandler = (error, vm, info) => {
      this.report({
        type: 'vue-error',
        message: error.message,
        stack: error.stack,
        component: vm?.$options?.name,
        info,
        timestamp: Date.now()
      })
    }
  }

  // 监听 React 错误
  listenReactErrors() {
    const originalConsoleError = console.error
    console.error = (...args) => {
      this.report({
        type: 'react-error',
        message: args.join(' '),
        timestamp: Date.now()
      })
      originalConsoleError.apply(console, args)
    }
  }

  // 上报错误
  report(error) {
    this.errors.push(error)
    this.send(error)
  }

  // 发送到服务器
  async send(error) {
    try {
      await fetch('/api/monitor/error', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(error)
      })
    } catch (e) {
      console.error('上报错误失败:', e)
    }
  }

  // 初始化
  init(options = {}) {
    this.listenJSErrors()
    this.listenPromiseErrors()
    this.listenResourceErrors()
    
    if (options.Vue) {
      this.listenVueErrors(options.Vue)
    }
    
    if (options.React) {
      this.listenReactErrors()
    }
  }
}

3. 行为监控

javascript
class BehaviorMonitor {
  constructor() {
    this.events = []
  }

  // 监听点击事件
  trackClick() {
    document.addEventListener('click', (e) => {
      this.record({
        type: 'click',
        target: e.target.tagName,
        text: e.target.innerText?.slice(0, 50),
        xpath: this.getXPath(e.target),
        timestamp: Date.now()
      })
    }, true)
  }

  // 监听页面访问
  trackPageView() {
    // 初始页面
    this.record({
      type: 'page-view',
      url: window.location.href,
      title: document.title,
      timestamp: Date.now()
    })

    // 监听路由变化
    const originalPushState = history.pushState
    history.pushState = function(...args) {
      originalPushState.apply(history, args)
      this.record({
        type: 'page-view',
        url: window.location.href,
        title: document.title,
        timestamp: Date.now()
      })
    }

    window.addEventListener('popstate', () => {
      this.record({
        type: 'page-view',
        url: window.location.href,
        title: document.title,
        timestamp: Date.now()
      })
    })
  }

  // 监听用户停留时间
  trackStayTime() {
    let startTime = Date.now()

    window.addEventListener('beforeunload', () => {
      const stayTime = Date.now() - startTime
      this.record({
        type: 'stay-time',
        url: window.location.href,
        duration: stayTime,
        timestamp: Date.now()
      })
    })
  }

  // 获取 XPath
  getXPath(element) {
    if (element.id) {
      return `//*[@id="${element.id}"]`
    }

    const parts = []
    let current = element

    while (current && current.nodeType === Node.ELEMENT_NODE) {
      let index = 0
      let sibling = current.previousSibling

      while (sibling) {
        if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {
          index++
        }
        sibling = sibling.previousSibling
      }

      const tagName = current.tagName.toLowerCase()
      const indexStr = index ? `[${index + 1}]` : ''
      parts.unshift(`${tagName}${indexStr}`)
      current = current.parentNode
    }

    return '/' + parts.join('/')
  }

  // 记录事件
  record(event) {
    this.events.push(event)
    this.send(event)
  }

  // 发送到服务器
  async send(event) {
    try {
      await fetch('/api/monitor/behavior', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(event)
      })
    } catch (e) {
      console.error('上报行为失败:', e)
    }
  }

  // 初始化
  init() {
    this.trackClick()
    this.trackPageView()
    this.trackStayTime()
  }
}

数据上报

1. 上报方式

javascript
class Reporter {
  constructor(options = {}) {
    this.url = options.url || '/api/monitor'
    this.batchSize = options.batchSize || 10
    this.batchInterval = options.batchInterval || 5000
    this.queue = []
    this.timer = null
  }

  // 添加到队列
  add(data) {
    this.queue.push(data)
    
    if (this.queue.length >= this.batchSize) {
      this.flush()
    } else if (!this.timer) {
      this.timer = setTimeout(() => this.flush(), this.batchInterval)
    }
  }

  // 批量发送
  async flush() {
    if (this.timer) {
      clearTimeout(this.timer)
      this.timer = null
    }

    if (this.queue.length === 0) return

    const data = this.queue.slice()
    this.queue = []

    try {
      // 使用 sendBeacon 优先
      if (navigator.sendBeacon) {
        const blob = new Blob([JSON.stringify(data)], { type: 'application/json' })
        navigator.sendBeacon(this.url, blob)
      } else {
        await fetch(this.url, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(data),
          keepalive: true
        })
      }
    } catch (e) {
      console.error('上报失败:', e)
      // 失败时重新加入队列
      this.queue.unshift(...data)
    }
  }
}

2. 数据格式

javascript
{
  // 应用信息
  appId: 'your-app-id',
  version: '1.0.0',
  
  // 用户信息
  userId: 'user-123',
  deviceId: 'device-456',
  
  // 环境信息
  url: 'https://example.com/page',
  userAgent: 'Mozilla/5.0...',
  screen: '1920x1080',
  viewport: '1200x800',
  
  // 监控数据
  type: 'performance', // performance, error, behavior
  data: {
    // 具体数据
  },
  
  // 时间戳
  timestamp: 1234567890000
}

最佳实践

1. 数据采样

  • 根据流量设置采样率
  • 重要错误 100% 上报
  • 性能数据采样上报

2. 性能优化

  • 使用 sendBeacon
  • 批量上报
  • 数据压缩

3. 隐私保护

  • 脱敏敏感信息
  • 遵守隐私法规
  • 用户授权

相关资源

基于 VitePress 的本地知识库