前端监控是每个成熟团队的标配,但市面上的方案要么太贵(Sentry),要么太重(自建 ELK)。于是我花了两周时间,从零撸了一个轻量级的前端监控平台,今天分享给大家。
🤔 为什么要自己造轮子?
先说说背景。我们团队之前用的是 Sentry,功能确实强大,但有几个痛点:
- 贵 - 按事件量收费,错误一多钱包就遭不住
- 慢 - 国外服务器,上报和查询都有延迟
- 重 - 很多功能用不上,但 SDK 体积在那摆着
更重要的是,作为一个前端,我想搞清楚监控平台到底是怎么实现的。
于是就有了这个项目 —— Sentinel,一个轻量级的前端监控平台。
✨ 先看效果
Dashboard 概览
错误详情 + 会话回放
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>
🔐 安全考虑
- 隐私保护:密码字段自动脱敏,支持自定义敏感字段
- SourceMap 安全:独立存储,不对外暴露
- 认证鉴权:JWT + 角色权限控制
- 数据传输:全程 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 ⭐️ 这个项目。
有问题欢迎评论区交流,我会持续更新这个系列。
相关链接:
- GitHub: github.com/name718/sen…
北京最靓的仔,一个喜欢折腾的前端。 👋