Skip to content

应用监控

在生产环境中,监控 Vue 应用的性能和健康状况至关重要。本指南将介绍如何设置全面的监控系统来跟踪应用的各个方面。

错误监控

全局错误处理

设置全局错误处理器来捕获和报告应用中的错误:

js
import { createApp } from 'vue'
import * as Sentry from '@sentry/vue'

const app = createApp(App)

// 配置 Sentry 错误监控
Sentry.init({
  app,
  dsn: 'YOUR_SENTRY_DSN',
  environment: process.env.NODE_ENV,
  integrations: [
    new Sentry.BrowserTracing({
      routingInstrumentation: Sentry.vueRouterInstrumentation(router),
    }),
  ],
  tracesSampleRate: 1.0,
})

// 自定义错误处理
app.config.errorHandler = (err, instance, info) => {
  console.error('Vue Error:', err)
  console.error('Component:', instance)
  console.error('Info:', info)
  
  // 发送错误到监控服务
  Sentry.captureException(err, {
    contexts: {
      vue: {
        componentName: instance?.$options.name,
        propsData: instance?.$props,
        info: info
      }
    }
  })
}

// 捕获未处理的 Promise 拒绝
window.addEventListener('unhandledrejection', event => {
  console.error('Unhandled promise rejection:', event.reason)
  Sentry.captureException(event.reason)
})

组件级错误边界

创建错误边界组件来优雅地处理组件错误:

vue
<template>
  <div>
    <div v-if="hasError" class="error-boundary">
      <h2>出现了错误</h2>
      <p>{{ errorMessage }}</p>
      <button @click="retry">重试</button>
    </div>
    <slot v-else />
  </div>
</template>

<script setup>
import { ref, onErrorCaptured } from 'vue'
import * as Sentry from '@sentry/vue'

const hasError = ref(false)
const errorMessage = ref('')

onErrorCaptured((err, instance, info) => {
  hasError.value = true
  errorMessage.value = err.message
  
  // 报告错误
  Sentry.captureException(err, {
    tags: {
      errorBoundary: true
    },
    contexts: {
      vue: {
        componentName: instance?.$options.name,
        info: info
      }
    }
  })
  
  return false // 阻止错误继续传播
})

const retry = () => {
  hasError.value = false
  errorMessage.value = ''
}
</script>

性能监控

Core Web Vitals 监控

监控关键的性能指标:

js
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'

// 监控 Core Web Vitals
function sendToAnalytics(metric) {
  // 发送到分析服务
  gtag('event', metric.name, {
    event_category: 'Web Vitals',
    event_label: metric.id,
    value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
    non_interaction: true,
  })
}

getCLS(sendToAnalytics)
getFID(sendToAnalytics)
getFCP(sendToAnalytics)
getLCP(sendToAnalytics)
getTTFB(sendToAnalytics)

Vue 组件性能监控

js
// 性能监控 mixin
const performanceMonitorMixin = {
  beforeCreate() {
    this._startTime = performance.now()
  },
  
  mounted() {
    const mountTime = performance.now() - this._startTime
    
    // 记录组件挂载时间
    if (mountTime > 100) { // 只记录超过 100ms 的组件
      console.warn(`Slow component mount: ${this.$options.name} took ${mountTime}ms`)
      
      // 发送到监控服务
      gtag('event', 'slow_component_mount', {
        event_category: 'Performance',
        event_label: this.$options.name,
        value: Math.round(mountTime)
      })
    }
  },
  
  beforeUpdate() {
    this._updateStartTime = performance.now()
  },
  
  updated() {
    if (this._updateStartTime) {
      const updateTime = performance.now() - this._updateStartTime
      
      if (updateTime > 50) { // 只记录超过 50ms 的更新
        console.warn(`Slow component update: ${this.$options.name} took ${updateTime}ms`)
        
        gtag('event', 'slow_component_update', {
          event_category: 'Performance',
          event_label: this.$options.name,
          value: Math.round(updateTime)
        })
      }
    }
  }
}

// 全局应用 mixin
app.mixin(performanceMonitorMixin)

路由性能监控

js
import { createRouter } from 'vue-router'

const router = createRouter({
  // ... 路由配置
})

// 监控路由导航性能
router.beforeEach((to, from, next) => {
  // 记录导航开始时间
  to.meta.startTime = performance.now()
  next()
})

router.afterEach((to, from) => {
  if (to.meta.startTime) {
    const navigationTime = performance.now() - to.meta.startTime
    
    // 记录路由导航时间
    gtag('event', 'page_view', {
      page_title: to.name,
      page_location: to.fullPath,
      custom_map: {
        navigation_time: Math.round(navigationTime)
      }
    })
    
    // 慢导航警告
    if (navigationTime > 1000) {
      console.warn(`Slow navigation to ${to.path}: ${navigationTime}ms`)
    }
  }
})

用户行为监控

用户交互追踪

js
// 用户交互追踪指令
const trackingDirective = {
  mounted(el, binding) {
    const { event, category, label } = binding.value
    
    el.addEventListener('click', () => {
      gtag('event', event || 'click', {
        event_category: category || 'User Interaction',
        event_label: label || el.textContent
      })
    })
  }
}

app.directive('track', trackingDirective)

使用示例:

vue
<template>
  <button v-track="{ event: 'button_click', category: 'CTA', label: 'Sign Up' }">
    注册
  </button>
</template>

页面停留时间监控

js
class PageTimeTracker {
  constructor() {
    this.startTime = null
    this.isVisible = true
    this.totalTime = 0
    
    this.setupVisibilityTracking()
  }
  
  startTracking(pageName) {
    this.pageName = pageName
    this.startTime = performance.now()
    this.totalTime = 0
  }
  
  stopTracking() {
    if (this.startTime && this.isVisible) {
      this.totalTime += performance.now() - this.startTime
    }
    
    if (this.totalTime > 0) {
      gtag('event', 'page_time', {
        event_category: 'Engagement',
        event_label: this.pageName,
        value: Math.round(this.totalTime / 1000) // 转换为秒
      })
    }
  }
  
  setupVisibilityTracking() {
    document.addEventListener('visibilitychange', () => {
      if (document.hidden) {
        if (this.isVisible && this.startTime) {
          this.totalTime += performance.now() - this.startTime
        }
        this.isVisible = false
      } else {
        this.isVisible = true
        this.startTime = performance.now()
      }
    })
  }
}

const pageTimeTracker = new PageTimeTracker()

// 在路由中使用
router.beforeEach((to, from, next) => {
  if (from.name) {
    pageTimeTracker.stopTracking()
  }
  next()
})

router.afterEach((to) => {
  pageTimeTracker.startTracking(to.name)
})

资源监控

网络请求监控

js
// Axios 拦截器用于监控 API 请求
import axios from 'axios'

axios.interceptors.request.use(
  config => {
    config.metadata = { startTime: performance.now() }
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

axios.interceptors.response.use(
  response => {
    const duration = performance.now() - response.config.metadata.startTime
    
    // 记录 API 响应时间
    gtag('event', 'api_response_time', {
      event_category: 'API',
      event_label: response.config.url,
      value: Math.round(duration)
    })
    
    // 慢请求警告
    if (duration > 2000) {
      console.warn(`Slow API request: ${response.config.url} took ${duration}ms`)
    }
    
    return response
  },
  error => {
    const duration = error.config?.metadata ? 
      performance.now() - error.config.metadata.startTime : 0
    
    // 记录 API 错误
    gtag('event', 'api_error', {
      event_category: 'API',
      event_label: error.config?.url || 'unknown',
      value: error.response?.status || 0
    })
    
    // 发送错误到 Sentry
    Sentry.captureException(error, {
      tags: {
        type: 'api_error'
      },
      contexts: {
        request: {
          url: error.config?.url,
          method: error.config?.method,
          duration: duration
        }
      }
    })
    
    return Promise.reject(error)
  }
)

资源加载监控

js
// 监控资源加载性能
function monitorResourceLoading() {
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      if (entry.entryType === 'resource') {
        const duration = entry.responseEnd - entry.startTime
        
        // 记录慢资源
        if (duration > 1000) {
          console.warn(`Slow resource: ${entry.name} took ${duration}ms`)
          
          gtag('event', 'slow_resource', {
            event_category: 'Performance',
            event_label: entry.name,
            value: Math.round(duration)
          })
        }
        
        // 记录失败的资源
        if (entry.transferSize === 0 && entry.decodedBodySize === 0) {
          gtag('event', 'resource_error', {
            event_category: 'Error',
            event_label: entry.name
          })
        }
      }
    }
  })
  
  observer.observe({ entryTypes: ['resource'] })
}

monitorResourceLoading()

实时监控仪表板

自定义监控组件

vue
<template>
  <div class="monitoring-dashboard" v-if="isDev">
    <div class="metric">
      <label>组件数量:</label>
      <span>{{ componentCount }}</span>
    </div>
    <div class="metric">
      <label>内存使用:</label>
      <span>{{ memoryUsage }}MB</span>
    </div>
    <div class="metric">
      <label>FPS:</label>
      <span>{{ fps }}</span>
    </div>
    <div class="metric">
      <label>API 请求:</label>
      <span>{{ apiRequestCount }}</span>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const isDev = process.env.NODE_ENV === 'development'
const componentCount = ref(0)
const memoryUsage = ref(0)
const fps = ref(0)
const apiRequestCount = ref(0)

let frameCount = 0
let lastTime = performance.now()
let animationId = null

function updateMetrics() {
  // 更新 FPS
  frameCount++
  const currentTime = performance.now()
  if (currentTime - lastTime >= 1000) {
    fps.value = Math.round((frameCount * 1000) / (currentTime - lastTime))
    frameCount = 0
    lastTime = currentTime
  }
  
  // 更新内存使用 (如果支持)
  if (performance.memory) {
    memoryUsage.value = Math.round(performance.memory.usedJSHeapSize / 1024 / 1024)
  }
  
  animationId = requestAnimationFrame(updateMetrics)
}

onMounted(() => {
  if (isDev) {
    updateMetrics()
    
    // 监听组件创建/销毁
    const originalCreateApp = createApp
    // ... 实现组件计数逻辑
  }
})

onUnmounted(() => {
  if (animationId) {
    cancelAnimationFrame(animationId)
  }
})
</script>

<style scoped>
.monitoring-dashboard {
  position: fixed;
  top: 10px;
  right: 10px;
  background: rgba(0, 0, 0, 0.8);
  color: white;
  padding: 10px;
  border-radius: 5px;
  font-family: monospace;
  font-size: 12px;
  z-index: 9999;
}

.metric {
  margin: 5px 0;
}

.metric label {
  margin-right: 10px;
}
</style>

监控最佳实践

1. 采样策略

js
// 根据环境和用户设置不同的采样率
const getSampleRate = () => {
  if (process.env.NODE_ENV === 'development') {
    return 1.0 // 开发环境 100% 采样
  }
  
  if (Math.random() < 0.1) {
    return 1.0 // 10% 的用户进行详细监控
  }
  
  return 0.1 // 其他用户 10% 采样
}

Sentry.init({
  tracesSampleRate: getSampleRate(),
  // ...
})

2. 性能预算

js
// 设置性能预算
const PERFORMANCE_BUDGET = {
  componentMount: 100, // 组件挂载不超过 100ms
  apiRequest: 2000,    // API 请求不超过 2s
  routeNavigation: 1000, // 路由导航不超过 1s
  bundleSize: 500 * 1024, // 包大小不超过 500KB
}

// 检查性能预算
function checkPerformanceBudget(metric, value) {
  if (value > PERFORMANCE_BUDGET[metric]) {
    console.warn(`Performance budget exceeded: ${metric} took ${value}ms (budget: ${PERFORMANCE_BUDGET[metric]}ms)`)
    
    // 发送警告到监控服务
    gtag('event', 'performance_budget_exceeded', {
      event_category: 'Performance',
      event_label: metric,
      value: value
    })
  }
}

3. 监控数据隐私

js
// 敏感数据过滤
Sentry.init({
  beforeSend(event) {
    // 过滤敏感信息
    if (event.request?.data) {
      event.request.data = filterSensitiveData(event.request.data)
    }
    
    if (event.extra) {
      event.extra = filterSensitiveData(event.extra)
    }
    
    return event
  }
})

function filterSensitiveData(data) {
  const sensitiveKeys = ['password', 'token', 'email', 'phone']
  const filtered = { ...data }
  
  sensitiveKeys.forEach(key => {
    if (filtered[key]) {
      filtered[key] = '[FILTERED]'
    }
  })
  
  return filtered
}

通过实施这些监控策略,你可以获得对 Vue 应用性能和健康状况的全面了解,从而能够主动识别和解决问题,提供更好的用户体验。

基于 Vue.js 官方文档构建的学习宝典