前端错误监控与排查体系实战指南
🚨 前端错误监控与排查体系实战指南
作为架构师,我会带你从零构建一个实用、高效的错误监控体系。这是每个项目都应该有的基础设施。
📦 第一步:基础错误监控SDK
1. 创建错误监控核心(可直接复制使用)
// src/utils/errorMonitor.js
class ErrorMonitor {
constructor(options = {}) {
this.config = {
// 必填:错误上报地址
endpoint: options.endpoint || 'https://your-api.com/error-collect',
// 采样率:1.0表示100%上报,0.1表示10%
sampleRate: options.sampleRate || 1.0,
// 开发环境显示友好错误界面
showUI: options.showUI ?? process.env.NODE_ENV !== 'production',
// 是否监控未处理的Promise错误
monitorUnhandledRejection: true,
// 是否监控控制台错误
monitorConsoleError: true,
// 错误聚合:相同错误10分钟内只报一次
deduplicate: true,
dedupeWindow: 10 * 60 * 1000, // 10分钟
};
this.errorQueue = [];
this.sentErrors = new Map(); // 用于去重
this.init();
}
// 🚀 初始化所有错误监控
init() {
// 1. 全局错误捕获
window.addEventListener('error', this.handleWindowError.bind(this));
// 2. Promise未捕获错误
window.addEventListener('unhandledrejection', this.handlePromiseError.bind(this));
// 3. 控制台错误重写
if (this.config.monitorConsoleError) {
this.wrapConsole();
}
// 4. 资源加载错误
window.addEventListener('error', this.handleResourceError.bind(this), true);
// 5. 网络请求错误
this.monitorFetch();
this.monitorXMLHttpRequest();
// 6. Vue/React框架错误(后面实现)
// 7. 用户行为追踪(辅助排查)
this.trackUserActions();
// 定期上报
setInterval(() => this.flush(), 10000);
window.addEventListener('beforeunload', () => this.flush(true));
console.log('✅ 错误监控已启动');
}
// 🪟 处理窗口错误(JS运行时错误)
handleWindowError(event) {
const { message, filename, lineno, colno, error } = event;
// 忽略跨域脚本错误(无法获取详情)
if (message === 'Script error.' && !filename) {
return;
}
const errorInfo = {
type: 'js_error',
message,
filename,
lineno,
colno,
stack: error?.stack,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
};
// 增强错误信息
this.enhanceError(errorInfo);
// 阻止默认错误显示
event.preventDefault();
// 上报
this.reportError(errorInfo);
// 开发环境显示友好界面
if (this.config.showUI) {
this.showErrorUI(errorInfo);
}
}
// 🤝 处理Promise错误
handlePromiseError(event) {
const error = event.reason;
const errorInfo = {
type: 'promise_error',
message: error?.message || 'Unhandled Promise rejection',
stack: error?.stack,
timestamp: Date.now(),
url: window.location.href,
};
this.reportError(errorInfo);
}
// 📦 处理资源加载错误
handleResourceError(event) {
const target = event.target;
// 只处理资源加载错误
if (target && (target.src || target.href)) {
const errorInfo = {
type: 'resource_error',
tagName: target.tagName,
resourceUrl: target.src || target.href,
timestamp: Date.now(),
url: window.location.href,
};
this.reportError(errorInfo);
}
}
// 🌐 监控fetch请求
monitorFetch() {
const originalFetch = window.fetch;
window.fetch = async (...args) => {
const startTime = Date.now();
const [url, options = {}] = args;
// 生成请求ID用于追踪
const requestId = Math.random().toString(36).substr(2, 9);
try {
const response = await originalFetch(...args);
const endTime = Date.now();
// 非200状态码视为错误
if (!response.ok) {
const errorInfo = {
type: 'http_error',
requestId,
url: url.toString(),
method: options.method || 'GET',
status: response.status,
statusText: response.statusText,
duration: endTime - startTime,
timestamp: Date.now(),
};
this.reportError(errorInfo);
}
return response;
} catch (error) {
const endTime = Date.now();
const errorInfo = {
type: 'network_error',
requestId,
url: url.toString(),
method: options.method || 'GET',
error: error.message,
duration: endTime - startTime,
timestamp: Date.now(),
};
this.reportError(errorInfo);
throw error;
}
};
}
// 📡 监控XMLHttpRequest
monitorXMLHttpRequest() {
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url) {
this._requestInfo = {
method,
url,
startTime: Date.now(),
requestId: Math.random().toString(36).substr(2, 9),
};
return originalOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function(body) {
this.addEventListener('loadend', () => {
const { method, url, startTime, requestId } = this._requestInfo;
const endTime = Date.now();
if (this.status >= 400) {
const errorInfo = {
type: 'xhr_error',
requestId,
url,
method,
status: this.status,
statusText: this.statusText,
duration: endTime - startTime,
timestamp: Date.now(),
};
ErrorMonitor.getInstance().reportError(errorInfo);
}
});
return originalSend.apply(this, arguments);
};
}
// 🎭 包装console.error
wrapConsole() {
const originalError = console.error;
console.error = (...args) => {
// 调用原始方法
originalError.apply(console, args);
// 上报错误
const errorInfo = {
type: 'console_error',
messages: args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
),
timestamp: Date.now(),
url: window.location.href,
};
this.reportError(errorInfo);
};
}
// 📝 追踪用户行为(辅助排查)
trackUserActions() {
let actions = [];
let lastActionTime = Date.now();
// 记录点击
document.addEventListener('click', (e) => {
const target = e.target;
const path = this.getDomPath(target);
actions.push({
type: 'click',
path,
text: target.textContent?.slice(0, 50),
timestamp: Date.now() - lastActionTime,
});
// 只保留最近20个动作
if (actions.length > 20) {
actions.shift();
}
lastActionTime = Date.now();
});
// 记录输入
document.addEventListener('input', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
actions.push({
type: 'input',
name: e.target.name || e.target.id,
value: e.target.value?.slice(0, 100),
timestamp: Date.now() - lastActionTime,
});
}
});
// 保存到全局,供错误上报时使用
window.__USER_ACTIONS = actions;
}
// 🔧 增强错误信息
enhanceError(errorInfo) {
// 添加用户行为轨迹
if (window.__USER_ACTIONS) {
errorInfo.userActions = window.__USER_ACTIONS.slice(-5); // 最近5个动作
}
// 添加页面状态
errorInfo.pageState = {
url: window.location.href,
referrer: document.referrer,
screen: `${window.screen.width}x${window.screen.height}`,
viewport: `${window.innerWidth}x${window.innerHeight}`,
userAgent: navigator.userAgent,
cookies: document.cookie ? 'available' : 'empty',
localStorage: localStorage.length > 0 ? 'available' : 'empty',
};
// 添加性能信息
if (performance.timing) {
errorInfo.performance = {
loadTime: performance.timing.loadEventEnd - performance.timing.navigationStart,
domReady: performance.timing.domContentLoadedEventStart - performance.timing.navigationStart,
};
}
// 添加内存信息(Chrome)
if (performance.memory) {
errorInfo.memory = {
used: Math.round(performance.memory.usedJSHeapSize / 1024 / 1024) + 'MB',
total: Math.round(performance.memory.totalJSHeapSize / 1024 / 1024) + 'MB',
};
}
}
// 📤 上报错误
reportError(errorInfo) {
// 采样控制
if (Math.random() > this.config.sampleRate) {
return;
}
// 错误去重
if (this.config.deduplicate) {
const errorKey = this.getErrorKey(errorInfo);
const lastSent = this.sentErrors.get(errorKey);
if (lastSent && Date.now() - lastSent < this.config.dedupeWindow) {
return; // 最近上报过相同错误
}
this.sentErrors.set(errorKey, Date.now());
}
// 添加到队列
this.errorQueue.push(errorInfo);
// 开发环境打印
if (this.config.showUI) {
console.group('🚨 捕获到错误');
console.error(errorInfo);
console.groupEnd();
}
// 立即上报严重错误
if (this.isCriticalError(errorInfo)) {
this.flush(true);
}
}
// 🚨 判断是否为严重错误
isCriticalError(errorInfo) {
const criticalMessages = [
'script error',
'syntax error',
'type error',
'reference error',
'network error',
'failed to fetch',
];
return (
errorInfo.type === 'network_error' ||
errorInfo.type === 'http_error' && errorInfo.status >= 500 ||
criticalMessages.some(msg =>
errorInfo.message?.toLowerCase().includes(msg)
)
);
}
// 🔑 生成错误唯一标识(用于去重)
getErrorKey(errorInfo) {
// 基于错误信息生成唯一key
if (errorInfo.stack) {
// 使用错误堆栈的第一行
const stackLines = errorInfo.stack.split('\n');
const firstLine = stackLines[0] || '';
return `${errorInfo.type}:${firstLine}`;
}
return `${errorInfo.type}:${errorInfo.message}:${errorInfo.filename}:${errorInfo.lineno}`;
}
// 🚚 批量上报
async flush(isUrgent = false) {
if (this.errorQueue.length === 0) return;
const batch = this.errorQueue.splice(0, 10); // 每次最多上报10个
try {
const payload = {
errors: batch,
environment: {
env: process.env.NODE_ENV,
version: process.env.REACT_APP_VERSION || 'unknown',
timestamp: Date.now(),
},
};
if (navigator.sendBeacon && isUrgent) {
// 页面卸载时使用sendBeacon
const blob = new Blob([JSON.stringify(payload)], {
type: 'application/json',
});
navigator.sendBeacon(this.config.endpoint, blob);
} else {
// 正常上报
await fetch(this.config.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
keepalive: isUrgent, // 页面卸载时保持连接
});
}
console.log(`✅ 上报 ${batch.length} 个错误`);
} catch (error) {
console.warn('错误上报失败:', error);
// 失败后放回队列
this.errorQueue.unshift(...batch);
}
}
// 🎨 开发环境显示友好错误界面
showErrorUI(errorInfo) {
// 如果已经显示过错误界面,不再重复显示
if (document.getElementById('error-monitor-ui')) {
return;
}
const errorUI = document.createElement('div');
errorUI.id = 'error-monitor-ui';
errorUI.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #fff3cd;
border: 1px solid #ffeaa7;
padding: 15px;
border-radius: 8px;
max-width: 400px;
z-index: 99999;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 14px;
`;
errorUI.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 10px;">
<strong style="color: #e74c3c;">🚨 发现错误</strong>
<button onclick="this.parentElement.parentElement.remove()"
style="background: none; border: none; font-size: 20px; cursor: pointer; color: #666;">
×
</button>
</div>
<div style="margin-bottom: 10px;">
<div><strong>类型:</strong> ${errorInfo.type}</div>
<div><strong>信息:</strong> ${errorInfo.message?.slice(0, 100) || '无'}</div>
<div><strong>页面:</strong> ${window.location.pathname}</div>
</div>
<div style="display: flex; gap: 10px;">
<button onclick="window.location.reload()"
style="background: #3498db; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;">
刷新页面
</button>
<button onclick="console.error(${JSON.stringify(errorInfo)})"
style="background: #95a5a6; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer;">
查看详情
</button>
</div>
<div style="margin-top: 10px; font-size: 12px; color: #666;">
此界面仅在开发环境显示
</div>
`;
document.body.appendChild(errorUI);
// 10秒后自动关闭
setTimeout(() => {
if (errorUI.parentNode) {
errorUI.remove();
}
}, 10000);
}
// 🔍 获取DOM路径(辅助排查)
getDomPath(element) {
const path = [];
while (element) {
let selector = element.tagName.toLowerCase();
if (element.id) {
selector += `#${element.id}`;
path.unshift(selector);
break;
} else {
let sibling = element;
let nth = 1;
while (sibling.previousElementSibling) {
sibling = sibling.previousElementSibling;
if (sibling.tagName === element.tagName) {
nth++;
}
}
if (nth !== 1) {
selector += `:nth-of-type(${nth})`;
}
}
path.unshift(selector);
element = element.parentElement;
}
return path.join(' > ');
}
// 📝 手动上报错误(业务代码中使用)
captureError(error, context = {}) {
const errorInfo = {
type: 'manual_error',
message: error?.message || String(error),
stack: error?.stack,
timestamp: Date.now(),
url: window.location.href,
context,
};
this.reportError(errorInfo);
}
// 🧪 测试错误上报
testError() {
try {
throw new Error('这是一个测试错误,用于验证错误监控系统');
} catch (error) {
this.captureError(error, { test: true });
}
}
// 📊 获取错误统计
getStats() {
const oneHourAgo = Date.now() - 3600000;
const recentErrors = this.errorQueue.filter(e => e.timestamp > oneHourAgo);
return {
total: this.errorQueue.length,
recent: recentErrors.length,
byType: this.groupBy(recentErrors, 'type'),
byPage: this.groupBy(recentErrors, 'url'),
};
}
groupBy(array, key) {
return array.reduce((result, item) => {
const value = item[key];
result[value] = (result[value] || 0) + 1;
return result;
}, {});
}
// 单例模式
static getInstance(options) {
if (!this.instance) {
this.instance = new ErrorMonitor(options);
}
return this.instance;
}
}
// 导出单例
export const errorMonitor = ErrorMonitor.getInstance({
endpoint: process.env.REACT_APP_ERROR_ENDPOINT,
sampleRate: parseFloat(process.env.REACT_APP_ERROR_SAMPLE_RATE || '1.0'),
});
⚛️ 第二步:集成到React项目
1. React错误边界组件
// src/components/ErrorBoundary.jsx
import React from 'react';
import { errorMonitor } from '../utils/errorMonitor';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null
};
}
static getDerivedStateFromError(error) {
// 更新state使下一次渲染显示降级UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 上报错误
errorMonitor.captureError(error, {
componentStack: errorInfo.componentStack,
componentName: this.props.componentName || 'Unknown',
props: this.safeProps(this.props),
});
this.setState({
error,
errorInfo
});
}
// 安全地序列化props(避免循环引用)
safeProps(props) {
const safe = {};
Object.keys(props).forEach(key => {
if (key !== 'children' && key !== 'history' && key !== 'location') {
try {
safe[key] = typeof props[key] === 'function'
? '[Function]'
: JSON.stringify(props[key]);
} catch (e) {
safe[key] = '[Unserializable]';
}
}
});
return safe;
}
handleReset = () => {
this.setState({ hasError: false, error: null, errorInfo: null });
window.location.reload();
};
render() {
if (this.state.hasError) {
return (
<div style={{
padding: '20px',
margin: '20px',
border: '1px solid #e74c3c',
borderRadius: '8px',
background: '#fdf2f2'
}}>
<h3 style={{ color: '#e74c3c', marginTop: 0 }}>
⚠️ 组件出错了
</h3>
<div style={{ marginBottom: '15px' }}>
<strong>错误信息:</strong> {this.state.error?.toString()}
</div>
{process.env.NODE_ENV === 'development' && (
<details style={{ marginBottom: '15px' }}>
<summary>错误堆栈</summary>
<pre style={{
background: '#f5f5f5',
padding: '10px',
overflow: 'auto',
fontSize: '12px'
}}>
{this.state.errorInfo?.componentStack}
</pre>
</details>
)}
<div style={{ display: 'flex', gap: '10px' }}>
<button
onClick={this.handleReset}
style={{
background: '#3498db',
color: 'white',
border: 'none',
padding: '8px 16px',
borderRadius: '4px',
cursor: 'pointer'
}}
>
刷新页面
</button>
<button
onClick={() => {
errorMonitor.captureError(this.state.error, {
manualReport: true,
componentStack: this.state.errorInfo?.componentStack
});
alert('错误已重新上报');
}}
style={{
background: '#95a5a6',
color: 'white',
border: 'none',
padding: '8px 16px',
borderRadius: '4px',
cursor: 'pointer'
}}
>
重新上报错误
</button>
</div>
</div>
);
}
return this.props.children;
}
}
// 高阶组件:包装组件添加错误边界
export function withErrorBoundary(WrappedComponent, componentName) {
return function WithErrorBoundary(props) {
return (
<ErrorBoundary componentName={componentName}>
<WrappedComponent {...props} />
</ErrorBoundary>
);
};
}
export default ErrorBoundary;
2. 在React应用中使用
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import ErrorBoundary from './components/ErrorBoundary';
import { errorMonitor } from './utils/errorMonitor';
// 初始化错误监控
errorMonitor.testError(); // 测试错误上报
// 监听React渲染错误
const originalConsoleError = console.error;
console.error = (...args) => {
// 检查是否是React渲染错误
if (typeof args[0] === 'string' && args[0].includes('React')) {
errorMonitor.captureError(new Error(args[0]), {
type: 'react_render_error',
args: args.slice(1)
});
}
originalConsoleError.apply(console, args);
};
ReactDOM.render(
<React.StrictMode>
<ErrorBoundary>
<App />
</ErrorBoundary>
</React.StrictMode>,
document.getElementById('root')
);
3. 包装业务组件
// src/components/ProductDetail.jsx
import React, { useState, useEffect } from 'react';
import { withErrorBoundary } from './ErrorBoundary';
import { errorMonitor } from '../utils/errorMonitor';
function ProductDetail({ productId }) {
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadProduct = async () => {
try {
const response = await fetch(`/api/products/${productId}`);
if (!response.ok) {
throw new Error(`产品加载失败: ${response.status}`);
}
const data = await response.json();
setProduct(data);
} catch (error) {
// 手动上报错误
errorMonitor.captureError(error, {
productId,
component: 'ProductDetail',
action: 'load_product'
});
// 显示错误状态
setProduct(null);
} finally {
setLoading(false);
}
};
loadProduct();
}, [productId]);
const handleAddToCart = async () => {
try {
const response = await fetch('/api/cart/add', {
method: 'POST',
body: JSON.stringify({ productId }),
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {
throw new Error('加入购物车失败');
}
alert('添加成功!');
} catch (error) {
// 业务错误上报
errorMonitor.captureError(error, {
productId,
action: 'add_to_cart',
severity: 'high' // 高优先级错误
});
alert('操作失败,请重试');
}
};
if (loading) return <div>加载中...</div>;
if (!product) return <div>产品不存在</div>;
return (
<div className="product-detail">
<h1>{product.name}</h1>
<button onClick={handleAddToCart}>加入购物车</button>
</div>
);
}
// 使用错误边界包装组件
export default withErrorBoundary(ProductDetail, 'ProductDetail');
🖥️ 第三步:搭建错误监控后台
1. 简单的Node.js错误接收服务
// server/error-server.js
const express = require('express');
const app = express();
const port = 3002;
app.use(express.json());
// 内存存储(生产环境用数据库)
const errorsDB = [];
// 接收错误数据
app.post('/api/error-collect', (req, res) => {
const { errors, environment } = req.body;
console.log(`📥 收到 ${errors?.length || 0} 个错误`);
if (Array.isArray(errors)) {
errors.forEach(error => {
// 添加环境信息
const enhancedError = {
...error,
environment,
receivedAt: new Date().toISOString(),
id: Date.now() + Math.random().toString(36).substr(2, 9),
};
errorsDB.push(enhancedError);
// 控制台输出重要错误
if (error.type === 'js_error' || error.type === 'promise_error') {
console.error(`🚨 ${error.type}: ${error.message}`);
if (error.stack) {
console.error(error.stack);
}
}
// 触发告警(这里简化处理)
checkAndAlert(enhancedError);
});
}
res.json({ success: true });
});
// 错误告警检查
function checkAndAlert(error) {
const alertRules = [
{
condition: (e) => e.type === 'network_error',
message: '网络请求失败',
level: 'high'
},
{
condition: (e) => e.type === 'http_error' && e.status >= 500,
message: '服务器内部错误',
level: 'critical'
},
{
condition: (e) => e.message?.includes('SyntaxError'),
message: '语法错误',
level: 'high'
},
];
alertRules.forEach(rule => {
if (rule.condition(error)) {
sendAlert(rule.message, error, rule.level);
}
});
}
// 发送告警(简化版)
function sendAlert(message, error, level) {
console.log(`🚨 [${level.toUpperCase()}] ${message}`);
console.log('错误详情:', {
type: error.type,
message: error.message,
url: error.url,
timestamp: new Date(error.timestamp).toLocaleString(),
});
// 实际项目这里应该:
// 1. 发送到钉钉/企业微信
// 2. 发送邮件
// 3. 调用告警平台API
}
// 获取错误列表API
app.get('/api/errors', (req, res) => {
const {
type,
page = 1,
limit = 20,
startTime,
endTime
} = req.query;
let filtered = [...errorsDB];
// 过滤
if (type) {
filtered = filtered.filter(e => e.type === type);
}
if (startTime) {
filtered = filtered.filter(e => e.timestamp >= parseInt(startTime));
}
if (endTime) {
filtered = filtered.filter(e => e.timestamp <= parseInt(endTime));
}
// 排序(最新的在前)
filtered.sort((a, b) => b.timestamp - a.timestamp);
// 分页
const start = (page - 1) * limit;
const paginated = filtered.slice(start, start + limit);
res.json({
total: filtered.length,
page: parseInt(page),
limit: parseInt(limit),
data: paginated,
});
});
// 错误统计API
app.get('/api/errors/stats', (req, res) => {
const oneHourAgo = Date.now() - 3600000;
const oneDayAgo = Date.now() - 86400000;
const recentErrors = errorsDB.filter(e => e.timestamp > oneHourAgo);
const dailyErrors = errorsDB.filter(e => e.timestamp > oneDayAgo);
const stats = {
total: errorsDB.length,
lastHour: recentErrors.length,
last24Hours: dailyErrors.length,
byType: groupBy(errorsDB, 'type'),
byPage: Object.entries(groupBy(errorsDB, 'url'))
.sort(([,a], [,b]) => b - a)
.slice(0, 10) // Top 10页面
.reduce((obj, [key, val]) => ({ ...obj, [key]: val }), {}),
trend: getHourlyTrend(),
};
res.json(stats);
});
// 分组函数
function groupBy(array, key) {
return array.reduce((result, item) => {
const value = item[key];
result[value] = (result[value] || 0) + 1;
return result;
}, {});
}
// 获取小时趋势
function getHourlyTrend() {
const now = new Date();
const trends = [];
for (let i = 23; i >= 0; i--) {
const hourStart = new Date(now);
hourStart.setHours(now.getHours() - i, 0, 0, 0);
const hourEnd = new Date(hourStart);
hourEnd.setHours(hourStart.getHours() + 1);
const count = errorsDB.filter(e => {
const time = new Date(e.timestamp);
return time >= hourStart && time < hourEnd;
}).length;
trends.push({
hour: hourStart.getHours(),
count,
time: hourStart.toLocaleTimeString('zh-CN', { hour: '2-digit' }),
});
}
return trends;
}
// 错误详情API
app.get('/api/errors/:id', (req, res) => {
const error = errorsDB.find(e => e.id === req.params.id);
if (!error) {
return res.status(404).json({ error: '错误不存在' });
}
// 查找相似错误(基于错误信息)
const similar = errorsDB.filter(e =>
e.id !== error.id &&
e.type === error.type &&
e.message === error.message
).slice(0, 5);
res.json({
error,
similar,
totalOccurrences: errorsDB.filter(e =>
e.type === error.type && e.message === error.message
).length,
});
});
// 标记错误为已解决
app.post('/api/errors/:id/resolve', (req, res) => {
const error = errorsDB.find(e => e.id === req.params.id);
if (!error) {
return res.status(404).json({ error: '错误不存在' });
}
error.resolved = true;
error.resolvedAt = new Date().toISOString();
error.resolvedBy = req.body.user || 'system';
res.json({ success: true });
});
app.listen(port, () => {
console.log(`🚨 错误监控服务运行在 http://localhost:${port}`);
console.log(`📤 错误接收地址: POST http://localhost:${port}/api/error-collect`);
console.log(`📊 错误查看地址: GET http://localhost:${port}/api/errors`);
});
2. 运行错误监控服务
# 启动错误监控服务
node server/error-server.js
# 前端配置错误上报地址
# 在.env文件中添加
REACT_APP_ERROR_ENDPOINT=http://localhost:3002/api/error-collect
REACT_APP_ERROR_SAMPLE_RATE=1.0
🖥️ 第四步:错误看板(前端界面)
1. 简易错误看板组件
// src/components/ErrorDashboard.jsx
import React, { useState, useEffect } from 'react';
function ErrorDashboard() {
const [errors, setErrors] = useState([]);
const [stats, setStats] = useState(null);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('all');
// 只在开发环境显示
if (process.env.NODE_ENV === 'production') {
return null;
}
useEffect(() => {
fetchErrors();
fetchStats();
// 每30秒刷新一次
const interval = setInterval(fetchErrors, 30000);
return () => clearInterval(interval);
}, [filter]);
const fetchErrors = async () => {
try {
const response = await fetch('http://localhost:3002/api/errors?limit=10');
const data = await response.json();
setErrors(data.data || []);
} catch (error) {
console.error('获取错误列表失败:', error);
} finally {
setLoading(false);
}
};
const fetchStats = async () => {
try {
const response = await fetch('http://localhost:3002/api/errors/stats');
const data = await response.json();
setStats(data);
} catch (error) {
console.error('获取统计失败:', error);
}
};
const filteredErrors = filter === 'all'
? errors
: errors.filter(e => e.type === filter);
const getErrorColor = (type) => {
const colors = {
js_error: '#e74c3c',
promise_error: '#e67e22',
http_error: '#3498db',
network_error: '#9b59b6',
console_error: '#95a5a6',
resource_error: '#1abc9c',
};
return colors[type] || '#7f8c8d';
};
const formatTime = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleTimeString('zh-CN');
};
return (
<div style={{
position: 'fixed',
bottom: '20px',
left: '20px',
background: 'white',
border: '1px solid #ddd',
borderRadius: '8px',
padding: '15px',
maxWidth: '500px',
maxHeight: '400px',
overflow: 'auto',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
zIndex: 9999,
fontSize: '12px',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
<h3 style={{ margin: 0 }}>🚨 错误监控面板</h3>
<div>
<button
onClick={fetchErrors}
style={{ marginRight: '10px', padding: '2px 8px' }}
>
刷新
</button>
<select
value={filter}
onChange={(e) => setFilter(e.target.value)}
style={{ padding: '2px 5px' }}
>
<option value="all">全部类型</option>
<option value="js_error">JS错误</option>
<option value="promise_error">Promise错误</option>
<option value="http_error">HTTP错误</option>
<option value="network_error">网络错误</option>
</select>
</div>
</div>
{stats && (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '10px',
marginBottom: '15px',
padding: '10px',
background: '#f8f9fa',
borderRadius: '4px'
}}>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '18px', fontWeight: 'bold' }}>{stats.total}</div>
<div style={{ fontSize: '11px', color: '#666' }}>总错误数</div>
</div>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '18px', fontWeight: 'bold', color: '#e74c3c' }}>{stats.lastHour}</div>
<div style={{ fontSize: '11px', color: '#666' }}>1小时内</div>
</div>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '18px', fontWeight: 'bold', color: '#e67e22' }}>{stats.last24Hours}</div>
<div style={{ fontSize: '11px', color: '#666' }}>24小时内</div>
</div>
</div>
)}
{loading ? (
<div>加载中...</div>
) : filteredErrors.length === 0 ? (
<div style={{ textAlign: 'center', color: '#666', padding: '20px' }}>
🎉 暂无错误
</div>
) : (
<div>
{filteredErrors.map((error, index) => (
<div
key={error.id || index}
style={{
borderLeft: `3px solid ${getErrorColor(error.type)}`,
padding: '10px',
marginBottom: '8px',
background: '#f8f9fa',
borderRadius: '0 4px 4px 0',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '5px' }}>
<strong>{error.type}</strong>
<span style={{ fontSize: '11px', color: '#666' }}>{formatTime(error.timestamp)}</span>
</div>
<div style={{
fontSize: '11px',
color: '#333',
marginBottom: '5px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}>
{error.message || '无错误信息'}
</div>
{error.url && (
<div style={{ fontSize: '10px', color: '#666' }}>
页面: {error.url.replace(window.location.origin, '')}
</div>
)}
<button
onClick={() => console.error('错误详情:', error)}
style={{
marginTop: '5px',
fontSize: '10px',
padding: '2px 6px',
background: 'none',
border: '1px solid #ddd',
borderRadius: '3px',
cursor: 'pointer'
}}
>
查看详情
</button>
</div>
))}
</div>
)}
<div style={{ marginTop: '10px', fontSize: '11px', color: '#666', textAlign: 'center' }}>
错误数据来自本地监控服务
</div>
</div>
);
}
export default ErrorDashboard;
2. 在应用中使用看板
// src/App.jsx
import React from 'react';
import ProductDetail from './components/ProductDetail';
import ErrorDashboard from './components/ErrorDashboard';
import { errorMonitor } from './utils/errorMonitor';
function App() {
// 测试错误按钮(开发环境)
const triggerTestError = () => {
errorMonitor.testError();
alert('测试错误已触发,查看控制台或错误面板');
};
return (
<div className="App">
<header style={{ padding: '20px', background: '#f5f5f5' }}>
<h1>电商网站 - 错误监控演示</h1>
{process.env.NODE_ENV !== 'production' && (
<button
onClick={triggerTestError}
style={{
background: '#e74c3c',
color: 'white',
border: 'none',
padding: '8px 16px',
borderRadius: '4px',
cursor: 'pointer'
}}
>
触发测试错误
</button>
)}
</header>
<main style={{ padding: '20px' }}>
<ProductDetail productId="123" />
</main>
{/* 开发环境显示错误面板 */}
{process.env.NODE_ENV !== 'production' && <ErrorDashboard />}
</div>
);
}
export default App;
🔍 第五步:错误排查实战指南
1. 常见错误排查流程
// src/utils/errorDebugger.js
class ErrorDebugger {
// 错误诊断工具
static diagnose(error) {
const diagnosis = {
severity: 'low',
likelyCause: '未知',
suggestedFix: '查看错误详情',
relatedIssues: [],
};
// 根据错误类型和消息诊断
if (error.message?.includes('NetworkError')) {
diagnosis.likelyCause = '网络连接问题';
diagnosis.suggestedFix = '检查网络连接,重试操作';
diagnosis.severity = 'medium';
}
if (error.message?.includes('SyntaxError')) {
diagnosis.likelyCause = 'JavaScript语法错误';
diagnosis.suggestedFix = '检查相关代码语法';
diagnosis.severity = 'high';
}
if (error.message?.includes('Unexpected token')) {
diagnosis.likelyCause = 'JSON解析错误';
diagnosis.suggestedFix = '检查API返回的数据格式';
diagnosis.severity = 'high';
}
if (error.status === 404) {
diagnosis.likelyCause = '请求的资源不存在';
diagnosis.suggestedFix = '检查API地址是否正确';
diagnosis.severity = 'medium';
}
if (error.status === 500) {
diagnosis.likelyCause = '服务器内部错误';
diagnosis.suggestedFix = '联系后端开发人员';
diagnosis.severity = 'high';
}
return diagnosis;
}
// 重现错误步骤
static generateReproSteps(error) {
const steps = [];
// 从用户行为中提取重现步骤
if (error.userActions) {
steps.push('重现步骤:');
error.userActions.forEach((action, i) => {
steps.push(`${i + 1}. ${action.type}: ${action.path || action.name}`);
});
}
// 添加环境信息
steps.push('\n环境信息:');
steps.push(`- 浏览器: ${navigator.userAgent}`);
steps.push(`- 页面URL: ${error.url}`);
steps.push(`- 时间: ${new Date(error.timestamp).toLocaleString()}`);
return steps.join('\n');
}
// 检查相关依赖
static checkDependencies(error) {
const issues = [];
// 检查API端点是否可达
if (error.type === 'network_error') {
issues.push('检查API服务是否运行正常');
}
// 检查资源文件
if (error.type === 'resource_error') {
issues.push('检查静态资源文件是否存在');
}
return issues;
}
}
// 在错误上报时自动诊断
errorMonitor.reportError = function(errorInfo) {
// 原上报逻辑...
// 添加诊断信息
errorInfo.diagnosis = ErrorDebugger.diagnose(errorInfo);
errorInfo.reproSteps = ErrorDebugger.generateReproSteps(errorInfo);
// 调用原始上报
// ...
};
2. 错误追踪ID系统
// 为每个用户会话生成唯一ID
class TraceIdSystem {
constructor() {
this.sessionId = this.generateSessionId();
this.traceIds = new Map();
}
generateSessionId() {
return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
// 为请求生成追踪ID
generateTraceId(context = '') {
const traceId = `trace_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
this.traceIds.set(traceId, {
timestamp: Date.now(),
context,
sessionId: this.sessionId,
});
return traceId;
}
// 在fetch请求中添加追踪ID
instrumentFetch() {
const originalFetch = window.fetch;
window.fetch = async (...args) => {
const traceId = this.generateTraceId(`fetch:${args[0]}`);
// 添加追踪头部
const [url, options = {}] = args;
const headers = {
...options.headers,
'X-Trace-Id': traceId,
'X-Session-Id': this.sessionId,
};
try {
const response = await originalFetch(url, { ...options, headers });
// 从响应中获取后端追踪ID
const backendTraceId = response.headers.get('X-Backend-Trace-Id');
if (backendTraceId) {
this.traceIds.get(traceId).backendTraceId = backendTraceId;
}
return response;
} catch (error) {
// 在错误中记录追踪ID
error.traceId = traceId;
throw error;
}
};
}
// 获取追踪信息
getTraceInfo(traceId) {
return this.traceIds.get(traceId) || null;
}
}
// 使用追踪ID
const traceSystem = new TraceIdSystem();
traceSystem.instrumentFetch();
// 在错误中记录追踪ID
errorMonitor.captureError = function(error, context) {
const traceId = traceSystem.generateTraceId(`error:${error.message}`);
const enhancedError = {
...error,
traceId,
sessionId: traceSystem.sessionId,
context,
};
this.reportError(enhancedError);
};
📋 第六步:项目部署清单
1. 快速启动清单
## 第一天:基础错误监控
- [ ] 复制 errorMonitor.js 到项目
- [ ] 在入口文件初始化监控
- [ ] 启动本地错误服务器
- [ ] 测试错误上报(点击测试按钮)
- [ ] 查看控制台是否有错误数据
## 第一周:完善监控
- [ ] 为所有页面组件添加ErrorBoundary
- [ ] 监控关键API接口
- [ ] 配置生产环境上报地址
- [ ] 设置错误告警(邮件/钉钉)
## 第一个月:深度优化
- [ ] 分析错误趋势,找到最多错误页面
- [ ] 修复高频错误
- [ ] 建立错误处理规范
- [ ] 设置错误处理SOP(标准作业程序)
## 长期维护
- [ ] 每周查看错误报告
- [ ] 每月分析错误趋势
- [ ] 更新错误处理策略
- [ ] 团队分享错误排查经验
2. 实际项目配置
// .env 配置示例
REACT_APP_ERROR_ENDPOINT_DEV=http://localhost:3002/api/error-collect
REACT_APP_ERROR_ENDPOINT_PROD=https://api.yourcompany.com/error-collect
REACT_APP_ERROR_SAMPLE_RATE_DEV=1.0 # 开发环境100%上报
REACT_APP_ERROR_SAMPLE_RATE_PROD=0.1 # 生产环境10%采样
REACT_APP_ENABLE_ERROR_DASHBOARD=true # 开发环境显示看板
// package.json 脚本
{
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"error-server": "node server/error-server.js",
"error-server:prod": "NODE_ENV=production node server/error-server.js",
"test-error": "curl -X POST http://localhost:3002/api/error-collect -H 'Content-Type: application/json' -d '{\"errors\":[{\"type\":\"test\",\"message\":\"测试错误\"}]}'"
}
}
3. 团队协作规范
# 错误处理规范
## 错误上报要求
1. 所有异步操作必须使用 try-catch
2. 关键业务操作必须手动上报错误
3. 组件必须使用 ErrorBoundary 包装
4. HTTP状态码 >= 400 必须上报
## 错误信息规范
- 错误信息必须包含上下文
- 网络错误必须包含URL和方法
- 业务错误必须包含用户ID和操作
- 组件错误必须包含组件名和props
## 错误处理SOP
1. 收到告警后,立即查看错误详情
2. 根据traceId追踪完整请求链路
3. 参考reproSteps重现错误
4. 根据diagnosis建议尝试修复
5. 修复后标记错误为已解决
## 错误分级处理
- P0(严重):立即修复,影响核心功能
- P1(高):24小时内修复,影响用户体验
- P2(中):本周内修复,功能降级可用
- P3(低):规划修复,不影响使用
🎯 总结:立即行动
5分钟快速开始:
- 复制
errorMonitor.js到你的项目 - 在
index.js添加:import { errorMonitor } from './utils/errorMonitor'; errorMonitor.testError(); // 测试 - 运行错误服务器:
node server/error-server.js - 启动项目,点击页面上的"测试错误"按钮
- 查看控制台,确认错误已上报
重点关注这些错误:
- 🔴 红色警报:JS语法错误、网络错误、500错误
- 🟠 橙色警报:404错误、API超时、资源加载失败
- 🟡 黄色警报:控制台错误、警告信息
- 🟢 正常:已处理并降级的错误
错误排查三板斧:
- 看堆栈:错误发生在哪一行?
- 看上下文:用户做了什么操作?
- 看环境:什么浏览器?什么网络?
记住:
- 没有完美的系统,只有完善的监控
- 错误是信息,不是失败
- 监控是为了发现,发现是为了改进
- 从最简单的开始,逐步完善
现在就开始,复制代码到你的项目,今天就能拥有错误监控能力!

浙公网安备 33010602011771号