前端监控体系 📊
前端监控是现代Web应用不可或缺的组成部分,它帮助我们了解应用的运行状况、用户行为和潜在问题。本文将详细介绍如何构建一个完整的前端监控体系。
监控体系概述 🌟
💡 小知识:前端监控体系通常包括性能监控、错误监控、用户行为分析和业务监控四大模块,通过这些数据可以全面了解应用的健康状况和用户体验。
为什么需要前端监控
在复杂的前端应用中,监控系统能够帮助我们:
-
及时发现问题
- 捕获JS运行时错误
- 监控API请求异常
- 检测性能瓶颈
- 发现用户体验问题
-
优化用户体验
- 分析页面加载性能
- 监控交互响应时间
- 追踪用户行为路径
- 识别体验痛点
-
辅助业务决策
- 收集用户行为数据
- 分析功能使用情况
- 评估新功能效果
- 指导产品优化
-
提升开发效率
- 快速定位问题
- 复现错误场景
- 评估代码质量
- 指导性能优化
性能监控实现 ⚡
核心性能指标
// performance-metrics.ts
import { WebVitals } from './types';export const collectWebVitals = (callback: (metrics: WebVitals) => void): void => {// First Contentful Paint (FCP)new PerformanceObserver((entryList) => {const entries = entryList.getEntries();entries.forEach(entry => {callback({name: 'FCP',value: entry.startTime,rating: getRating('FCP', entry.startTime)});});}).observe({ entryTypes: ['paint'] });// Largest Contentful Paint (LCP)new PerformanceObserver((entryList) => {const entries = entryList.getEntries();entries.forEach(entry => {callback({name: 'LCP',value: entry.startTime,rating: getRating('LCP', entry.startTime)});});}).observe({ entryTypes: ['largest-contentful-paint'] });// First Input Delay (FID)new PerformanceObserver((entryList) => {const entries = entryList.getEntries();entries.forEach(entry => {callback({name: 'FID',value: entry.processingStart - entry.startTime,rating: getRating('FID', entry.duration)});});}).observe({ entryTypes: ['first-input'] });// Cumulative Layout Shift (CLS)let clsValue = 0;new PerformanceObserver((entryList) => {const entries = entryList.getEntries();entries.forEach(entry => {if (!entry.hadRecentInput) {clsValue += entry.value;callback({name: 'CLS',value: clsValue,rating: getRating('CLS', clsValue)});}});}).observe({ entryTypes: ['layout-shift'] });
};// 性能指标评级
const getRating = (metric: string, value: number): 'good' | 'needs-improvement' | 'poor' => {const thresholds = {FCP: [1800, 3000],LCP: [2500, 4000],FID: [100, 300],CLS: [0.1, 0.25]};const [good, poor] = thresholds[metric];if (value <= good) return 'good';if (value <= poor) return 'needs-improvement';return 'poor';
};
资源加载监控
// resource-monitoring.ts
interface ResourceTiming {name: string;initiatorType: string;duration: number;transferSize: number;startTime: number;
}export const monitorResourceLoading = (): void => {// 监控资源加载性能new PerformanceObserver((list) => {const resources = list.getEntries().map(entry => ({name: entry.name,initiatorType: entry.initiatorType,duration: entry.duration,transferSize: entry.transferSize,startTime: entry.startTime}));// 分析资源加载情况analyzeResources(resources);}).observe({ entryTypes: ['resource'] });
};const analyzeResources = (resources: ResourceTiming[]): void => {// 按资源类型分组const groupedResources = resources.reduce((acc, resource) => {const type = resource.initiatorType;if (!acc[type]) acc[type] = [];acc[type].push(resource);return acc;}, {});// 计算每种类型资源的统计信息Object.entries(groupedResources).forEach(([type, items]) => {const totalSize = items.reduce((sum, item) => sum + item.transferSize, 0);const avgDuration = items.reduce((sum, item) => sum + item.duration, 0) / items.length;console.log(`Resource Type: ${type}`);console.log(`Total Size: ${(totalSize / 1024).toFixed(2)}KB`);console.log(`Average Loading Time: ${avgDuration.toFixed(2)}ms`);});
};
错误监控实现 🐛
全局错误捕获
// error-monitoring.ts
interface ErrorInfo {type: string;message: string;stack?: string;timestamp: number;url: string;userAgent: string;
}export class ErrorMonitor {private static instance: ErrorMonitor;private errorQueue: ErrorInfo[] = [];private readonly maxQueueSize = 100;private readonly reportThreshold = 10;private constructor() {this.setupErrorListeners();}public static getInstance(): ErrorMonitor {if (!ErrorMonitor.instance) {ErrorMonitor.instance = new ErrorMonitor();}return ErrorMonitor.instance;}private setupErrorListeners(): void {// 捕获JS运行时错误window.addEventListener('error', (event) => {this.captureError({type: 'runtime',message: event.message,stack: event.error?.stack,timestamp: Date.now(),url: window.location.href,userAgent: navigator.userAgent});}, true);// 捕获Promise未处理的rejectionwindow.addEventListener('unhandledrejection', (event) => {this.captureError({type: 'promise',message: event.reason?.message || 'Promise Rejection',stack: event.reason?.stack,timestamp: Date.now(),url: window.location.href,userAgent: navigator.userAgent});});// 捕获资源加载错误window.addEventListener('error', (event) => {if (event.target && (event.target as HTMLElement).tagName) {const target = event.target as HTMLElement;this.captureError({type: 'resource',message: `Resource load failed: ${target.tagName.toLowerCase()}`,timestamp: Date.now(),url: window.location.href,userAgent: navigator.userAgent});}}, true);}private captureError(error: ErrorInfo): void {this.errorQueue.push(error);// 队列超过阈值时上报if (this.errorQueue.length >= this.reportThreshold) {this.reportErrors();}// 队列超过最大大小时清理if (this.errorQueue.length > this.maxQueueSize) {this.errorQueue = this.errorQueue.slice(-this.maxQueueSize);}}private async reportErrors(): Promise<void> {if (this.errorQueue.length === 0) return;try {const errors = [...this.errorQueue];this.errorQueue = [];await fetch('/api/errors', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(errors)});} catch (error) {console.error('Error reporting failed:', error);// 报告失败时,将错误重新加入队列this.errorQueue = [...this.errorQueue, ...errors];}}
}
React错误边界
// ErrorBoundary.tsx
import React, { Component, ErrorInfo } from 'react';interface Props {fallback: React.ReactNode;onError?: (error: Error, errorInfo: ErrorInfo) => void;children: React.ReactNode;
}interface State {hasError: boolean;
}export class ErrorBoundary extends Component<Props, State> {constructor(props: Props) {super(props);this.state = { hasError: false };}static getDerivedStateFromError(): State {return { hasError: true };}componentDidCatch(error: Error, errorInfo: ErrorInfo): void {if (this.props.onError) {this.props.onError(error, errorInfo);}// 上报错误到监控系统ErrorMonitor.getInstance().captureError({type: 'react',message: error.message,stack: error.stack,timestamp: Date.now(),url: window.location.href,userAgent: navigator.userAgent});}render(): React.ReactNode {if (this.state.hasError) {return this.props.fallback;}return this.props.children;}
}
用户行为监控 👥
行为追踪实现
// behavior-tracking.ts
interface UserAction {type: string;target: string;timestamp: number;path: string;metadata?: Record<string, any>;
}export class BehaviorTracker {private static instance: BehaviorTracker;private actions: UserAction[] = [];private readonly maxActions = 100;private constructor() {this.setupEventListeners();}public static getInstance(): BehaviorTracker {if (!BehaviorTracker.instance) {BehaviorTracker.instance = new BehaviorTracker();}return BehaviorTracker.instance;}private setupEventListeners(): void {// 点击事件追踪document.addEventListener('click', (event) => {const target = event.target as HTMLElement;this.trackAction({type: 'click',target: this.getElementPath(target),timestamp: Date.now(),path: window.location.pathname,metadata: {text: target.textContent?.trim(),className: target.className}});});// 页面访问追踪window.addEventListener('popstate', () => {this.trackAction({type: 'navigation',target: window.location.pathname,timestamp: Date.now(),path: window.location.pathname});});// 表单提交追踪document.addEventListener('submit', (event) => {const form = event.target as HTMLFormElement;this.trackAction({type: 'form_submit',target: this.getElementPath(form),timestamp: Date.now(),path: window.location.pathname,metadata: {formId: form.id,formAction: form.action}});});}private getElementPath(element: HTMLElement): string {const path: string[] = [];let current = element;while (current && current !== document.body) {let selector = current.tagName.toLowerCase();if (current.id) {selector += `#${current.id}`;} else if (current.className) {selector += `.${current.className.split(' ').join('.')}`;}path.unshift(selector);current = current.parentElement!;}return path.join(' > ');}private trackAction(action: UserAction): void {this.actions.push(action);// 超过最大数量时清理旧数据if (this.actions.length > this.maxActions) {this.actions = this.actions.slice(-this.maxActions);}// 异步上报数据this.reportActions();}private async reportActions(): Promise<void> {if (this.actions.length === 0) return;try {const actions = [...this.actions];this.actions = [];await fetch('/api/behavior', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(actions)});} catch (error) {console.error('Behavior reporting failed:', error);// 报告失败时,将行为数据重新加入队列this.actions = [...this.actions, ...actions];}}
}
业务监控实现 📈
自定义事件追踪
// business-monitoring.ts
interface BusinessEvent {category: string;action: string;label?: string;value?: number;timestamp: number;metadata?: Record<string, any>;
}export class BusinessMonitor {private static instance: BusinessMonitor;private events: BusinessEvent[] = [];private readonly batchSize = 10;private readonly reportInterval = 5000; // 5秒private constructor() {this.setupAutoReporting();}public static getInstance(): BusinessMonitor {if (!BusinessMonitor.instance) {BusinessMonitor.instance = new BusinessMonitor();}return BusinessMonitor.instance;}public trackEvent(event: Omit<BusinessEvent, 'timestamp'>): void {this.events.push({...event,timestamp: Date.now()});// 达到批量上报阈值时立即上报if (this.events.length >= this.batchSize) {this.reportEvents();}}private setupAutoReporting(): void {// 定期上报数据setInterval(() => {this.reportEvents();}, this.reportInterval);}private async reportEvents(): Promise<void> {if (this.events.length === 0) return;try {const events = [...this.events];this.events = [];await fetch('/api/business-events', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(events)});} catch (error) {console.error('Business event reporting failed:', error);// 报告失败时,将事件重新加入队列this.events = [...this.events, ...events];}}
}// 使用示例
const monitor = BusinessMonitor.getInstance();// 追踪用户注册
monitor.trackEvent({category: 'user',action: 'register',label: 'email',metadata: {source: 'homepage',campaign: 'spring_promotion'}
});// 追踪订单完成
monitor.trackEvent({category: 'order',action: 'complete',value: 99.99,metadata: {orderId: 'ORDER123',products: ['SKU1', 'SKU2']}
});
数据分析与可视化 📊
数据聚合处理
// analytics.ts
interface AnalyticsData {metrics: {performance: {fcp: number[];lcp: number[];fid: number[];cls: number[];};errors: {count: number;types: Record<string, number>;};behavior: {pageViews: number;clicks: number;formSubmits: number;};business: {conversions: number;revenue: number;};};period: string;
}export class AnalyticsProcessor {public static aggregateData(rawData: any): AnalyticsData {return {metrics: {performance: this.processPerformanceData(rawData.performance),errors: this.processErrorData(rawData.errors),behavior: this.processBehaviorData(rawData.behavior),business: this.processBusinessData(rawData.business)},period: rawData.period};}private static processPerformanceData(data: any) {// 处理性能数据return {fcp: this.calculatePercentiles(data.fcp),lcp: this.calculatePercentiles(data.lcp),fid: this.calculatePercentiles(data.fid),cls: this.calculatePercentiles(data.cls)};}private static processErrorData(data: any) {// 处理错误数据return {count: data.length,types: data.reduce((acc, error) => {acc[error.type] = (acc[error.type] || 0) + 1;return acc;}, {})};}private static processBehaviorData(data: any) {// 处理用户行为数据return {pageViews: data.filter(action => action.type === 'navigation').length,clicks: data.filter(action => action.type === 'click').length,formSubmits: data.filter(action => action.type === 'form_submit').length};}private static processBusinessData(data: any) {// 处理业务数据return {conversions: data.filter(event => event.category === 'conversion').length,revenue: data.filter(event => event.category === 'order' && event.action === 'complete').reduce((sum, event) => sum + (event.value || 0), 0)};}private static calculatePercentiles(values: number[]): number[] {const sorted = [...values].sort((a, b) => a - b);return [sorted[Math.floor(sorted.length * 0.5)], // p50sorted[Math.floor(sorted.length * 0.75)], // p75sorted[Math.floor(sorted.length * 0.9)], // p90sorted[Math.floor(sorted.length * 0.95)] // p95];}
}
数据可视化组件
// visualization.tsx
import React from 'react';
import { Line, Bar, Pie } from 'react-chartjs-2';
import { AnalyticsData } from './types';interface DashboardProps {data: AnalyticsData;
}export const AnalyticsDashboard: React.FC<DashboardProps> = ({ data }) => {const performanceChartData = {labels: ['P50', 'P75', 'P90', 'P95'],datasets: [{label: 'FCP',data: data.metrics.performance.fcp,borderColor: 'rgb(75, 192, 192)',tension: 0.1},{label: 'LCP',data: data.metrics.performance.lcp,borderColor: 'rgb(255, 99, 132)',tension: 0.1}]};const errorChartData = {labels: Object.keys(data.metrics.errors.types),datasets: [{data: Object.values(data.metrics.errors.types),backgroundColor: ['rgb(255, 99, 132)','rgb(54, 162, 235)','rgb(255, 206, 86)','rgb(75, 192, 192)']}]};const behaviorChartData = {labels: ['Page Views', 'Clicks', 'Form Submits'],datasets: [{label: 'User Behavior',data: [data.metrics.behavior.pageViews,data.metrics.behavior.clicks,data.metrics.behavior.formSubmits],backgroundColor: 'rgb(54, 162, 235)'}]};return (<div className="analytics-dashboard"><div className="chart-container"><h3>Performance Metrics</h3><Line data={performanceChartData} /></div><div className="chart-container"><h3>Error Distribution</h3><Pie data={errorChartData} /></div><div className="chart-container"><h3>User Behavior</h3><Bar data={behaviorChartData} /></div><div className="metrics-summary"><div className="metric-card"><h4>Total Errors</h4><p>{data.metrics.errors.count}</p></div><div className="metric-card"><h4>Conversions</h4><p>{data.metrics.business.conversions}</p></div><div className="metric-card"><h4>Revenue</h4><p>${data.metrics.business.revenue.toFixed(2)}</p></div></div></div>);
};
监控系统最佳实践 ⭐
数据采集建议
-
采样策略
- 根据流量确定采样率
- 关键用户全量采集
- 错误日志优先级采集
- 性能数据定期采集
-
数据精简
- 只采集必要信息
- 合理设置采集频率
- 避免采集敏感数据
- 遵守数据隐私规范
-
上报优化
- 批量上报减少请求
- 使用信标API上报
- 考虑网络状况
- 失败重试机制
监控告警配置
// alert-system.ts
interface AlertRule {metric: string;condition: 'gt' | 'lt' | 'eq';threshold: number;duration: number; // 持续时间(分钟)severity: 'low' | 'medium' | 'high';
}interface Alert {rule: AlertRule;value: number;timestamp: number;status: 'active' | 'resolved';
}export class AlertSystem {private rules: AlertRule[] = [];private alerts: Alert[] = [];public addRule(rule: AlertRule): void {this.rules.push(rule);}public checkMetric(metric: string, value: number): void {const matchingRules = this.rules.filter(rule => rule.metric === metric);for (const rule of matchingRules) {const isViolated = this.evaluateRule(rule, value);if (isViolated) {this.createAlert(rule, value);}}}private evaluateRule(rule: AlertRule, value: number): boolean {switch (rule.condition) {case 'gt':return value > rule.threshold;case 'lt':return value < rule.threshold;case 'eq':return value === rule.threshold;default:return false;}}private createAlert(rule: AlertRule, value: number): void {const alert: Alert = {rule,value,timestamp: Date.now(),status: 'active'};this.alerts.push(alert);this.notifyAlert(alert);}private async notifyAlert(alert: Alert): Promise<void> {// 根据告警级别选择通知方式switch (alert.rule.severity) {case 'high':await this.sendUrgentNotification(alert);break;case 'medium':await this.sendEmailNotification(alert);break;case 'low':await this.sendSystemNotification(alert);break;}}private async sendUrgentNotification(alert: Alert): Promise<void> {// 发送紧急通知(如电话、短信)}private async sendEmailNotification(alert: Alert): Promise<void> {// 发送邮件通知}private async sendSystemNotification(alert: Alert): Promise<void> {// 发送系统内部通知}
}
结语 📝
前端监控体系是保障应用质量和用户体验的重要工具。通过本文,我们学习了:
- 前端监控的重要性和基本概念
- 性能监控的实现方法
- 错误监控和异常处理
- 用户行为追踪技术
- 业务监控的实现
- 数据分析和可视化方案
💡 学习建议:
- 从基础监控开始,逐步扩展监控范围
- 注重数据安全和用户隐私保护
- 持续优化监控系统性能
- 根据实际需求调整监控策略
- 重视监控数据的分析和应用
如果你觉得这篇文章有帮助,欢迎点赞收藏,也期待在评论区看到你的想法和建议!👇
终身学习,共同成长。
咱们下一期见
💻