前端可观测性实战:从性能指标采集到异常归因的全链路监控体系
一、前端监控的"盲区":用户报障才发现问题
线上前端应用出了问题,最常见的发现路径是:用户投诉 → 客服反馈 → 研发排查 → 复现困难 → 不了了之。服务端监控面板一切正常,API 响应时间 P99 在 50ms 以内,但用户就是觉得页面卡。问题出在前端:首屏加载慢、JS 执行阻塞、接口请求在客户端超时、白屏无响应。这些发生在用户浏览器里的问题,服务端监控完全看不到。更棘手的是,前端异常的归因链路很长——白屏可能是 JS 报错导致渲染中断,也可能是接口超时导致数据为空,还可能是 CDN 节点故障导致资源加载失败。没有全链路监控,排障只能靠猜。本文直接给出前端可观测性体系的建设方案。
二、前端可观测性的三大支柱与数据流架构
前端可观测性建立在三大支柱上:指标(Metrics)、日志(Logs)、链路追踪(Traces)。三者协同才能完成从"发现问题"到"定位原因"的完整闭环。
flowchart TD
subgraph "数据采集层"
A1[Web Vitals: LCP/FID/CLS/INP]
A2[JS 异常: Error + UnhandledRejection]
A3[资源加载: 失败率 + 加载耗时]
A4[API 请求: 状态码 + 延迟 + 请求体大小]
A5[用户行为: PV/UV + 点击流 + 页面停留]
end
subgraph "数据处理层"
B1[SDK 聚合: 批量上报 + 采样率控制]
B2[OTel Collector: 接收 + 转换 + 导出]
B3[ClickHouse: 指标与日志存储]
B4[Jaeger: 链路追踪存储]
end
subgraph "分析展示层"
C1[Grafana: 指标大盘 + 告警]
C2[异常聚合: Error Grouping]
C3[慢链路分析: Trace Waterfall]
C4[用户回放: Session Replay]
end
A1 & A2 & A3 & A4 & A5 –> B1
B1 –> B2
B2 –> B3 & B4
B3 –> C1 & C2
B4 –> C3
A5 –> C4
数据采集的关键设计是采样率控制。全量采集的代价太高——一个日活 100 万的应用,每天产生的监控数据量可能超过 100GB。需要按数据类型设置差异化采样率:JS 异常 100% 采集(低频高价值),API 请求 10% 采样(高频低价值),用户行为 1% 采样(超高频)。
三、前端可观测性体系的生产级实现
3.1 基于 OpenTelemetry 的统一采集 SDK
OpenTelemetry 提供了统一的采集标准,前端通过 @opentelemetry/sdk-web 集成:
// monitoring-sdk.ts — 前端监控 SDK 核心实现
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { ZoneContextManager } from '@opentelemetry/context-zone';
import { B3Propagator } from '@opentelemetry/propagator-b3';
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
class FrontendMonitoringSDK {
private provider: WebTracerProvider;
private samplingRate: number;
constructor(config: {
endpoint: string;
serviceName: string;
samplingRate?: number;
}) {
this.samplingRate = config.samplingRate ?? 0.1; // 默认 10% 采样
// 创建 Trace Provider
this.provider = new WebTracerProvider({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: config.serviceName,
[SemanticResourceAttributes.SERVICE_VERSION]: APP_VERSION,
}),
});
// OTLP Exporter:通过 HTTP 发送到 OTel Collector
const exporter = new OTLPTraceExporter({
url: `${config.endpoint}/v1/traces`,
headers: {
'X-App-Name': config.serviceName,
},
});
// BatchSpanProcessor:批量发送,减少网络请求
this.provider.addSpanProcessor(
new BatchSpanProcessor(exporter, {
maxQueueSize: 100, // 队列最大 100 条
maxExportBatchSize: 20, // 每批最多 20 条
scheduledDelayMillis: 5000, // 每 5 秒发送一次
})
);
// 注册上下文管理器和传播器
this.provider.register({
contextManager: new ZoneContextManager(),
propagator: new B3Propagator(), // B3 传播格式,与服务端链路打通
});
// 自动埋点:XMLHttpRequest 和 Fetch
registerInstrumentations({
instrumentations: [
new XMLHttpRequestInstrumentation({
propagateTraceHeaderCorsUrls: [
/api\\.example\\.com/,
],
}),
new FetchInstrumentation({
propagateTraceHeaderCorsUrls: [
/api\\.example\\.com/,
],
}),
],
});
}
/**
* 上报 Web Vitals 指标
* LCP/FID/CLS/INP 通过 PerformanceObserver 采集
*/
reportWebVitals() {
import('web-vitals').then(({ onLCP, onFID, onCLS, onINP, onTTFB }) => {
onLCP(metric => this.sendMetric('LCP', metric));
onFID(metric => this.sendMetric('FID', metric));
onCLS(metric => this.sendMetric('CLS', metric));
onINP(metric => this.sendMetric('INP', metric));
onTTFB(metric => this.sendMetric('TTFB', metric));
});
}
/**
* JS 异常捕获
* window.onerror + unhandledrejection 双通道
*/
captureErrors() {
window.addEventListener('error', (event) => {
this.sendError({
type: 'js_error',
message: event.message,
stack: event.error?.stack,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
});
});
window.addEventListener('unhandledrejection', (event) => {
this.sendError({
type: 'promise_rejection',
message: String(event.reason),
stack: event.reason?.stack,
});
});
}
private sendMetric(name: string, metric: any) {
// 使用 sendBeacon 确保页面卸载时也能发送
const payload = {
name,
value: metric.value,
rating: metric.rating,
page: window.location.href,
timestamp: Date.now(),
};
if (navigator.sendBeacon) {
navigator.sendBeacon(
'/api/v1/metrics',
JSON.stringify(payload)
);
} else {
fetch('/api/v1/metrics', {
method: 'POST',
body: JSON.stringify(payload),
keepalive: true, // 页面卸载后仍能发送
});
}
}
private sendError(error: Record<string, any>) {
// 错误 100% 上报,不做采样
fetch('/api/v1/errors', {
method: 'POST',
body: JSON.stringify({
…error,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now(),
}),
keepalive: true,
});
}
}
// 初始化
const monitor = new FrontendMonitoringSDK({
endpoint: 'https://otel-collector.example.com',
serviceName: 'web-frontend',
samplingRate: 0.1,
});
monitor.reportWebVitals();
monitor.captureErrors();
3.2 前后端链路打通
前端发起的 API 请求需要携带 Trace Context,服务端才能将前后端链路串联:
// trace-propagation.ts — 前端请求拦截器
import { context, propagation, trace } from '@opentelemetry/api';
// Axios 拦截器:自动注入 Trace Header
axios.interceptors.request.use((config) => {
const span = trace.getSpan(context.active());
if (span) {
// 将 Trace Context 注入请求头
// B3 格式: X-B3-TraceId, X-B3-SpanId, X-B3-Sampled
propagation.inject(context.active(), config.headers, {
set: (headers, key, value) => {
headers[key] = value;
},
});
}
return config;
});
服务端从请求头提取 Trace Context 后,继续创建子 Span,形成完整的调用链。在 Jaeger 中可以看到:前端点击 → API 请求 → 服务端处理 → 数据库查询 → 响应返回的全链路耗时。
3.3 异常聚合与告警
前端异常数量巨大,必须聚合后才能有效分析。基于错误堆栈的 fingerprint 实现自动分组:
# error_grouper.py — 前端异常聚合服务
import hashlib
import re
from collections import defaultdict
class ErrorGrouper:
"""基于堆栈指纹的异常聚合"""
def __init__(self):
# 错误组: fingerprint → 错误详情 + 计数
self.groups: dict[str, dict] = {}
def _generate_fingerprint(self, stack: str) -> str:
"""
生成错误指纹:提取堆栈关键帧
去除行号和变量名,只保留文件名 + 函数名
"""
if not stack:
return hashlib.md5(b"unknown").hexdigest()
# 提取堆栈中的文件名和函数名
frames = re.findall(r'at\\s+(\\w+)\\s+\\(([^:]+)', stack)
# 取前 3 帧(最相关的调用点)
key_frames = frames[:3]
fingerprint_str = "|".join(f"{fn}:{file}" for fn, file in key_frames)
return hashlib.md5(fingerprint_str.encode()).hexdigest()
def add_error(self, error: dict) -> str:
"""添加错误,返回所属的组 ID"""
fingerprint = self._generate_fingerprint(error.get("stack", ""))
if fingerprint not in self.groups:
self.groups[fingerprint] = {
"id": fingerprint[:8],
"message": error["message"],
"stack": error.get("stack"),
"count": 0,
"first_seen": error["timestamp"],
"last_seen": error["timestamp"],
"affected_pages": set(),
}
group = self.groups[fingerprint]
group["count"] += 1
group["last_seen"] = error["timestamp"]
group["affected_pages"].add(error.get("url", "unknown"))
return group["id"]
def get_top_errors(self, limit: int = 20) -> list[dict]:
"""获取影响最大的 Top N 错误组"""
sorted_groups = sorted(
self.groups.values(),
key=lambda g: g["count"],
reverse=True
)
return sorted_groups[:limit]
3.4 Grafana 告警规则
# grafana-alerts.yaml — 前端监控告警规则
groups:
– name: frontend_alerts
rules:
– alert: HighErrorRate
expr: |
sum(rate(frontend_errors_total[5m])) by (service)
/ sum(rate(frontend_requests_total[5m])) by (service)
> 0.01
for: 3m
labels:
severity: critical
annotations:
summary: "前端错误率超过 1%"
description: "服务 {{ $labels.service }} 的前端错误率已达 {{ $value | humanizePercentage }}"
– alert: SlowPageLoad
expr: |
histogram_quantile(0.95,
sum(rate(frontend_page_load_duration_bucket[5m])) by (le, page)
) > 5000
for: 5m
labels:
severity: warning
annotations:
summary: "页面加载 P95 超过 5 秒"
description: "页面 {{ $labels.page }} 的 P95 加载时间为 {{ $value }}ms"
四、前端监控的隐私合规与性能开销
前端监控最大的合规风险是用户隐私。Session Replay 功能会录制用户的完整操作过程,包括输入的密码、身份证号等敏感信息。必须在采集阶段做脱敏处理:对 input[type=password] 的内容替换为 ***,对包含身份证号模式的文本做正则替换。GDPR 和国内《个人信息保护法》都要求用户知情同意,监控 SDK 必须在用户授权后才能启动采集。
性能开销是另一个权衡点。SDK 的每次数据上报都会占用网络带宽和 CPU 时间。BatchSpanProcessor 的批量发送策略可以减少网络请求次数,但队列积压时内存占用会增加。建议 SDK 的内存占用控制在 5MB 以内,CPU 占用控制在 1% 以内。
采样率的设置直接影响监控数据的统计准确性。10% 的采样率意味着 90% 的问题可能被遗漏。对于关键业务路径(如支付流程),建议 100% 采集;对于普通页面浏览,10% 采样足够发现趋势异常。
适用边界:前端可观测性体系适用于日活 10 万以上的应用,监控数据量足够支撑统计分析。小规模应用(日活 < 1 万)建议使用第三方 SaaS 服务(如 Sentry),自建监控的运维成本不划算。链路追踪需要前后端协同改造,如果服务端未接入 OpenTelemetry,前端单独接入的收益有限。
五、总结
前端可观测性体系的核心是三大支柱的协同:指标发现趋势异常,日志定位具体错误,链路追踪还原问题现场。OpenTelemetry 提供了统一的采集标准和前后端链路打通能力,是当前最推荐的方案。生产落地的关键路径:先接入 Web Vitals 和 JS 异常采集(投入小收益大),再实现 API 请求监控和链路追踪,最后按需开启 Session Replay。采样率需要按数据类型差异化配置,关键路径 100% 采集,普通路径 10% 采样。隐私合规是硬约束,必须在采集阶段完成脱敏。


