查看: 88|回复: 1

前端监控体系实战:性能指标采集、错误捕获与用户行为追踪

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
在现代前端工程中,监控是保障用户体验和服务稳定性的基础设施。本文围绕性能监控、错误监控和行为监控三大维度,给出可直接落地的代码实现,涵盖 Web API 的 PerformanceObserver、全局错误钩子、自定义埋点、数据上报以及告警规则设计。
  1. /* 以下代码均基于现代浏览器,适用于 Vue/React 项目集成 */
  2. // 1. 性能监控:核心指标采集
  3. const observer = new PerformanceObserver((list) => {
  4.   for (const entry of list.getEntries()) {
  5.     if (entry.name === 'first-contentful-paint') {
  6.       console.log(`FCP: ${entry.startTime}ms`);
  7.       reportMetric('fcp', entry.startTime);
  8.     }
  9.   }
  10. });
  11. observer.observe({ entryTypes: ['paint'] });
  12. const lcpObserver = new PerformanceObserver((list) => {
  13.   const entries = list.getEntries();
  14.   const lastEntry = entries[entries.length - 1];
  15.   console.log(`LCP: ${lastEntry.startTime}ms`);
  16.   reportMetric('lcp', lastEntry.startTime);
  17. });
  18. lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });
  19. let clsValue = 0;
  20. const clsObserver = new PerformanceObserver((list) => {
  21.   for (const entry of list.getEntries()) {
  22.     if (!entry.hadRecentInput) {
  23.       clsValue += entry.value;
  24.       console.log(`CLS: ${clsValue}`);
  25.       reportMetric('cls', clsValue);
  26.     }
  27.   }
  28. });
  29. clsObserver.observe({ entryTypes: ['layout-shift'] });
复制代码

使用 PerformanceObserver 监听首屏渲染(FCP)、最大内容绘制(LCP)和布局偏移(CLS),浏览器会回调最新值。CLS 需要累加 value 属性(未在有用户输入偏移后的事件中记录)。

2. 自定义性能埋点:封装 PerformanceMonitor 类
除了浏览器原生指标,还需要 DNS、TCP连接、DOM解析及页面完全加载耗时。通过 performance.timing 计算:
  1. class PerformanceMonitor {
  2.   constructor() {
  3.     this.metrics = {};
  4.   }
  5.   recordPageLoad() {
  6.     const timing = performance.timing;
  7.     const metrics = {
  8.       dnsLookup: timing.domainLookupEnd - timing.domainLookupStart,
  9.       tcpConnect: timing.connectEnd - timing.connectStart,
  10.       domParse: timing.domComplete - timing.domLoading,
  11.       fullLoad: timing.loadEventEnd - timing.navigationStart
  12.     };
  13.     Object.entries(metrics).forEach(([key, value]) => {
  14.       this.reportMetric(`page_${key}`, value);
  15.     });
  16.   }
  17.   recordResourceLoad() {
  18.     performance.getEntriesByType('resource').forEach(resource => {
  19.       if (resource.duration > 1000) {
  20.         this.reportMetric('slow_resource', {
  21.           name: resource.name,
  22.           duration: resource.duration,
  23.           type: resource.initiatorType
  24.         });
  25.       }
  26.     });
  27.   }
  28.   reportMetric(name, value) {
  29.     fetch('/api/metrics', {
  30.       method: 'POST',
  31.       body: JSON.stringify({ name, value, timestamp: Date.now() }),
  32.       headers: { 'Content-Type': 'application/json' }
  33.     });
  34.   }
  35. }
  36. const perfMonitor = new PerformanceMonitor();
  37. perfMonitor.recordPageLoad();
  38. perfMonitor.recordResourceLoad();
复制代码

3. 错误监控:全局捕获运行时与 Promise 异常
通过 window.addEventListener('error', ... , true) 捕获未被 try/catch 的 JS 错误;通过 unhandledrejection 监听未处理的 Promise 拒绝。
  1. window.addEventListener('error', (event) => {
  2.   reportError({
  3.     type: 'runtime',
  4.     message: event.message,
  5.     filename: event.filename,
  6.     lineno: event.lineno,
  7.     colno: event.colno,
  8.     stack: event.error?.stack,
  9.     timestamp: Date.now()
  10.   });
  11. }, true);
  12. window.addEventListener('unhandledrejection', (event) => {
  13.   reportError({
  14.     type: 'promise',
  15.     message: event.reason?.message || 'Unhandled Promise Rejection',
  16.     stack: event.reason?.stack,
  17.     timestamp: Date.now()
  18.   });
  19. });
复制代码

4. 框架级错误捕获:Vue 和 React
Vue 3 通过 app.config.errorHandler 全局钩子捕获组件渲染错误;React 16+ 使用 ErrorBoundary 类组件和 getDerivedStateFromError/componentDidCatch。
  1. // Vue 3
  2. import { createApp } from 'vue';
  3. const app = createApp(App);
  4. app.config.errorHandler = (err, instance, info) => {
  5.   reportError({
  6.     type: 'vue',
  7.     message: err.message,
  8.     stack: err.stack,
  9.     component: instance?.name || 'Anonymous',
  10.     lifecycleHook: info,
  11.     timestamp: Date.now()
  12.   });
  13. };
  14. // React ErrorBoundary
  15. import React from 'react';
  16. class ErrorBoundary extends React.Component {
  17.   constructor(props) {
  18.     super(props);
  19.     this.state = { hasError: false, error: null };
  20.   }
  21.   static getDerivedStateFromError(error) {
  22.     return { hasError: true, error };
  23.   }
  24.   componentDidCatch(error, errorInfo) {
  25.     reportError({
  26.       type: 'react',
  27.       message: error.message,
  28.       stack: error.stack,
  29.       componentStack: errorInfo.componentStack,
  30.       timestamp: Date.now()
  31.     });
  32.   }
  33.   render() {
  34.     if (this.state.hasError) {
  35.       return <div>页面出错了,请稍后刷新</div>;
  36.     }
  37.     return this.props.children;
  38.   }
  39. }
复制代码

5. 资源加载与 HTTP 请求错误
通过全局 error 事件检测图片加载失败,通过重写 fetch 方法捕获 HTTP 非 2xx 响应或网络异常。
  1. window.addEventListener('error', (event) => {
  2.   if (event.target instanceof HTMLImageElement) {
  3.     reportError({
  4.       type: 'resource',
  5.       resourceType: 'image',
  6.       url: event.target.src,
  7.       timestamp: Date.now()
  8.     });
  9.   }
  10. }, true);
  11. const originalFetch = window.fetch;
  12. window.fetch = async function(...args) {
  13.   try {
  14.     const response = await originalFetch(...args);
  15.     if (!response.ok) {
  16.       reportError({
  17.         type: 'http',
  18.         url: args[0],
  19.         status: response.status,
  20.         method: args[1]?.method || 'GET'
  21.       });
  22.     }
  23.     return response;
  24.   } catch (error) {
  25.     reportError({
  26.       type: 'http',
  27.       url: args[0],
  28.       message: error.message,
  29.       method: args[1]?.method || 'GET'
  30.     });
  31.     throw error;
  32.   }
  33. };
复制代码

6. 行为监控:用户点击、滚动、页面停留
使用 BehaviorTracker 类记录交互行为:点击元素信息、页面滚动深度(防抖)、页面浏览时长(beforeunload 时上报)。利用 navigator.sendBeacon 保证页面关闭时数据不丢。
  1. class BehaviorTracker {
  2.   constructor() {
  3.     this.sessionId = this.generateSessionId();
  4.     this.pageViewTime = 0;
  5.   }
  6.   trackClick(element) {
  7.     const eventData = {
  8.       type: 'click',
  9.       element: element.tagName,
  10.       className: element.className,
  11.       text: element.textContent?.slice(0, 50),
  12.       x: element.getBoundingClientRect().left,
  13.       y: element.getBoundingClientRect().top,
  14.       pageUrl: window.location.href,
  15.       timestamp: Date.now()
  16.     };
  17.     this.reportBehavior(eventData);
  18.   }
  19.   trackPageView() {
  20.     const startTime = Date.now();
  21.     window.addEventListener('beforeunload', () => {
  22.       const duration = Date.now() - startTime;
  23.       this.reportBehavior({
  24.         type: 'pageview',
  25.         url: window.location.href,
  26.         duration,
  27.         sessionId: this.sessionId,
  28.         timestamp: Date.now()
  29.       });
  30.     });
  31.   }
  32.   trackScroll() {
  33.     let scrollTimeout;
  34.     window.addEventListener('scroll', () => {
  35.       clearTimeout(scrollTimeout);
  36.       scrollTimeout = setTimeout(() => {
  37.         const scrollDepth = Math.round(
  38.           (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100
  39.         );
  40.         this.reportBehavior({
  41.           type: 'scroll',
  42.           scrollDepth,
  43.           timestamp: Date.now()
  44.         });
  45.       }, 500);
  46.     });
  47.   }
  48.   reportBehavior(data) {
  49.     navigator.sendBeacon('/api/behavior', JSON.stringify(data));
  50.   }
  51.   generateSessionId() {
  52.     return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  53.   }
  54. }
  55. const tracker = new BehaviorTracker();
  56. tracker.trackPageView();
  57. tracker.trackScroll();
  58. document.addEventListener('click', (event) => {
  59.   tracker.trackClick(event.target);
  60. });
复制代码

7. 性能与行为关联分析
通过 Analytics 类将用户操作与性能数据关联,例如统计某类操作后的平均响应时间和错误率。
  1. class Analytics {
  2.   constructor() {
  3.     this.userActions = [];
  4.   }
  5.   recordAction(action) {
  6.     this.userActions.push({
  7.       ...action,
  8.       sessionId: this.getSessionId(),
  9.       userId: this.getUserId()
  10.     });
  11.   }
  12.   getSessionId() {
  13.     return localStorage.getItem('session_id') ||
  14.       (localStorage.setItem('session_id', `sess_${Date.now()}`),
  15.       localStorage.getItem('session_id'));
  16.   }
  17.   getUserId() {
  18.     return localStorage.getItem('user_id') || 'anonymous';
  19.   }
  20.   analyzePerformanceAfterAction(actionType) {
  21.     const actions = this.userActions.filter(a => a.type === actionType);
  22.     const recentActions = actions.slice(-10);
  23.     return {
  24.       avgResponseTime: recentActions.reduce((sum, a) => sum + (a.responseTime || 0), 0) / recentActions.length,
  25.       errorRate: recentActions.filter(a => a.error).length / recentActions.length
  26.     };
  27.   }
  28. }
复制代码

8. 统一上报与告警
MonitorService 封装批量上报逻辑,使用 sendBeacon 或 fetch 定时 flush,支持 error 优先即时上报。告警规则按指标阈值触发通知。
  1. class MonitorService {
  2.   constructor(config) {
  3.     this.endpoint = config.endpoint;
  4.     this.appId = config.appId;
  5.     this.queue = [];
  6.     this.flushInterval = 5000;
  7.     this.init();
  8.   }
  9.   init() {
  10.     setInterval(() => this.flush(), this.flushInterval);
  11.     window.addEventListener('beforeunload', () => this.flush());
  12.   }
  13.   report(type, data) {
  14.     const payload = {
  15.       appId: this.appId,
  16.       type,
  17.       data,
  18.       timestamp: Date.now(),
  19.       userAgent: navigator.userAgent,
  20.       url: window.location.href,
  21.       referrer: document.referrer
  22.     };
  23.     this.queue.push(payload);
  24.     if (type === 'error') {
  25.       this.flush();
  26.     }
  27.   }
  28.   flush() {
  29.     if (this.queue.length === 0) return;
  30.     const data = [...this.queue];
  31.     this.queue = [];
  32.     navigator.sendBeacon(`${this.endpoint}/batch`, JSON.stringify(data));
  33.   }
  34. }
  35. const monitor = new MonitorService({
  36.   endpoint: 'https://monitor.example.com',
  37.   appId: 'frontend-app-001'
  38. });
  39. // 告警规则示例
  40. const alertRules = {
  41.   errorRate: { threshold: 0.05, window: 300 },
  42.   fcp: { threshold: 2500 },
  43.   lcp: { threshold: 4000 },
  44.   cls: { threshold: 0.25 },
  45.   apiErrorRate: { threshold: 0.1, window: 60 }
  46. };
  47. function checkAlerts(metric, value) {
  48.   const rule = alertRules[metric];
  49.   if (!rule) return;
  50.   if (value > rule.threshold) {
  51.     sendAlert({
  52.       metric,
  53.       value,
  54.       threshold: rule.threshold,
  55.       timestamp: Date.now()
  56.     });
  57.   }
  58. }
复制代码

总结:上述代码覆盖了性能(FCP/LCP/CLS及自定义埋点)、错误(runtime/promise/资源/框架)、行为(点击/滚动/页面停留)的采集与上报。建议在项目中集成 MonitorService 并设置合理的告警阈值,从而主动发现性能劣化与异常。监控的本质是预防问题,而非事后补救。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: 前端监控体系实战:性能指标采集、错误捕获与用户行为追踪

感谢分享!正好最近在搭监控系统,你这套代码可以直接拿来做参考。CLS 累加时加了 `hadRecentInput` 判断,这个细节很到位,之前踩过坑。另外你提到还有“告警规则设计”,正文里好像没展开?我比较关心聚合告警(比如单页应用下低概率偶发错误怎么收敛),如果方便的话希望能再讲讲策略。还有最后 `unhandledrejection` 那段的 `unha` 后面是不是缺了东西?代码看起来没写完。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-6-11 23:07 , Processed in 0.042335 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部