前端錯誤監控全攻略:Sentry、SourceMap 與使用者無感知的異常擷取
前端工程(更新於 2026年6月2日)
為什麼前端錯誤監控不可或缺?
線上使用者遇到白屏、按鈕無回應、介面報錯——如果你不知道,就無法修復。前端錯誤監控是連接使用者真實體驗與開發者認知的橋樑。
| 不監控的代價 | 監控的收益 |
|---|---|
| 使用者投訴才發現 Bug | 即時感知異常,分鐘級回應 |
| 無法重現的偶發問題 | 完整堆疊 + 使用者行為回放 |
| 線上故障持續數小時 | 自動告警,MTTR 大幅縮短 |
| 修復靠猜測 | 資料驅動,精準定位 |
前端錯誤的五大來源
1. 語法與執行時錯誤
// ReferenceError: x is not defined
console.log(x);
// TypeError: Cannot read properties of undefined
const user = undefined;
user.name;
2. 資源載入錯誤
// 圖片、指令碼、樣式表載入失敗
const img = new Image();
img.src = '/broken-image.png';
img.onerror = () => {
trackError({ type: 'resource', tag: 'img', src: img.src });
};
3. 未擷取的 Promise 例外
// 最容易被遺漏的錯誤型別
fetch('/api/data')
.then(res => res.json())
// .then 中丟擲例外但無 .catch
.then(data => data.results.map(r => r.name));
4. 介面例外
// HTTP 錯誤碼 + 業務錯誤碼
fetch('/api/order')
.then(res => {
if (!res.ok) {
trackError({ type: 'http', status: res.status, url: res.url });
}
return res.json();
});
5. Web Worker / iframe 錯誤
// Worker 內部例外不會冒泡到主執行緒
const worker = new Worker('/worker.js');
worker.onerror = (e) => {
trackError({
type: 'worker',
message: e.message,
filename: e.filename,
lineno: e.lineno,
});
};
全域錯誤擷取:四道防線
防線一:window.onerror
window.onerror = (message, source, lineno, colno, error) => {
trackError({
type: 'runtime',
message,
source,
lineno,
colno,
stack: error?.stack,
});
return false; // 不阻止預設控制台輸出
};
侷限:無法擷取資源載入錯誤和 Promise 例外。
防線二:window.addEventListener('error')
window.addEventListener('error', (event) => {
if (event.target instanceof HTMLElement) {
// 資源載入錯誤
trackError({
type: 'resource',
tagName: event.target.tagName,
src: event.target.src || event.target.href,
});
} else {
// 執行時錯誤(與 onerror 重複,需去重)
trackError({
type: 'runtime',
message: event.message,
stack: event.error?.stack,
});
}
}, true); // 使用擷取階段
防線三:window.addEventListener('unhandledrejection')
window.addEventListener('unhandledrejection', (event) => {
trackError({
type: 'unhandledRejection',
reason: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
});
});
防線四:React Error Boundary
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
trackError({
type: 'react',
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
});
}
render() {
if (this.state.hasError) {
return <FallbackUI onRetry={() => this.setState({ hasError: false })} />;
}
return this.props.children;
}
}
SourceMap 反解:從壓縮程式碼還原真實堆疊
線上程式碼經過壓縮混淆,錯誤堆疊毫無可讀性。SourceMap 反解是線上排障的核心能力。
壓縮後的堆疊範例
TypeError: Cannot read properties of undefined (reading 'name')
at a (main.abc123.js:1:2345)
at o (main.abc123.js:1:5678)
at t (main.abc123.js:1:9012)
反解後的真實堆疊
TypeError: Cannot read properties of undefined (reading 'name')
at getUserInfo (src/services/user.ts:42:15)
at fetchDashboard (src/pages/dashboard.tsx:128:22)
at useEffectCallback (src/pages/dashboard.tsx:115:5)
SourceMap 安全策略
| 方案 | 安全性 | 複雜度 | 適用場景 |
|---|---|---|---|
| 私有 NPM + CI 反解 | 高 | 中 | 大多數團隊 |
| Sentry 伺服器端反解 | 中 | 低 | 快速接入 |
| Base64 內聯 SourceMap | 低 | 低 | 僅開發環境 |
| 獨立 .map 檔案上線 | 極低 | 低 | 不推薦 |
// 使用 source-map 庫反解
import { SourceMapConsumer } from 'source-map';
async function applySourceMap(position, mapContent) {
const consumer = await new SourceMapConsumer(mapContent);
const original = consumer.originalPositionFor({
line: position.line,
column: position.column,
});
return original;
}
Sentry 整合實戰
安裝與初始化
npm install @sentry/react @sentry/tracing
import * as Sentry from '@sentry/react';
Sentry.init({
dsn: 'https://xxx@o123456.ingest.sentry.io/789',
environment: process.env.NODE_ENV,
release: process.env.APP_VERSION,
tracesSampleRate: 0.1,
replaysSessionSampleRate: 0.01,
replaysOnErrorSampleRate: 1.0,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration(),
],
beforeSend(event, hint) {
// 過濾無意義錯誤
if (event.exception?.values?.[0]?.type === 'ResizeObserver loop limit exceeded') {
return null;
}
return event;
},
});
React 路由整合
const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
function App() {
return (
<SentryRoutes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</SentryRoutes>
);
}
手動上報與麵包屑
// 手動上報業務例外
Sentry.captureException(new Error('訂單支付逾時'), {
tags: { orderId: 'ORD-12345', paymentMethod: 'alipay' },
extra: { retryCount: 3, lastAttempt: Date.now() },
});
// 添加麵包屑(使用者行為軌跡)
Sentry.addBreadcrumb({
category: 'ui.click',
message: '點選提交按鈕',
level: 'info',
});
自建輕量監控體系
如果不想依賴第三方服務,可以自建監控:
資料採集 SDK
class ErrorMonitor {
constructor(options) {
this.endpoint = options.endpoint;
this.queue = [];
this.timer = null;
this.install();
}
install() {
window.addEventListener('error', this.handleError.bind(this), true);
window.addEventListener('unhandledrejection', this.handleRejection.bind(this));
}
handleError(event) {
this.report({
type: event.target instanceof HTMLElement ? 'resource' : 'runtime',
message: event.message || event.target?.outerHTML,
stack: event.error?.stack,
timestamp: Date.now(),
url: location.href,
ua: navigator.userAgent,
});
}
handleRejection(event) {
this.report({
type: 'unhandledRejection',
message: event.reason?.message || String(event.reason),
stack: event.reason?.stack,
timestamp: Date.now(),
});
}
report(data) {
this.queue.push(data);
if (!this.timer) {
this.timer = setTimeout(() => this.flush(), 5000);
}
}
flush() {
if (this.queue.length === 0) return;
const batch = this.queue.splice(0);
navigator.sendBeacon(this.endpoint, JSON.stringify(batch));
this.timer = null;
}
}
關鍵指標儀表板
| 指標 | 計算方式 | 告警閾值 |
|---|---|---|
| JS 錯誤率 | 錯誤 PV / 總 PV | > 0.1% |
| 介面失敗率 | 5xx 數 / 總請求數 | > 1% |
| 首屏崩潰率 | 白屏 PV / 總 PV | > 0.01% |
| 平均修復時間 | 首次告警到部署耗時 | > 30min |
最佳實踐清單
- 四道防線全覆蓋:onerror + error 事件 + unhandledrejection + Error Boundary
- SourceMap 不上線:構建產物分離,CI 環境反解
- 錯誤取樣與過濾:避免高頻無意義錯誤淹沒真實問題
- 使用者行為回放:麵包屑 + Session Replay 加速定位
- 告警分級:P0 立即通知,P1 工單跟蹤,P2 週報彙總
- 版本關聯:每次釋出攜帶版本號,錯誤自動關聯程式碼變更
- 隱私合規:上報資料脫敏,不包含使用者個人資訊
- 效能關聯:錯誤監控與效能監控聯動,發現效能劣化根因
本站提供瀏覽器本地工具,免註冊即可試用 →
#错误监控#Sentry#SourceMap#异常捕获#DevOps