Skip to content

前端監控體系演進:從錯誤上報到全鏈路可觀測

前端監控不只是"報個錯"。隨着項目複雜度增長,我們需要的是一個完整的可觀測性體系。

監控的三個層次

L1: 錯誤監控(Error Tracking)
    - JS 運行時錯誤
    - Promise rejection
    - 資源加載失敗
    - 接口異常

L2: 性能監控(Performance Monitoring)
    - Web Vitals(LCP, FID, CLS, INP, FCP, TTFB)
    - 自定義業務指標
    - 資源加載瀑布圖

L3: 用户行為追蹤(User Analytics)
    - 頁面訪問路徑
    - 點擊熱力圖
    - 用户操作序列(Session Replay)

大部分團隊只做了 L1。L2 和 L3 才是真正能驅動優化的。

Web Vitals 採集

typescript
import { onLCP, onINP, onCLS, onFCP, onTTFB } from "web-vitals";

interface MetricEntry {
  name: string;
  value: number;
  rating: "good" | "needs-improvement" | "poor";
  delta: number;
  id: string;
  navigationType: string;
}

function sendMetric(metric: MetricEntry) {
  // 用 Beacon API 發送,不阻塞頁面卸載
  navigator.sendBeacon(
    "/api/metrics",
    JSON.stringify({
      ...metric,
      page: window.location.pathname,
      userAgent: navigator.userAgent,
      timestamp: Date.now(),
    })
  );
}

onCLS(sendMetric);
onINP(sendMetric);
onFCP(sendMetric);
onLCP(sendMetric);
onTTFB(sendMetric);

錯誤邊界和錯誤捕獲

typescript
// 全局錯誤捕獲
window.addEventListener("error", (event) => {
  if (event.target instanceof HTMLScriptElement ||
      event.target instanceof HTMLImageElement) {
    // 資源加載錯誤
    reportError({
      type: "resource",
      source: event.target.src || event.target.href,
      tagName: event.target.tagName,
    });
    return;
  }

  // JS 運行時錯誤
  reportError({
    type: "runtime",
    message: event.message,
    filename: event.filename,
    lineno: event.lineno,
    colno: event.colno,
    stack: event.error?.stack,
  });
});

window.addEventListener("unhandledrejection", (event) => {
  reportError({
    type: "promise",
    message: event.reason?.message || String(event.reason),
    stack: event.reason?.stack,
  });
});

React Error Boundary

typescript
import React from "react";

interface ErrorBoundaryState {
  hasError: boolean;
  errorId: string | null;
}

class AppErrorBoundary extends React.Component<
  { children: React.ReactNode },
  ErrorBoundaryState
> {
  state: ErrorBoundaryState = { hasError: false, errorId: null };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    const errorId = reportError({
      type: "react",
      message: error.message,
      stack: error.stack,
      componentStack: errorInfo.componentStack,
    });
    this.setState({ errorId });
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-fallback">
          <h2>出了點問題</h2>
          <p>錯誤編號:{this.state.errorId}</p>
          <button onClick={() => window.location.reload()}>
            刷新頁面
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

自定義業務指標

typescript
// 首次可交互時間
const fidObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.name === "first-input") {
      reportMetric({
        name: "FID",
        value: entry.processingStart - entry.startTime,
      });
    }
  }
});
fidObserver.observe({ type: "first-input", buffered: true });

// 自定義指標:首屏列表渲染完成
const listRenderStart = performance.now();
// ... 組件渲染邏輯
const listRenderEnd = performance.now();
reportMetric({
  name: "list-render-time",
  value: listRenderEnd - listRenderStart,
});

數據聚合和告警

typescript
// 後端聚合邏輯(簡化)
interface MetricSummary {
  page: string;
  p50: number;
  p90: number;
  p99: number;
  errorRate: number;
  sampleSize: number;
}

function aggregateMetrics(metrics: RawMetric[]): MetricSummary[] {
  return Object.entries(groupBy(metrics, "page")).map(
    ([page, entries]) => {
      const values = entries.map((e) => e.value).sort((a, b) => a - b);
      return {
        page,
        p50: percentile(values, 0.5),
        p90: percentile(values, 0.9),
        p99: percentile(values, 0.99),
        errorRate: entries.filter((e) => e.rating === "poor").length / values.length,
        sampleSize: values.length,
      };
    }
  );
}

小結

  • 前端監控應覆蓋錯誤、性能、用户行為三個層次
  • Web Vitals 是性能監控的標準化基礎,必採
  • 錯誤捕獲要覆蓋運行時錯誤、Promise rejection、資源加載、React 渲染
  • 自定義業務指標讓監控更貼近產品價值
  • 聚合分析比單條錯誤日誌更重要——看趨勢、看 P99

MIT Licensed