前端监控之数据采集篇

284 阅读9分钟

概述

前端监控的重要性不必多说,一个完整的前端监控系统主要包含以下几个部分:

  • 数据采集 采集什么数据?怎么上报数据?
  • 数据回收 数据上报到哪里?如何存储数据?
  • 数据加工 是否需要清洗数据或者二次处理?
  • 数据消费

以什么样的形式来使用数据? 本文将主要围绕数据采集这一部分来介绍监控的数据类型和捕获方法,以及如何进行上报。

监控类型

从前端监控的数据类型看,主要分为三类:异常监控、性能监控、业务监控。

异常监控

JS异常

当js代码在运行时产生错误,会抛出Error的实例对象。除了Error外,JS还有其他6个内建的标准错误类型,都继承自Error。

SyntaxError

语法错误,可以分为两种: 1.发生在解析阶段

 let a,
 //Uncaught SyntaxError: Unexpected end of input

try-catch是无法捕获这类语法错误的,因为这个代码块并不会执行。如果使用window.onerror,需要保证出现语法错误的代码和window.onerror不在同一个代码块中。不过一般在本地开发时就能检测到这类语法错误,基本不会出现在线上。 2.发生在运行阶段

try {
  let a = JSON.parse('')
} catch(err){
  //SyntaxError: Unexpected end of JSON input
}

json解析出错时也会报SyntaxError,使用try-catch或者window.onerror都能捕获到。

ReferenceError

引用不存在的变量

try {
  let a = b + 1;
} catch (e) {
  //ReferenceError: b is not defined
}
TypeError

变量或参数的类型不符合预期

try {
  let a = 1;
  a();
} catch (e) {
  //TypeError: a is not a function
}
RangeError

数值变量或参数超出其有效范围

//函数堆栈超过最大值
try {
  (function fn() {
    fn()
  })()
} catch (err) {
  //RangeError: Maximum call stack size exceeded
}

//数组长度为负数
try {
  [].length = -1 
} catch (err) {
  //RangeError: Invalid array length
}

//Number对象的方法参数超出范围
try {
  var a = 1;
  a.toFixed(-1);
} catch (err) {
  //RangeError: toFixed() digits argument must be between 0 and 100
}
URIError

URI相关参数无效

try {
  decodeURIComponent('%D')
} catch (err) {
  //Uncaught URIError: URI malformed
}

主要涉及和URI相关的函数,例如encodeURI、decodeURI、encodeURIComponent、decodeURIComponent等

EvalError

eval函数执行异常,已经不在当前 ECMAScript 规范中使用,因此不会被运行时抛出。但是对象本身仍然向后兼容,可以通过new关键字来自定义该类型的错误提示。 对于JS异常,除了发生在解析阶段的的syntaxError,都可以用try-catch或者window.onError进行捕获。使用try-catch需要开发者能预判存在风险的代码,并进行包裹。除了手动包裹外,也可以通过自动化工具完成,不过对代码的侵入性较强。还有一点需要注意的是,try-catch无法捕获异步错误。

window.onerror = (message, source, lineno, colno, error) => {
  //Uncaught ReferenceError: a is not defined
}
try {
  setTimeout(()=>{
    let b = a + 1
  })
} catch (err) {
  //无法捕获
}

Promise异常

一般在写Promise时都会在最后加上catch或者在then的onReject函数中来处理Promise异常。如果没用显式捕获Promise异常,就会触发unhandlerejection事件,可以全局绑定unhandlerejection进行监听。

window.addEventListener("unhandledrejection", event => {
    console.log(event.reason)
    //promise error
});

Promise.reject("promise error");

跨域脚本异常

在一些场景下需要加载跨域脚本,如果跨域脚本内部发生异常,window.onError可能只能只会返回一个“Script error”,没有额外的有效信息。

<script src="http://another-domain.com/app.js"></script>
<script>
  window.onerror = function(message, url, line, column, error) {
    console.log(message)
    //Script error.
  };
  foo(); // error occurred when call function declared in app.js 
</script>

浏览器基于安全考虑隐藏了其它域js文件抛出的具体错误信息,避免敏感信息无意中被第三方脚本捕获。如果想要获取到其他域脚本的具体报错信息,需要设置crossorigin属性并标记静态资源响应头Access-Control-Allow-Origin允许跨域

<script src="http://another-domain.com/app.js" crossorigin="anonymous"></script>
Access-Control-Allow-Origin: *
//or
Access-Control-Allow-Origin: http://www.example.com

资源加载异常

当加载脚本或是其他静态资源时,有可能因为服务端异常、网络异常等原因导致加载出错。

<script src="https://gw.alipayobjects.com/os/lib/prop-types/15.7.0/prop-types.min.js"></script>

image.png 为了捕获这类加载错误,可以在script标签上加上onerror

<body>
  <script>
    handleScriptError = (err) => {
      console.log(err.message)
      //undefined
    }
  </script>
  <script src="https://gw.alipayobjects.com/os/lib/prop-types/15.7.0/prop-types.min.js" onerror="handleScriptError(this)"></script>
</body>

也可以使用window.addEventListener, 但要注意的是网络请求异常事件不会冒泡,只能在捕获阶段采集。所以自然也无法使用window.onerror捕获异常。

<body>
  <script>
    window.addEventListener('error', (err)=>{
      console.log(err.message)
      //undefined
    }, true)
  </script>
  <script src="https://gw.alipayobjects.com/os/lib/prop-types/15.7.0/prop-types.min.js"></script>
</body>

从上面的案例可以看到,虽然资源异常被捕获到了,但是error.message却是undefined,前端无法判断具体导致出错的原因,可以考虑结合服务端日志排查。

框架错误

React16之后使用componentDidCatch来监测错误,一般会配合getDerivedStateFromError使用。通过componentDidCatch上报错误信息,而getDerivedStateFromError则用来渲染备用UI。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  
  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    // 你同样可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      // 你可以自定义降级后的 UI 并渲染
      return <h1>Something went wrong.</h1>;
    }
    
    return this.props.children; 
  }
}

Vue中通过Vue.config.errorHandler来处理错误。

Vue.config.errorHandler = (err, vm, info) => {}

请求异常

前端如果需要监听请求异常,可以考虑覆写请求接口对象XMLHttpRequest、fetch,如果使用类库的话一般都可以放在拦截器里做统一处理。不过,对请求接口的拦截可能会对性能产生轻微的影响。

性能监控

衡量指标

性能监控旨在于不断优化用户体验,首先就需要明确衡量性能的指标。常见的感官指标有: FP(First Paint) 首次绘制:首次出现视觉上不同于页面跳转前效果的时间 FCP(First Contentful Paint) 首次有内容绘制:首次绘制用户有直观感受的内容的时间 LCP(Largest Contentful Paint) 最大内容绘制:视口中可见的最大图像或文本块的渲染时间 TTI(Time To Interactive) 可交互时间:页面可以达到可交互状态的时间,例如按钮可点击或文本框可输入文字 FID(First Input Delay) 首次交互延迟:用户首次与页面交互到浏览器实际能开始处理交互事件的时间差 除此之外,还可以自定义符合业务特点的指标。

采集方式

性能数据的采集主要还是通过Performance接口获取当前页面中与性能相关的信息,融合了 Performance Timeline API、Navigation Timing API、 User Timing API 和 Resource Timing API等,下面会介绍一下常用的属性和方法。

window.performance.navigation

呈现了如何导航到当前文档

属性名含义
type如何导航到当前页面
0: 点击链接,书签和表单提交,或者脚本操作,或者在 url 中直接输入地址
1: 点击刷新页面按钮或者通过Location.reload()方法显示的页面
2: 页面通过历史记录和前进后退访问时
redirectCount在到达这个页面之前重定向了多少次

如果需要统计如何导航到当前页面,可以使用该方法

window.performance.timing

提供了在加载和使用当前页面期间发生的各种事件的性能计时信息,属性详见:developer.mozilla.org/zh-CN/docs/… 可以根据这些时间节点,可以计算出一些常用的典型指标

指标计算备注
上一个页面卸载耗时unloadEventEnd - unloadEventStart
重定向耗时redirectEnd - redirectStart
DNS查询耗时domainLookupEnd - domainLookupStart
TCP连接耗时connectEnd - connectStart
SSL安全连接耗时connectEnd - secureConnectionStart只在HTTPS下有效
TTFB请求响应耗时responseStart - requestStart
内容传输耗时responseEnd - responseStart
dom解析耗时domInteractive - domLoading
资源加载耗时loadEventStart - domContentLoadedEventEnd
页面完全加载耗时loadEventStart - fetchStart
window.performance.getEntries()

该方法为Performance Timeline API的一个拓展方法,返回PerformanceEntry对象数组, 该对象又由PerformanceResourceTiming、PerformanceNavigationTiming、PerformancePaintTiming、PerformanceMark、PerformanceMeasure、PerformanceEventTiming等扩展。 PerformanceResourceTiming 可以检索和分析有关加载资源的详细网络计时数据,包括资源加载、ajax请求等 image.png PerformanceNavigationTiming 提供了用于存储和检索有关浏览器文档事件的指标的方法和属性 image.png PerformancePaintTiming 提供页面在构建过程中的“绘制”时间点信息,包含"first-paint" 或"first-contentful-paint" image.png image.png PerformanceMark 如果有自定义的时间标记需求,还可以使用window.performance.mark()在浏览器的性能缓冲区中使用给定名称添加一个时间戳,再通过window.performance.getEntries()获取时间戳

//打标
performance.mark("dog");
let i=1;
while(i<100){
  i++;
}
performance.mark("dog");

//获取entries
const dogEntries = performance.getEntriesByName("dog");

image.png PerformanceMeasure window.performance.measure()用来记录两个mark的时间差

//打标
performance.mark("dog-start");
let i=1;
while(i<100){
  i++;
}
performance.mark("dog-end");
performance.measure(
    "dog",
    "dog-start",
    "dog-end"
  );

//获取entries
const dogEntries = performance.getEntriesByName("dog");

image.png 除了getEntries()方法,还可以使用getEntriesByName()和getEntriesByType()来过滤特定的entries。 PerformanceEventTiming 公开了事件生命周期中许多时间戳,详细事件类型见: developer.mozilla.org/en-US/docs/… image.png

PerformanceObserver

性能检测对象,用于监测性能度量事件,在有性能数据产生时会主动通知。

function perf_observer(list, observer) {
  // 处理 "mark" 事件
}
var observer2 = new PerformanceObserver(perf_observer);
observer2.observe({entryTypes: ["mark"]});

//打标
performance.mark("dog");
let i=1;
while(i<100){
  i++;
}
performance.mark("dog");

只有在observe方法中指定entryTypes的性能度量事件发生时才会触发回调,可以先获取浏览器支持的类型: image.png 需要注意的是有些api没有通过Performance对象公开,getEntries()方法无法获取到,只能通过 PerformanceObserver监听。

业务监控

业务监控主要指业务自定义的埋点,比如说某个组件的pv、uv、click等,本文不做进一步介绍。

数据上报

我们应该经常会看到使用Image标签进行埋点的写法,它的好处在于不会涉及跨域问题,但会对url长度有限制。

new Image().src = '***'

如果使用post请求,可以优先考虑navigator.sendBeacon()方法,它避免了使用传统XMLHttpRequest发送分析数据的一些问题。有时我们需要在页面卸载前再将统计发送到服务端,但是异步的XMLHttpRequest无法保证数据安全发送,如下使用同步的XMLHttpRequest,虽然能正常完成数据上报,但会影响页面跳准的流畅度。

window.addEventListener('unload', (err)=>{
  let httpRequest = new XMLHttpRequest();
  httpRequest.open("POST","***", false); //false表示同步请求
  //...
})

navigator.sendBeacon()既能保证数据的可靠发送,也不影响下一页面的载入。综上所述,数据上报的代码可以考虑下面这种写法:

const log = (url, data) => {
  if(url < 2083) {
    //使用Image标签
  }else if(navigator.sendBeacon){
    //navigator.sendBeacon()
  }else{
    //XMLHttpRequest
  }
}

结语

本文梳理了前端监控系统中数据采集所涉及到的一些通用问题,然而各类前端业务千差万别,还是需要结合实际情况来设计更加符合业务特点的解决方案。