普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月13日首页

前端监控实践

作者 李元_霸
2026年2月13日 13:51

从零开发前端监控 SDK:异常、性能、访问量一网打尽

本文将带你从零开发一个完整的前端监控 SDK,涵盖异常监控、性能监控和访问量统计三大核心功能。

目录

  1. 为什么需要前端监控
  2. SDK 架构设计
  3. 核心功能实现
  4. 使用示例
  5. 总结与展望

为什么需要前端监控

在现代 Web 应用中,前端监控已经成为保障用户体验的重要手段:

  • 异常监控:及时发现并修复线上 Bug,减少用户流失
  • 性能监控:优化页面加载速度,提升用户体验
  • 访问统计:了解用户行为,指导产品决策

市面上已有 Sentry、Fundebug 等成熟的监控服务,但开发自己的 SDK 能让我们:

  1. 完全掌控数据,保障隐私安全
  2. 根据业务需求定制功能
  3. 深入理解监控原理,提升技术能力

SDK 架构设计

整体架构

┌─────────────────────────────────────────────────────────────┐
│                        Monitor SDK                          │
├─────────────────────────────────────────────────────────────┤
│  Core Layer  │  Reporter (上报中心)  │  Config (配置管理)    │
├─────────────────────────────────────────────────────────────┤
│  Module Layer│  ErrorMonitor │ PerformanceMonitor │ VisitMonitor│
├─────────────────────────────────────────────────────────────┤
│  Utils Layer │  Device │ Storage │ UUID │ Sampling           │
└─────────────────────────────────────────────────────────────┘

设计原则

  1. 模块化:每个监控功能独立模块,可单独启用/禁用
  2. 插件化:Reporter 统一管理上报,支持批量和即时发送
  3. 低侵入:自动捕获异常,业务代码零改动
  4. 高兼容:支持多种引入方式(ESM/CJS/UMD)

核心功能实现

1. 异常监控模块

异常监控是 SDK 的核心功能,我们需要捕获多种类型的错误:

1.1 JavaScript 运行时错误
// src/modules/error/globalError.ts
export function initGlobalError(reporter: Reporter): () => void {
  const handler = (event: ErrorEvent) => {
    const errorData: ErrorData = {
      type: 'js',
      message: event.message,
      filename: event.filename,
      lineno: event.lineno,
      colno: event.colno,
      stack: event.error?.stack
    };
    reporter.report('error', errorData);
  };

  window.addEventListener('error', handler);
  return () => window.removeEventListener('error', handler);
}

通过监听 window.onerror,我们可以捕获所有同步和异步的 JavaScript 错误。

1.2 Promise 未捕获异常
// src/modules/error/promiseError.ts
export function initPromiseError(reporter: Reporter): () => void {
  const handler = (event: PromiseRejectionEvent) => {
    const errorData: ErrorData = {
      type: 'promise',
      message: event.reason?.message || String(event.reason),
      stack: event.reason?.stack
    };
    reporter.report('error', errorData);
  };

  window.addEventListener('unhandledrejection', handler);
  return () => window.removeEventListener('unhandledrejection', handler);
}

现代前端大量使用 Promise,未捕获的 Promise 错误会导致应用崩溃。

1.3 资源加载错误
// src/modules/error/resourceError.ts
export function initResourceError(reporter: Reporter): () => void {
  const handler = (event: Event) => {
    const target = event.target as HTMLElement;
    const tagName = target.tagName?.toLowerCase();

    if (!['img', 'script', 'link'].includes(tagName)) return;

    const src = (target as any).src || (target as any).href || '';
    const errorData: ErrorData = {
      type: 'resource',
      message: `Failed to load ${tagName}: ${src}`,
      filename: src,
      extra: { tagName }
    };
    reporter.report('error', errorData);
  };

  window.addEventListener('error', handler, true); // 捕获阶段监听
  return () => window.removeEventListener('error', handler, true);
}

使用捕获阶段(true)可以监听到资源加载错误。

1.4 网络请求错误

通过劫持 XMLHttpRequest 和 fetch API,监控所有网络请求:

// src/modules/error/networkError.ts
const originalFetch = window.fetch;
window.fetch = function(input: RequestInfo | URL, init?: RequestInit) {
  const startTime = Date.now();
  const url = typeof input === 'string' ? input : input.toString();

  return originalFetch.apply(this, arguments as any)
    .then(response => {
      if (!response.ok) {
        reporter.report('error', {
          type: 'network',
          message: `Fetch ${response.status}: ${response.statusText}`,
          extra: { method: init?.method || 'GET', url, status: response.status }
        });
      }
      return response;
    })
    .catch(error => {
      reporter.report('error', {
        type: 'network',
        message: `Fetch failed: ${error.message}`,
        extra: { method: init?.method || 'GET', url }
      });
      throw error;
    });
};

2. 性能监控模块

2.1 Web Vitals 指标

Core Web Vitals 是 Google 提出的衡量用户体验的关键指标:

// src/modules/performance/webVitals.ts

// LCP - 最大内容绘制
export function observeLCP(reporter: Reporter): void {
  const observer = new PerformanceObserver((list) => {
    const entries = list.getEntries();
    const lastEntry = entries[entries.length - 1];
    const value = (lastEntry as any).renderTime || lastEntry.startTime;

    reporter.report('performance', {
      type: 'web-vitals',
      name: 'LCP',
      value: Math.round(value),
      rating: value <= 2500 ? 'good' : value <= 4000 ? 'needs-improvement' : 'poor'
    });
  });

  observer.observe({ entryTypes: ['largest-contentful-paint'] as any });
}

// CLS - 累积布局偏移
export function observeCLS(reporter: Reporter): void {
  let clsValue = 0;

  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      const layoutEntry = entry as PerformanceEntry & { hadRecentInput: boolean; value: number };
      if (!layoutEntry.hadRecentInput) {
        clsValue += layoutEntry.value;
      }
    }
  });

  observer.observe({ entryTypes: ['layout-shift'] as any });

  // 页面隐藏时上报
  window.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'hidden') {
      reporter.report('performance', {
        type: 'web-vitals',
        name: 'CLS',
        value: Math.round(clsValue * 1000) / 1000,
        rating: clsValue <= 0.1 ? 'good' : clsValue <= 0.25 ? 'needs-improvement' : 'poor'
      });
    }
  });
}
2.2 导航性能

利用 Navigation Timing API 获取页面加载各阶段耗时:

export function observeNavigation(reporter: Reporter): void {
  window.addEventListener('load', () => {
    setTimeout(() => {
      const navigation = performance.getEntriesByType('navigation')[0]
        as PerformanceNavigationTiming;

      const metrics = [
        { name: 'DNS', value: navigation.domainLookupEnd - navigation.domainLookupStart },
        { name: 'TCP', value: navigation.connectEnd - navigation.connectStart },
        { name: 'TTFB', value: navigation.responseStart - navigation.startTime },
        { name: 'DOM解析', value: navigation.domInteractive - navigation.responseEnd },
        { name: 'Load', value: navigation.loadEventEnd - navigation.startTime }
      ];

      metrics.forEach(({ name, value }) => {
        if (value > 0) {
          reporter.report('performance', {
            type: 'navigation',
            name,
            value: Math.round(value)
          });
        }
      });
    }, 0);
  });
}
2.3 API 耗时监控

劫持 XMLHttpRequest 和 fetch,统计所有 API 请求耗时:

export function observeAPI(reporter: Reporter): () => void {
  // 劫持 XMLHttpRequest
  const originalXHRSend = XMLHttpRequest.prototype.send;

  XMLHttpRequest.prototype.send = function() {
    const startTime = Date.now();

    this.addEventListener('loadend', function() {
      const duration = Date.now() - startTime;
      reporter.report('performance', {
        type: 'api',
        name: `API: ${this._url}`,
        value: duration
      });
    });

    return originalXHRSend.apply(this, arguments);
  };

  // 劫持 fetch...
}

3. 访问监控模块

3.1 PV 统计
// src/modules/visit/pv.ts
export function observePV(reporter: Reporter, enableSPA: boolean): () => void {
  // 初始页面 PV
  reportPV(reporter);

  if (!enableSPA) return;

  // 劫持 history API 监听路由变化
  const originalPushState = history.pushState;
  history.pushState = function(...args) {
    originalPushState.apply(this, args);
    reportPV(reporter);
  };

  window.addEventListener('popstate', () => reportPV(reporter));
  window.addEventListener('hashchange', () => reportPV(reporter));
}
3.2 Session 管理
// src/modules/visit/session.ts
const SESSION_TIMEOUT = 30 * 60 * 1000; // 30分钟

export function initSession(reporter: Reporter): void {
  const startTime = Date.now();

  // 上报会话开始
  reporter.report('visit', { type: 'session-start' });

  // 页面可见性变化
  document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'visible') {
      const lastActive = parseInt(storage.get('session_time') || '0');
      if (Date.now() - lastActive > SESSION_TIMEOUT) {
        // 新会话
        reporter.report('visit', { type: 'session-start' });
      }
    } else {
      storage.set('session_time', Date.now().toString());
    }
  });

  // 页面卸载时上报会话结束
  window.addEventListener('beforeunload', () => {
    reporter.report('visit', {
      type: 'session-end',
      duration: Date.now() - startTime
    });
  });
}

4. 数据上报中心

4.1 上报策略
// src/core/reporter.ts
export class Reporter {
  private queue: QueueItem[] = [];
  private readonly FLUSH_INTERVAL = 5000; // 5秒刷新
  private readonly MAX_QUEUE_SIZE = 10;   // 10条批量发送

  report(type: ReportData['type'], data: ReportData['data']): void {
    // 采样检查
    const sampleRate = this.config.sampleRate?.[type] || 1;
    if (!shouldSample(sampleRate)) return;

    const url = this.config.reportUrl[type];
    if (!url) return;

    // 异常数据立即上报
    if (type === 'error') {
      this.sendImmediately(data, url);
    } else {
      // 性能和访问数据批量上报
      this.addToQueue(data, url);
    }
  }

  private addToQueue(data: ReportData, url: string): void {
    this.queue.push({ data, url });

    if (this.queue.length >= this.MAX_QUEUE_SIZE) {
      this.flush();
    } else {
      this.scheduleFlush();
    }
  }
}
4.2 页面关闭补发

使用 sendBeacon API 在页面关闭前发送剩余数据:

private bindEvents(): void {
  const sendRemaining = () => {
    if (this.queue.length === 0) return;

    this.queue.forEach(({ data, url }) => {
      navigator.sendBeacon?.(url, JSON.stringify(data));
    });
  };

  window.addEventListener('beforeunload', sendRemaining);
  document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'hidden') {
      sendRemaining();
    }
  });
}

5. 设备信息解析

// src/utils/device.ts
export function getDeviceInfo(): DeviceInfo {
  const ua = navigator.userAgent;

  // 解析操作系统
  let os = 'unknown';
  let osVersion = 'unknown';

  if (ua.indexOf('Win') !== -1) {
    os = 'Windows';
    const match = ua.match(/Windows NT (\d+\.\d+)/);
    if (match) osVersion = match[1];
  } else if (ua.indexOf('Mac') !== -1) {
    os = 'macOS';
    // ...
  } else if (/iPad|iPhone|iPod/.test(ua)) {
    os = 'iOS';
    // ...
  } else if (ua.indexOf('Android') !== -1) {
    os = 'Android';
    // ...
  }

  // 解析浏览器
  let browser = 'unknown';
  let browserVersion = 'unknown';

  if (ua.indexOf('Chrome') !== -1 && ua.indexOf('Edg') === -1) {
    browser = 'Chrome';
    const match = ua.match(/Chrome\/(\d+\.\d+)/);
    if (match) browserVersion = match[1];
  }
  // ... Safari, Firefox, Edge

  return {
    ua,
    os,
    osVersion,
    browser,
    browserVersion,
    screen: `${window.screen.width}x${window.screen.height}`,
    language: navigator.language
  };
}

使用示例

基础使用

import Monitor from 'frontend-monitor-sdk';

Monitor.init({
  appId: 'my-app',
  appVersion: '1.0.0',
  env: 'production',
  reportUrl: {
    error: 'https://api.example.com/error',
    performance: 'https://api.example.com/perf',
    visit: 'https://api.example.com/visit'
  },
  sampleRate: {
    error: 1,         // 异常100%上报
    performance: 0.1, // 性能10%采样
    visit: 0.1        // 访问10%采样
  },
  enableSPA: true,
  beforeReport: (data) => {
    // 上报前钩子,可修改数据或返回 false 阻止上报
    if (data.type === 'error' && data.data.message?.includes('ignore')) {
      return false;
    }
    return data;
  }
});

Vue 集成

import { createApp } from 'vue';
import Monitor from 'frontend-monitor-sdk';

Monitor.init({ /* ... */ });

const app = createApp(App);
app.config.errorHandler = Monitor.vueErrorHandler;

React 集成

class ErrorBoundary extends React.Component {
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    Monitor.reportError(error, {
      componentStack: errorInfo.componentStack
    });
  }

  render() {
    return this.props.children;
  }
}

总结与展望

已实现功能

异常监控:JS 错误、Promise 错误、资源错误、网络错误、控制台错误、框架错误 ✅ 性能监控:Web Vitals、导航计时、资源性能、API 耗时、长任务 ✅ 访问监控:PV/UV、Session、设备信息、SPA 路由监听 ✅ 数据上报:分类上报、采样控制、批量上报、页面关闭补发

技术亮点

  1. 类型安全:完整的 TypeScript 类型定义
  2. 模块化设计:各功能独立,可灵活组合
  3. 低侵入性:自动捕获,业务代码零改动
  4. 高兼容性:支持 ESM/CJS/UMD 多种格式

未来优化方向

🔲 SourceMap 解析:实现错误堆栈的源码还原 🔲 用户行为录屏:记录用户操作路径,辅助问题定位 🔲 性能面板可视化:开发 Chrome 插件查看性能数据 🔲 离线缓存:支持网络断开时的数据本地存储


参考资源


本文完,如有问题欢迎留言讨论!

❌
❌