Yasin

Yasin

前端监控体系设计

前端监控体系设计


监控什么

前端监控分四大类:

前端监控
├── 1. 性能监控(页面加载快不快)
├── 2. 错误监控(有没有报错)
├── 3. 行为监控(用户在做什么)
└── 4. 接口监控(请求成功率、耗时)

1. 性能监控

采集指标

// utils/monitor/performance.ts

export function collectPerformance() {
  // FCP
  new PerformanceObserver((list) => {
    const entry = list.getEntries()[0];
    report({ type: "performance", name: "FCP", value: entry.startTime });
  }).observe({ type: "paint", buffered: true });

  // LCP
  new PerformanceObserver((list) => {
    const entries = list.getEntries();
    const lastEntry = entries[entries.length - 1];
    report({ type: "performance", name: "LCP", value: lastEntry.startTime });
  }).observe({ type: "largest-contentful-paint", buffered: true });

  // CLS
  let clsScore = 0;
  new PerformanceObserver((list) => {
    list.getEntries().forEach((entry) => {
      clsScore += (entry as any).value;
    });
    report({ type: "performance", name: "CLS", value: clsScore });
  }).observe({ type: "layout-shift", buffered: true });

  // TTFB + 其他导航指标
  window.addEventListener("load", () => {
    const timing = performance.getEntriesByType(
      "navigation",
    )[0] as PerformanceNavigationTiming;
    report({
      type: "performance",
      name: "TTFB",
      value: timing.responseStart - timing.requestStart,
    });
    report({
      type: "performance",
      name: "DOMContentLoaded",
      value: timing.domContentLoadedEventEnd,
    });
    report({ type: "performance", name: "Load", value: timing.loadEventEnd });
  });
}

2. 错误监控

JS 运行时错误

// utils/monitor/error.ts

// 全局 JS 错误
window.addEventListener("error", (event) => {
  report({
    type: "js-error",
    message: event.message,
    filename: event.filename,
    lineno: event.lineno,
    colno: event.colno,
    stack: event.error?.stack,
  });
});

// Promise 未捕获错误
window.addEventListener("unhandledrejection", (event) => {
  report({
    type: "promise-error",
    message: event.reason?.message || String(event.reason),
    stack: event.reason?.stack,
  });
});

资源加载错误(图片、JS、CSS 加载失败)

window.addEventListener(
  "error",
  (event) => {
    const target = event.target as HTMLElement;
    if (target.tagName) {
      report({
        type: "resource-error",
        tagName: target.tagName,
        src:
          (target as HTMLImageElement).src || (target as HTMLScriptElement).src,
      });
    }
  },
  true,
); // 注意用捕获阶段,资源错误不冒泡

React 错误边界

// components/ErrorBoundary.tsx
import React from "react";

class ErrorBoundary extends React.Component<
  { children: React.ReactNode },
  { hasError: boolean }
> {
  state = { hasError: false };

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

  componentDidCatch(error: Error, info: React.ErrorInfo) {
    report({
      type: "react-error",
      message: error.message,
      stack: error.stack,
      componentStack: info.componentStack,
    });
  }

  render() {
    if (this.state.hasError) {
      return <div>页面出错了,请刷新重试</div>;
    }
    return this.props.children;
  }
}

3. 行为监控

PV / UV 统计

// 页面访问
export function trackPageView() {
  report({
    type: "pv",
    url: window.location.href,
    referrer: document.referrer,
    timestamp: Date.now(),
  });
}

用户行为路径(面包屑)

// 记录用户操作路径,出错时一起上报
const breadcrumbs: any[] = [];

document.addEventListener("click", (event) => {
  const target = event.target as HTMLElement;
  breadcrumbs.push({
    type: "click",
    tagName: target.tagName,
    text: target.innerText?.slice(0, 50),
    path: getXPath(target),
    timestamp: Date.now(),
  });

  // 只保留最近 20 条
  if (breadcrumbs.length > 20) breadcrumbs.shift();
});

// 报错时带上行为路径
function reportWithBreadcrumbs(error: any) {
  report({
    ...error,
    breadcrumbs: [...breadcrumbs],
  });
}

4. 接口监控

拦截 fetch / XMLHttpRequest

// 拦截 fetch
const originalFetch = window.fetch;
window.fetch = async (input, init) => {
  const url = typeof input === "string" ? input : input.url;
  const startTime = Date.now();

  try {
    const response = await originalFetch(input, init);
    report({
      type: "api",
      url,
      method: init?.method || "GET",
      status: response.status,
      duration: Date.now() - startTime,
      success: response.ok,
    });
    return response;
  } catch (error) {
    report({
      type: "api-error",
      url,
      method: init?.method || "GET",
      duration: Date.now() - startTime,
      message: (error as Error).message,
    });
    throw error;
  }
};

数据上报

// utils/monitor/report.ts

const reportQueue: any[] = [];
let timer: ReturnType<typeof setTimeout> | null = null;

export function report(data: any) {
  reportQueue.push({
    ...data,
    timestamp: Date.now(),
    url: window.location.href,
    userAgent: navigator.userAgent,
  });

  // 批量上报(每 5 秒或满 10 条)
  if (reportQueue.length >= 10) {
    flush();
  } else if (!timer) {
    timer = setTimeout(flush, 5000);
  }
}

function flush() {
  if (reportQueue.length === 0) return;

  const data = [...reportQueue];
  reportQueue.length = 0;
  timer = null;

  // 用 sendBeacon 上报(页面关闭时也能发出去)
  if (navigator.sendBeacon) {
    navigator.sendBeacon("/api/monitor", JSON.stringify(data));
  } else {
    fetch("/api/monitor", {
      method: "POST",
      body: JSON.stringify(data),
      keepalive: true,
    });
  }
}

// 页面关闭时上报剩余数据
window.addEventListener("beforeunload", flush);

在项目中怎么接入

统一初始化入口

// utils/monitor/index.ts
import { collectPerformance } from "./performance";
import { initErrorMonitor } from "./error";
import { trackPageView } from "./behavior";
import { interceptFetch } from "./api";

export function initMonitor() {
  collectPerformance();
  initErrorMonitor();
  trackPageView();
  interceptFetch();
}

在应用入口调用

// app/layout.tsx(Next.js)
"use client";
import { useEffect } from "react";
import { initMonitor } from "@/utils/monitor";

export default function RootLayout({ children }) {
  useEffect(() => {
    initMonitor();
  }, []);

  return (
    <html>
      <body>
        <ErrorBoundary>{children}</ErrorBoundary>
      </body>
    </html>
  );
}

完整项目结构

utils/
└── monitor/
    ├── index.ts         // 统一入口
    ├── performance.ts   // 性能监控
    ├── error.ts         // 错误监控
    ├── behavior.ts      // 行为监控
    ├── api.ts           // 接口监控
    └── report.ts        // 数据上报
components/
└── ErrorBoundary.tsx    // React 错误边界

上报后端做什么

前端上报数据 → API 接收 → 存储(ES/ClickHouse)→ 可视化面板(Grafana)
                                              报警规则(错误率突增、接口慢)
                                              通知(钉钉/企微/邮件)

也可以用现成方案

方案 说明
Sentry 错误监控,开源,最流行
阿里 ARMS 全链路监控
腾讯 Aegis 前端监控
自建 灵活,但维护成本高