Appearance
错误上报系统搭建 - 前端监控体系建设
1. 错误上报系统概述
1.1 系统架构
typescript
const errorReportingArchitecture = {
客户端层: [
'错误捕获: window.onerror, unhandledrejection等',
'错误收集: 收集错误信息、上下文、环境',
'错误过滤: 去重、采样、黑名单',
'错误上报: 发送到服务器',
'本地缓存: 离线时存储,联网后发送'
],
传输层: [
'HTTP请求: POST /api/errors',
'图片上报: new Image().src',
'Beacon API: navigator.sendBeacon',
'WebSocket: 实时上报'
],
服务端层: [
'接收错误: API接口',
'存储错误: 数据库',
'错误分析: 聚合、统计',
'告警通知: 邮件、短信、钉钉',
'可视化: Dashboard展示'
],
管理层: [
'错误查询: 搜索、过滤',
'错误详情: 堆栈、上下文',
'错误追踪: SourceMap映射',
'用户追踪: Session Replay'
]
};1.2 核心功能
typescript
const coreFeatures = {
错误捕获: ['JS错误', 'Promise错误', '资源加载错误', 'React错误', 'API错误'],
错误信息: ['错误消息', '错误堆栈', '发生时间', '用户信息', '设备信息', '页面信息'],
上报策略: ['实时上报', '批量上报', '采样上报', '离线缓存'],
错误处理: ['去重', '过滤', '聚合', 'SourceMap还原'],
分析展示: ['错误统计', '趋势分析', '影响范围', '告警通知']
};2. 客户端错误捕获
2.1 全局错误捕获
javascript
// 错误捕获器类
class ErrorCatcher {
constructor(config = {}) {
this.config = {
dsn: config.dsn,
environment: config.environment || 'production',
release: config.release || '1.0.0',
sampleRate: config.sampleRate || 1.0, // 采样率
maxBreadcrumbs: config.maxBreadcrumbs || 50, // 最大面包屑数量
...config
};
this.breadcrumbs = []; // 面包屑
this.errorQueue = []; // 错误队列
this.isOnline = navigator.onLine;
this.init();
}
init() {
// JS错误
window.addEventListener('error', this.handleError.bind(this));
// Promise错误
window.addEventListener('unhandledrejection', this.handleUnhandledRejection.bind(this));
// 资源加载错误
window.addEventListener('error', this.handleResourceError.bind(this), true);
// 网络状态
window.addEventListener('online', () => {
this.isOnline = true;
this.flushQueue();
});
window.addEventListener('offline', () => {
this.isOnline = false;
});
// 页面卸载时发送剩余错误
window.addEventListener('beforeunload', () => {
this.flushQueue(true);
});
}
// 处理JS错误
handleError(event) {
const error = {
type: 'javascript',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack,
timestamp: Date.now()
};
this.captureError(error);
}
// 处理Promise错误
handleUnhandledRejection(event) {
const error = {
type: 'unhandledrejection',
message: event.reason?.message || event.reason,
stack: event.reason?.stack,
timestamp: Date.now()
};
this.captureError(error);
}
// 处理资源加载错误
handleResourceError(event) {
if (event.target !== window && (event.target.tagName === 'SCRIPT' || event.target.tagName === 'LINK' || event.target.tagName === 'IMG')) {
const error = {
type: 'resource',
message: `Failed to load ${event.target.tagName.toLowerCase()}`,
filename: event.target.src || event.target.href,
timestamp: Date.now()
};
this.captureError(error);
}
}
// 捕获错误
captureError(error) {
// 采样
if (Math.random() > this.config.sampleRate) {
return;
}
// 增强错误信息
const enhancedError = {
...error,
environment: this.config.environment,
release: this.config.release,
userAgent: navigator.userAgent,
url: window.location.href,
breadcrumbs: this.breadcrumbs.slice(),
deviceInfo: this.getDeviceInfo(),
userInfo: this.getUserInfo()
};
// 添加到队列
this.errorQueue.push(enhancedError);
// 立即发送或批量发送
if (this.config.immediate) {
this.sendError(enhancedError);
} else {
this.debouncedFlush();
}
}
// 获取设备信息
getDeviceInfo() {
return {
screenWidth: window.screen.width,
screenHeight: window.screen.height,
windowWidth: window.innerWidth,
windowHeight: window.innerHeight,
pixelRatio: window.devicePixelRatio,
platform: navigator.platform,
language: navigator.language
};
}
// 获取用户信息(需要自行实现)
getUserInfo() {
return {
userId: window.currentUser?.id,
userName: window.currentUser?.name,
userEmail: window.currentUser?.email
};
}
// 添加面包屑
addBreadcrumb(breadcrumb) {
this.breadcrumbs.push({
...breadcrumb,
timestamp: Date.now()
});
if (this.breadcrumbs.length > this.config.maxBreadcrumbs) {
this.breadcrumbs.shift();
}
}
// 发送错误
sendError(error) {
if (!this.config.dsn) return;
const payload = {
...error,
sdkVersion: '1.0.0'
};
// 使用Beacon API确保发送成功
if (navigator.sendBeacon) {
const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
navigator.sendBeacon(this.config.dsn, blob);
} else {
// 降级到fetch
fetch(this.config.dsn, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
keepalive: true
}).catch(err => {
console.error('Failed to send error:', err);
// 存储到本地,稍后重试
this.saveToLocalStorage(payload);
});
}
}
// 刷新队列
flushQueue(force = false) {
if (this.errorQueue.length === 0) return;
if (force || this.isOnline) {
const errors = this.errorQueue.splice(0);
if (errors.length === 1) {
this.sendError(errors[0]);
} else {
// 批量发送
this.sendBatch(errors);
}
}
}
// 批量发送
sendBatch(errors) {
if (!this.config.dsn) return;
const payload = {
batch: errors,
timestamp: Date.now()
};
fetch(this.config.dsn + '/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}).catch(err => {
console.error('Failed to send batch:', err);
});
}
// 防抖刷新
debouncedFlush = (() => {
let timer = null;
return () => {
clearTimeout(timer);
timer = setTimeout(() => {
this.flushQueue();
}, 2000);
};
})();
// 本地存储
saveToLocalStorage(error) {
try {
const stored = JSON.parse(localStorage.getItem('errorReports') || '[]');
stored.push(error);
// 限制数量
if (stored.length > 50) {
stored.splice(0, stored.length - 50);
}
localStorage.setItem('errorReports', JSON.stringify(stored));
} catch (err) {
console.error('Failed to save to localStorage:', err);
}
}
}
// 初始化
const errorCatcher = new ErrorCatcher({
dsn: 'https://api.yourapp.com/errors',
environment: process.env.NODE_ENV,
release: process.env.REACT_APP_VERSION,
sampleRate: 1.0,
immediate: false
});
// 暴露全局API
window.ErrorCatcher = errorCatcher;2.2 React集成
jsx
// React错误捕获
class ReactErrorCatcher extends React.Component {
componentDidCatch(error, errorInfo) {
// 捕获React错误
errorCatcher.captureError({
type: 'react',
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: Date.now()
});
}
render() {
return this.props.children;
}
}
// Hook方式捕获
function useErrorCatcher() {
const captureError = useCallback((error, context = {}) => {
errorCatcher.captureError({
type: 'hook',
message: error.message,
stack: error.stack,
context,
timestamp: Date.now()
});
}, []);
return captureError;
}
// 使用
function Component() {
const captureError = useErrorCatcher();
const handleClick = async () => {
try {
await riskyOperation();
} catch (error) {
captureError(error, { action: 'button-click' });
}
};
return <button onClick={handleClick}>Click Me</button>;
}2.3 API错误捕获
javascript
// 拦截fetch
const originalFetch = window.fetch;
window.fetch = function(...args) {
const [url, options] = args;
// 添加面包屑
errorCatcher.addBreadcrumb({
category: 'fetch',
message: `${options?.method || 'GET'} ${url}`,
level: 'info'
});
return originalFetch(...args).then(response => {
if (!response.ok) {
// 捕获HTTP错误
errorCatcher.captureError({
type: 'http',
message: `HTTP ${response.status}: ${response.statusText}`,
url,
method: options?.method || 'GET',
status: response.status,
timestamp: Date.now()
});
}
return response;
}).catch(error => {
// 捕获网络错误
errorCatcher.captureError({
type: 'network',
message: error.message,
url,
method: options?.method || 'GET',
timestamp: Date.now()
});
throw error;
});
};
// 拦截XMLHttpRequest
const originalXHROpen = XMLHttpRequest.prototype.open;
const originalXHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url, ...args) {
this._method = method;
this._url = url;
return originalXHROpen.call(this, method, url, ...args);
};
XMLHttpRequest.prototype.send = function(...args) {
this.addEventListener('error', () => {
errorCatcher.captureError({
type: 'xhr',
message: 'XHR request failed',
url: this._url,
method: this._method,
timestamp: Date.now()
});
});
this.addEventListener('load', () => {
if (this.status >= 400) {
errorCatcher.captureError({
type: 'xhr',
message: `XHR ${this.status}: ${this.statusText}`,
url: this._url,
method: this._method,
status: this.status,
timestamp: Date.now()
});
}
});
return originalXHRSend.call(this, ...args);
};3. 错误上报优化
3.1 错误去重
javascript
class ErrorDeduplicator {
constructor(maxSize = 100) {
this.cache = new Map();
this.maxSize = maxSize;
}
// 生成错误指纹
getFingerprint(error) {
const parts = [
error.type,
error.message,
error.filename,
error.lineno,
error.colno
].filter(Boolean);
return parts.join(':');
}
// 检查是否重复
isDuplicate(error, timeWindow = 60000) {
const fingerprint = this.getFingerprint(error);
const cached = this.cache.get(fingerprint);
if (cached) {
const timeDiff = Date.now() - cached.timestamp;
if (timeDiff < timeWindow) {
// 更新计数
cached.count++;
return true;
} else {
// 超时,更新缓存
this.cache.set(fingerprint, {
timestamp: Date.now(),
count: 1
});
return false;
}
}
// 首次出现
this.cache.set(fingerprint, {
timestamp: Date.now(),
count: 1
});
// 限制缓存大小
if (this.cache.size > this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
return false;
}
// 获取错误统计
getStats(error) {
const fingerprint = this.getFingerprint(error);
return this.cache.get(fingerprint);
}
}
// 使用去重器
const deduplicator = new ErrorDeduplicator();
// 在captureError中使用
function captureErrorWithDedup(error) {
if (deduplicator.isDuplicate(error)) {
console.log('Duplicate error, skipped');
return;
}
const stats = deduplicator.getStats(error);
errorCatcher.captureError({
...error,
occurrenceCount: stats.count
});
}3.2 错误采样
javascript
class ErrorSampler {
constructor(config = {}) {
this.globalSampleRate = config.globalSampleRate || 1.0;
this.ruleSampleRates = config.ruleSampleRates || {};
this.userSampleRates = new Map();
}
// 判断是否应该上报
shouldSample(error) {
// 规则采样
const ruleRate = this.getRuleSampleRate(error);
if (ruleRate !== null && Math.random() > ruleRate) {
return false;
}
// 用户采样
const userRate = this.getUserSampleRate(error);
if (userRate !== null && Math.random() > userRate) {
return false;
}
// 全局采样
return Math.random() <= this.globalSampleRate;
}
// 获取规则采样率
getRuleSampleRate(error) {
for (const [pattern, rate] of Object.entries(this.ruleSampleRates)) {
if (error.message && error.message.includes(pattern)) {
return rate;
}
}
return null;
}
// 获取用户采样率
getUserSampleRate(error) {
const userId = error.userInfo?.userId;
if (!userId) return null;
if (this.userSampleRates.has(userId)) {
return this.userSampleRates.get(userId);
}
// 根据用户ID哈希决定采样率
const hash = this.hashCode(userId);
const rate = (hash % 100) / 100;
this.userSampleRates.set(userId, rate);
return rate;
}
// 简单哈希函数
hashCode(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash);
}
}
// 使用采样器
const sampler = new ErrorSampler({
globalSampleRate: 0.8, // 80%采样率
ruleSampleRates: {
'ResizeObserver': 0.1, // 降低ResizeObserver错误采样
'Non-Error': 0.2 // 降低非标准错误采样
}
});
// 在captureError中使用
function captureErrorWithSampling(error) {
if (!sampler.shouldSample(error)) {
console.log('Error sampled out');
return;
}
errorCatcher.captureError(error);
}3.3 错误压缩
javascript
class ErrorCompressor {
// 压缩错误数据
compress(error) {
return {
t: error.type,
m: error.message?.substring(0, 200), // 限制长度
f: this.shortenFilename(error.filename),
l: error.lineno,
c: error.colno,
s: this.compressStack(error.stack),
ts: error.timestamp,
u: error.url && new URL(error.url).pathname, // 只保留路径
ua: this.compressUserAgent(error.userAgent)
};
}
// 解压错误数据
decompress(compressed) {
return {
type: compressed.t,
message: compressed.m,
filename: compressed.f,
lineno: compressed.l,
colno: compressed.c,
stack: compressed.s,
timestamp: compressed.ts,
url: compressed.u,
userAgent: compressed.ua
};
}
// 缩短文件名
shortenFilename(filename) {
if (!filename) return null;
const url = new URL(filename);
return url.pathname.split('/').pop();
}
// 压缩堆栈
compressStack(stack) {
if (!stack) return null;
// 只保留前5层堆栈
const lines = stack.split('\n').slice(0, 5);
// 去除重复信息
return lines.map(line => {
return line
.replace(/https?:\/\/[^/]+/, '') // 移除域名
.replace(/\?.+$/, '') // 移除查询参数
.trim();
}).join('\n');
}
// 压缩UserAgent
compressUserAgent(ua) {
if (!ua) return null;
// 提取关键信息
const match = ua.match(/(Chrome|Firefox|Safari|Edge)\/(\d+)/);
if (match) {
return `${match[1]}/${match[2]}`;
}
return ua.substring(0, 50);
}
}
const compressor = new ErrorCompressor();
// 在发送前压缩
function sendCompressedError(error) {
const compressed = compressor.compress(error);
fetch(dsn, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(compressed)
});
}4. 服务端实现
4.1 Express错误接收
javascript
// server.js
const express = require('express');
const app = express();
app.use(express.json());
// 错误接收接口
app.post('/api/errors', async (req, res) => {
try {
const error = req.body;
// 验证错误数据
if (!error.type || !error.message) {
return res.status(400).json({ error: 'Invalid error data' });
}
// 增强错误信息
const enhancedError = {
...error,
receivedAt: new Date(),
ip: req.ip,
headers: {
userAgent: req.get('user-agent'),
referer: req.get('referer')
}
};
// 存储到数据库
await saveError(enhancedError);
// 检查是否需要告警
await checkAndAlert(enhancedError);
res.json({ success: true });
} catch (err) {
console.error('Failed to process error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
// 批量接收接口
app.post('/api/errors/batch', async (req, res) => {
try {
const { batch } = req.body;
if (!Array.isArray(batch)) {
return res.status(400).json({ error: 'Invalid batch data' });
}
// 批量处理
await Promise.all(batch.map(error => {
const enhancedError = {
...error,
receivedAt: new Date(),
ip: req.ip
};
return saveError(enhancedError);
}));
res.json({ success: true, count: batch.length });
} catch (err) {
console.error('Failed to process batch:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
// 存储错误
async function saveError(error) {
// 使用MongoDB示例
const db = await getDatabase();
await db.collection('errors').insertOne(error);
}
// 检查并告警
async function checkAndAlert(error) {
// 错误数量阈值告警
const count = await countRecentErrors(error.type, 5 * 60 * 1000); // 5分钟内
if (count > 10) {
await sendAlert({
title: `Error Spike: ${error.type}`,
message: `${error.type} occurred ${count} times in the last 5 minutes`,
severity: 'high'
});
}
}
app.listen(3000, () => {
console.log('Error reporting server running on port 3000');
});4.2 数据库存储
javascript
// MongoDB Schema
const errorSchema = {
type: String,
message: String,
stack: String,
filename: String,
lineno: Number,
colno: Number,
url: String,
userAgent: String,
environment: String,
release: String,
timestamp: Date,
receivedAt: Date,
userId: String,
deviceInfo: Object,
breadcrumbs: Array,
resolved: Boolean,
resolvedAt: Date,
resolvedBy: String,
fingerprint: String, // 用于分组
occurrenceCount: Number
};
// 创建索引
db.collection('errors').createIndex({ fingerprint: 1, timestamp: -1 });
db.collection('errors').createIndex({ type: 1, timestamp: -1 });
db.collection('errors').createIndex({ userId: 1, timestamp: -1 });
db.collection('errors').createIndex({ environment: 1, timestamp: -1 });
// TTL索引,自动删除旧数据
db.collection('errors').createIndex(
{ receivedAt: 1 },
{ expireAfterSeconds: 30 * 24 * 60 * 60 } // 30天后删除
);4.3 错误聚合
javascript
// 错误聚合服务
class ErrorAggregator {
constructor(db) {
this.db = db;
}
// 按指纹聚合
async aggregateByFingerprint(startDate, endDate) {
return await this.db.collection('errors').aggregate([
{
$match: {
timestamp: { $gte: startDate, $lte: endDate }
}
},
{
$group: {
_id: '$fingerprint',
count: { $sum: 1 },
firstSeen: { $min: '$timestamp' },
lastSeen: { $max: '$timestamp' },
affectedUsers: { $addToSet: '$userId' },
type: { $first: '$type' },
message: { $first: '$message' },
sample: { $first: '$$ROOT' }
}
},
{
$sort: { count: -1 }
},
{
$limit: 100
}
]).toArray();
}
// 按时间聚合
async aggregateByTime(interval = 'hour') {
const groupFormat = interval === 'hour'
? { $dateToString: { format: '%Y-%m-%d %H:00', date: '$timestamp' } }
: { $dateToString: { format: '%Y-%m-%d', date: '$timestamp' } };
return await this.db.collection('errors').aggregate([
{
$group: {
_id: groupFormat,
count: { $sum: 1 },
types: { $addToSet: '$type' }
}
},
{
$sort: { _id: 1 }
}
]).toArray();
}
// Top错误
async getTopErrors(limit = 10) {
return await this.db.collection('errors').aggregate([
{
$match: {
timestamp: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) }
}
},
{
$group: {
_id: { type: '$type', message: '$message' },
count: { $sum: 1 },
sample: { $first: '$$ROOT' }
}
},
{
$sort: { count: -1 }
},
{
$limit: limit
}
]).toArray();
}
}5. SourceMap支持
5.1 SourceMap解析
javascript
// SourceMap解析器
const sourceMap = require('source-map');
const fs = require('fs').promises;
class SourceMapResolver {
constructor(mapDirectory) {
this.mapDirectory = mapDirectory;
this.cache = new Map();
}
// 解析错误堆栈
async resolveStack(stack, filename) {
const lines = stack.split('\n');
const resolvedLines = [];
for (const line of lines) {
const match = line.match(/at .+ \((.+):(\d+):(\d+)\)/);
if (match) {
const [, file, lineStr, colStr] = match;
const lineNum = parseInt(lineStr);
const colNum = parseInt(colStr);
try {
const original = await this.resolve(file, lineNum, colNum);
resolvedLines.push(
line.replace(
`${file}:${lineNum}:${colNum}`,
`${original.source}:${original.line}:${original.column}`
)
);
} catch (err) {
resolvedLines.push(line);
}
} else {
resolvedLines.push(line);
}
}
return resolvedLines.join('\n');
}
// 解析单个位置
async resolve(filename, line, column) {
const consumer = await this.getConsumer(filename);
const original = consumer.originalPositionFor({
line,
column
});
return {
source: original.source,
line: original.line,
column: original.column,
name: original.name
};
}
// 获取SourceMap Consumer
async getConsumer(filename) {
if (this.cache.has(filename)) {
return this.cache.get(filename);
}
const mapFile = `${this.mapDirectory}/${filename}.map`;
const mapContent = await fs.readFile(mapFile, 'utf-8');
const rawSourceMap = JSON.parse(mapContent);
const consumer = await new sourceMap.SourceMapConsumer(rawSourceMap);
this.cache.set(filename, consumer);
return consumer;
}
}
// 使用
const resolver = new SourceMapResolver('./build/static/js');
// 在错误处理中使用
async function processError(error) {
if (error.stack && error.filename) {
try {
error.resolvedStack = await resolver.resolveStack(error.stack, error.filename);
} catch (err) {
console.error('Failed to resolve source map:', err);
}
}
await saveError(error);
}6. 告警通知
6.1 告警规则引擎
javascript
class AlertEngine {
constructor() {
this.rules = [];
this.alertHistory = new Map();
}
// 添加规则
addRule(rule) {
this.rules.push(rule);
}
// 检查是否应该告警
async shouldAlert(error, stats) {
for (const rule of this.rules) {
if (await rule.match(error, stats)) {
// 检查冷却期
const lastAlert = this.alertHistory.get(rule.id);
if (lastAlert && Date.now() - lastAlert < rule.cooldown) {
continue;
}
// 发送告警
await rule.alert(error, stats);
this.alertHistory.set(rule.id, Date.now());
}
}
}
}
// 定义告警规则
const alertEngine = new AlertEngine();
// 规则1: 错误数量激增
alertEngine.addRule({
id: 'error-spike',
cooldown: 5 * 60 * 1000, // 5分钟冷却
match: async (error, stats) => {
const count = stats.last5Minutes;
return count > 50;
},
alert: async (error, stats) => {
await sendDingTalkAlert({
title: '错误数量激增',
text: `过去5分钟内发生${stats.last5Minutes}个错误`,
level: 'critical'
});
}
});
// 规则2: 新错误类型
alertEngine.addRule({
id: 'new-error-type',
cooldown: 10 * 60 * 1000,
match: async (error) => {
const count = await countErrorsByFingerprint(error.fingerprint);
return count === 1; // 首次出现
},
alert: async (error) => {
await sendEmailAlert({
subject: '发现新错误类型',
body: `错误类型: ${error.type}\n错误消息: ${error.message}`,
level: 'warning'
});
}
});
// 规则3: 影响用户数过多
alertEngine.addRule({
id: 'affected-users',
cooldown: 15 * 60 * 1000,
match: async (error, stats) => {
return stats.affectedUsers > 100;
},
alert: async (error, stats) => {
await sendSlackAlert({
channel: '#alerts',
text: `错误影响了${stats.affectedUsers}个用户`,
level: 'high'
});
}
});6.2 通知渠道
javascript
// 钉钉通知
async function sendDingTalkAlert(alert) {
const webhook = process.env.DINGTALK_WEBHOOK;
await fetch(webhook, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
msgtype: 'markdown',
markdown: {
title: alert.title,
text: `### ${alert.title}\n\n${alert.text}\n\n**级别**: ${alert.level}`
}
})
});
}
// 邮件通知
const nodemailer = require('nodemailer');
async function sendEmailAlert(alert) {
const transporter = nodemailer.createTransporter({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
});
await transporter.sendMail({
from: 'alerts@yourapp.com',
to: 'team@yourapp.com',
subject: alert.subject,
text: alert.body
});
}
// Slack通知
async function sendSlackAlert(alert) {
const webhook = process.env.SLACK_WEBHOOK;
await fetch(webhook, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
channel: alert.channel,
text: alert.text,
attachments: [{
color: alert.level === 'critical' ? 'danger' : 'warning',
fields: [{
title: 'Level',
value: alert.level,
short: true
}]
}]
})
});
}7. 总结
构建完整的错误上报系统需要:
- 客户端: 全局错误捕获、React集成、API拦截
- 优化: 去重、采样、压缩
- 服务端: 接收、存储、聚合
- SourceMap: 堆栈还原
- 告警: 规则引擎、多渠道通知
完善的错误上报系统是保障应用质量的重要基础设施。