
前端线上问题怎么排查?Sentry + 埋点监控完整实战指南
本文详细介绍前端线上问题的排查思路,重点讲解 Sentry 错误监控和埋点系统的实战应用。
🎯 前言
作为前端开发,你是否遇到过这些场景:
- 用户反馈页面白屏,但本地无法复现?
- 线上某个功能突然报错,不知道影响了多少用户?
- 性能指标下降,但不清楚具体是哪里出了问题?
这些问题的解决,都离不开一套完善的前端监控体系。本文将从实战角度,带你搭建完整的监控和问题排查方案。
📊 一、前端监控体系架构
完整的前端监控体系包括三大核心:
javascript
┌─────────────────────────────────────┐
│ 前端监控体系 │
├─────────────────────────────────────┤
│ 1. 错误监控(Sentry) │
│ - JS 运行时错误 │
│ - Promise 异常 │
│ - 资源加载失败 │
│ - 接口请求错误 │
│ │
│ 2. 性能监控 │
│ - Core Web Vitals │
│ - 页面加载性能 │
│ - 接口性能 │
│ │
│ 3. 用户行为埋点 │
│ - 页面访问(PV/UV) │
│ - 用户点击行为 │
│ - 业务流程追踪 │
└─────────────────────────────────────┘
🔥 二、Sentry 错误监控实战
1. Sentry 初始化配置
javascript
import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';
Sentry.init({
dsn: 'YOUR_SENTRY_DSN',
environment: process.env.NODE_ENV, // 区分环境
release: `my-app@${process.env.RELEASE_VERSION}`, // 版本号
// 集成性能监控和会话重放
integrations: [
new BrowserTracing({
tracePropagationTargets: ['api.yourapp.com'], // 追踪的 API 域名
}),
new Sentry.Replay({
maskAllText: false, // 是否遮罩文本
blockAllMedia: false, // 是否阻止媒体
}),
],
// 采样率配置(控制成本)
tracesSampleRate: 0.1, // 性能监控 10% 采样
replaysSessionSampleRate: 0.1, // 正常会话 10% 录制
replaysOnErrorSampleRate: 1.0, // 错误会话 100% 录制
// 过滤敏感信息
beforeSend(event, hint) {
// 移除敏感数据
if (event.request) {
delete event.request.cookies;
delete event.request.headers?.Authorization;
}
// 过滤本地开发环境
if (window.location.hostname === 'localhost') {
return null;
}
return event;
},
// 忽略已知的无害错误
ignoreErrors: [
'ResizeObserver loop limit exceeded',
'Non-Error promise rejection captured',
'Network request failed', // 网络问题
],
});
2. 设置用户上下文信息
javascript
// 用户登录后设置用户信息
Sentry.setUser({
id: user.id,
email: user.email,
username: user.name,
// 自定义字段
vipLevel: user.vipLevel,
});
// 设置标签(用于过滤和分组)
Sentry.setTag('page', 'checkout');
Sentry.setTag('feature', 'payment');
Sentry.setTag('app_version', '2.1.0');
// 设置额外上下文
Sentry.setContext('device', {
brand: getBrand(),
model: getModel(),
os: getOS(),
network: getNetworkType(), // 4G/5G/WiFi
});
// 记录用户行为面包屑
Sentry.addBreadcrumb({
category: 'ui.click',
message: '用户点击了提交订单按钮',
level: 'info',
data: {
orderId: '123456',
amount: 299.0,
},
});
3. React/Vue 框架集成
React Error Boundary:
javascript
import { ErrorBoundary } from '@sentry/react';
function App() {
return (
<ErrorBoundary
fallback={({ error, componentStack, resetError }) => (
<div>
<h1>出错了!</h1>
<button onClick={resetError}>重试</button>
</div>
)}
showDialog={false} // 是否显示错误反馈对话框
>
<Routes />
</ErrorBoundary>
);
}
Vue 集成:
javascript
import * as Sentry from '@sentry/vue';
const app = createApp(App);
Sentry.init({
app,
trackComponents: true, // 追踪组件性能
hooks: ['mount', 'update'], // 追踪的生命周期
});
4. 手动捕获错误
javascript
// 捕获异常
try {
riskyOperation();
} catch (error) {
Sentry.captureException(error, {
tags: {
section: 'payment',
},
contexts: {
order: {
id: orderId,
amount: amount,
},
},
});
}
// 记录消息
Sentry.captureMessage('用户尝试访问已下架的商品', {
level: 'warning',
extra: {
productId: 12345,
},
});
5. SourceMap 配置(重要!)
javascript
// vite.config.js
export default {
build: {
sourcemap: true, // 生成 sourcemap
},
};
// 使用 Sentry CLI 上传 sourcemap
// package.json
{
"scripts": {
"build": "vite build && sentry-cli sourcemaps upload --org=your-org --project=your-project ./dist"
}
}
// .sentryclirc 配置文件
[auth]
token=YOUR_AUTH_TOKEN
[defaults]
url=https://sentry.io/
org=your-org
project=your-project
生产环境删除 sourcemap:
javascript
// 上传后删除,防止暴露源码
"build": "vite build && sentry-cli sourcemaps upload ./dist && rm ./dist/**/*.map"
📈 三、用户行为埋点系统
1. 埋点 SDK 设计
javascript
class Tracker {
constructor(config) {
this.config = config; // { url: '上报地址', appId: '应用ID' }
this.queue = []; // 事件队列
this.timer = null;
// 启动定时上报
this.startTimer();
// 页面卸载时上报
window.addEventListener('beforeunload', () => this.flush());
}
// 获取公共参数
getCommonParams() {
return {
app_id: this.config.appId,
app_version: APP_VERSION,
platform: 'web',
user_id: this.getUserId(),
session_id: this.getSessionId(),
device_id: this.getDeviceId(),
page_url: window.location.href,
page_title: document.title,
referrer: document.referrer,
screen_resolution: `${window.screen.width}x${window.screen.height}`,
viewport_size: `${window.innerWidth}x${window.innerHeight}`,
user_agent: navigator.userAgent,
network_type: this.getNetworkType(),
timestamp: Date.now(),
};
}
// 上报事件
track(eventName, params = {}) {
const event = {
event_name: eventName,
...this.getCommonParams(),
...params,
};
this.queue.push(event);
// 队列达到阈值立即上报
if (this.queue.length >= 10) {
this.flush();
}
}
// 批量上报
flush() {
if (this.queue.length === 0) return;
const events = [...this.queue];
this.queue = [];
// 使用 sendBeacon 保证页面卸载时也能发送
const data = JSON.stringify(events);
if (navigator.sendBeacon) {
navigator.sendBeacon(this.config.url, data);
} else {
fetch(this.config.url, {
method: 'POST',
body: data,
headers: { 'Content-Type': 'application/json' },
keepalive: true, // 页面卸载后仍继续请求
}).catch(console.error);
}
}
// 定时上报(每 5 秒)
startTimer() {
this.timer = setInterval(() => {
this.flush();
}, 5000);
}
// 获取设备 ID
getDeviceId() {
let deviceId = localStorage.getItem('device_id');
if (!deviceId) {
deviceId = `device_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
localStorage.setItem('device_id', deviceId);
}
return deviceId;
}
// 获取会话 ID
getSessionId() {
let sessionId = sessionStorage.getItem('session_id');
if (!sessionId) {
sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
sessionStorage.setItem('session_id', sessionId);
}
return sessionId;
}
// 获取网络类型
getNetworkType() {
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
return connection?.effectiveType || 'unknown';
}
// 获取用户 ID
getUserId() {
return window.userInfo?.id || 'anonymous';
}
}
// 初始化
const tracker = new Tracker({
url: 'https://api.yourapp.com/track',
appId: 'your-app-id',
});
export default tracker;
2. 常见埋点场景
页面访问埋点(PV/UV):
javascript
// 路由变化时自动埋点
router.afterEach((to, from) => {
tracker.track('page_view', {
page_name: to.name,
page_path: to.path,
from_page: from.path,
});
});
点击埋点:
javascript
// Vue 指令方式
app.directive('track', {
mounted(el, binding) {
el.addEventListener('click', () => {
tracker.track('click', {
element_id: binding.value.id,
element_name: binding.value.name,
element_type: el.tagName.toLowerCase(),
});
});
},
});
// 使用
<button v-track="{ id: 'submit_order', name: '提交订单' }">
提交订单
</button>
业务埋点:
javascript
// 下单成功
tracker.track('purchase_complete', {
order_id: order.id,
total_amount: order.totalAmount,
product_count: order.products.length,
payment_method: order.paymentMethod,
coupon_used: order.couponId ? 'yes' : 'no',
});
// 搜索行为
tracker.track('search', {
keyword: searchKeyword,
result_count: results.length,
search_type: 'product',
});
曝光埋点(使用 IntersectionObserver):
javascript
class ExposureTracker {
constructor(tracker) {
this.tracker = tracker;
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const { elementId, elementData } = entry.target.dataset;
this.tracker.track('exposure', {
element_id: elementId,
...JSON.parse(elementData || '{}'),
});
// 上报后停止观察
this.observer.unobserve(entry.target);
}
});
},
{ threshold: 0.5 } // 50% 可见时触发
);
}
observe(element, data) {
element.dataset.elementId = data.id;
element.dataset.elementData = JSON.stringify(data);
this.observer.observe(element);
}
}
⚡ 四、性能监控
1. Core Web Vitals 监控
javascript
import { onLCP, onFID, onCLS, onFCP, onTTFB } from 'web-vitals';
function sendToAnalytics(metric) {
tracker.track('web_vitals', {
name: metric.name,
value: metric.value,
rating: metric.rating, // good/needs-improvement/poor
delta: metric.delta,
id: metric.id,
});
}
// 最大内容绘制
onLCP(sendToAnalytics);
// 首次输入延迟
onFID(sendToAnalytics);
// 累积布局偏移
onCLS(sendToAnalytics);
// 首次内容绘制
onFCP(sendToAnalytics);
// 首字节时间
onTTFB(sendToAnalytics);
2. 页面加载性能
javascript
window.addEventListener('load', () => {
// 使用 Performance API
const perfData = performance.getEntriesByType('navigation')[0];
tracker.track('page_performance', {
// DNS 查询时间
dns: perfData.domainLookupEnd - perfData.domainLookupStart,
// TCP 连接时间
tcp: perfData.connectEnd - perfData.connectStart,
// 请求响应时间
ttfb: perfData.responseStart - perfData.requestStart,
// 内容下载时间
download: perfData.responseEnd - perfData.responseStart,
// DOM 解析时间
domParse: perfData.domInteractive - perfData.responseEnd,
// 资源加载时间
resourceLoad: perfData.loadEventStart - perfData.domContentLoadedEventEnd,
// 总加载时间
total: perfData.loadEventEnd - perfData.fetchStart,
});
});
3. 接口性能监控
javascript
// 拦截 fetch
const originalFetch = window.fetch;
window.fetch = function (...args) {
const startTime = performance.now();
const url = args[0];
const method = args[1]?.method || 'GET';
return originalFetch.apply(this, args)
.then((response) => {
const duration = performance.now() - startTime;
tracker.track('api_performance', {
url: url,
method: method,
status: response.status,
duration: Math.round(duration),
success: response.ok,
});
return response;
})
.catch((error) => {
const duration = performance.now() - startTime;
tracker.track('api_error', {
url: url,
method: method,
duration: Math.round(duration),
error: error.message,
});
// 同时上报到 Sentry
Sentry.captureException(error, {
tags: { api: url },
});
throw error;
});
};
🔍 五、线上问题排查实战流程
步骤 1:问题发现
监控告警渠道:
- Sentry 邮件/钉钉告警
- 性能监控平台告警
- 用户客服反馈
- 业务数据异常
步骤 2:信息收集(从 Sentry)
关键信息:
- 错误堆栈 - 定位具体代码位置(结合 SourceMap)
- 用户信息 - 影响的用户 ID、邮箱
- 设备环境 - 浏览器、操作系统、网络
- Session Replay - 回放用户操作录像
- Breadcrumbs - 错误前的操作轨迹
- 发生频率 - 错误发生次数、影响用户数
步骤 3:埋点数据分析
javascript
// 通过埋点还原用户操作路径
1. 进入商品详情页 (14:30:25)
2. 点击"立即购买" (14:30:40)
3. 选择规格 (14:30:45)
4. 点击"提交订单" (14:30:50)
5. ❌ 错误发生 (14:30:51)
步骤 4:影响范围评估
多维度分析:
- 时间维度 - 从什么时候开始出现?
- 用户维度 - 影响多少用户?占比多少?
- 版本维度 - 是否特定版本才有?
- 设备维度 - 是否特定浏览器/OS?
- 地域维度 - 是否特定地区?
步骤 5:问题复现
- 根据 Sentry 信息在本地构造场景
- 使用相同浏览器版本
- 查看 Session Replay 模拟操作
- 检查网络状态(Chrome DevTools 限速)
步骤 6:解决问题
快速止损:
- 回滚到上一个稳定版本
- 灰度关闭出问题的功能
- 降级处理(禁用非核心功能)
根本解决:
- 修复代码 bug
- 增加容错逻辑
- 补充单元测试
💡 六、实战案例分享
案例 1:白屏问题排查
现象: 部分移动端用户反馈页面白屏
Sentry 信息:
vbnet
ChunkLoadError: Loading chunk 3 failed.
(error: https://cdn.example.com/static/js/3.abc123.js)
浏览器:Safari 14.0 / iOS 14.5
影响用户:235 人
错误率:2.3%
埋点数据:
- 错误集中在路由切换时
- 用户从首页点击"我的订单"后白屏
Session Replay:
- 用户点击按钮 → 页面加载状态 → 白屏
原因分析: 路由懒加载的 chunk 文件加载失败(CDN 缓存问题)
解决方案:
javascript
// 添加 chunk 加载失败重试机制
const loadWithRetry = (importFunc, retries = 3) => {
return new Promise((resolve, reject) => {
importFunc()
.then(resolve)
.catch((error) => {
if (retries > 0 && error.name === 'ChunkLoadError') {
console.log(`Chunk 加载失败,重试中... (剩余 ${retries} 次)`);
// 延迟后重试
setTimeout(() => {
loadWithRetry(importFunc, retries - 1).then(resolve, reject);
}, 1000);
} else {
reject(error);
}
});
});
};
// 路由配置
const routes = [
{
path: '/orders',
component: () => loadWithRetry(() => import('./views/Orders.vue')),
},
];
案例 2:接口请求突然大量 500 错误
现象: 监控告警,某接口 5xx 错误率从 0.1% 升至 5%
Sentry 信息:
makefile
POST /api/orders/create
Status: 500 Internal Server Error
影响用户:1,200+ 人
时间段:14:00-14:30
埋点分析:
- 错误集中在"提交订单"步骤
- 特定商品 ID: 12345 触发高频
排查过程:
- 联系后端查看日志 → 数据库查询超时
- 发现商品 12345 的库存数据异常(负数)
- 查询导致死锁
解决方案:
- 前端:添加超时重试 + 降级提示
- 后端:修复数据 + 增加熔断机制
🛡️ 七、最佳实践总结
1. Sentry 使用技巧
✅ DO:
- 设置合理的采样率(控制成本)
- 上传 SourceMap 到 Sentry(生产删除)
- 配置错误分组规则
- 设置告警通知规则
- 定期清理过期数据
❌ DON'T:
- 不要上报敏感信息(密码、token)
- 不要把所有错误都上报(过滤无用错误)
- 不要在生产环境保留 SourceMap 文件
- 不要忘记设置 environment 和 release
2. 埋点设计原则
- 准确性 - 埋点数据要准确反映用户行为
- 完整性 - 覆盖关键业务流程
- 性能 - 批量上报,避免阻塞主线程
- 隐私 - 不采集敏感个人信息
3. 监控告警策略
javascript
// 错误率阈值告警
错误率 > 1% → 发送告警
错误率 > 5% → 紧急告警 + 自动回滚
// 性能阈值告警
LCP > 2.5s → 警告
LCP > 4.0s → 严重
// 业务指标告警
下单转化率下降 > 20% → 告警
4. SourceMap 管理
bash
# 构建时生成 sourcemap
npm run build
# 上传到 Sentry
sentry-cli sourcemaps upload --org=your-org --project=your-project ./dist
# 删除本地 sourcemap(防止泄露源码)
find ./dist -name "*.map" -delete
📚 八、推荐工具和资源
监控工具对比
工具 | 优势 | 适用场景 |
---|---|---|
Sentry | 开源、功能强大、支持多语言 | 中小型团队、个人项目 |
Fundebug | 国内服务、中文支持好 | 国内项目 |
阿里云 ARMS | 集成度高、适合阿里云生态 | 阿里云用户 |
腾讯云前端性能监控 | 性能监控强 | 腾讯云用户 |
自建方案 | 完全可控 | 大型团队、特殊需求 |
学习资源
🎉 总结
一套完善的前端监控体系包括:
- 错误监控(Sentry)- 及时发现问题
- 性能监控(Web Vitals)- 保证用户体验
- 用户行为埋点 - 还原问题场景
- 告警机制 - 快速响应
- 问题排查流程 - 高效解决
只有建立了完善的监控体系,才能在线上问题发生时,快速定位、快速解决,保障用户体验。
如果觉得这篇文章对你有帮助,欢迎点赞收藏!有任何问题欢迎在评论区讨论 💬
相关文章推荐:
#前端 #监控 #Sentry #性能优化 #前端工程化