生产环境中,超过 60% 的前端 Bug 是用户先发现的,而不是开发者。根据 Sentry 2025 年度报告,接入错误监控的应用平均 Bug 修复时间从 72 小时缩短到 4 小时。前端错误监控不再是"锦上添花",而是每一个严肃项目的必备基础设施。本文将从零搭建一套完整的前端错误监控方案,涵盖 Sentry 自部署、Sourcemap 还原、自定义上报、React ErrorBoundary 等核心内容,全部附可运行代码。
🔧 一、错误监控方案选型
主流方案对比
选择错误监控方案时,核心考量维度包括:是否支持私有部署、Sourcemap 还原能力、价格、以及对前端框架的集成深度。
| 方案 | 私有部署 | Sourcemap | 免费额度 | 框架集成 | 推荐场景 |
|---|---|---|---|---|---|
| Sentry | ✅ 支持 | ✅ 自动 | 5K 事件/月 | React/Vue/Angular | 中大型项目首选 |
| LogRocket | ❌ 仅 SaaS | ✅ 自动 | 1K 会话/月 | React/Vue | 需要会话回放 |
| Bugsnag | ❌ 仅 SaaS | ✅ 自动 | 7.5K 事件/月 | 全框架 | 企业级 SaaS |
| 自研方案 | ✅ 完全控制 | ⚠️ 需自行实现 | 无限制 | 自定义 | 极致定制需求 |
| Fundebug | ❌ 仅 SaaS | ✅ 支持 | 1K 事件/月 | 全框架 | 国内项目 |
⚠️ **警告:**纯 SaaS 方案意味着你的错误堆栈、用户行为数据都存储在第三方服务器上。对于涉及敏感数据的项目,务必评估数据合规风险。
⚡ 关键结论
对于大多数项目,Sentry 自部署是最佳选择——开源免费、功能完整、社区活跃。如果你的团队不超过 20 人,Sentry 官方 SaaS 的免费额度完全够用,不必自部署。
🚀 二、Sentry 自部署与前端接入
Docker Compose 一键部署 Sentry
Sentry 官方提供了 self-hosted 项目,一个 docker-compose.yml 搞定全部依赖(PostgreSQL、Redis、Kafka、ClickHouse 等):
# 克隆 Sentry 自部署项目
git clone https://github.com/getsentry/self-hosted.git
cd self-hosted
# 执行安装脚本(会自动创建 .env 配置)
# 首次运行约需 10-15 分钟,取决于网络速度
./install.sh
# 启动所有服务
docker compose up -d
# 检查服务状态
docker compose ps
📌 **记住:**Sentry 自部署需要至少 4GB 内存和 20GB 磁盘空间。如果你的服务器只有 2GB 内存,建议直接使用 Sentry SaaS 免费版。
启动完成后访问 http://your-server:9000,创建管理员账号和第一个项目。
Vue 3 项目接入 Sentry
以 Vue 3 + Vite 项目为例,安装并配置 Sentry SDK:
# 安装 Sentry Vue SDK
npm install @sentry/vue
// main.ts - Sentry 初始化配置
import { createApp } from 'vue'
import { createRouter } from 'vue-router'
import * as Sentry from '@sentry/vue'
import App from './App.vue'
const app = createApp(App)
const router = createRouter({ /* ... */ })
Sentry.init({
app,
dsn: 'http://your-public-key@your-server:9000/2', // 自部署 DSN
environment: import.meta.env.MODE, // development / production
release: 'my-app@1.2.3', // 版本号,必须与 Sourcemap 上传时一致
integrations: [
Sentry.browserTracingIntegration({ router }),
Sentry.replayIntegration({ maskAllText: false }),
],
tracesSampleRate: 0.2, // 生产环境采样 20% 的请求
replaysSessionSampleRate: 0.01, // 1% 的会话录制
replaysOnErrorSampleRate: 1.0, // 出错时 100% 录制
beforeSend(event) {
// 过滤开发环境的错误
if (import.meta.env.DEV) return null
// 过滤浏览器扩展导致的错误
if (event.exception?.values?.[0]?.stacktrace?.frames?.some(
f => f.filename?.includes('extension://')
)) return null
return event
},
})
app.use(router)
app.mount('#app')
接入后,Sentry 会自动捕获未捕获异常(uncaught error)和未处理的 Promise rejection。你也可以手动上报:
// 手动捕获异常
try {
await riskyOperation()
} catch (error) {
Sentry.captureException(error, {
tags: { module: 'payment', action: 'checkout' },
extra: { orderId: '12345', amount: 99.9 },
})
}
// 捕获消息(非异常事件)
Sentry.captureMessage('用户触发了异常操作流程', 'warning')
🔐 三、Sourcemap 还原:从混淆代码定位源码
为什么 Sourcemap 如此重要
构建后的 JavaScript 经过压缩和混淆,错误堆栈指向的是 app.a1b2c3d4.js:1:23456 这样的位置,完全无法定位问题。Sourcemap(源码映射)能将压缩代码的位置还原回源文件的精确行号和列号。
💡 **提示:**Sourcemap 文件通常体积很大(与源码相当),绝对不能直接部署到生产环境的 CDN 上,否则等于公开你的完整源码。
Vite + Sentry Sourcemap 上传
// vite.config.ts - Sentry Sourcemap 上传配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { sentryVitePlugin } from '@sentry/vite-plugin'
export default defineConfig({
plugins: [
vue(),
sentryVitePlugin({
org: 'my-org',
project: 'my-project',
// 自部署 Sentry 的 URL
url: 'http://your-server:9000',
authToken: process.env.SENTRY_AUTH_TOKEN,
release: {
name: process.env.npm_package_version,
// 构建后自动上传 Sourcemap
uploadSourceMaps: true,
// 上传后自动删除本地 Sourcemap 文件
cleanArtifacts: true,
},
sourcemaps: {
assets: ['./dist/assets/**'],
// 忽略 node_modules 的 Sourcemap
ignore: ['node_modules/**'],
},
}),
],
build: {
// 必须生成 Sourcemap
sourcemap: true,
},
})
# 构建并上传 Sourcemap
SENTRY_AUTH_TOKEN=your-token npm run build
# 或者使用 Sentry CLI 手动上传
npx @sentry/cli releases new my-app@1.2.3
npx @sentry/cli releases files my-app@1.2.3 upload-sourcemaps ./dist/assets \
--url-prefix '~/assets' \
--validate
npx @sentry/cli releases finalize my-app@1.2.3
CI/CD 中集成 Sourcemap 上传
# .github/workflows/deploy.yml
name: Build & Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Build with Sourcemap
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: npm run build
- name: Upload Sourcemap to Sentry
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: |
npx @sentry/cli releases new "${{ github.sha }}"
npx @sentry/cli releases files "${{ github.sha }}" upload-sourcemaps ./dist/assets --url-prefix '~/assets'
npx @sentry/cli releases finalize "${{ github.sha }}"
- name: Deploy to server
run: |
# 部署时不包含 Sourcemap 文件
find ./dist -name '*.map' -delete
rsync -avz ./dist/ deploy@server:/www/site/
⚠️ **警告:**CI 中的
SENTRY_AUTH_TOKEN权限范围应设置为org:read+project:releases,绝不要使用管理员 Token。
🎯 四、React ErrorBoundary 优雅降级
ErrorBoundary 是 React 中捕获渲染错误的唯一手段——try/catch 无法捕获 JSX 渲染阶段的异常。
通用 ErrorBoundary 组件
// components/ErrorBoundary.jsx
import React from 'react'
import * as Sentry from '@sentry/react'
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error) {
return { hasError: true, error }
}
componentDidCatch(error, errorInfo) {
// 上报到 Sentry
Sentry.withScope((scope) => {
scope.setExtras(errorInfo)
scope.setTag('errorBoundary', this.props.name || 'root')
Sentry.captureException(error)
})
// 也可以上报到自定义服务
this.reportToCustomService(error, errorInfo)
}
reportToCustomService = async (error, errorInfo) => {
try {
await fetch('/api/error-report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now(),
}),
})
} catch (e) {
// 上报本身失败时静默处理
console.error('Error report failed:', e)
}
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div style={{ padding: 40, textAlign: 'center' }}>
<h2>😵 页面出错了</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => window.location.reload()}>
刷新页面
</button>
</div>
)
}
return this.props.children
}
}
// 使用方式
function App() {
return (
<ErrorBoundary name="app-root">
<Header />
<ErrorBoundary name="main-content">
<MainContent />
</ErrorBoundary>
<Footer />
</ErrorBoundary>
)
}
export default ErrorBoundary
分级错误处理策略
不是所有错误都需要同样的处理方式。根据组件的重要程度,设置不同的降级策略:
| 组件级别 | 错误影响 | 降级策略 | 示例 |
|---|---|---|---|
| 页面级 | 整页不可用 | 显示全屏错误页 + 自动上报 | 路由页面崩溃 |
| 区块级 | 局部功能失效 | 显示区块错误提示 + 上报 | 评论区组件崩溃 |
| 增强级 | 功能降级但可用 | 静默处理 + 批量上报 | 图表渲染失败 |
💡 **提示:**ErrorBoundary 无法捕获以下场景的错误:事件处理器(用
try/catch)、异步代码(用.catch())、服务端渲染、ErrorBoundary 自身的错误。
💡 五、自定义错误上报方案
如果你的项目规模较小,或者公司内部已有日志系统,可以搭建一套轻量级的自定义错误监控。
错误采集 SDK 实现
// error-tracker.js - 轻量级错误采集 SDK
class ErrorTracker {
constructor(options = {}) {
this.endpoint = options.endpoint || '/api/errors'
this.appId = options.appId || 'default'
this.batchSize = options.batchSize || 10
this.flushInterval = options.flushInterval || 5000
this.queue = []
this._init()
}
_init() {
// 1. 捕获未处理异常
window.addEventListener('error', (event) => {
this.capture({
type: 'js-error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
})
})
// 2. 捕获未处理的 Promise rejection
window.addEventListener('unhandledrejection', (event) => {
this.capture({
type: 'unhandled-rejection',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
})
})
// 3. 捕获资源加载失败
window.addEventListener('error', (event) => {
if (event.target?.tagName) {
this.capture({
type: 'resource-error',
tagName: event.target.tagName,
src: event.target.src || event.target.href,
})
}
}, true)
// 4. 捕获网络请求失败(fetch 拦截)
const originalFetch = window.fetch
window.fetch = async (...args) => {
try {
const response = await originalFetch(...args)
if (!response.ok) {
this.capture({
type: 'http-error',
url: typeof args[0] === 'string' ? args[0] : args[0].url,
status: response.status,
statusText: response.statusText,
})
}
return response
} catch (error) {
this.capture({
type: 'network-error',
url: typeof args[0] === 'string' ? args[0] : args[0].url,
message: error.message,
})
throw error
}
}
// 定时批量上报
setInterval(() => this._flush(), this.flushInterval)
// 页面关闭前上报剩余错误
window.addEventListener('beforeunload', () => this._flush())
}
capture(error) {
this.queue.push({
...error,
appId: this.appId,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now(),
})
if (this.queue.length >= this.batchSize) {
this._flush()
}
}
_flush() {
if (this.queue.length === 0) return
const errors = [...this.queue]
this.queue = []
// 使用 sendBeacon 保证页面关闭时也能发送
const blob = new Blob(
[JSON.stringify({ errors })],
{ type: 'application/json' }
)
navigator.sendBeacon(this.endpoint, blob)
}
}
// 初始化
const tracker = new ErrorTracker({
endpoint: 'https://your-server/api/errors',
appId: 'jsjson-web',
batchSize: 5,
flushInterval: 3000,
})
📌 记住:
navigator.sendBeacon是页面关闭前发送数据的最可靠方式。它比fetch的keepalive选项兼容性更好,且不受页面卸载阻塞。
后端接收与存储
// server/error-receiver.js - Node.js + Express 接收端
const express = require('express')
const app = express()
app.use(express.json({ limit: '1mb' }))
app.post('/api/errors', (req, res) => {
const { errors } = req.body
for (const error of errors) {
// 过滤噪音错误
if (isNoiseError(error)) continue
// 存储到数据库
db.collection('errors').insertOne({
...error,
receivedAt: new Date(),
fingerprint: generateFingerprint(error), // 错误指纹,用于去重
})
// 严重错误触发告警
if (error.type === 'js-error' && isCriticalError(error)) {
sendAlert({
title: `🚨 前端严重错误`,
message: error.message,
url: error.url,
stack: error.stack?.substring(0, 500),
})
}
}
res.status(204).end()
})
function isNoiseError(error) {
const noisePatterns = [
'Script error', // 跨域脚本错误
'ResizeObserver loop', // 无害的观察者循环
'Loading chunk', // 网络波动导致的 chunk 加载失败
'Non-Error promise rejection', // 空 Promise rejection
]
return noisePatterns.some(p => error.message?.includes(p))
}
function generateFingerprint(error) {
const crypto = require('crypto')
const key = `${error.type}:${error.filename}:${error.lineno}:${error.message}`
return crypto.createHash('md5').update(key).digest('hex')
}
✅ 六、生产环境最佳实践
错误采样策略
在高流量场景下,全量上报会带来巨大的网络和存储开销。推荐采用分级采样:
Sentry.init({
tracesSampleRate: 0.1, // 性能数据:10% 采样
replaysSessionSampleRate: 0.01, // 会话录制:1%
replaysOnErrorSampleRate: 1.0, // 出错时:100%
sampleRate: 1.0, // 错误事件:全量上报
})
隐私保护
错误上报可能包含用户敏感信息(URL 参数、表单数据),务必做脱敏处理:
beforeSend(event) {
// 脱敏 URL 中的敏感参数
if (event.request?.url) {
const url = new URL(event.request.url)
const sensitiveParams = ['token', 'password', 'code', 'phone']
sensitiveParams.forEach(p => url.searchParams.delete(p))
event.request.url = url.toString()
}
// 脱敏面包屑中的用户输入
if (event.breadcrumbs) {
event.breadcrumbs = event.breadcrumbs.map(b => {
if (b.category === 'ui.input') {
return { ...b, message: '[REDACTED]' }
}
return b
})
}
return event
}
监控指标
接入错误监控后,关注以下核心指标:
| 指标 | 健康值 | 警告值 | 说明 |
|---|---|---|---|
| 错误率 | < 0.1% | > 0.5% | 每千次页面访问的错误次数 |
| 影响用户比 | < 1% | > 5% | 受错误影响的独立用户占比 |
| 首次错误时间 | < 5 分钟 | > 30 分钟 | 错误发生到被发现的时间 |
| Sourcemap 还原率 | > 95% | < 80% | 能还原到源码的错误占比 |
📋 总结
前端错误监控的核心不是选择最贵的工具,而是建立一套从采集→上报→聚合→告警→修复的完整闭环。对于绝大多数项目,Sentry(无论是 SaaS 还是自部署)已经足够成熟。关键在于:
- ✅ 尽早接入——项目初期接入成本远低于后期补接
- ✅ Sourcemap 必须上传——没有 Sourcemap 的错误堆栈等于废纸
- ✅ 做好采样和脱敏——平衡监控精度与隐私合规
- ✅ ErrorBoundary 兜底——用户看到优雅降级比白屏好一万倍
- ✅ 建立告警机制——错误不被发现等于没监控
相关工具推荐:
- Sentry — 最成熟的开源错误监控平台
- LogRocket — 会话回放 + 错误追踪
- web-vitals — Google 出品的性能指标采集库
- jsjson.com JSON 格式化工具 — 格式化错误日志中的 JSON 数据
- jsjson.com Base64 编解码工具 — 处理错误上报中的编码数据