字节笔记本

2026年2月22日

miracle90/monitor:完整的前端监控体系开源项目

本文介绍 miracle90/monitor,一个完整的前端监控体系开源项目。该项目提供了错误监控、白屏检测、性能监控、卡顿检测、PV 统计等核心功能,帮助开发者构建完整的前端监控解决方案。

项目简介

miracle90/monitor 是一个开源的前端监控 SDK 项目,由开发者 miracle90 维护。截至目前,该项目在 GitHub 上已获得 860+ stars163 forks,是一个值得关注的前端监控方案。

该项目涵盖了前端监控的完整链路,从数据采集、上报到可视化展示,为前端工程师提供了技术深度和广度的实践案例。

为什么要做前端监控

  • 更快地发现和解决问题 - 实时捕获线上异常,缩短故障响应时间
  • 做产品的决策依据 - 基于真实用户数据优化产品体验
  • 为业务扩展提供更多可能性 - 数据驱动业务增长
  • 提升前端工程师的技术深度和广度 - 打造简历亮点

前端监控目标

稳定性 (Stability)

  • JS 错误:JS 执行错误、Promise 异常
  • 资源错误:JS、CSS 资源加载异常
  • 接口错误:Ajax、Fetch 请求接口异常
  • 白屏:页面空白

用户体验 (Experience)

  • 页面加载性能
  • 交互响应速度
  • 卡顿检测

业务 (Business)

  • PV:页面浏览量和点击量
  • UV:访问站点的不同 IP 人数
  • 用户在每个页面的停留时间

前端监控流程

  1. 前端埋点 - 采集用户行为和性能数据
  2. 数据上报 - 将采集数据发送到服务端
  3. 加工汇总 - 服务端处理和存储数据
  4. 可视化展示 - 数据报表和图表展示
  5. 监控报警 - 异常实时告警通知

核心功能实现

1. 错误监控

JS 执行错误 + 资源加载错误

javascript
window.addEventListener(
  "error",
  function (event) {
    // 资源加载错误
    if (event.target && (event.target.src || event.target.href)) {
      tracker.send({
        kind: "stability",
        type: "error",
        errorType: "resourceError",
        filename: event.target.src || event.target.href,
        tagName: event.target.tagName,
        timeStamp: formatTime(event.timeStamp),
        selector: getSelector(event.path || event.target),
      });
    } else {
      // JS 执行错误
      tracker.send({
        kind: "stability",
        type: "error",
        errorType: "jsError",
        message: event.message,
        filename: event.filename,
        position: (event.lineNo || 0) + ":" + (event.columnNo || 0),
        stack: getLines(event.error.stack),
        selector: lastEvent ? getSelector(lastEvent.path || lastEvent.target) : "",
      });
    }
  },
  true
);

Promise 异常

javascript
window.addEventListener(
  "unhandledrejection",
  function (event) {
    let message = "";
    let file = "";
    let line = 0;
    let column = 0;
    let stack = "";

    if (typeof event.reason === "string") {
      message = event.reason;
    } else if (typeof event.reason === "object") {
      message = event.reason.message;
    }

    let reason = event.reason;
    if (typeof reason === "object" && reason.stack) {
      var matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/);
      if (matchResult) {
        file = matchResult[1];
        line = matchResult[2];
        column = matchResult[3];
      }
      stack = getLines(reason.stack);
    }

    tracker.send({
      kind: "stability",
      type: "error",
      errorType: "promiseError",
      message: message,
      filename: file,
      position: line + ":" + column,
      stack,
      selector: lastEvent ? getSelector(lastEvent.path || lastEvent.target) : "",
    });
  },
  true
);

2. 接口异常监控

XMLHttpRequest 拦截

javascript
export function injectXHR() {
  let XMLHttpRequest = window.XMLHttpRequest;
  let oldOpen = XMLHttpRequest.prototype.open;

  XMLHttpRequest.prototype.open = function (
    method,
    url,
    async,
    username,
    password
  ) {
    if (!url.match(/logstores/) && !url.match(/sockjs/)) {
      this.logData = { method, url, async, username, password };
    }
    return oldOpen.apply(this, arguments);
  };

  let oldSend = XMLHttpRequest.prototype.send;
  let start;

  XMLHttpRequest.prototype.send = function (body) {
    if (this.logData) {
      start = Date.now();
      let handler = (type) => (event) => {
        let duration = Date.now() - start;
        let status = this.status;
        let statusText = this.statusText;

        tracker.send({
          kind: "stability",
          type: "xhr",
          eventType: type,
          pathname: this.logData.url,
          status: status + "-" + statusText,
          duration: "" + duration,
          response: this.response ? JSON.stringify(this.response) : "",
          params: body || "",
        });
      };

      this.addEventListener("load", handler("load"), false);
      this.addEventListener("error", handler("error"), false);
      this.addEventListener("abort", handler("abort"), false);
    }
    oldSend.apply(this, arguments);
  };
}

Fetch API 拦截

javascript
export function injectFetch() {
  let oldFetch = window.fetch;

  function hijackFetch(url, options) {
    let startTime = Date.now();

    return new Promise((resolve, reject) => {
      oldFetch.apply(this, [url, options]).then(async (response) => {
        const oldResponseJson = response.__proto__.json;
        response.__proto__.json = function (...responseRest) {
          return new Promise((responseResolve, responseReject) => {
            oldResponseJson.apply(this, responseRest).then(
              (result) => {
                responseResolve(result);
              },
              (responseRejection) => {
                sendLogData({
                  url,
                  startTime,
                  statusText: response.statusText,
                  status: response.status,
                  eventType: "error",
                  response: responseRejection.stack,
                  options,
                });
                responseReject(responseRejection);
              }
            );
          });
        };
        resolve(response);
      }, rejection => {
        sendLogData({
          url,
          startTime,
          eventType: "load",
          response: rejection.stack,
          options,
        });
        reject(rejection);
      });
    });
  }

  window.fetch = hijackFetch;
}

3. 白屏检测

使用 elementsFromPoint API 检测页面是否白屏:

javascript
export function blankScreen() {
  let NUM = 20; // 控制检测密集度
  let wrapperElements = ["html", "body", "#container"];
  let emptyPoints = 0;

  function isWrapper(element) {
    if (!element) {
      emptyPoints++;
      return;
    }
    let selector = getSelector(element);
    if (wrapperElements.indexOf(selector) !== -1) {
      emptyPoints++;
    }
  }

  onload(function () {
    const portion = NUM + 1;
    const xPortion = window.innerWidth / portion;
    const yPortion = window.innerHeight / portion;

    for (let i = 0; i < NUM; i++) {
      let xElements = document.elementFromPoint(xPortion * i, window.innerHeight / 2);
      let yElements = document.elementFromPoint(window.innerWidth / 2, yPortion * i);
      let xyDownElements = document.elementFromPoint(xPortion * i, yPortion * i);
      let xyUpElements = document.elementFromPoint(xPortion * i, yPortion * (NUM - i));

      isWrapper(xElements);
      isWrapper(yElements);
      isWrapper(xyDownElements);
      isWrapper(xyUpElements);
    }

    // 检测到白屏
    if (emptyPoints == 4 * NUM) {
      const centerElements = document.elementsFromPoint(
        window.innerWidth / 2,
        window.innerHeight / 2
      );

      tracker.send({
        kind: "stability",
        type: "blank",
        emptyPoints: emptyPoints + "",
        screen: window.screen.width + "X" + window.screen.height,
        viewPoint: window.innerWidth + "X" + window.innerHeight,
        selector: getSelector(centerElements[0]),
      });
    }
  });
}

4. 性能指标监控

javascript
export function timing() {
  onload(function () {
    setTimeout(() => {
      const {
        fetchStart,
        connectStart,
        connectEnd,
        requestStart,
        responseStart,
        responseEnd,
        domLoading,
        domInteractive,
        domContentLoadedEventStart,
        domContentLoadedEventEnd,
        loadEventStart,
      } = performance.timing;

      tracker.send({
        kind: "experience",
        type: "timing",
        connectTime: connectEnd - connectStart, // TCP连接耗时
        ttfbTime: responseStart - requestStart, // 首字节到达时间
        responseTime: responseEnd - responseStart, // Response响应耗时
        parseDOMTime: loadEventStart - domLoading, // DOM解析耗时
        domContentLoadedTime: domContentLoadedEventEnd - domContentLoadedEventStart, // DOMContentLoaded事件耗时
        timeToInteractive: domInteractive - fetchStart, // 首次可交互时间
        loadTime: loadEventStart - fetchStart, // 完整的加载时间
      });
    }, 3000);
  });
}

5. 卡顿检测 (FPS)

javascript
export function fps() {
  let frame = 0;
  let lastSecond = performance.now();
  let lastFrameTime = performance.now();
  let longTaskDuration = 0;

  function calculateFPS(currentTime) {
    frame++;
    const offset = currentTime - lastSecond;

    if (offset >= 1000) {
      const fps = Math.round((frame * 1000) / offset);

      if (fps < 20) {
        tracker.send({
          kind: "experience",
          type: "fps",
          fps: fps,
          longTaskDuration: longTaskDuration,
        });
      }

      frame = 0;
      longTaskDuration = 0;
      lastSecond = currentTime;
    }

    // 检测长任务 (>50ms)
    const frameDuration = currentTime - lastFrameTime;
    if (frameDuration > 50) {
      longTaskDuration += frameDuration;
    }
    lastFrameTime = currentTime;

    requestAnimationFrame(calculateFPS);
  }

  requestAnimationFrame(calculateFPS);
}

数据上报格式

JS 错误数据

json
{
  "title": "前端监控系统",
  "url": "http://localhost:8080/",
  "timestamp": "1590815288710",
  "userAgent": "Chrome",
  "kind": "stability",
  "type": "error",
  "errorType": "jsError",
  "message": "Uncaught TypeError: Cannot set property 'error' of undefined",
  "filename": "http://localhost:8080/",
  "position": "0:0",
  "stack": "btnClick (http://localhost:8080/:20:39)^HTMLInputElement.onclick...",
  "selector": "HTML BODY #container .content INPUT"
}

接口监控数据

json
{
  "title": "前端监控系统",
  "url": "http://localhost:8080/",
  "timestamp": "1590817024490",
  "userAgent": "Chrome",
  "kind": "stability",
  "type": "xhr",
  "eventType": "load",
  "pathname": "/api/user",
  "status": "200-OK",
  "duration": "7",
  "response": "{\"id\":1}",
  "params": "name=zhufeng"
}

埋点方案对比

方案优点缺点
代码埋点精确,任意时刻,数据量全面代码工作量大
可视化埋点业务代码和埋点代码分离,系统化管理需要额外系统支持
无痕埋点采集全量数据,不会漏埋和误埋数据传输和服务器压力大,无法灵活定制数据结构

项目结构

text
monitor/
├── public/           # 前端监控 SDK
├── src/              # 源码目录
│   ├── index.js      # 入口文件
│   ├── utils/        # 工具函数
│   └── monitor/      # 监控模块
├── server.js         # 服务端接收脚本
├── package.json
└── README.md

快速开始

安装依赖

bash
# 克隆项目
git clone https://github.com/miracle90/monitor.git
cd monitor

# 安装依赖
npm install
# 或
yarn

启动服务

bash
node server.js

接入监控 SDK

html
<script src="./monitor/index.js"></script>
<script>
  // 初始化监控
  monitor.init({
    url: 'http://localhost:3000/log',
    appId: 'your-app-id'
  });
</script>

适用场景

  • 大型 Web 应用线上监控
  • 微前端架构下的统一监控
  • 需要自定义监控指标的项目
  • 学习前端监控体系实现原理

项目链接

总结

miracle90/monitor 是一个功能完整、实现清晰的前端监控开源项目,涵盖了错误监控、性能监控、白屏检测、卡顿检测等核心功能。项目代码结构清晰,注释详细,非常适合作为学习前端监控体系实现的参考资料,也可作为实际项目的监控方案基础。

分享: