如何检测 Network 请求异常 - PerformanceObserver

131 阅读4分钟

image.png

图片来自 bing.com

背景

我们想实现一个 Tampermonkey 插件,当发现某个接口报错则使用替代接口。比如某个 AI 网站接口挂了,我们想替换成 kimi 或 deepseek 的接口,但是我们没法修改这个网站的源码。

其次我们经常在 Chrome 控制台的 Network 面板看到接口报错 500,如何监听这类错误呢?

GET https://example.com/api/completion?query=3 500 (Internal Server Error)

如何实现?可以使用 PerformanceObserver

const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    console.log('entry', entry)
    
    if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') {
      if (entry.responseStatus >= 400) {
        console.error('网络请求错误:', entry.name, '状态码:', entry.responseStatus);
      }
    }
  });
});

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

注意点entry.initiatorType 会报 ts 错误,因为 entry 的类型被误认为是 PerformanceEntry 没有该字段,TS 无法做到如此精确的推导问题 github.com/microsoft/T… 。从逻辑上看 type: resource 则 entry 的类型为 PerformanceResourceTiming 则自然有 initiatorType

image.png

PerformanceResourceTiming 继承自 PerformanceEntry

我们可以通过 @ts-expect-error https://github.com/microsoft/TypeScript/issues/58644 或更好的方式采用 type assertion。

const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
+   const entry = perEntry as PerformanceResourceTiming;
    console.log('entry', entry)
    
    if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') {
      if (entry.responseStatus >= 400) {
        console.error('网络请求错误:', entry.name, '状态码:', entry.responseStatus);
      }
    }
  });
});

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

我们封装下“检测网络请求异常”的函数。

type IProps = {
  when: (entry: PerformanceResourceTiming) => boolean;
  callback: (entry: PerformanceResourceTiming) => unknown;
  buffered?: boolean;
}

function observeNetwork({ when, callback, buffered = true }: IProps) {
    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((perfEntry) => {
        const entry = perfEntry as PerformanceResourceTiming;

        if (when(entry)) {
          callback(entry)
        }
      });
    });

    observer.observe({ type: 'resource', buffered });
    
    return () => {
      observer.disconnect()
    }
}

用法:

const doSthOnNetworkError = (entry: PerformanceResourceTiming) => {
  console.error('网络请求错误:', entry.name, '状态码:', entry.responseStatus);
}

const disconnect = observeNetwork({
  when: ({ initiatorType, responseStatus }) => {
    return responseStatus >= 400 && (initiatorType === 'fetch' || initiatorType === 'xmlhttprequest')
  },
  
  callback: doSthOnNetworkError
})

// 当不需要监听的时候
disconnect()
代码解释

代码解释

创建一个 PerformanceObserver 来监控资源加载性能指标,重点解释下 typebufferedinitiatorType 参数的作用:

type: 'resource'

  • 含义:指定要观察的性能条目类型
  • 作用:这里设置为 'resource' 表示观察所有资源加载的性能数据
  • 其他常见类型,每一种类型都对应一个 interface PerformanceXxxTiming,更多见 PerformanceEntry: entryType property - MDN
    • 'largest-contentful-paint' - interface LargestContentfulPaint
    • 'navigation' - 页面导航性能 PerformanceNavigationTiming
    • 'paint' - 绘制性能(如 FP、FCP)。描述从 render tree 到绘画在屏幕上的一个个像素 PerformancePaintTiming
    • 'longtask' - 长任务,阻塞 UI 进程超过 50ms 的任务 PerformanceLongTaskTiming
    • 'element' - 特定元素的性能 PerformanceElementTiming

buffered: true

  • 含义:是否处理缓冲区中的历史性能条目
  • 作用
    • 当设置为 true 时,会立即返回在调用 observe() 之前已经存在的性能条目
    • 当设置为 false 时,只观察调用 observe() 之后新产生的性能条目
  • 使用场景
    • 如果你希望在代码初始化时就能获取到页面加载早期的性能数据,应该设为 true
    • 如果只关心后续发生的性能事件,可以设为 false

initiatorType: fetch | xmlhttprequest

  • 含义PerformanceResourceTiming 对象中的一个属性,用于表示 资源是由谁/什么发起的请求(即资源的初始化器类型)。它可以帮助开发者分析页面资源加载的来源和依赖关系。
  • 作用
    • 筛选出通过 fetch 或 xmlhttprequest 发起的请求
  • 使用场景
    1. 性能优化分析
      • 可以统计哪些类型的资源加载最慢(例如 scriptcssimg)。
      • 检测是否有意外的 fetch/xmlhttprequest 请求影响性能。
    2. 错误监控
      • 结合 responseStatus(如我们代码所示),可以监控 AJAX/Fetch 请求是否失败(>= 400)。
initiatorType 的可能值及含义

以下是常见的 initiatorType 值及其对应的含义:

说明
"img"资源由 <img> 标签加载(例如 <img src="image.jpg">
"script"资源由 <script> 标签加载(例如 <script src="app.js">
"link"资源由 <link> 标签加载(例如 <link rel="stylesheet" href="style.css">
"css"资源由 CSS 规则加载(例如 @import "theme.css" 或 background: url(...)
"xmlhttprequest"资源由 XMLHttpRequest 请求加载(AJAX)
"fetch"资源由 fetch() 请求加载
"iframe"资源由 <iframe>src 加载(例如 <iframe src="page.html">
"navigation"资源是页面本身(即 HTML 文档的加载)
"audio" / "video"资源由 <audio> 或 <video> 标签加载
"beacon"资源由 navigator.sendBeacon() 发送
"other"其他未明确分类的请求

组合效果

{ type: 'resource', buffered: true } 表示:

  1. 观察所有资源加载的性能数据
  2. 立即获取已经存在的资源加载性能数据(而不仅仅是后续新发生的)

实际应用示例

// 这样设置可以获取到页面加载初期所有资源的性能数据
observer.observe({ 
  type: 'resource', 
  buffered: true 
});

// 如果只想观察后续发生的资源加载
observer.observe({ 
  type: 'resource', 
  buffered: false 
});

注意事项

  1. buffered: true 可能会一次性返回大量历史数据,要注意处理性能
  2. 不是所有性能条目类型都支持 buffered 选项
  3. 使用完毕后应调用 observer.disconnect() 停止观察