跳到主要内容

前端监控体系设计

Agent 产品上线后,用户反馈页面卡死、回复到一半就没了、偶尔白屏。后端日志一切正常——花了三天才定位到原因:某个工具调用返回了超大 JSON,前端解析时内存溢出,而我们没有前端监控系统。

Agent 应用链路更长、异步操作更多,没有监控就像开车没有仪表盘。


监控体系全景

先画一张图,看看前端监控体系到底包含哪些东西:

+====================================================================+
| 前端监控体系 (Frontend Monitoring) |
+====================================================================+
| |
| +------------------+ +------------------+ +------------------+ |
| | 错误监控 | | 性能监控 | | 行为监控 | |
| | (Error) | | (Performance) | | (Behavior) | |
| +------------------+ +------------------+ +------------------+ |
| | JS Runtime Error | | Web Vitals | | 页面 PV/UV | |
| | Promise Rejection | | FCP / LCP / CLS | | 点击热力图 | |
| | 资源加载失败 | | TTFB / TTI | | 用户路径追踪 | |
| | 接口错误 | | 自定义指标 | | 留存分析 | |
| | 白屏检测 | | 资源瀑布图 | | 功能使用率 | |
| +------------------+ +------------------+ +------------------+ |
| |
| +--------------------------------------------------------------+ |
| | Agent 场景专项监控 | |
| +--------------------------------------------------------------+ |
| | Token 消耗 | 响应延迟 | 工具调用成功率 | 流式中断率 | |
| | Agent 回复质量 | 多轮对话上下文命中率 | 并发会话数 | |
| +--------------------------------------------------------------+ |
| |
+====================================================================+
| | |
v v v
+--------------+ +--------------+ +--------------+
| 数据采集层 | | 数据传输层 | | 数据展示层 |
| SDK / Beacon| -------> | HTTP / WS | -------> | Dashboard |
| Performance | | 采样 / 聚合 | | Alert |
+--------------+ +--------------+ +--------------+

简单来说,前端监控分三大块:

  1. 错误监控 — 应用出了什么问题
  2. 性能监控 — 应用跑得快不快
  3. 行为监控 — 用户在干什么

对于 Agent 应用,我们还需要额外加一层 Agent 场景专项监控,后面会详细讲。

整个数据流是:采集 -> 传输 -> 存储 -> 分析 -> 告警 -> 可视化


错误监控

错误监控是整个体系的基石。用户不会告诉你"我的页面报了一个 TypeError",他们只会说"页面挂了"或者"不好使了"。

错误分类

前端错误
|
+--- JS 运行时错误 (Runtime Error)
| - TypeError / ReferenceError / SyntaxError
| - async/await 未捕获异常
| - React 组件渲染错误
|
+--- 资源加载错误 (Resource Error)
| - JS / CSS / 图片 / 字体加载失败
| - CDN 节点异常
|
+--- 接口错误 (API Error)
| - HTTP 状态码异常 (4xx / 5xx)
| - 接口超时
| - 数据格式不符合预期
|
+--- 白屏错误 (White Screen)
- 首屏渲染失败
- 关键 JS 阻塞导致页面无响应

全局错误捕获

第一层是全局兜底,用 window.onerrorunhandledrejection 把所有漏网之鱼捞起来:

// error-monitor.ts - 全局错误监听
class ErrorMonitor {
private static instance: ErrorMonitor;
private errorHandler?: (error: ErrorInfo) => void;

private constructor() {}

static getInstance(): ErrorMonitor {
if (!ErrorMonitor.instance) {
ErrorMonitor.instance = new ErrorMonitor();
}
return ErrorMonitor.instance;
}

init(handler: (error: ErrorInfo) => void) {
this.errorHandler = handler;
this.setupListeners();
}

private setupListeners() {
// 捕获同步运行时错误
window.onerror = (
message: string | Event,
source: string,
lineno: number,
colno: number,
error: Error
) => {
this.report({
type: 'runtime-error',
message: String(message),
source,
lineno,
colno,
stack: error?.stack,
level: 'error',
timestamp: Date.now(),
});
return true; // 阻止默认的错误处理
};

// 捕获异步 Promise 错误
window.addEventListener('unhandledrejection', (event) => {
const error = event.reason;
this.report({
type: 'unhandled-rejection',
message: error?.message || String(error),
stack: error?.stack,
level: 'error',
timestamp: Date.now(),
});
// 生产环境可以不阻止默认行为
// event.preventDefault();
});

// 捕获资源加载错误(通过事件冒泡)
window.addEventListener(
'error',
(event) => {
const target = event.target as HTMLScriptElement | HTMLLinkElement | HTMLImageElement;
if (target && target !== window) {
this.report({
type: 'resource-error',
message: `Failed to load: ${target.tagName} - ${(target as any).src || (target as any).href}`,
level: 'warning',
timestamp: Date.now(),
});
}
},
true // 使用捕获阶段,因为资源错误不冒泡到 window
);

// React 18+ 的 onCaughtError 等会覆盖 Error Boundary 的行为
}

private report(errorInfo: ErrorInfo) {
// 采样:不要每条错误都上报,避免数据洪泛
if (Math.random() > 0.1) return; // 10% 采样

this.errorHandler?.(errorInfo);
}
}

interface ErrorInfo {
type: string;
message: string;
source?: string;
lineno?: number;
colno?: number;
stack?: string;
level: 'error' | 'warning' | 'info';
timestamp: number;
}

Error Boundary 优雅降级

React 的 Error Boundary 是组件级别的错误兜底,比起全局捕获,它能更精细地处理局部崩溃:

// ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
// Agent 场景:支持指定降级 UI 类型
fallbackType?: 'page' | 'component' | 'agent-panel';
}

interface State {
hasError: boolean;
error?: Error;
}

class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };

static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// 上报错误到监控系统
this.props.onError?.(error, errorInfo);

// 生产环境上报 Sentry
if (import.meta.env.PROD) {
Sentry.captureException(error, {
contexts: { react: { componentStack: errorInfo.componentStack } },
tags: { component: 'ErrorBoundary' },
});
}
}

render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return this.getDefaultFallback();
}
return this.props.children;
}

private getDefaultFallback() {
switch (this.props.fallbackType) {
case 'agent-panel':
return (
<div className="agent-error-panel">
<div className="agent-error-icon">!</div>
<p>Agent 服务暂时不可用,请稍后重试</p>
<button onClick={() => this.setState({ hasError: false })}>
重新连接
</button>
</div>
);
case 'component':
return (
<div className="component-error">
<p>该模块加载出错</p>
<button onClick={() => this.setState({ hasError: false })}>
重试
</button>
</div>
);
default:
return (
<div className="page-error">
<h2>页面出了点问题</h2>
<p>我们已经记录了这个错误,请刷新页面试试</p>
<button onClick={() => window.location.reload()}>刷新页面</button>
</div>
);
}
}
}

使用方式也很简单,关键组件外面包一层就行:

// 在 Agent 面板外使用
<ErrorBoundary fallbackType="agent-panel" onError={handleError}>
<AgentChatPanel conversationId={id} />
</ErrorBoundary>

性能监控

Web Vitals 核心指标

Google 提出的 Web Vitals 是衡量用户体验的黄金标准。做 Agent 应用也一样要关注这些:

指标含义目标值Agent 场景特殊性
LCP (Largest Contentful Paint)最大内容绘制< 2.5sAgent 聊天气泡渲染速度
CLS (Cumulative Layout Shift)累积布局偏移< 0.1流式输出时文字不断插入,容易造成布局抖动
INP (Interaction to Next Paint)交互到下次绘制< 200ms工具调用结果渲染的响应速度
FCP (First Contentful Paint)首次内容绘制< 1.8s聊天界面首屏加载
TTFB (Time to First Byte)首字节时间< 800msAPI Gateway 响应时间
TTI (Time to Interactive)可交互时间< 3.8sAgent 应用可开始对话的时间

Web Vitals 采集

// performance-monitor.ts
import { onLCP, onCLS, onINP, onFCP, onTTFB } from 'web-vitals';

interface PerformanceMetric {
name: string;
value: number;
rating: 'good' | 'needs-improvement' | 'poor';
delta: number;
timestamp: number;
url: string;
userAgent: string;
}

class PerformanceMonitor {
private metrics: PerformanceMetric[] = [];

init() {
const reportMetric = (metric: any) =&gt; {
const data: PerformanceMetric = {
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
};
this.metrics.push(data);
this.send(data);
};

onLCP(reportMetric); // 最大内容绘制
onCLS(reportMetric); // 累积布局偏移
onINP(reportMetric); // 交互到下次绘制
onFCP(reportMetric); // 首次内容绘制
onTTFB(reportMetric); // 首字节时间

// 页面关闭前确保数据发出
window.addEventListener('visibilitychange', () =&gt; {
if (document.visibilityState === 'hidden') {
this.flush();
}
});
}

private send(metric: PerformanceMetric) {
// 使用 sendBeacon 在页面关闭时也能可靠发送
if (navigator.sendBeacon) {
const blob = new Blob([JSON.stringify(metric)], {
type: 'application/json',
});
navigator.sendBeacon('/api/metrics', blob);
} else {
fetch('/api/metrics', {
method: 'POST',
body: JSON.stringify(metric),
headers: { 'Content-Type': 'application/json' },
keepalive: true,
});
}
}

private flush() {
if (this.metrics.length === 0) return;
const data = [...this.metrics];
this.metrics = [];
navigator.sendBeacon(
'/api/metrics/batch',
new Blob([JSON.stringify(data)], { type: 'application/json' })
);
}
}

Agent 应用自定义性能指标

除了标准 Web Vitals,Agent 应用还有一些独特的性能指标需要关注:

// agent-metrics.ts

interface AgentMetrics {
// 首次 Token 延迟(用户发消息到第一个 token 返回的时间)
firstTokenLatency: number;
// 流式输出完成时间
streamCompleteTime: number;
// Agent 思考时间(用户发消息到 Agent 开始调用工具的时间)
thinkingTime: number;
// 单次对话的 Token 消耗
tokenConsumption: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
// 工具调用链路耗时
toolCallDurations: Array&lt;{
toolName: string;
duration: number;
success: boolean;
}&gt;;
}

class AgentPerformanceTracker {
private startTime: number = 0;
private firstTokenTime: number = 0;

onUserSend() {
this.startTime = performance.now();
this.firstTokenTime = 0;
}

onFirstToken() {
if (this.firstTokenTime === 0) {
this.firstTokenTime = performance.now();
const latency = this.firstTokenTime - this.startTime;

// 上报首次 Token 延迟
this.report({
metric: 'first_token_latency',
value: latency,
timestamp: Date.now(),
});

// 延迟过高告警
if (latency &gt; 3000) {
this.report({
metric: 'slow_first_token',
value: latency,
level: 'warning',
});
}
}
}

onStreamComplete() {
const totalTime = performance.now() - this.startTime;
this.report({
metric: 'stream_complete_time',
value: totalTime,
timestamp: Date.now(),
});
}

onToolCall(toolName: string, duration: number, success: boolean) {
this.report({
metric: 'tool_call',
value: { toolName, duration, success },
timestamp: Date.now(),
});
}

onTokenUsage(usage: { prompt_tokens: number; completion_tokens: number }) {
this.report({
metric: 'token_usage',
value: usage,
timestamp: Date.now(),
});
}

private report(data: any) {
// 发送到监控后端
fetch('/api/agent-metrics', {
method: 'POST',
body: JSON.stringify(data),
headers: { 'Content-Type': 'application/json' },
}).catch(() =&gt; {}); // 监控上报失败不能影响正常业务
}
}

行为监控

行为监控回答的是"用户在干什么"这个问题。对于 Agent 应用来说,行为数据特别有价值,因为它能帮你理解用户到底想用 Agent 做什么。

基础行为埋点

// behavior-tracker.ts
interface TrackEvent {
event: string;
properties: Record&lt;string, any&gt;;
timestamp: number;
sessionId: string;
userId?: string;
}

class BehaviorTracker {
private sessionId: string;
private eventQueue: TrackEvent[] = [];
private flushTimer?: ReturnType&lt;typeof setInterval&gt;;

constructor() {
this.sessionId = this.generateSessionId();
this.flushTimer = setInterval(() =&gt; this.flush(), 10000); // 10s 批量上报
}

// 页面浏览
trackPageView(page: string) {
this.track('page_view', { page, referrer: document.referrer });
}

// 按钮点击
trackClick(element: string, context?: Record&lt;string, any&gt;) {
this.track('click', { element, ...context });
}

// 自定义事件
track(event: string, properties: Record&lt;string, any&gt; = {}) {
this.eventQueue.push({
event,
properties,
timestamp: Date.now(),
sessionId: this.sessionId,
userId: this.getUserId(),
});

// 队列满了立即发
if (this.eventQueue.length &gt;= 50) {
this.flush();
}
}

private flush() {
if (this.eventQueue.length === 0) return;
const events = [...this.eventQueue];
this.eventQueue = [];

navigator.sendBeacon?.(
'/api/events',
new Blob([JSON.stringify(events)], { type: 'application/json' })
);
}

private generateSessionId(): string {
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
}

private getUserId(): string | undefined {
return localStorage.getItem('user_id') || undefined;
}

destroy() {
if (this.flushTimer) clearInterval(this.flushTimer);
this.flush(); // 最后一批数据
}
}

Agent 行为埋点

Agent 应用需要追踪一些特有行为:

// agent-behavior.ts
class AgentBehaviorTracker {
constructor(private tracker: BehaviorTracker) {}

// 用户发起对话
trackConversationStart(mode: 'text' | 'voice') {
this.tracker.track('agent_conversation_start', { mode });
}

// 用户发送消息
trackMessageSend(messageLength: number, hasAttachment: boolean) {
this.tracker.track('agent_message_send', {
messageLength,
hasAttachment,
});
}

// Agent 回复完成
trackAgentReply(tokenCount: number, toolCallsUsed: string[]) {
this.tracker.track('agent_reply_complete', {
tokenCount,
toolCallsUsed,
});
}

// 用户对 Agent 回复的反馈
trackFeedback(rating: 1 | 2 | 3 | 4 | 5, feedbackType?: string) {
this.tracker.track('agent_feedback', { rating, feedbackType });
}

// 工具调用触发(用户主动选择使用某个工具)
trackToolSelect(toolName: string) {
this.tracker.track('agent_tool_select', { toolName });
}

// 用户中断 Agent 回复
trackReplyInterrupt(position: number) {
this.tracker.track('agent_reply_interrupt', { tokenPosition: position });
}

// Agent 切换话题(用户纠正 Agent 方向)
trackConversationRedirect() {
this.tracker.track('agent_redirect', {});
}
}

Agent 场景专项监控

这是我认为做 Agent 产品最需要额外关注的部分。传统的前端监控指标不够用了,我们需要更贴近 Agent 特性的监控维度。

Agent 监控维度
|
+--- 服务质量 (QoS)
| +--- 首次 Token 延迟
| +--- 端到端响应时间
| +--- 流式输出稳定性(有没有中断/卡顿)
| +--- Agent 回复完整率
|
+--- 资源消耗
| +--- Token 消耗趋势
| +--- 单次对话平均 Token
| +--- Prompt 效率(有效回复 token / 总 token)
| +--- 上下文窗口使用率
|
+--- 工具调用
| +--- 工具调用成功率
| +--- 各工具调用耗时分布
| +--- 工具调用失败原因分类
| +--- 工具链深度(一次回复调了多少次工具)
|
+--- 用户体验
| +--- 用户满意度(反馈评分)
| +--- 中断率(用户主动停止回复)
| +--- 追问率(用户是否需要补充说明)
| +--- 多轮对话完成率
|
+--- 异常检测
+--- 回复质量下降检测
+--- 幻觉率(回复中出现不实信息)
+--- 安全事件(Prompt 注入尝试)
+--- 上下文溢出导致的截断

实际监控 SDK 实现

// agent-monitoring-sdk.ts

interface AgentMonitorConfig {
endpoint: string;
appId: string;
sampleRate: number; // 采样率 0-1
batchSize: number;
flushInterval: number;
enablePerformanceObserver: boolean;
}

class AgentMonitoringSDK {
private config: AgentMonitorConfig;
private eventBuffer: any[] = [];
private flushTimer?: ReturnType&lt;typeof setInterval&gt;;

constructor(config: Partial&lt;AgentMonitorConfig&gt; = {}) {
this.config = {
endpoint: '/api/monitoring',
appId: 'agent-app',
sampleRate: 1,
batchSize: 30,
flushInterval: 5000,
enablePerformanceObserver: true,
...config,
};
this.init();
}

private init() {
this.setupFlushTimer();
if (this.config.enablePerformanceObserver) {
this.setupPerformanceObserver();
}
this.setupBeforeUnload();
}

// 记录 Agent 会话开始
trackSessionStart(sessionId: string, userId: string) {
this.push({
type: 'session_start',
sessionId,
userId,
timestamp: Date.now(),
meta: {
screen: `${screen.width}x${screen.height}`,
device: navigator.userAgent,
language: navigator.language,
},
});
}

// 记录消息往返
trackMessageRoundtrip(data: {
sessionId: string;
messageId: string;
firstTokenMs: number;
totalMs: number;
tokenCount: number;
toolCalls: string[];
success: boolean;
errorType?: string;
}) {
this.push({
type: 'message_roundtrip',
...data,
timestamp: Date.now(),
});
}

// 记录工具调用详情
trackToolInvocation(data: {
sessionId: string;
toolName: string;
inputSize: number;
outputSize: number;
durationMs: number;
success: boolean;
errorCode?: string;
}) {
this.push({
type: 'tool_invocation',
...data,
timestamp: Date.now(),
});
}

// 记录流式输出异常
trackStreamAnomaly(data: {
sessionId: string;
messageId: string;
anomalyType: 'timeout' | 'interrupt' | 'malformed_chunk' | 'truncated';
position: number; // 在哪个位置出的问题
partialContent: string;
}) {
this.push({
type: 'stream_anomaly',
...data,
timestamp: Date.now(),
});
}

// 记录安全事件
trackSecurityEvent(data: {
sessionId: string;
eventType: 'prompt_injection' | 'jailbreak_attempt' | 'sensitive_data_leak';
details: string;
}) {
this.push({
type: 'security_event',
...data,
timestamp: Date.now(),
// 安全事件 100% 采样,不能丢
forceSample: true,
});
}

// 性能Observer:监听长任务、长连接等
private setupPerformanceObserver() {
// 监听长任务(&gt;50ms 的任务会阻塞主线程)
const longTaskObserver = new PerformanceObserver((list) =&gt; {
for (const entry of list.getEntries()) {
if (entry.duration &gt; 100) {
this.push({
type: 'long_task',
duration: entry.duration,
startTime: entry.startTime,
timestamp: Date.now(),
});
}
}
});
longTaskObserver.observe({ entryTypes: ['longtask'] });

// 监听资源加载
const resourceObserver = new PerformanceObserver((list) =&gt; {
for (const entry of list.getEntries()) {
const resource = entry as PerformanceResourceTiming;
if (resource.duration &gt; 3000) {
this.push({
type: 'slow_resource',
name: resource.name,
duration: resource.duration,
type: resource.initiatorType,
timestamp: Date.now(),
});
}
}
});
resourceObserver.observe({ entryTypes: ['resource'] });
}

private push(event: any) {
if (!event.forceSample &amp;&amp; Math.random() &gt; this.config.sampleRate) {
return;
}
this.eventBuffer.push(event);
if (this.eventBuffer.length &gt;= this.config.batchSize) {
this.flush();
}
}

private flush() {
if (this.eventBuffer.length === 0) return;
const events = this.eventBuffer.splice(0, this.config.batchSize);
navigator.sendBeacon?.(
this.config.endpoint,
new Blob(
[JSON.stringify({ appId: this.config.appId, events })],
{ type: 'application/json' }
)
);
}

private setupFlushTimer() {
this.flushTimer = setInterval(() =&gt; this.flush(), this.config.flushInterval);
}

private setupBeforeUnload() {
window.addEventListener('visibilitychange', () =&gt; {
if (document.visibilityState === 'hidden') {
this.flush();
}
});
}
}

数据采集层

有了 SDK 采集数据,接下来要考虑数据怎么传、怎么存。

采集架构

浏览器端 服务端 存储层
+-----------+ +-----------+ +-----------+
| 监控 SDK | --beacon--> | API GW | --写入--> | Kafka |
| | | | | (消息队列) |
| - 错误 | | - 限流 | +-----------+
| - 性能 | | - 采样 | |
| - 行为 | | - 验证 | +-----------+
| - Agent | +-----------+ | 消费者 |
| 指标 | | |
+-----------+ +-----------+ | - 清洗 |
| 丢弃 | | - 聚合 |
| 无效数据 ----------------->| 不合规 | | - 入库 |
| 重复数据 ----------------->| 数据 | +-----------+
+-----------+ |
+-----------+
| ClickHouse|
| (分析) |
+-----------+
| Redis |
| (实时) |
+-----------+

采集层最佳实践

// data-collection.ts

// 1. 数据清洗:过滤无效和敏感数据
function sanitizeEvent(event: any): any {
// 移除可能的 PII 数据
const sanitized = { ...event };
delete sanitized.password;
delete sanitized.creditCard;
delete sanitized.idCard;

// 截断过大的字段
if (sanitized.stack &amp;&amp; sanitized.stack.length &gt; 1000) {
sanitized.stack = sanitized.stack.slice(0, 1000);
}

// 确保必要字段存在
sanitized.timestamp = sanitized.timestamp || Date.now();
sanitized.url = sanitized.url || window.location.href;

return sanitized;
}

// 2. 采样策略:不同类型的事件用不同的采样率
const SAMPLING_RATES: Record&lt;string, number&gt; = {
// 错误:100% 采样,不能漏
'runtime-error': 1,
'unhandled-rejection': 1,
'resource-error': 0.5,

// 性能:10% 采样足够
'performance-metric': 0.1,

// 行为:5% 采样
'page_view': 0.05,
'click': 0.05,

// Agent 指标:30% 采样
'message_roundtrip': 0.3,
'tool_invocation': 0.3,

// 安全事件:100% 采样
'security_event': 1,
};

function shouldSample(eventType: string): boolean {
const rate = SAMPLING_RATES[eventType] ?? 0.1;
return Math.random() &lt; rate;
}

// 3. 批量发送 + 失败重试
class ReliableTransport {
private retryQueue: any[] = [];
private maxRetries = 3;

async send(events: any[]) {
try {
const response = await fetch('/api/monitoring/batch', {
method: 'POST',
body: JSON.stringify({ events }),
headers: { 'Content-Type': 'application/json' },
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
// 放入重试队列
events.forEach((e) =&gt; {
if ((e._retryCount || 0) &lt; this.maxRetries) {
e._retryCount = (e._retryCount || 0) + 1;
this.retryQueue.push(e);
}
});
this.scheduleRetry();
}
}

private scheduleRetry() {
setTimeout(() =&gt; {
if (this.retryQueue.length &gt; 0) {
const batch = this.retryQueue.splice(0, 20);
this.send(batch);
}
}, 5000);
}
}

Sentry 集成

Sentry 是目前前端错误监控的事实标准。在 Agent 应用中,我们需要对 Sentry 做一些定制化配置。

基础配置

// sentry-init.ts
import * as Sentry from '@sentry/react';

Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.MODE,
release: import.meta.env.VITE_APP_VERSION,

// 采样率
tracesSampleRate: import.meta.env.PROD ? 0.1 : 1.0, // 性能追踪
replaysSessionSampleRate: 0.01, // 会话录制
replaysOnErrorSampleRate: 1.0, // 出错时 100% 录制

// 优化:忽略某些不重要的错误
ignoreErrors: [
'ResizeObserver loop limit exceeded',
'Non-Error promise rejection captured',
'NetworkError', // 网络错误通常不需要上报
/ChunkLoadError/i, // Webpack chunk 加载失败
],

// 过滤掉特定 URL 的请求错误
denyUrls: [
/extensions\//i, // 浏览器扩展
/^chrome:\/\//i,
],

// 附加全局上下文
beforeSend(event) {
// 添加 Agent 会话信息
if (window.__agentSessionId) {
event.tags = {
...event.tags,
agentSessionId: window.__agentSessionId,
};
}

// 过滤掉开发环境的错误
if (import.meta.env.DEV) {
return null;
}

return event;
},

integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({
maskAllText: true, // 隐藏用户输入(保护隐私)
blockAllMedia: true, // 阻止媒体录制
}),
],
});

Agent 上下文增强

// sentry-agent-context.ts
import * as Sentry from '@sentry/react';

// 为 Sentry 添加 Agent 特有的上下文
export function setAgentContext(session: {
sessionId: string;
model: string;
userId: string;
conversationHistory: number; // 历史消息数
}) {
Sentry.setContext('agent_session', {
sessionId: session.sessionId,
model: session.model,
conversationLength: session.conversationHistory,
});

Sentry.setUser({ id: session.userId });

Sentry.setTag('agent.model', session.model);
}

// 在 Agent 出错时附加更多调试信息
export function withAgentErrorContext&lt;T&gt;(
fn: () =&gt; T,
context: { messageHistory?: any[]; tools?: string[] }
): T {
try {
return fn();
} catch (error) {
Sentry.withScope((scope) =&gt; {
scope.setExtras({
recentMessages: context.messageHistory?.slice(-5),
availableTools: context.tools,
});
Sentry.captureException(error);
});
throw error;
}
}

自托管方案对比

对于数据敏感的企业,可能不想把监控数据送到第三方。主流的自托管方案:

方案优势劣势适合场景
Sentry Self-Hosted功能完整、社区活跃资源消耗大(推荐 4GB+ 内存)中大型团队
SigNoz基于 ClickHouse,查询快生态不如 Sentry 成熟已有 ClickHouse 基础设施的团队
Uptrace轻量、Go 编写功能相对较少小团队、预算有限
Grafana + Loki + Tempo全链路可观测需要自己拼装已在用 Grafana 生态的团队

我的建议是:小团队直接用 Sentry Cloud,中大型团队考虑 Sentry Self-Hosted 或 SigNoz。自建监控系统投入产出比很低,除非你有非常特殊的合规要求。


数据可视化与 Dashboard

光采集数据没有用,得让人看得懂、用得上。

Dashboard 设计原则

一个合格的前端监控 Dashboard 应该包含这几层:

+------------------------------------------------------------------+
| 监控大屏 Dashboard |
+------------------------------------------------------------------+
| |
| 第一层:概览 |
| +-------------+ +-------------+ +-------------+ +-----------+ |
| | 错误率 | | P95 延迟 | | 活跃会话 | | Token 成本 | |
| | 0.03% | | 1.2s | | 1,234 | | $89.5 | |
| | -2% vs 昨日 | | +5% vs 昨日 | | +15% vs 昨日 | | +8% vs 昨日| |
| +-------------+ +-------------+ +-------------+ +-----------+ |
| |
| 第二层:趋势图 |
| +----------------------------------------------------------+ |
| | 错误趋势 (按小时) | 响应延迟趋势 | Token 消耗趋势 | |
| | [折线图] | [折线图] | [面积图] | |
| +----------------------------------------------------------+ |
| |
| 第三层:详情 |
| +----------------------------------------------------------+ |
| | 错误排行 Top10 | 慢接口列表 | 工具调用成功率 | |
| | [列表] | [表格] | [柱状图] | |
| +----------------------------------------------------------+ |
| |
| 第四层:Agent 专项 |
| +----------------------------------------------------------+ |
| | Token 效率 | 工具链分布 | 用户满意度 | 流式稳定性 | |
| | [饼图] | [桑基图] | [评分分布] | [热力图] | |
| +----------------------------------------------------------+ |
| |
+------------------------------------------------------------------+

用 Grafana 构建 Agent 监控看板

如果你选择 Grafana 方案,以下是一些核心查询(PromQL / SQL)示例:

-- 首次 Token 延迟 P50 / P95 / P99
SELECT
percentile_cont(0.50) WITHIN GROUP (ORDER BY first_token_ms) as p50,
percentile_cont(0.95) WITHIN GROUP (ORDER BY first_token_ms) as p95,
percentile_cont(0.99) WITHIN GROUP (ORDER BY first_token_ms) as p99
FROM agent_metrics
WHERE timestamp &gt; now() - interval '1 hour'
AND event_type = 'message_roundtrip';

-- 工具调用成功率(按工具分类)
SELECT
tool_name,
COUNT(*) as total,
SUM(CASE WHEN success THEN 1 ELSE 0 END) as success_count,
ROUND(SUM(CASE WHEN success THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) as success_rate
FROM tool_invocations
WHERE timestamp &gt; now() - interval '24 hours'
GROUP BY tool_name
ORDER BY total DESC;

-- 每小时 Token 消耗趋势
SELECT
date_trunc('hour', timestamp) as hour,
SUM(completion_tokens) as total_tokens,
COUNT(DISTINCT session_id) as sessions
FROM agent_metrics
WHERE timestamp &gt; now() - interval '7 days'
GROUP BY hour
ORDER BY hour;

告警策略

有了数据和看板,还需要在出问题时主动通知你。告警策略的设计直接决定了你是被噪音淹没还是真的能快速发现问题。

告警分级

P0 (紧急 - 立即通知)
+--- 全站白屏率 &gt; 5%
+--- Agent 服务完全不可用
+--- 安全事件 (Prompt 注入攻击)
+--- 数据泄露风险

P1 (严重 - 5分钟内通知)
+--- 错误率突增 &gt; 3 倍基线
+--- 首次 Token 延迟 P95 &gt; 10s
+--- 工具调用成功率 &lt; 80%
+--- 核心接口 5xx 率 &gt; 1%

P2 (警告 - 工作时间通知)
+--- 性能指标退化 &gt; 20%
+--- Token 消耗异常波动
+--- 非核心工具调用失败率升高
+--- 用户满意度评分下降

P3 (提醒 - 日报/周报汇总)
+--- 代码覆盖率下降
+--- 新增 Warning 级别错误
+--- 资源加载时间增长趋势

告警规则实现

// alert-rules.ts

interface AlertRule {
name: string;
metric: string;
condition: 'gt' | 'lt' | 'eq' | 'change_pct';
threshold: number;
window: number; // 分钟
severity: 'P0' | 'P1' | 'P2' | 'P3';
cooldown: number; // 冷却时间(分钟),避免告警风暴
channels: ('sms' | 'email' | 'feishu' | 'webhook')[];
}

const AGENT_ALERT_RULES: AlertRule[] = [
{
name: 'Agent 服务不可用',
metric: 'agent_availability',
condition: 'lt',
threshold: 99.0,
window: 5,
severity: 'P0',
cooldown: 10,
channels: ['sms', 'feishu'],
},
{
name: '首次 Token 延迟过高',
metric: 'first_token_latency_p95',
condition: 'gt',
threshold: 10000,
window: 10,
severity: 'P1',
cooldown: 15,
channels: ['feishu', 'webhook'],
},
{
name: '错误率突增',
metric: 'error_rate',
condition: 'change_pct',
threshold: 300, // 增长 300%
window: 15,
severity: 'P1',
cooldown: 30,
channels: ['feishu', 'email'],
},
{
name: '工具调用成功率下降',
metric: 'tool_call_success_rate',
condition: 'lt',
threshold: 80,
window: 30,
severity: 'P2',
cooldown: 60,
channels: ['feishu'],
},
{
name: 'Token 消耗异常',
metric: 'avg_token_per_message',
condition: 'gt',
threshold: 4000,
window: 60,
severity: 'P2',
cooldown: 120,
channels: ['feishu'],
},
];

// 防告警风暴:同一规则在冷却期内不重复发送
class AlertManager {
private lastAlertTime: Map&lt;string, number&gt; = new Map();

evaluateRules(metrics: Record&lt;string, number&gt;) {
for (const rule of AGENT_ALERT_RULES) {
const value = metrics[rule.metric];
if (value === undefined) continue;

if (this.shouldFire(rule, value)) {
const lastTime = this.lastAlertTime.get(rule.name) || 0;
if (Date.now() - lastTime &gt; rule.cooldown * 60 * 1000) {
this.fireAlert(rule, value);
this.lastAlertTime.set(rule.name, Date.now());
}
}
}
}

private shouldFire(rule: AlertRule, value: number): boolean {
switch (rule.condition) {
case 'gt': return value &gt; rule.threshold;
case 'lt': return value &lt; rule.threshold;
case 'eq': return value === rule.threshold;
default: return false;
}
}

private fireAlert(rule: AlertRule, value: number) {
const message = `[${rule.severity}] ${rule.name}: 当前值 ${value}, 阈值 ${rule.threshold}`;

for (const channel of rule.channels) {
switch (channel) {
case 'feishu':
this.sendFeishu(message);
break;
case 'sms':
this.sendSMS(message);
break;
case 'email':
this.sendEmail(message);
break;
}
}
}

private sendFeishu(message: string) {
fetch(import.meta.env.VITE_FEISHU_WEBHOOK, {
method: 'POST',
body: JSON.stringify({
msg_type: 'text',
content: { text: message },
}),
});
}

private sendSMS(message: string) { /* ... */ }
private sendEmail(message: string) { /* ... */ }
}

告警注意事项

几个踩过的坑分享一下:

  1. 一定要有冷却期。不然一次告警能发几十条短信,半夜把你吵醒三次。
  2. 用相对变化而不是绝对值。错误率从 0.01% 涨到 0.03%,绝对值很小但相对变化是 200%,应该告警。
  3. P0 告警走电话,不要走微信。你可能不会随时看微信,但电话一定会接。
  4. 告警内容要包含上下文和排查入口。不要只说"服务异常",要告诉人去哪里看、怎么查。

常见坑与避坑指南

做前端监控这几年,我总结了几个高频踩坑点:

坑 1:监控 SDK 本身影响性能

监控代码跑在主线程上,如果处理逻辑太重,反而会拖慢应用。特别是 Agent 应用需要处理大量流式数据,监控 SDK 不能在主线程做 JSON 解析和网络请求。

解决方案:用 Web Worker 处理数据聚合,用 requestIdleCallback 延迟非紧急上报,用 sendBeacon 保证页面关闭时也能发出数据。

坑 2:错误风暴导致监控系统崩溃

某次代码发布后有个 bug,每个用户操作都会触发 10 条错误日志。我们的监控后端在 5 分钟内收到了 500 万条错误数据,直接挂了。

解决方案:前端做采样和去重,后端做限流。对相同错误(相同 message + stack)在 1 分钟内只保留 1 条。设置全局错误上报上限,超过就丢弃。

坑 3:Sentry Source Map 泄露

为了线上能看源码级的错误栈,我们需要上传 Source Map 到 Sentry。但如果不小心,Source Map 会被浏览器开发者工具直接访问到,等于把源码公开了。

解决方案:Source Map 只上传到 Sentry,不要部署到 CDN。Sentry 支持上传后立即删除 Source Map 文件。nginx 配置禁止 .map 文件被外部访问。

坑 4:Agent 流式输出场景下的错误丢失

传统的 window.onerror 无法捕获 SSE/WebSocket 连接中断或流式解析过程中的错误。Agent 应用大量使用流式输出,这些场景的错误很容易被忽略。

解决方案:在 SSE 的 EventSource.onerrorReadableStream 的读取循环中单独做错误捕获和上报。流式输出还需要监控 stream.cancel()abort 事件。

坑 5:监控数据导致隐私合规问题

行为监控采集的用户操作数据可能包含个人信息。如果采集了用户的聊天内容用于分析,可能违反 GDPR 等隐私法规。

解决方案:敏感字段在采集端就做脱敏处理,不上传原始聊天内容,只上传元数据(消息长度、工具类型等)。前端 SDK 加入用户同意机制,未授权前不采集行为数据。

参考资源