发布于

前端监控与错误处理:异常捕获、性能监控与用户体验优化

作者

前端监控与错误处理:异常捕获、性能监控与用户体验优化

前端监控和错误处理是保障用户体验的重要环节。本文将分享前端监控系统的构建经验和错误处理的最佳实践。

错误捕获与处理

全局错误捕获机制

// 全局错误处理器
class GlobalErrorHandler {
  constructor(options = {}) {
    this.options = {
      enableConsoleLog: true,
      enableRemoteLog: true,
      maxErrors: 50,
      reportUrl: '/api/errors',
      ...options
    };
    
    this.errors = [];
    this.errorCounts = new Map();
    this.init();
  }
  
  init() {
    // 捕获JavaScript运行时错误
    window.addEventListener('error', (event) => {
      this.handleError({
        type: 'javascript',
        message: event.message,
        filename: event.filename,
        lineno: event.lineno,
        colno: event.colno,
        error: event.error,
        stack: event.error?.stack,
        timestamp: Date.now(),
        url: window.location.href,
        userAgent: navigator.userAgent
      });
    });
    
    // 捕获Promise未处理的rejection
    window.addEventListener('unhandledrejection', (event) => {
      this.handleError({
        type: 'promise',
        message: event.reason?.message || 'Unhandled Promise Rejection',
        reason: event.reason,
        stack: event.reason?.stack,
        timestamp: Date.now(),
        url: window.location.href,
        userAgent: navigator.userAgent
      });
    });
    
    // 捕获资源加载错误
    window.addEventListener('error', (event) => {
      if (event.target !== window) {
        this.handleError({
          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: window.location.href,
          userAgent: navigator.userAgent
        });
      }
    }, true);
    
    // 拦截console.error
    this.interceptConsoleError();
    
    // 拦截网络请求错误
    this.interceptNetworkErrors();
  }
  
  handleError(errorInfo) {
    // 错误去重
    const errorKey = this.generateErrorKey(errorInfo);
    const count = this.errorCounts.get(errorKey) || 0;
    this.errorCounts.set(errorKey, count + 1);
    
    // 如果同一错误出现次数过多,降低上报频率
    if (count > 10 && count % 10 !== 0) {
      return;
    }
    
    // 添加额外信息
    errorInfo.count = count + 1;
    errorInfo.id = this.generateErrorId();
    errorInfo.sessionId = this.getSessionId();
    errorInfo.userId = this.getUserId();
    errorInfo.pageInfo = this.getPageInfo();
    errorInfo.deviceInfo = this.getDeviceInfo();
    
    // 存储错误
    this.errors.push(errorInfo);
    
    // 限制错误数量
    if (this.errors.length > this.options.maxErrors) {
      this.errors.shift();
    }
    
    // 控制台输出
    if (this.options.enableConsoleLog) {
      console.error('Global Error Caught:', errorInfo);
    }
    
    // 远程上报
    if (this.options.enableRemoteLog) {
      this.reportError(errorInfo);
    }
    
    // 触发自定义事件
    this.dispatchErrorEvent(errorInfo);
  }
  
  generateErrorKey(errorInfo) {
    return `${errorInfo.type}-${errorInfo.message}-${errorInfo.filename}-${errorInfo.lineno}`;
  }
  
  generateErrorId() {
    return Date.now().toString(36) + Math.random().toString(36).substr(2);
  }
  
  getSessionId() {
    let sessionId = sessionStorage.getItem('sessionId');
    if (!sessionId) {
      sessionId = Date.now().toString(36) + Math.random().toString(36).substr(2);
      sessionStorage.setItem('sessionId', sessionId);
    }
    return sessionId;
  }
  
  getUserId() {
    // 从localStorage、cookie或其他地方获取用户ID
    return localStorage.getItem('userId') || 'anonymous';
  }
  
  getPageInfo() {
    return {
      url: window.location.href,
      title: document.title,
      referrer: document.referrer,
      timestamp: Date.now()
    };
  }
  
  getDeviceInfo() {
    return {
      userAgent: navigator.userAgent,
      language: navigator.language,
      platform: navigator.platform,
      cookieEnabled: navigator.cookieEnabled,
      onLine: navigator.onLine,
      screen: {
        width: screen.width,
        height: screen.height,
        colorDepth: screen.colorDepth
      },
      viewport: {
        width: window.innerWidth,
        height: window.innerHeight
      }
    };
  }
  
  interceptConsoleError() {
    const originalError = console.error;
    console.error = (...args) => {
      this.handleError({
        type: 'console',
        message: args.join(' '),
        args: args,
        timestamp: Date.now(),
        url: window.location.href,
        stack: new Error().stack
      });
      
      originalError.apply(console, args);
    };
  }
  
  interceptNetworkErrors() {
    // 拦截fetch错误
    const originalFetch = window.fetch;
    window.fetch = async (...args) => {
      try {
        const response = await originalFetch(...args);
        
        if (!response.ok) {
          this.handleError({
            type: 'network',
            message: `HTTP ${response.status}: ${response.statusText}`,
            url: args[0],
            status: response.status,
            statusText: response.statusText,
            timestamp: Date.now()
          });
        }
        
        return response;
      } catch (error) {
        this.handleError({
          type: 'network',
          message: `Fetch failed: ${error.message}`,
          url: args[0],
          error: error,
          stack: error.stack,
          timestamp: Date.now()
        });
        
        throw error;
      }
    };
    
    // 拦截XMLHttpRequest错误
    const originalOpen = XMLHttpRequest.prototype.open;
    const originalSend = XMLHttpRequest.prototype.send;
    
    XMLHttpRequest.prototype.open = function(method, url, ...args) {
      this._method = method;
      this._url = url;
      return originalOpen.call(this, method, url, ...args);
    };
    
    XMLHttpRequest.prototype.send = function(...args) {
      this.addEventListener('error', () => {
        globalErrorHandler.handleError({
          type: 'network',
          message: `XMLHttpRequest failed: ${this._method} ${this._url}`,
          method: this._method,
          url: this._url,
          status: this.status,
          statusText: this.statusText,
          timestamp: Date.now()
        });
      });
      
      this.addEventListener('timeout', () => {
        globalErrorHandler.handleError({
          type: 'network',
          message: `XMLHttpRequest timeout: ${this._method} ${this._url}`,
          method: this._method,
          url: this._url,
          timestamp: Date.now()
        });
      });
      
      return originalSend.call(this, ...args);
    };
  }
  
  async reportError(errorInfo) {
    try {
      await fetch(this.options.reportUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(errorInfo)
      });
    } catch (error) {
      console.warn('Failed to report error:', error);
    }
  }
  
  dispatchErrorEvent(errorInfo) {
    const event = new CustomEvent('globalError', {
      detail: errorInfo
    });
    window.dispatchEvent(event);
  }
  
  // 手动报告错误
  reportCustomError(message, extra = {}) {
    this.handleError({
      type: 'custom',
      message,
      ...extra,
      timestamp: Date.now(),
      url: window.location.href,
      stack: new Error().stack
    });
  }
  
  // 获取错误统计
  getErrorStats() {
    const stats = {
      total: this.errors.length,
      byType: {},
      byPage: {},
      recent: this.errors.slice(-10)
    };
    
    this.errors.forEach(error => {
      stats.byType[error.type] = (stats.byType[error.type] || 0) + 1;
      stats.byPage[error.url] = (stats.byPage[error.url] || 0) + 1;
    });
    
    return stats;
  }
  
  // 清除错误记录
  clearErrors() {
    this.errors = [];
    this.errorCounts.clear();
  }
}

// 全局错误处理器实例
const globalErrorHandler = new GlobalErrorHandler({
  reportUrl: '/api/frontend-errors',
  maxErrors: 100
});

// React错误边界
import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null, errorInfo: null };
  }
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    this.setState({
      error,
      errorInfo
    });
    
    // 报告React错误
    globalErrorHandler.reportCustomError(error.message, {
      type: 'react',
      componentStack: errorInfo.componentStack,
      errorBoundary: this.constructor.name,
      props: this.props,
      stack: error.stack
    });
  }
  
  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div className="error-boundary">
          <h2>Something went wrong.</h2>
          <details style={{ whiteSpace: 'pre-wrap' }}>
            {this.state.error && this.state.error.toString()}
            <br />
            {this.state.errorInfo.componentStack}
          </details>
        </div>
      );
    }
    
    return this.props.children;
  }
}

// 使用示例
function App() {
  return (
    <ErrorBoundary fallback={<div>应用出现错误,请刷新页面</div>}>
      <Router>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </Router>
    </ErrorBoundary>
  );
}

异步错误处理

// 异步操作错误处理工具
class AsyncErrorHandler {
  constructor() {
    this.pendingPromises = new Set();
    this.retryConfig = new Map();
  }
  
  // 包装Promise以添加错误处理
  wrapPromise(promise, options = {}) {
    const {
      timeout = 10000,
      retries = 3,
      retryDelay = 1000,
      onError = null,
      onRetry = null
    } = options;
    
    const wrappedPromise = this.addTimeout(promise, timeout)
      .catch(error => {
        if (retries > 0) {
          return this.retryPromise(promise, {
            ...options,
            retries: retries - 1
          });
        }
        
        if (onError) {
          onError(error);
        }
        
        globalErrorHandler.reportCustomError(error.message, {
          type: 'async',
          operation: options.operation || 'unknown',
          retries: options.originalRetries || 3,
          timeout
        });
        
        throw error;
      });
    
    this.pendingPromises.add(wrappedPromise);
    
    wrappedPromise.finally(() => {
      this.pendingPromises.delete(wrappedPromise);
    });
    
    return wrappedPromise;
  }
  
  addTimeout(promise, timeout) {
    return Promise.race([
      promise,
      new Promise((_, reject) => {
        setTimeout(() => {
          reject(new Error(`Operation timed out after ${timeout}ms`));
        }, timeout);
      })
    ]);
  }
  
  async retryPromise(promiseFactory, options) {
    const { retries, retryDelay, onRetry } = options;
    
    await this.delay(retryDelay);
    
    if (onRetry) {
      onRetry(options.originalRetries - retries);
    }
    
    return this.wrapPromise(
      typeof promiseFactory === 'function' ? promiseFactory() : promiseFactory,
      options
    );
  }
  
  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
  
  // 安全的异步函数执行器
  async safeExecute(asyncFn, options = {}) {
    const {
      fallback = null,
      silent = false,
      operation = 'unknown'
    } = options;
    
    try {
      return await this.wrapPromise(asyncFn(), {
        ...options,
        operation
      });
    } catch (error) {
      if (!silent) {
        console.error(`Async operation failed: ${operation}`, error);
      }
      
      return fallback;
    }
  }
  
  // 批量处理异步操作
  async batchExecute(operations, options = {}) {
    const {
      concurrency = 5,
      failFast = false,
      onProgress = null
    } = options;
    
    const results = [];
    const errors = [];
    
    for (let i = 0; i < operations.length; i += concurrency) {
      const batch = operations.slice(i, i + concurrency);
      
      const batchPromises = batch.map(async (operation, index) => {
        try {
          const result = await this.safeExecute(operation.fn, {
            ...operation.options,
            operation: operation.name || `operation-${i + index}`
          });
          
          if (onProgress) {
            onProgress(i + index + 1, operations.length);
          }
          
          return { success: true, result, index: i + index };
        } catch (error) {
          const errorInfo = { success: false, error, index: i + index };
          errors.push(errorInfo);
          
          if (failFast) {
            throw error;
          }
          
          return errorInfo;
        }
      });
      
      const batchResults = await Promise.all(batchPromises);
      results.push(...batchResults);
    }
    
    return {
      results: results.filter(r => r.success),
      errors,
      total: operations.length,
      successCount: results.filter(r => r.success).length,
      errorCount: errors.length
    };
  }
  
  // 获取待处理的Promise数量
  getPendingCount() {
    return this.pendingPromises.size;
  }
  
  // 等待所有Promise完成
  async waitForAll() {
    await Promise.allSettled(Array.from(this.pendingPromises));
  }
}

// 全局异步错误处理器
const asyncErrorHandler = new AsyncErrorHandler();

// 使用示例
async function fetchUserData(userId) {
  return asyncErrorHandler.safeExecute(
    () => fetch(`/api/users/${userId}`).then(res => res.json()),
    {
      timeout: 5000,
      retries: 3,
      retryDelay: 1000,
      operation: 'fetchUserData',
      fallback: { id: userId, name: 'Unknown User' },
      onRetry: (attempt) => {
        console.log(`Retrying fetchUserData, attempt ${attempt}`);
      }
    }
  );
}

// React Hook for async error handling
import { useState, useCallback } from 'react';

function useAsyncError() {
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);
  
  const execute = useCallback(async (asyncFn, options = {}) => {
    setLoading(true);
    setError(null);
    
    try {
      const result = await asyncErrorHandler.safeExecute(asyncFn, options);
      return result;
    } catch (err) {
      setError(err);
      throw err;
    } finally {
      setLoading(false);
    }
  }, []);
  
  const clearError = useCallback(() => {
    setError(null);
  }, []);
  
  return { execute, error, loading, clearError };
}

// 使用示例
function UserProfile({ userId }) {
  const { execute, error, loading, clearError } = useAsyncError();
  const [user, setUser] = useState(null);
  
  const loadUser = useCallback(async () => {
    const userData = await execute(
      () => fetchUserData(userId),
      { operation: 'loadUserProfile' }
    );
    setUser(userData);
  }, [userId, execute]);
  
  useEffect(() => {
    loadUser();
  }, [loadUser]);
  
  if (loading) return <div>Loading...</div>;
  if (error) return (
    <div>
      <p>Error: {error.message}</p>
      <button onClick={() => { clearError(); loadUser(); }}>
        Retry
      </button>
    </div>
  );
  
  return user ? <div>Welcome, {user.name}!</div> : null;
}

性能监控

性能指标收集

// 性能监控器
class PerformanceMonitor {
  constructor(options = {}) {
    this.options = {
      enableAutoReport: true,
      reportInterval: 30000, // 30秒
      reportUrl: '/api/performance',
      ...options
    };
    
    this.metrics = {
      navigation: {},
      resources: [],
      vitals: {},
      custom: {}
    };
    
    this.observers = [];
    this.init();
  }
  
  init() {
    // 监控页面加载性能
    this.monitorNavigation();
    
    // 监控资源加载性能
    this.monitorResources();
    
    // 监控Core Web Vitals
    this.monitorWebVitals();
    
    // 监控长任务
    this.monitorLongTasks();
    
    // 监控内存使用
    this.monitorMemory();
    
    // 自动上报
    if (this.options.enableAutoReport) {
      this.startAutoReport();
    }
  }
  
  monitorNavigation() {
    if ('performance' in window && 'getEntriesByType' in performance) {
      window.addEventListener('load', () => {
        setTimeout(() => {
          const navigation = performance.getEntriesByType('navigation')[0];
          
          if (navigation) {
            this.metrics.navigation = {
              // DNS查询时间
              dnsLookup: navigation.domainLookupEnd - navigation.domainLookupStart,
              
              // TCP连接时间
              tcpConnect: navigation.connectEnd - navigation.connectStart,
              
              // SSL握手时间
              sslConnect: navigation.secureConnectionStart > 0 
                ? navigation.connectEnd - navigation.secureConnectionStart 
                : 0,
              
              // 请求响应时间
              request: navigation.responseEnd - navigation.requestStart,
              
              // DOM解析时间
              domParse: navigation.domContentLoadedEventEnd - navigation.responseEnd,
              
              // 资源加载时间
              resourceLoad: navigation.loadEventEnd - navigation.domContentLoadedEventEnd,
              
              // 总加载时间
              totalLoad: navigation.loadEventEnd - navigation.navigationStart,
              
              // 首字节时间
              ttfb: navigation.responseStart - navigation.navigationStart,
              
              // DOM准备时间
              domReady: navigation.domContentLoadedEventEnd - navigation.navigationStart,
              
              // 页面完全加载时间
              pageLoad: navigation.loadEventEnd - navigation.navigationStart
            };
          }
        }, 0);
      });
    }
  }
  
  monitorResources() {
    if ('PerformanceObserver' in window) {
      const observer = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          if (entry.entryType === 'resource') {
            this.metrics.resources.push({
              name: entry.name,
              type: this.getResourceType(entry.name),
              duration: entry.duration,
              size: entry.transferSize || 0,
              startTime: entry.startTime,
              endTime: entry.responseEnd
            });
          }
        }
      });
      
      observer.observe({ entryTypes: ['resource'] });
      this.observers.push(observer);
    }
  }
  
  monitorWebVitals() {
    // First Contentful Paint (FCP)
    this.observePerformanceEntry('paint', (entries) => {
      const fcp = entries.find(entry => entry.name === 'first-contentful-paint');
      if (fcp) {
        this.metrics.vitals.fcp = fcp.startTime;
      }
    });
    
    // Largest Contentful Paint (LCP)
    this.observePerformanceEntry('largest-contentful-paint', (entries) => {
      const lcp = entries[entries.length - 1];
      if (lcp) {
        this.metrics.vitals.lcp = lcp.startTime;
      }
    });
    
    // First Input Delay (FID)
    this.observePerformanceEntry('first-input', (entries) => {
      const fid = entries[0];
      if (fid) {
        this.metrics.vitals.fid = fid.processingStart - fid.startTime;
      }
    });
    
    // Cumulative Layout Shift (CLS)
    let clsValue = 0;
    this.observePerformanceEntry('layout-shift', (entries) => {
      for (const entry of entries) {
        if (!entry.hadRecentInput) {
          clsValue += entry.value;
        }
      }
      this.metrics.vitals.cls = clsValue;
    });
  }
  
  monitorLongTasks() {
    if ('PerformanceObserver' in window) {
      try {
        const observer = new PerformanceObserver((list) => {
          for (const entry of list.getEntries()) {
            if (entry.duration > 50) { // 长任务阈值50ms
              this.metrics.custom.longTasks = this.metrics.custom.longTasks || [];
              this.metrics.custom.longTasks.push({
                duration: entry.duration,
                startTime: entry.startTime,
                name: entry.name
              });
            }
          }
        });
        
        observer.observe({ entryTypes: ['longtask'] });
        this.observers.push(observer);
      } catch (e) {
        // longtask可能不被支持
      }
    }
  }
  
  monitorMemory() {
    if ('memory' in performance) {
      setInterval(() => {
        this.metrics.custom.memory = {
          usedJSHeapSize: performance.memory.usedJSHeapSize,
          totalJSHeapSize: performance.memory.totalJSHeapSize,
          jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
          timestamp: Date.now()
        };
      }, 10000); // 每10秒记录一次
    }
  }
  
  observePerformanceEntry(entryType, callback) {
    if ('PerformanceObserver' in window) {
      try {
        const observer = new PerformanceObserver((list) => {
          callback(list.getEntries());
        });
        
        observer.observe({ entryTypes: [entryType] });
        this.observers.push(observer);
      } catch (e) {
        console.warn(`Cannot observe ${entryType}:`, e);
      }
    }
  }
  
  getResourceType(url) {
    if (url.match(/\.(css)$/)) return 'css';
    if (url.match(/\.(js)$/)) return 'js';
    if (url.match(/\.(png|jpg|jpeg|gif|svg|webp)$/)) return 'image';
    if (url.match(/\.(woff|woff2|ttf|eot)$/)) return 'font';
    return 'other';
  }
  
  // 自定义性能标记
  mark(name) {
    if ('performance' in window && 'mark' in performance) {
      performance.mark(name);
    }
  }
  
  // 测量性能
  measure(name, startMark, endMark) {
    if ('performance' in window && 'measure' in performance) {
      performance.measure(name, startMark, endMark);
      
      const measures = performance.getEntriesByName(name, 'measure');
      const measure = measures[measures.length - 1];
      
      if (measure) {
        this.metrics.custom[name] = measure.duration;
      }
    }
  }
  
  // 记录自定义指标
  recordMetric(name, value, unit = 'ms') {
    this.metrics.custom[name] = {
      value,
      unit,
      timestamp: Date.now()
    };
  }
  
  // 获取性能报告
  getPerformanceReport() {
    return {
      ...this.metrics,
      timestamp: Date.now(),
      url: window.location.href,
      userAgent: navigator.userAgent,
      connection: this.getConnectionInfo()
    };
  }
  
  getConnectionInfo() {
    if ('connection' in navigator) {
      const conn = navigator.connection;
      return {
        effectiveType: conn.effectiveType,
        downlink: conn.downlink,
        rtt: conn.rtt,
        saveData: conn.saveData
      };
    }
    return null;
  }
  
  // 上报性能数据
  async reportPerformance() {
    try {
      const report = this.getPerformanceReport();
      
      await fetch(this.options.reportUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(report)
      });
    } catch (error) {
      console.warn('Failed to report performance:', error);
    }
  }
  
  // 开始自动上报
  startAutoReport() {
    // 页面卸载时上报
    window.addEventListener('beforeunload', () => {
      this.reportPerformance();
    });
    
    // 定期上报
    setInterval(() => {
      this.reportPerformance();
    }, this.options.reportInterval);
  }
  
  // 清理观察器
  destroy() {
    this.observers.forEach(observer => observer.disconnect());
    this.observers = [];
  }
}

// 全局性能监控器
const performanceMonitor = new PerformanceMonitor({
  reportUrl: '/api/performance-metrics'
});

// 页面卸载时清理
window.addEventListener('beforeunload', () => {
  performanceMonitor.destroy();
});

// 使用示例
// 标记关键操作开始
performanceMonitor.mark('api-call-start');

// 执行API调用
fetch('/api/data')
  .then(response => response.json())
  .then(data => {
    // 标记操作结束
    performanceMonitor.mark('api-call-end');
    
    // 测量耗时
    performanceMonitor.measure('api-call-duration', 'api-call-start', 'api-call-end');
    
    // 记录自定义指标
    performanceMonitor.recordMetric('data-size', JSON.stringify(data).length, 'bytes');
  });

用户体验监控

用户行为追踪

// 用户体验监控器
class UXMonitor {
  constructor(options = {}) {
    this.options = {
      trackClicks: true,
      trackScrolls: true,
      trackPageViews: true,
      trackFormInteractions: true,
      reportUrl: '/api/ux-metrics',
      ...options
    };
    
    this.sessions = {
      startTime: Date.now(),
      pageViews: [],
      interactions: [],
      scrollDepth: 0,
      timeOnPage: 0
    };
    
    this.init();
  }
  
  init() {
    if (this.options.trackPageViews) {
      this.trackPageView();
    }
    
    if (this.options.trackClicks) {
      this.trackClicks();
    }
    
    if (this.options.trackScrolls) {
      this.trackScrollBehavior();
    }
    
    if (this.options.trackFormInteractions) {
      this.trackFormInteractions();
    }
    
    this.trackTimeOnPage();
    this.trackVisibilityChanges();
  }
  
  trackPageView() {
    const pageView = {
      url: window.location.href,
      title: document.title,
      referrer: document.referrer,
      timestamp: Date.now(),
      loadTime: performance.now()
    };
    
    this.sessions.pageViews.push(pageView);
  }
  
  trackClicks() {
    document.addEventListener('click', (event) => {
      const target = event.target;
      const interaction = {
        type: 'click',
        element: target.tagName.toLowerCase(),
        className: target.className,
        id: target.id,
        text: target.textContent?.slice(0, 100),
        x: event.clientX,
        y: event.clientY,
        timestamp: Date.now()
      };
      
      this.sessions.interactions.push(interaction);
    });
  }
  
  trackScrollBehavior() {
    let maxScrollDepth = 0;
    let scrollTimer;
    
    window.addEventListener('scroll', () => {
      clearTimeout(scrollTimer);
      
      scrollTimer = setTimeout(() => {
        const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
        const windowHeight = window.innerHeight;
        const documentHeight = document.documentElement.scrollHeight;
        
        const scrollDepth = Math.round((scrollTop + windowHeight) / documentHeight * 100);
        
        if (scrollDepth > maxScrollDepth) {
          maxScrollDepth = scrollDepth;
          this.sessions.scrollDepth = maxScrollDepth;
          
          // 记录滚动里程碑
          if (scrollDepth >= 25 && scrollDepth < 50 && maxScrollDepth >= 25) {
            this.recordScrollMilestone(25);
          } else if (scrollDepth >= 50 && scrollDepth < 75 && maxScrollDepth >= 50) {
            this.recordScrollMilestone(50);
          } else if (scrollDepth >= 75 && scrollDepth < 100 && maxScrollDepth >= 75) {
            this.recordScrollMilestone(75);
          } else if (scrollDepth >= 100 && maxScrollDepth >= 100) {
            this.recordScrollMilestone(100);
          }
        }
      }, 100);
    });
  }
  
  recordScrollMilestone(depth) {
    this.sessions.interactions.push({
      type: 'scroll',
      depth: depth,
      timestamp: Date.now()
    });
  }
  
  trackFormInteractions() {
    // 表单焦点事件
    document.addEventListener('focusin', (event) => {
      if (event.target.matches('input, textarea, select')) {
        this.sessions.interactions.push({
          type: 'form_focus',
          element: event.target.tagName.toLowerCase(),
          name: event.target.name,
          id: event.target.id,
          timestamp: Date.now()
        });
      }
    });
    
    // 表单提交事件
    document.addEventListener('submit', (event) => {
      const form = event.target;
      const formData = new FormData(form);
      const fields = Array.from(formData.keys());
      
      this.sessions.interactions.push({
        type: 'form_submit',
        formId: form.id,
        formClass: form.className,
        fieldCount: fields.length,
        fields: fields,
        timestamp: Date.now()
      });
    });
    
    // 表单验证错误
    document.addEventListener('invalid', (event) => {
      this.sessions.interactions.push({
        type: 'form_error',
        element: event.target.tagName.toLowerCase(),
        name: event.target.name,
        validationMessage: event.target.validationMessage,
        timestamp: Date.now()
      });
    });
  }
  
  trackTimeOnPage() {
    this.startTime = Date.now();
    
    // 页面卸载时记录总时间
    window.addEventListener('beforeunload', () => {
      this.sessions.timeOnPage = Date.now() - this.startTime;
    });
    
    // 定期更新时间
    setInterval(() => {
      this.sessions.timeOnPage = Date.now() - this.startTime;
    }, 10000);
  }
  
  trackVisibilityChanges() {
    let hiddenTime = 0;
    let lastHiddenTime = 0;
    
    document.addEventListener('visibilitychange', () => {
      if (document.hidden) {
        lastHiddenTime = Date.now();
      } else {
        if (lastHiddenTime > 0) {
          hiddenTime += Date.now() - lastHiddenTime;
        }
      }
      
      this.sessions.interactions.push({
        type: 'visibility_change',
        hidden: document.hidden,
        totalHiddenTime: hiddenTime,
        timestamp: Date.now()
      });
    });
  }
  
  // 记录自定义用户行为
  trackCustomEvent(eventName, data = {}) {
    this.sessions.interactions.push({
      type: 'custom',
      eventName,
      data,
      timestamp: Date.now()
    });
  }
  
  // 获取用户体验报告
  getUXReport() {
    return {
      ...this.sessions,
      sessionDuration: Date.now() - this.sessions.startTime,
      url: window.location.href,
      userAgent: navigator.userAgent,
      timestamp: Date.now()
    };
  }
  
  // 上报用户体验数据
  async reportUX() {
    try {
      const report = this.getUXReport();
      
      await fetch(this.options.reportUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(report)
      });
    } catch (error) {
      console.warn('Failed to report UX metrics:', error);
    }
  }
}

// 全局用户体验监控器
const uxMonitor = new UXMonitor();

// 页面卸载时上报
window.addEventListener('beforeunload', () => {
  uxMonitor.reportUX();
});

// 定期上报
setInterval(() => {
  uxMonitor.reportUX();
}, 60000); // 每分钟上报一次

// 使用示例
// 追踪自定义事件
uxMonitor.trackCustomEvent('video_play', {
  videoId: 'video-123',
  duration: 120,
  quality: '1080p'
});

uxMonitor.trackCustomEvent('search', {
  query: 'javascript tutorial',
  resultsCount: 25
});

总结

前端监控与错误处理的核心要点:

  1. 全局错误捕获:JavaScript错误、Promise rejection、资源加载错误
  2. 异步错误处理:超时控制、重试机制、降级策略
  3. 性能监控:Core Web Vitals、资源加载、自定义指标
  4. 用户体验追踪:用户行为、交互数据、页面停留时间
  5. 数据上报:实时上报、批量上报、离线缓存

建立完善的前端监控体系能够帮助我们及时发现问题、优化用户体验,是现代Web应用不可或缺的基础设施。