前言

     在我们开发网站和应用程序的过程中,经常会遇到各种莫名其妙的错误。错误原因也五花八门,可能是浏览器兼容问题,可能是代码里面没做兜底,也可能是后端接口挂掉了等等错误,这些错误可能会直接影响到用户的使用体验,所以监控线上错误变得特别重要。

     通过前端监控,我们能够实时监测 JavaScript 错误、性能问题和用户界面的 bug。这么一来,我们就可以迅速找到问题所在,及时解决错误。还有一些可视化报告、自动通知和监控警报系统等工具,能够让我们更高效地处理监控反馈和解决问题。本文将会从前端监控的介绍入手,然后讲解前端监控的目标、流程以及如何实现基本的错误、行为监控等内容。

前端监控介绍

1. 定义

前端监控(Frontend Monitoring)是一种用于捕获、分析和报告网站或应用程序中的异常、错误和性能问题的方法。通过前端监控,我们可以实时了解用户在使用我们的产品时可能遇到的问题,从而快速响应和解决这些问题。

2. 错误类型

首先先介绍一下前端监控的错误类型:

2.1 语法错误

比如常见的单词拼写错误,中英文符号错误等,然后就是 语法错误是无法被 try-catch 捕获的,但其实如果存在语法错误一般编写代码的软件直接就会爆红,所以语法错误在可发阶段就可以发现,一般不会发布到线上环境。

2.2 同步错误

同步错误指的是在js同步执行过程中的错误,比如变量未定义等,同步错误是可以被 try-catch 给捕获到的

 1try {  
 2  const name = 'zs';   
 3  console.log(nam); 
 4} catch (error) {  
 5  console.log('同步错误!') // 会输出!
 6}

2.3 异步错误

  • 异步错误指的是在异步操作中发生的错误,这些错误 无法被常规的 try-catch 块捕获

  • 在 JavaScript 中,当使用像 setTimeoutfetch 或者 Promise 等异步函数时,它们会在执行过程中将错误捕获并存储起来,直到该异步操作完成后,错误才会被抛出。由于这些错误是在异步操作的上下文中发生的,并且不会阻塞主线程,因此无法使用常规的 try-catch 块捕获这些错误。

     1// 例如:setTimeout 异步函数里面的 undefined.map() 这个错误,
     2// 在页面执行过程中检测到错误后不会被 `try-catch` 捕获到然后 输出“异步错误!”。
     3try {  
     4  setTimeout(() => { 
     5    undefined.map();  
     6  }, 0); 
     7} catch (error) { 
     8  console.log('-异步错误!') 
     9}
  • 异步错误的话可以用 window.onerror 来进行处理,window.onerrortry-catch 强,它是一种更强大的全局错误处理机制,特别适用于捕获异步操作中的错误。

    window.onerror 方法相对于 try-catch 的优势:

    (1)全局错误处理:window.onerror 方法可以全局捕获 JavaScript 错误,无论是同步代码还是异步代码。而 try-catch 仅能捕获同步代码中的错误。

    (2)异步错误捕获:window.onerror 方法可以捕获异步操作中发生的错误。例如,在 setTimeout、fetch 或者事件处理程序等异步操作中发生的错误都可以通过 window.onerror 进行捕获。

    (3)堆栈信息:window.onerror 方法的错误对象会包含更详细的堆栈信息,包括错误发生的位置和调用栈等。相比之下,try-catch 仅提供有限的错误信息。

    (4)跨域错误:window.onerror 方法可以捕获跨域脚本中的错误,而 try-catch 无法跨域捕获,因为浏览器会将它们视为安全性风险。

     1// 例如,还是上面写的那个异步错误,但是利用 `window.onerror`  
     2// 不仅可以捕获得到异步错误,还可以提供很多错误信息。 
     3window.onerror = function (msg, url, row, col, error) {   
     4  console.log("异步错误!"); 
     5  console.log("错误描述:" + msg); 
     6  console.log("报错文件:" + url);  
     7  console.log("行号:" + row);    
     8  console.log("列号:" + col);  
     9  console.log("错误对象:" + error); 
    10};

2.4 promise错误

  • 在使用 Promise 进行异步操作时,需要在链式调用中使用 catch 方法来捕获异步错误。 如果没有显式地使用 catch 捕获错误,那么错误将会被传递到全局,就连 window.onerror 方法也无法捕获这些未被处理的 Promise 错误。

  • 因此,在编写 Promise 时,最好在链式调用的末尾添加 catch 方法来处理错误,以及 在全局范围内添加对 unhandledrejection 事件的监听,以捕获那些未被处理的 Promise 错误 ,这可以确保在发生异步错误时能够及时捕获和处理。

     1function getData(  ) {    
     2  return new Promise((resolve, reject) => {   
     3    // 模拟一个异步操作     
     4    setTimeout(() => {    
     5      // 模拟一个错误       
     6      const error = new Error("Something went wrong");  
     7      reject(error);    
     8    }, 2000);   
     9  }); 
    10}  
    11// 使用 catch 方法捕获 Promise 错误
    12getData().then((data) => { 
    13  // 在 resolve 时执行的操作 
    14  console.log(data); 
    15}).catch((error) => { 
    16  // 在 reject 时执行的操作 
    17  console.error(error);
    18});  
    19// 全局监听 unhandledrejection 事件来捕获未被处理的 Promise 错误 
    20window.addEventListener('unhandledrejection', event => {   
    21  // 捕获未被处理的 Promise 错误    
    22  console.error(event.reason);
    23});

2.5 资源加载错误

当加载资源(如图片、脚本、样式表等)时,如果出现了错误,比如服务器挂掉、网络断开等问题,这就被称为资源加载错误。这种错误是比较严重的,因为它可能导致整个页面的功能出现问题或无法正常展示。为了及时处理资源加载错误,您可以使用 window.addEventListener 方法来捕获 error 事件。通过监听全局的 error 事件,可以捕获到页面中发生的各种错误,包括资源加载错误。

 1window.addEventListener("error", (event) => {  
 2  // 判断是否是资源加载错误    
 3  if (event.target instanceof HTMLImageElement ||event.target instanceof HTMLScriptElement ||event.target instanceof HTMLLinkElement) { 
 4    console.error("资源加载错误:", event.target.src || event.target.href);  
 5  }  
 6});

3. 埋点方式

3.1 手动埋点

手动在代码中插入埋点代码来记录特定的事件或行为。

 1// 例子一:
 2<button   onClick={() => {  
 3  // 业务代码   
 4  tracker('click', '用户去支付');  
 5  }} 
 6  >
 7  手动埋点
 8</button>
 1// 例子二:
 2// 点击事件埋点 
 3document.getElementById("button").addEventListener("click", function (  ) { 
 4  // 上报点击事件   
 5  monitor.trackEvent("button_click", { buttonId: "button" }); 
 6});
  • 优点:可控性强,可以自定义上报具体的数据。
  • 缺点:对业务代码侵入性强,如果有很多地方需要埋点就得一个一个手动的去添加埋点代码。

3.2 无痕埋点

通过监听浏览器或应用程序的内置事件来自动采集数据,而无需手动埋点。

 1// 自动埋点错误监控
 2window.onerror = function (message, source, lineno, colno, error) { 
 3  // 上报错误   
 4  monitor.trackError("JavaScript Error", {       message,       source,       lineno,       colno,       error,     });
 5};  
 6// 自动埋点页面路由变化 
 7window.addEventListener("hashchange", function (  ) { 
 8  // 上报页面路由变化  
 9  monitor.trackEvent("page_route", { route: window.location.hash }); 
10});
  • 优点:不用侵入务代码就能实现全局的埋点。
  • 缺点:只能上报基本的行为交互信息,无法上报自定义的数据;上报次数多,服务器性能压力大。

前端监控目标

讲完错误类型,我们来说一下前端监控的目标是什么?

1. 稳定性

异常监测前端应用程序中的异常情况,如 JavaScript 错误、未捕获的异常、网络错误等。通过监控异常,可以及时定位并解决问题,提高应用程序的稳定性和用户体验。

2. 用户体验

监测应用程序的性能指标,如页面加载时间、资源加载时间、请求耗时等。通过监控性能,可以识别潜在的性能瓶颈,优化应用程序的加载速度和响应时间。

3. 业务

监控用户在应用程序中的行为和交互,如页面访问量、点击事件、页面停留时间等。通过监测用户行为,可以了解用户的使用习惯和需求,优化用户体验,提高用户满意度和留存率;也可以分析用户的喜好来进行推送等等。

前端监控流程

  1. 目标设定:确定需要监控的指标和目标,例如页面加载时间、错误率、用户行为等。

  2. 数据采集:使用适当的工具或技术来收集监控数据、或者自己封装函数进行监控数据。

  3. 数据传输:将采集到的监控数据传输到监控系统或后端服务器。数据传输可以使用HTTP请求、WebSocket等方式进行。

  4. 数据存储和处理:将传输过来的监控数据进行存储和处理。可以使用数据库、日志文件或数据分析平台进行存储和处理,以便后续的分析和展示。

  5. 数据分析和可视化:对存储的监控数据进行分析和展示,生成可视化报表、图表或仪表盘,以帮助开发人员和运营人员了解系统的性能和用户行为。

  6. 告警和预警:根据设定的监控指标和阈值,当监控数据超过或达到预设阈值时,触发告警或预警机制,通知相关人员进行处理。

  7. 问题定位和优化:根据监控数据和告警信息,定位问题的具体原因和位置,进行优化和改进,提升系统的性能和用户体验。

前端监控实现

1. 前端监控指标

下面是一些常见的前端监控数据指标:

1.1 环境信息:

  • 用户 id 或者用户 token:记录当前使用的用户是谁。
  • 操作系统、浏览器类型、版本等:记录访问该页面时该用户的 userAgent 信息。
  • URL: 记录正在监控的页面地址。

1.2 页面性能信息:

  • 网络层面

    • DNS解析时间、TCP连接时间、SSL握手时间等:页面加载过程中的各个阶段所需的时间。
    • 资源加载时间:各个资源(如图片、CSS、JavaScript文件)的加载时间。
    • 缓存命中率:页面加载时资源的缓存命中率。
    • 数据传输耗时:浏览器接受内容所耗费的时间。
    • 重定向耗时:页面请求重定向所耗费时间。
    • TTFB网络请求耗时:从发送请求到接收到服务器返回的首个字节所花费的时间,这个时间包含了网络请求时间、后端处理时间等等。
  • 页面展示层面

    • 页面大小:页面的大小,通常以字节数表示。
    • 卡顿:超过50ms的长任务
    • 以及页面常见性能指标如下图:

1.3 错误信息:

  • 错误类型(type):错误的类型,如JavaScript错误、网络请求错误等。
  • 错误消息(message):错误的具体信息。
  • 错误文件(filename):错误发生的文件或URL。
  • 错误行号(colno)、列号(lineno):错误发生的行号和列号。

1.4 用户行为信息:

  • 事件类型:用户进行的具体操作,如点击、滚动、输入等。
  • 事件元素(srcElement):操作发生在哪个元素上。
  • 触发时间(timeStamp):操作发生的时间。
  • 事件参数:某些特定事件可能会带有额外的参数,如输入事件可能会有输入的内容。

比如根据上面的指标可以设计以下前端监控的上报的数据指标结构:

  • 页面访问数据指标结构:
 1{   
 2  "title": "前端监控系统", // 页面标题  
 3  "url": "http://localhost:8080/", // 页面URL 
 4  "timestamp": 158812378, // 访问时间戳   
 5  "userAgent": "chrome", // 用户浏览器类型  
 6  "kind": "stability", // 大类 (稳定性) 
 7  "type": "visit" // 小类 (访问) 
 8}
  • 错误数据指标结构:
 1{   
 2  "title": "前端监控系统", // 页面标题 
 3  "url": "http://localhost:8080/", // 页面URL 
 4  "timestamp": 158812378, // 访问时间戳  
 5  "userAgent": "chrome", // 用户浏览器类型  
 6  "kind": "stability", // 大类 (稳定性)  
 7  "type": "error", // 小类 (报错)   
 8  "errorType": "jsError", // 错误类型 
 9  "message": "Uncaught TypeError: Cannot set property 'error' of undefined", // 类型详情  
10  "fileName": "http://localhost:8080/", // 访问文件名 
11  "position": "0:0", // 行列信息   
12  "stack": "btnclick (http://localhost:8080/:20:39)^HTMLInputElement.onclick (http://localhost:8080/:14:72)" // 堆栈信息 
13}
  • 网络请求数据指标结构:
 1{   
 2  "title": "前端监控系统", // 页面标题  
 3  "url": "http://localhost:8080/", // 页面URL  
 4  "timestamp": 158812378, // 访问时间戳 
 5  "userAgent": "chrome", // 用户浏览器类型  
 6  "kind": "performance", // 大类 (性能)  
 7  "type": "request", // 小类 (网络请求)   
 8  "url": "http://api.example.com/data", // 请求的URL   
 9  "method": "GET", // 请求的方法  
10  "status": 200, // 请求的状态码  
11  "duration": 100, // 请求时长   
12  "requestSize": 500, // 请求大小   
13  "responseSize": 1500 // 响应大小 
14}

2. 监控指标获取

2.1 错误信息

window.addEventListener(“error”, (event) => {}) 中 event 的错误信息解析:(通过这个 errorEvent 我们可以获取到大多数需要的错误信息指标)

 1
 2ErrorEvent {     
 3  isTrusted: true, // 表示事件是否由用户操作触发。当事件是由用户操作触发时,它的值为 `true`,否则为 `false`。  
 4  bubbles: false, // 指示事件是否会向上传播到父元素。当事件可以冒泡时,它的值为 `true`,否则为 `false`。     
 5  cancelBubble: false, // 表示是否取消进一步的事件传播。如果将 `cancelBubble` 设置为 `true`,则事件不会进一步冒泡到父元素。   
 6  cancelable: true, // 指示事件是否可以被取消。当事件可以取消时,它的值为 `true`,否则为 `false`。    
 7  colno: 34, // 表示发生错误的列号。  
 8  composed: false, // 指示事件是否可以穿过Shadow DOM边界传播。当事件可以穿过Shadow DOM边界时,它的值为 `true`,否则为 `false`。
 9  currentTarget:Window {0WindowwindowWindowselfWindowdocumentdocumentname''locationLocation, …}, // 表示正在处理事件的当前元素。   
10defaultPrevented: false, // 指示事件的默认行为是否已经被取消。如果默认行为已经被取消,它的值为 `true`,否则为 `false`   
11error: {
12  // 一个包含有关错误的对象。它可能包含错误的类型、消息和堆栈信息等。           
13  **message**: "Cannot set properties of undefined (setting 'error')"       
14  **stack**: "TypeError: Cannot set properties of undefined (setting 'error')\n    at errorClick (http://localhost:8080/:17:34)\n    at HTMLInputElement.onclick (http://localhost:8080/:11:72)"      
15  [[Prototype]]: Error   
16}       
17eventPhase: 0, // 表示事件传播的当前阶段。它的值可以是 0(无事件阶段)、1(捕获阶段)或 2(冒泡阶段)。 
18filename: "http://localhost:8080/", // 表示发生错误的文件名或 URL。    
19lineno: 17, // 表示发生错误的行号。     
20message: "Uncaught TypeError: Cannot set properties of undefined (setting 'error')", // 表示事件的错误消息。  
21returnValue: true, // 指示在事件处理完成后是否应继续执行事件的默认操作。如果应该继续执行默认操作,它的值为 `true`,否则为 `false`。 
22srcElement:Window {0WindowwindowWindowselfWindowdocumentdocumentname''locationLocation, …}, 
23// 表示触发事件的元素。     
24target:Window {0WindowwindowWindowselfWindowdocumentdocumentname''locationLocation, …}, // 表示触发事件的元素。    
25timeStamp: 3406, // 表示事件生成的时间戳。  
26type: "error", // 表示事件的类型。  
27[[Prototype]]: ErrorEvent    
28}

2.2 页面性能指标

如果需要获取页面性能相关的指标的话,得借助 js 中的内置对象 Performance 。它提供了与浏览器性能相关的信息和方法,用于度量和监测网页加载和执行的性能。

通过 window.performance 可以访问到 “Performance” 对象。“Performance” 对象包含各种属性和方法,用于获取关于网页加载、资源获取、导航时间、脚本执行等方面的性能信息。一些常用的”Performance”对象的属性和方法包括:

  • performance.timing:提供了与页面导航相关的时间戳信息,例如页面开始加载、DOM解析完成、资源加载完成等。(已被弃用)

  • performance.now():返回当前时间戳,用于测量代码执行时间和性能。

  • performance.memory:提供了与浏览器内存使用情况相关的信息,如内存限制和已使用内存量等。

  • performance.getEntriesByType(type):可以通过该方法在浏览器中获取性能条目。

    • 参数type:表示要获取的性能条目类型

      • 'navigation':获取所有导航相关的性能条目。
      • 'resource':获取所有资源相关的性能条目
      • ……
    • 返回值:该方法返回一个性能条目对象(PerformanceEntry)的数组。

2.2.1 网络层面

  • DNS 解析时间
 1const {     domainLookupEnd,     domainLookupStart  } = window.performance.timing; 
 2let DNS = domainLookupEnd - domainLookupStart;
 1const {      domainLookupEnd,      domainLookupStart  } = performance.getEntriesByType("navigation")[0]; 
 2let DNS = domainLookupEnd - domainLookupStart;
  • TCP 连接时间
 1const {     connectEnd,     secureConnectionStart  } = window.performance.timing; 
 2let TCP = connectEnd - secureConnectionStart;
 1const {      connectEnd,      secureConnectionStart  } = performance.getEntriesByType("navigation")[0]; 
 2let TCP = connectEnd - secureConnectionStart;
  • SSL 握手时间
 1const {     connectEnd,     connectStart  } = window.performance.timing; 
 2let SSL = connectEnd - connectStart;
 1const {      connectEnd,      connectStart  } = performance.getEntriesByType("navigation")[0]; 
 2let SSL = connectEnd - connectStart;
  • 资源加载时间
 1const {     loadEventStart,     domContentLoadedEventStart  } = window.performance.timing; 
 2let resourceLoadTime = loadEventStart - domContentLoadedEventStart;
 1const {      loadEventStart,      domContentLoadedEventStart  } = performance.getEntriesByType("navigation")[0]; 
 2let resourceLoadTime = loadEventStart - domContentLoadedEventStart;
  • 缓存命中率
 11. 在网站统计工具或服务器日志中获取总请求数这可以是一个时间段内的请求总数或页面级别的请求总数
 22. HTTP 响应头中查看是否存在 "Cache-Control""Expires" "Etag" 等缓存相关的响应头字段判断资源是否被缓存
 33. 根据相关响应头字段和请求的有效性判断请求是否命中缓存如果资源是从缓存中获取的则命中次数加1
 44. 根据总请求数和命中次数即可计算缓存命中率
  • 数据传输耗时
 1const {     responseEnd,     responseStart  } = window.performance.timing; 
 2let dataResponseTime = responseEnd - responseStart;
 1const {      responseEnd,      responseStart  } = performance.getEntriesByType("navigation")[0];
 2let dataResponseTime = responseEnd - responseStart;
  • 重定向耗时
 1const {     redirectEnd,     redirectStart  } = window.performance.timing; 
 2let redirect = redirectEnd - redirectStart;
 1const {      redirectEnd,      redirectStart  } = performance.getEntriesByType("navigation")[0];
 2let redirect = redirectEnd - redirectStart;
  • TTFB 网络请求耗时
 1const {     responseStart,     requestStart  } = window.performance.timing; 
 2let TTFB = responseStart - requestStart;
 1const {      responseStart,      requestStart  } = performance.getEntriesByType("navigation")[0]; 
 2let TTFB = responseStart - requestStart;

2.2.2 页面展示层面

检测这一层面的性能指标,除了上面说到的 window.performance, 又要使用到 js 提供的一个接口 PerformanceObserver,用于监测页面性能和资源使用情况。创建一个 PerformanceObserver 实例后,可以使用 observe() 方法来指定你感兴趣的性能事件类型。下面是一些常见的事件类型:

  • FP (首次绘制) 和 FCP (首次内容绘制)
 1let FP = performance.getEntriesByName("first-paint")[0];
 2let FCP = performance.getEntriesByName("first-contentful-paint")[0]; 
 3let firstPant = FP.startTime, let firstContentPant = FCP.startTime,
  • FMP (首次有意义绘制)
 1let FMP; 
 2// 增加一个性能条目的观察者 
 3new PerformanceObserver((entryList, observer) => {  
 4  let perEntries = entryList.getEntries(); 
 5  FMP = perEntries[0];   
 6  observer.disconnect(); // 不再观察 
 7}).observe({ entryTypes: ["element"] }); // 观察页面中有意义的元素
  • LCP (最大内容渲染)
 1let LCP; // 增加一个性能条目的观察者 
 2new PerformanceObserver((entryList, observer) => {  
 3  let perEntries = entryList.getEntries(); 
 4  LCP = perEntries[0];  
 5  observer.disconnect(); // 不再观察 
 6}).observe({ entryTypes: ["largest-contentful-paint"] });
  • DCL (DOM加载完成)
 1const {     domContentLoadedEventEnd,     domContentLoadedEventStart  } = window.performance.timing;
 2let DCL = domContentLoadedEventEnd - domContentLoadedEventStart;
 1const {      domContentLoadedEventEnd,      domContentLoadedEventStart  } = performance.getEntriesByType("navigation")[0]; 
 2let DCL = domContentLoadedEventEnd - domContentLoadedEventStart;
  • L (完整的加载时间)
 1const {     loadEventStart,     fetchStart  } = window.performance.timing; 
 2let L = loadEventStart - fetchStart;
 1const {      loadEventStart,      fetchStart  } = performance.getEntriesByType("navigation")[0]; 
 2let L = loadEventStart - fetchStart;
  • TTI (可交互时间)
 1const {     domInteractive,     fetchStart  } = window.performance.timing; 
 2let TTI = domInteractive - fetchStart;
 1const {      domInteractive,      fetchStart  } = performance.getEntriesByType("navigation")[0]; 
 2let TTI = domInteractive - fetchStart;
  • FID (首次输入延迟)
 1new PerformanceObserver((entryList, observer) => {   
 2  let firstInput = entryList.getEntries()[0];   
 3  if (firstInput) {    
 4    // processingStart 开始处理的时间   
 5    // startTime 开始点击的时间 
 6    // 差值就是处理的延迟     
 7    let FID = firstInput.processingStart - firstInput.startTime; 
 8  }  
 9  observer.disconnect(); // 不再观察 
10}).observe({ type: "first-input", buffered: true }); // 用户的第一次交互

3. 编写前端监控脚本

3.1 页面层面

  • 非 promise 错误:利用 window 的监听事件监听 ‘error’ 都可以监听到,比如 js错误,资源加载错误,同步错误,异步错误等等,然后再利用 ErrorEvent 对象 或者其他方式 去获取需要上报的监控指标即可。

     1window.addEventListener("error", (evt) => {   
     2  if (evt.target && (evt.target.src || evt.target.href)) {  
     3    // 资源加载错误数据处理上报操作   
     4  } else {    
     5    // 其他错误数据处理上报操作  
     6  } 
     7}
  • promise 错误:正如上面介绍错误类型时说过,promise的错误会触发 unhandledrejection 事件,所以 promise 的错误需要另外监听捕获然后上报监控指标。

     1// 监听 promise 错误  
     2window.addEventListener("unhandledrejection", (evt) => {  
     3  //  promise 错误数据处理上报操作
     4});
  • 白屏:检测页面是否出现了白屏现象,然后进行监控指标上报。检测白屏的方式有很多种,以下举例两种思路:

    • 第一种:通过监听页面的 DOMContentLoaded 事件,检查页面加载完成后是否还是空白状态。如果页面仍然为空白,即表示页面出现了白屏。在白屏事件发生时,会将相关的监控数据(例如页面标题、URL、加载时长等)上报给服务器端进行处理和存储。
     1// 检测页面白屏
     2function monitorPageBlank(  ) {
     3  // 记录页面加载开始的时间戳 
     4  const startTime = Date.now();  
     5  // 在DOMContentLoaded事件之后检查页面是否还处于白屏状态  
     6  window.addEventListener('DOMContentLoaded', () => { 
     7    // 计算页面加载时长    
     8    const loadTime = Date.now() - startTime;   
     9    // 检查页面是否还是空白的    
    10    if (document.documentElement.innerHTML.trim() === '') {  
    11      // 上报页面白屏事件,包括加载时长等信息    
    12      reportBlankPage(loadTime);  
    13    }  
    14  }); 
    15} 
    16// 上报页面白屏事件
    17function reportBlankPage(loadTime) {
    18  const data = {   
    19    title: document.title, // 页面标题    
    20    url: window.location.href, // 页面URL  
    21    timestamp: Date.now(), // 上报时间戳  
    22    loadTime: loadTime // 页面加载时长  
    23    // 其他自定义的监控指标...  
    24  };   
    25  // 上报监控指标  
    26  sendToServer(data); 
    27}
    • 第二种:通过在页面视口的多个采样点处判断元素是否为不参与渲染的父容器,来判断页面是否出现了白屏现象。如果检测到白屏,便会上报监控指标。
     1function blankScreen(  ) { 
     2  let warpperElements = ["html", "body", "#container", ".content"]; // 不是渲染的父容器  
     3  let emptyPoint = 0; // 记录白点的个数     
     4  // 根据元素的id、class等信息获取选择器,用于标识该元素。
     5  const getSelector = (element) => {   
     6    if (element.id) {       
     7      return `#${element.id}`;   
     8    } else if (element.className) {  
     9      return `.${element.className.split(" ").filter((item) => !!item).join(".")}`;  
    10    } else {      
    11      return (element.nodeName + "").toLowerCase();    
    12    } 
    13  };    
    14  // 判断给定的元素是否在`warpperElements`数组中  
    15  const isWrapper = (element) => {  
    16    let selector = getSelector(element); 
    17    if (warpperElements.indexOf(selector) != -1) {    
    18      emptyPoint++;  
    19    }   
    20  };    
    21  // 加载页面时触发
    22  window.addEventListener("load", () => {  
    23    for (let i = 0; i < 9; i++) {   
    24      let xElements = document.elementsFromPoint(    
    25        (window.innerWidth * i) / 10,      
    26        window.innerHeight / 2     
    27      );     
    28      let yElements = document.elementsFromPoint(window.innerWidth / 2,(window.innerHeight * i) / 10);      
    29      // 判断是否是白点       
    30      isWrapper(xElements[0]);    
    31      isWrapper(yElements[0]);    
    32    }    
    33    // 如果空白点大于16个点就记录白屏  
    34    if (emptyPoint >= 16) {   
    35      // 上报监控指标     
    36      sendToServer({      
    37        ……      
    38      });  
    39    }  
    40  }); 
    41}

    上述代码:

    • 首先,定义了一个数组warpperElements,用于存储不参与渲染的父容器的选择器信息;定义了一个变量emptyPoint,用于记录白点的个数,即页面上不参与渲染的区域。

    • 然后定义了两个辅助函数:

      • getSelector(element):根据元素的id、class等信息获取选择器,用于标识该元素。
      • isWrapper(element):判断给定的元素是否在warpperElements数组中,如果在,就将emptyPoint加1。
    • 最后使用 window.addEventListener("load", () => {} 实现页面的加载监听,在页面加载完成后执行以下逻辑:

      • 利用 document.elementsFromPoint(x, y) 方法,以页面视口的一定间隔在水平和垂直(当然为了保险可以多个方向进行判断,本段代码只是简略的从水平和垂直方向去判断)方向上采样,并获取采样点处的元素数组。
      • 对采样点处的元素使用 isWrapper 函数进行判断,如果元素在 warpperElements 数组中,则将 emptyPoint 加1。
      • 重复采样和判断过程,循环9次。(即水平和垂直方向都是采集10个样本点)
      • 如果emptyPoint的值大于等于16,表示页面中有出现白屏现象,然后上报监控指标即可。

    请注意以上两段代码一个根据检查页面加载完成后是否还是空白状态判断是否为白屏,另一个根据元素是否为最大的那几层父容器来判断是否为白屏,并没有考虑其他因素。如果需要更准确的白能需要综合考虑页面内容、样式等因素。

3.2 接口层面

一般的浏览器所有的接口都是基于 XHR 和 Fetch 实现的,所以可以通过重写该方法去实现监控作用,并且上报监控指标。

举个例子:

 1function injectXHR(  ) {  
 2  const originalXHR = window.XMLHttpRequest;  
 3  function reportXHRData(xhr, eventType) {   
 4    const duration = Date.now() - xhr._startTime;  
 5    const status = xhr.status;   
 6    const statusText = xhr.statusText;   
 7    const url = xhr._url;   
 8    const method = xhr._method;    
 9    // 上报监控指标     
10    sendToServer({   
11      eventType,    
12      url,    
13      method,   
14      status,     
15      statusText,  
16      duration,    
17      // 其他自定义的监控指标...  
18    });   
19  }    
20  window.XMLHttpRequest = function (  ) { 
21    const xhr = new originalXHR();   
22    xhr._startTime = 0; 
23    xhr._url = "";  
24    xhr._method = "";   
25    xhr.open = function (method, url, async) {   
26      xhr._url = url;     
27      xhr._method = method;  
28      return originalXHR.prototype.open.apply(this, arguments);  
29    };    
30    xhr.send = function (body) {  
31      xhr._startTime = Date.now();  
32      xhr.addEventListener("load", () => {   
33        reportXHRData(xhr, "load");  
34      });      
35      xhr.addEventListener("error", () => {  
36        reportXHRData(xhr, "error");   
37      });      
38      xhr.addEventListener("abort", () => {   
39        reportXHRData(xhr, "abort"); 
40      });     
41      originalXHR.prototype.send.apply(this, arguments); 
42    };    
43    return xhr; 
44  }; 
45} 
46// 处理上报的操作 
47function sendToServer(data) {} 
48// 开始注入监控 
49injectXHR();

上述代码实现步骤及其所做的事情:

  1. injectXHR 函数:

    • 重写了原生的 XMLHttpRequest 构造函数,以创建一个新的构造函数来替代它。
    • 在新构造函数中,覆盖了 opensend 方法,以实现接口监控的逻辑。
  2. reportXHRData 函数:在接口请求的事件处理函数中调用,用于汇报接口请求的监控数据。

  3. 重写 window.XMLHttpRequest

    • 创建一个新的 XMLHttpRequest 实例,并记录请求的开始时间、请求的 URL 和请求的方法。
    • 重写 open 方法,将传入的方法、URL 和是否异步保存到实例属性中。
    • 重写 send 方法,设置请求开始时间,并添加加载、错误和中止事件监听器。
  4. sendToServer 函数:用于发送监控数据到服务器。

  5. 开始注入监控:调用 injectXHR 函数,以实现对接口层面的监控。

3.3 业务层面

每个项目需要的业务信息都不同,而且业务信息包括很多,例如页面访问量,用户在线时长,pv,uv,用户的事件监听等等,这部分可以按照需求来写,以下只是打个样:

  • 页面访问量(Page Views):可以通过统计网站或应用程序的页面加载事件来计算页面访问量。可以通过使用 DOMContentLoaded 事件或 load 事件来监听页面加载完成的时间点,并记录每次触发这些事件时的计数;如果是框架(vue、react等等)可以通过路由变化来监听页面访问量。

    • 利用 load 监听页面访问量
     1// 统计页面访问量的变量 
     2let pageViewsCount = 0; 
     3// 上报页面访问量数据到服务器 
     4function sendPageViewsCount(  ) { 
     5  const data = {    
     6    pageViews: pageViewsCount,  
     7    timestamp: Date.now()   
     8    // 其他自定义的监控指标...  
     9  };   
    10  // 发送监控数据到服务器  
    11  sendToServer(data);
    12}  
    13// 页面加载完成时触发
    14window.addEventListener('load', () => { 
    15  // 页面访问量加一 
    16  pageViewsCount++;  
    17  // 发送页面访问量数据到服务器   
    18  sendPageViewsCount();
    19});
    
    • vue路由层面监听页面访问量
     1import Vue from 'vue'; 
     2import VueRouter from 'vue-router'; 
     3Vue.use(VueRouter);  
     4// 统计页面访问量的变量
     5let pageViewsData = {};  
     6// 上报页面访问量数据到服务器 
     7function sendPageViewsData(  ) {   
     8  const data = {  
     9    pageViews: pageViewsData,  
    10    timestamp: Date.now()   
    11    // 其他自定义的监控指标... 
    12  };   
    13  // 发送监控数据到服务器  
    14  sendToServer(data); 
    15}  
    16const router = new VueRouter({ 
    17  routes: [  
    18    { path: '/',
    19     component: {
    20       template: '<div></div>', 
    21       beforeRouteEnter: (to, from, next) => { 
    22         trackPageView('/');
    23         next(); 
    24       } 
    25     }
    26    }, 
    27    { path: '/about',
    28     component: { 
    29       template: '<div></div>',
    30       beforeRouteEnter: (to, from, next) => { 
    31         trackPageView('/about'); 
    32         next(); 
    33       } 
    34     } 
    35    },    
    36    { path: '/contact', 
    37     component: { 
    38       template: '<div></div>',
    39       beforeRouteEnter: (to, from, next) => { 
    40         trackPageView('/contact'); 
    41         next(); 
    42       } 
    43     } 
    44    },  
    45    // 其他路由配置... 
    46  ] 
    47}); 
    48// 页面切换时触发
    49function trackPageView(path) { 
    50  if (!pageViewsData[path]) {    
    51    pageViewsData[path] = 1;  
    52  } else {    
    53    pageViewsData[path]++; 
    54  } 
    55}  
    56// 路由切换时调用页面切换的监听
    57router.beforeEach((to, from, next) => { 
    58  trackPageView(to.path);  
    59  next(); 
    60});
  • 事件监听(Events Listen):可以通过监听用户的事件对象来监控用户的行为。

    例如:可以使用 addEventListener 方法为页面上的元素添加点击事件的监听器,并在每次点击时触发相关逻辑或记录点击次数。

     1
     2// 监听按钮的点击事件
     3const button = document.querySelector("#myButton");
     4button.addEventListener("click", function(  ) {  
     5  // 在此处触发逻辑或记录点击次数 
     6});
  • 页面停留时间(Time Spent on Page):可以通过记录用户进入页面和离开页面的时间戳,并计算两者之间的时间差来估计用户在页面上停留的时间。

     1const startTime = Date.now(); 
     2// 记录进入页面的时间戳
     3window.addEventListener("beforeunload", function(  ) { 
     4  const endTime = Date.now(); // 记录离开页面的时间戳  
     5  const timeSpent = endTime - startTime; // 计算停留时间 
     6  // 在此处触发逻辑或记录停留时间 
     7});

本文只是针对前端监控进行介绍与实现层面的一些简单赘述,实际操作落地上肯定会有各种问题。那么本次文章就是这样啦!如有错漏,还请指正!

个人笔记记录 2021 ~ 2025