从零搭建一个前端监控平台,我是怎么做到的?

53 阅读5分钟

前端监控是每个成熟团队的标配,但市面上的方案要么太贵(Sentry),要么太重(自建 ELK)。于是我花了两周时间,从零撸了一个轻量级的前端监控平台,今天分享给大家。

🤔 为什么要自己造轮子?

先说说背景。我们团队之前用的是 Sentry,功能确实强大,但有几个痛点:

  1. - 按事件量收费,错误一多钱包就遭不住
  2. - 国外服务器,上报和查询都有延迟
  3. - 很多功能用不上,但 SDK 体积在那摆着

更重要的是,作为一个前端,我想搞清楚监控平台到底是怎么实现的。

于是就有了这个项目 —— Sentinel,一个轻量级的前端监控平台。

✨ 先看效果

Dashboard 概览

image.png

错误详情 + 会话回放

image.png

image.png

SourceMap 还原

// 压缩后的报错,看了想打人
Error at index-Cx2rbgKI.js:1:5678

// 还原后,瞬间清晰
📍 src/utils/calculate.ts:15:10 (divideNumbers)
   const result = a / b;  // b 是 0!

🏗️ 整体架构

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   SDK       │ ──▶ │   Server    │ ──▶ │  Dashboard  │
│  (采集上报)  │     │  (存储处理)  │     │  (可视化)   │
└─────────────┘     └─────────────┘     └─────────────┘

技术栈:

  • SDK: TypeScript,< 10KB gzip
  • Server: Express + PostgreSQL
  • Dashboard: Vue 3 + ECharts
  • 工具链: Vite/Webpack 插件 + VSCode 扩展

🔧 核心实现

1. 错误捕获

这是监控的基础,主要靠两个 API:

// 捕获同步错误
window.onerror = (message, source, lineno, colno, error) => {
  report({
    type: 'error',
    message,
    stack: error?.stack,
    filename: source,
    lineno,
    colno
  });
};

// 捕获 Promise 异常
window.onunhandledrejection = (event) => {
  report({
    type: 'unhandledrejection',
    message: event.reason?.message || String(event.reason),
    stack: event.reason?.stack
  });
};

踩坑点:跨域脚本的错误只能拿到 Script error.,需要给 script 标签加 crossorigin 属性。

2. 性能采集

PerformanceObserver 采集 Web Vitals:

// LCP - 最大内容绘制
new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const lcp = entries[entries.length - 1];
  console.log('LCP:', lcp.startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });

// 长任务监控
new PerformanceObserver((list) => {
  list.getEntries().forEach(entry => {
    if (entry.duration > 50) {
      console.log('Long Task:', entry.duration, 'ms');
    }
  });
}).observe({ type: 'longtask', buffered: true });

采集的指标:

指标说明建议值
FCP首次内容绘制< 1.8s
LCP最大内容绘制< 2.5s
FID首次输入延迟< 100ms
CLS累积布局偏移< 0.1
TTFB首字节时间< 600ms

3. 用户行为追踪

错误发生时,光有堆栈还不够,还需要知道用户做了什么。

// 追踪点击
document.addEventListener('click', (e) => {
  addBreadcrumb({
    type: 'click',
    message: `Click on ${getSelector(e.target)}`,
    timestamp: Date.now()
  });
}, true);

// 追踪路由
const originalPushState = history.pushState;
history.pushState = function(...args) {
  originalPushState.apply(this, args);
  addBreadcrumb({
    type: 'route',
    message: `Navigate to ${location.href}`,
    timestamp: Date.now()
  });
};

// 追踪请求
const originalFetch = window.fetch;
window.fetch = async (url, options) => {
  const start = Date.now();
  const response = await originalFetch(url, options);
  addBreadcrumb({
    type: 'fetch',
    message: `${options?.method || 'GET'} ${url}`,
    data: { status: response.status, duration: Date.now() - start }
  });
  return response;
};

最终效果:

[10:23:45] Click on button.submit-btn
[10:23:46] POST /api/login → 200 (234ms)
[10:23:47] Navigate to /dashboard
[10:23:48] GET /api/user → 401 (89ms)
[10:23:48] ❌ Error: Unauthorized  ← 错误发生

4. 会话录制(杀手锏)

这是我最喜欢的功能。基于 rrweb 实现,可以录制用户操作并回放。

import { record } from 'rrweb';

const events = [];
record({
  emit: (event) => {
    events.push(event);
    // 保留最近 30 秒的录制
    if (events.length > 1000) events.shift();
  },
  maskAllInputs: true,  // 隐私保护:屏蔽输入内容
});

// 错误发生时,保存最近 10 秒的录制
function onError(error) {
  const recentEvents = getRecentEvents(10); // 最近 10 秒
  report({
    ...error,
    sessionReplay: recentEvents
  });
}

效果:用户说"页面白屏了",你可以直接看到他做了什么操作导致的,不用再猜了!

5. SourceMap 解析

生产环境代码都是压缩过的,报错信息根本看不懂。解决方案是上传 SourceMap:

// Vite 插件
export function sentinelSourcemapPlugin(options) {
  return {
    name: 'sentinel-sourcemap',
    async writeBundle(_, bundle) {
      for (const [filename, chunk] of Object.entries(bundle)) {
        if (filename.endsWith('.map')) {
          await uploadSourcemap({
            file: chunk.source,
            filename,
            dsn: options.dsn,
            version: options.version
          });
        }
      }
    }
  };
}

服务端用 source-map 库解析:

import { SourceMapConsumer } from 'source-map';

async function parseStack(stack, sourcemap) {
  const consumer = await new SourceMapConsumer(sourcemap);
  // index-Cx2rbgKI.js:1:5678 → src/utils.ts:15:10
  const original = consumer.originalPositionFor({ line: 1, column: 5678 });
  return {
    file: original.source,    // src/utils.ts
    line: original.line,      // 15
    column: original.column,  // 10
    name: original.name       // divideNumbers
  };
}

6. 错误聚合

同一个错误可能上报成千上万次,不能每条都存。解决方案是生成指纹:

function generateFingerprint(error) {
  // 归一化:去掉动态内容
  const normalized = error.message
    .replace(/\d+/g, '{N}')           // 数字 → {N}
    .replace(/['"][^'"]+['"]/g, '{S}') // 字符串 → {S}
    .replace(/[a-f0-9]{8,}/gi, '{H}'); // hash → {H}
  
  // 生成指纹
  return md5(`${error.type}:${normalized}:${error.filename}`);
}

// "User 12345 not found" 和 "User 67890 not found"
// 会被归类为同一个错误

7. 可靠上报

数据采集了,还得确保能发出去:

class Reporter {
  private queue: any[] = [];
  
  push(data) {
    this.queue.push(data);
    // 批量上报,减少请求
    if (this.queue.length >= 10) this.flush();
  }
  
  flush() {
    if (!navigator.onLine) {
      // 离线时存到 localStorage
      this.saveOffline(this.queue);
      return;
    }
    this.send(this.queue);
    this.queue = [];
  }
  
  // 页面关闭时用 sendBeacon 确保发送
  setupBeforeUnload() {
    window.addEventListener('beforeunload', () => {
      navigator.sendBeacon(this.url, JSON.stringify(this.queue));
    });
  }
}

📊 Dashboard 实现

用 Vue 3 + ECharts 搭建,主要功能:

  • 错误列表:支持搜索、筛选、分页
  • 错误详情:堆栈、用户行为、会话回放
  • 趋势图表:错误数量趋势、类型分布
  • 性能分析:Web Vitals 评分、资源瀑布图

核心组件:

<!-- 会话回放播放器 -->
<template>
  <div class="replay-player">
    <iframe ref="iframe" sandbox="allow-same-origin" />
    <div class="controls">
      <button @click="play">▶️</button>
      <input type="range" v-model="progress" />
      <span>{{ currentTime }} / {{ duration }}</span>
    </div>
  </div>
</template>

<script setup>
import { Replayer } from 'rrweb';

const replayer = new Replayer(props.events, {
  root: iframe.value.contentDocument.body
});
</script>

🔐 安全考虑

  1. 隐私保护:密码字段自动脱敏,支持自定义敏感字段
  2. SourceMap 安全:独立存储,不对外暴露
  3. 认证鉴权:JWT + 角色权限控制
  4. 数据传输:全程 HTTPS

📈 性能影响

SDK 对页面性能的影响:

指标影响
体积< 10KB gzip
内存< 5MB(含会话录制)
CPU< 1%(采样模式)
网络批量上报,每 5s 一次

🚀 快速体验

# 克隆项目
git clone https://github.com/name718/sentinel
cd sentinel

# 安装依赖
pnpm install

# 启动服务
pnpm dev:server    # 后端 http://localhost:3000
pnpm dev:demo      # 演示应用 http://localhost:5173
pnpm dev:dashboard # 管理后台 http://localhost:5174

SDK 接入:

import { init } from '@sentinel/sdk';

init({
  dsn: 'your-project-id',
  reportUrl: 'https://your-server.com/api/report',
  enableSessionReplay: true,
});

🗺️ 后续计划

  • 告警系统(钉钉、飞书、邮件)
  • 多项目支持
  • 小程序 SDK
  • AI 错误分类

💬 写在最后

这个项目从想法到落地花了大概两周时间,代码量不大但涉及的知识点挺多的:

  • 浏览器 API(Performance、MutationObserver、sendBeacon)
  • 数据处理(聚合、采样、压缩)
  • 隐私安全(脱敏、权限)
  • 工程化(Monorepo、插件开发)

如果你也想搭建自己的监控平台,或者想深入了解前端监控的原理,欢迎 Star ⭐️ 这个项目。

有问题欢迎评论区交流,我会持续更新这个系列。


相关链接


北京最靓的仔,一个喜欢折腾的前端。 👋