我在公司整前端监控(一)

2,379 阅读6分钟

我这几年在公司做了一些关于前端监控相关的工作,下面我会用两篇文章写前端监控主要需要做哪些事情。

前端监控主要分三个方向

前端性能(用户体验优化)
异常监控
业务指标跟

下面我来分别介绍三类指标如何获取

1)前端性能指标:

一、用户体验相关的:

  1. 页面加载时间(Page Load Time)
    • 定义:从用户请求页面到页面完全加载的时间。
    • 测量方法:使用Performance API中的performance.timing对象。
  1. 首次内容绘制(First Contentful Paint, FCP)
    • 定义:浏览器从DOM中渲染出第一个内容的时间点。
    • 测量方法:使用PerformanceObserver来监听paint条目。
  1. 最大内容绘制(Largest Contentful Paint, LCP)
    • 定义:页面加载过程中最大可见内容的绘制时间。
    • 测量方法:使用PerformanceObserver来监听largest-contentful-paint条目。
  1. 首次输入延迟(First Input Delay, FID)
    • 定义:用户首次与页面交互(如点击按钮)到浏览器开始响应交互的时间。
    • 测量方法:使用EventListeners结合PerformanceObserver
  1. 累积布局偏移(Cumulative Layout Shift, CLS)
    • 定义:页面布局在加载过程中发生的意外变化的累积得分。
    • 测量方法:使用PerformanceObserver来监听layout-shift条目。
  1. 总阻塞时间(Total Blocking Time, TBT):
    • 定义: TBT 是一个网页性能指标,用于衡量从首次内容绘制(First Contentful Paint, FCP)到可交互时间(Time to Interactive, TTI)之间,所有超过 50 毫秒的长任务阻塞主线程的时间总和。TBT 反映了页面在加载过程中用户交互的延迟情况。
    • 测量方法: 使用PerformanceObserver来监听longtask条目。通过这种方式,可以捕获并计算页面在加载过程中所有长任务的阻塞时间。
  1. 交互时间(Time to Interactive, TTI)
    • 定义:页面从开始加载到完全可交互的时间。
    • 测量方法:通常通过分析资源加载和长任务来估算。虽然没有直接的API,但可以结合Long Tasks API和其他指标来推测。
示例代码

以下是一个简单的示例代码,展示如何使用Performance APIPerformanceObserver来测量这些指标:

// 页面加载时间
window.addEventListener('load', () => {
    const timing = performance.timing;
    const pageLoadTime = timing.loadEventEnd - timing.navigationStart;
    console.log(`Page Load Time: ${pageLoadTime} ms`);
});

// 首次内容绘制和最大内容绘制
const observer = new PerformanceObserver((list) => {
    const entries = list.getEntries();
    entries.forEach((entry) => {
        if (entry.name === 'first-contentful-paint') {
            console.log(`First Contentful Paint: ${entry.startTime} ms`);
        }
        if (entry.entryType === 'largest-contentful-paint') {
            console.log(`Largest Contentful Paint: ${entry.startTime} ms`);
        }
    });
});
observer.observe({ type: 'paint', buffered: true });
observer.observe({ type: 'largest-contentful-paint', buffered: true });

// 首次输入延迟
let firstInputDelay = 0;
const fidObserver = new PerformanceObserver((list) => {
    const entries = list.getEntries();
    for (const entry of entries) {
        firstInputDelay = entry.processingStart - entry.startTime;
        console.log(`First Input Delay: ${firstInputDelay} ms`);
    }
});
fidObserver.observe({ type: 'first-input', buffered: true });

// 累积布局偏移
let cumulativeLayoutShiftScore = 0;
const clsObserver = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
            cumulativeLayoutShiftScore += entry.value;
            console.log(`Cumulative Layout Shift: ${cumulativeLayoutShiftScore}`);
        }
    }
});
clsObserver.observe({ type: 'layout-shift', buffered: true });

// 长任务
const ttiObserver = new PerformanceObserver((entryList) => {
    const entries = entryList.getEntries();
    entries.forEach(entry => {
        console.log(`Long Task detected: ${entry.startTime} ms, duration: ${entry.duration} ms`);
    });
});

ttiObserver.observe({ entryTypes: ['longtask'] });

总结:上面的指标performance.timing主要是采集一些基础的时间,目前都是前后端分离的项目参考意义已经不大,除非是ssr和服务端的项目这部分指标有一些参考价值。像最大内容绘制largest-contentful-paint 是一个持续变动的指标,最后计算出最大时间,位移数据也是如此,长任务也是如此持续监听过程。

二、页面资源相关

在前端监控中资源监控方面,通常使用的是浏览器提供的Performance API。这个 API 提供了一组接口,对于资源监控,具体来说,可以使用以下接口:

  1. PerformanceResourceTiming:
    • 这个接口提供了关于每个资源加载的详细计时信息。通过它,你可以获取到资源加载的各个阶段的时间,例如 DNS 查询时间、TCP 连接时间、请求时间、响应时间等。
  1. PerformanceObserver:
    • 这是一个更为通用的接口,可以用来监听各种性能条目(performance entry)。通过PerformanceObserver,你可以监听到资源加载事件(resource类型),并在资源加载完成时获取到相应的PerformanceResourceTiming对象。

以下是一个简单的示例,展示如何使用PerformanceObserver来监控资源加载:

if ('PerformanceObserver' in window) {
  const observer = new PerformanceObserver((list) => {
    const entries = list.getEntriesByType('resource');
    entries.forEach((entry) => {
      console.log(`Resource: ${entry.name}`);
      console.log(`Start Time: ${entry.startTime}`);
      console.log(`Duration: ${entry.duration}`);
      console.log(`Initiator Type: ${entry.initiatorType}`);
    });
  });

  observer.observe({ type: 'resource', buffered: true });
}

总结:他可以对页面所有的资源加载进行监控,比如 js,css,图片还能够对xmlhttprequestfetch监控。也就是说他能够对接口进行监控,当我们的页面有一些超长的请求可以单独识别出来进行报警。

下面是对请求接口的监控:

if ('PerformanceObserver' in window) {
  const observer = new PerformanceObserver((list) => {
    const entries = list.getEntriesByType('resource');
    entries.forEach((entry) => {
      if (entry.initiatorType === 'xmlhttprequest' || entry.initiatorType === 'fetch') {
        console.log(`API Request: ${entry.name}`);
        console.log(`Start Time: ${entry.startTime}`);
        console.log(`Duration: ${entry.duration}`);
        console.log(`Fetch Start: ${entry.fetchStart}`);
        console.log(`Response End: ${entry.responseEnd}`);
        console.log(`Transfer Size: ${entry.transferSize}`);
      }
    });
  });

  observer.observe({ type: 'resource', buffered: true });
}

三、导航时间

下面是比较新的版本获取方式,以前的版本是performance.timing直接获取,兼容性比较好但是时间算法的精度跟新版的不一样,这个指标我理解在ssr里面比较有用,现在都是客户端渲染的情况下参考意义不大

// 获取导航时间数据
const navigation = performance.getEntriesByType('navigation')[0];

// 主要时间点
console.log({
  domainLookupStart: navigation.domainLookupStart,     // DNS查询开始
  domainLookupEnd: navigation.domainLookupEnd,         // DNS查询结束
  connectStart: navigation.connectStart,               // 开始建立连接
  connectEnd: navigation.connectEnd,                   // 连接建立完成
  requestStart: navigation.requestStart,               // 开始发送请求
  responseStart: navigation.responseStart,             // 开始接收响应
  responseEnd: navigation.responseEnd,                 // 响应接收完成
  domContentLoadedEventStart: navigation.domContentLoadedEventStart,
  domContentLoadedEventEnd: navigation.domContentLoadedEventEnd,
  loadEventStart: navigation.loadEventStart,
  loadEventEnd: navigation.loadEventEnd
});

四、其他一些不太重要的监控(不细讲)

  • navigator.getBattery() : 电池监控
  • Performance Memory API: 内存监控

2)异常监控

一、监听形式

监听形式主要是通过捕获全局错误事件来实现。这种方式可以捕获大部分未处理的异常,并可以将其记录或发送到服务器进行分析。

1. 使用window.onerror

window.onerror是最传统的异常捕获方法,可以捕获运行时的 JavaScript 错误。

window.onerror = function(message, source, lineno, colno, error) {
  console.error('Error caught:', message, source, lineno, colno, error);
  // 可以在这里将错误信息发送到服务器
  sendErrorToServer({
    message,
    source,
    lineno,
    colno,
    error: error ? error.stack : null
  });
};

function sendErrorToServer(errorInfo) {
  fetch('/log-error', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(errorInfo)
  });
}
2. 使用window.addEventListener('error')

这种方法可以捕获资源加载错误(如图片、脚本加载失败)。

window.addEventListener('error', function(event) {
  console.error('Resource Error:', event);
  // 处理资源加载错误
  if (event.target instanceof HTMLImageElement) {
    console.error('Image failed to load:', event.target.src);
  }
  // 可以在这里将错误信息发送到服务器
  sendErrorToServer({
    message: event.message,
    source: event.filename,
    lineno: event.lineno,
    colno: event.colno,
    error: event.error ? event.error.stack : null
  });
}, true);
3. 使用window.addEventListener('unhandledrejection')

捕获未处理的 Promise 异常,此处主要用来捕获Promise没有被catch的异常,属于最后的兜底。

window.addEventListener('unhandledrejection', function(event) {
  console.error('Unhandled promise rejection:', event.reason);
  // 可以在这里将错误信息发送到服务器
  sendErrorToServer({
    message: event.reason ? event.reason.toString() : 'Unknown reason',
    error: event.reason && event.reason.stack ? event.reason.stack : null
  });
});

二、主动上报形式

主动上报形式是指在代码中手动捕获异常并上报。适用于需要在特定逻辑中捕获异常的场景。

1. 使用try...catch

在代码中使用try...catch来捕获异常,并在catch块中进行上报。

function riskyOperation() {
  try {
    // 执行可能抛出异常的代码
    let result = potentiallyFailingFunction();
    console.log('Operation successful:', result);
  } catch (error) {
    console.error('Caught an error:', error);
    // 在这里上报错误
    sendErrorToServer({
      message: error.message,
      error: error.stack
    });
  }
}

function potentiallyFailingFunction() {
  // 模拟可能抛出异常的代码
  if (Math.random() > 0.5) {
    throw new Error('Random failure');
  }
  return 'Success';
}
2. 集成到应用框架

在现代前端框架(如 React、Vue)中,可以利用框架的错误边界或错误处理机制来捕获和上报异常。

  • React: 使用 Error Boundary
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

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

  componentDidCatch(error, errorInfo) {
    console.error('ErrorBoundary caught an error:', error, errorInfo);
    // 在这里上报错误
    sendErrorToServer({
      message: error.message,
      error: error.stack,
      componentStack: errorInfo.componentStack
    });
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children; 
  }
}

// 使用 ErrorBoundary 包裹组件
<ErrorBoundary>
  <YourComponent />
</ErrorBoundary>

总结:我本人更加倾向于主动上报形式,因为在现在的开发形式里面,编译工具和开发工具能够帮我们避免绝大多数的异常情况,我们的监控往往因为业务逻辑和接口数据返回不对导致对业务的影响,所以主动上报能够更加准确和清晰的给出异常,这样利于我们去监控,往往比较多的异常问题不是错误导致更多就是接口不符合预期这样window.onerror形式不太全,最多只能当一个辅助使用。

3、业务指标跟踪

业务指标跟踪也是前端监控的一部分,主要用于收集和分析用户在应用中的行为数据,以帮助理解用户的使用模式、评估功能效果、优化用户体验等。常见的业务指标包括页面访问量、点击事件、用户停留时间、转化率等,很多公司都是单独做,我所在的公司就是这样,其实原理很简单就是调用一个接口把想要上报的数据给带过去就行。

let clickCount = 0;

// 获取按钮元素
const button = document.getElementById('trackButton');

// 为按钮添加点击事件监听器
button.addEventListener('click', () => {
    clickCount++;
    console.log('按钮点击次数:', clickCount);
});

// 页面卸载时发送数据
window.addEventListener('beforeunload', () => {
    const data = JSON.stringify({ clickCount });

    // 使用 sendBeacon 发送数据,确保在页面卸载时也能发送成功
    if (navigator.sendBeacon) {
        navigator.sendBeacon('/track-clicks', data);
    } else {
        // 备用方案,使用 fetch 发送数据
        fetch('/track-clicks', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: data
        });
    }
});

总结:

上面写的demo 大家会注意到我没有用传统的上报方式用的navigator.sendBeacon下一篇文章我会讲具体的上报形式。

本章总结:本章主要讲了前端监控的方向和用的api,下一张讲下相对细节的一些问题。 我在公司整前端监控(二)