当前位置: 首页 > article >正文

JavaScript系列(92)--前端监控体系

前端监控体系 📊

前端监控是现代Web应用不可或缺的组成部分,它帮助我们了解应用的运行状况、用户行为和潜在问题。本文将详细介绍如何构建一个完整的前端监控体系。

监控体系概述 🌟

💡 小知识:前端监控体系通常包括性能监控、错误监控、用户行为分析和业务监控四大模块,通过这些数据可以全面了解应用的健康状况和用户体验。

为什么需要前端监控

在复杂的前端应用中,监控系统能够帮助我们:

  1. 及时发现问题

    • 捕获JS运行时错误
    • 监控API请求异常
    • 检测性能瓶颈
    • 发现用户体验问题
  2. 优化用户体验

    • 分析页面加载性能
    • 监控交互响应时间
    • 追踪用户行为路径
    • 识别体验痛点
  3. 辅助业务决策

    • 收集用户行为数据
    • 分析功能使用情况
    • 评估新功能效果
    • 指导产品优化
  4. 提升开发效率

    • 快速定位问题
    • 复现错误场景
    • 评估代码质量
    • 指导性能优化

性能监控实现 ⚡

核心性能指标

// 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未处理的rejection
        window.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)],  // p50
            sorted[Math.floor(sorted.length * 0.75)], // p75
            sorted[Math.floor(sorted.length * 0.9)],  // p90
            sorted[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>
    );
};

监控系统最佳实践 ⭐

数据采集建议

  1. 采样策略

    • 根据流量确定采样率
    • 关键用户全量采集
    • 错误日志优先级采集
    • 性能数据定期采集
  2. 数据精简

    • 只采集必要信息
    • 合理设置采集频率
    • 避免采集敏感数据
    • 遵守数据隐私规范
  3. 上报优化

    • 批量上报减少请求
    • 使用信标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> {
        // 发送系统内部通知
    }
}

结语 📝

前端监控体系是保障应用质量和用户体验的重要工具。通过本文,我们学习了:

  1. 前端监控的重要性和基本概念
  2. 性能监控的实现方法
  3. 错误监控和异常处理
  4. 用户行为追踪技术
  5. 业务监控的实现
  6. 数据分析和可视化方案

💡 学习建议:

  1. 从基础监控开始,逐步扩展监控范围
  2. 注重数据安全和用户隐私保护
  3. 持续优化监控系统性能
  4. 根据实际需求调整监控策略
  5. 重视监控数据的分析和应用

如果你觉得这篇文章有帮助,欢迎点赞收藏,也期待在评论区看到你的想法和建议!👇

终身学习,共同成长。

咱们下一期见

💻


http://www.kler.cn/a/565398.html

相关文章:

  • Vue 3 + Vite 项目配置访问地址到服务器某个文件夹的解决方案
  • 全面解析:如何查找电脑的局域网与公网IP地址‌
  • reallocate() 和 allocate() 的区别
  • vue实现根据点击或滑动展示对应高亮
  • Django ORM 的常用字段类型、外键关联的跨表引用技巧,以及 `_` 和 `__` 的使用场景
  • 基于EasyExcel封装的Excel工具类,支持高效导出和读取操作
  • Qt for Android下QMessageBox背景黑色、文字点击闪烁
  • JSX 实现列表渲染
  • react绑定ref调用
  • 转化率(漏斗分析)——mysql计算过程
  • Amazon Neptune深度解析:高性能图形分析和无服务器数据库的场景化实践与技术优
  • XFeat:轻量级的深度学习图像特征匹配
  • 机器学习数学基础:34.二列相关教程
  • Redis 面试
  • 矩阵的 正定(Positive Definite)与负定(Negative Definite):从Fisher信息矩阵看“曲率”的秘密
  • smolagents学习笔记系列(十)Examples - Web Browser Automation with Agents
  • Linux设备驱动开发-Pinctrl子系统使用详解
  • 导入 Excel 规则批量修改或删除 Word 内容
  • 【Linux】进程间通信——命名管道
  • Python解决“比赛配对”问题