前端异常监控

1,024 阅读6分钟

前端监控

1. 监控类型

  1. 数据监控

数据监控即监控用户行为,大概可以分为以下几个方面:

  • PV/UV:
    • PV:page view页面浏览量或点击量;
    • UV:指访问某个站点或点击某条新闻的不同 IP 地址的人数
  • 用户在每一个页面的停留时间
  • 用户通过什么入口来访问该网页
  • 用户在相应的页面中触发的行为
  1. 性能监控

性能监控即通过监控分析产品性能,分为首屏性能和使用性能。大概可以分为以下几个方面:

  • 首屏性能:
    • 不同用户,不同机型和不同系统下的首屏加载时间
    • 白屏时间
    • 静态资源整体下载时间
  • 使用性能:
    • http请求的响应时间
    • 页面渲染时间
    • 页面交互动画完成时间
  1. 异常监控

异常监控即检测到产品使用异常,并及时上报异常情况。

  • Javascript的异常监控
  • 样式丢失的异常监控

2. 数据上报

向服务器端上报数据的方式有很多种,大致可以分为:ajax、img、navigator.sendBeacon

  1. img上报
  • 防止跨域:图片的src属性不会跨域,但是可以发起请求
  • 防止阻塞页面加载,影响用户体验
  • 相比JPG/PNG,GIF体积更小
  1. navigator.sendBeacon上报

请求发出后会与当前页面脱离关联,作为浏览器的任务单独执行,可以保证数据一定会发出去,而且不会拖延卸载流程

  • 异步上报
  • 可跨域
  • 可靠性

监控页面显示隐藏事件

document.addEventListener("visibilitychange", function logData() {
  if (document.visibilityState === "hidden") {
    navigator.sendBeacon("/log", analyticsData);
  }
});

监控页面关闭事件

let _beforeUnload_time = 0;
window.onunload = function () {
    let _gap_time = new Date().getTime() - _beforeUnload_time;
    if (_gap_time <= 5) {
        //浏览器关闭
        console.log('浏览器关闭');
    } else {
        //浏览器刷新
        console.log('浏览器刷新',document.domain);
    }
};
window.onbeforeunload = function () {
    console.log('关闭前');
    _beforeUnload_time = new Date().getTime();
};

文档卸载期间发送数据

对于开发者来说保证在文档卸载期间发送数据一直是一个困难。因为用户代理通常会忽略在 unload (en-US) 事件处理器中产生的异步 XMLHttpRequest。

为了解决这个问题, 统计和诊断代码通常要在 unload 或者 beforeunload (en-US) 事件处理器中发起一个同步 XMLHttpRequest 来发送数据。同步的 XMLHttpRequest 迫使用户代理延迟卸载文档,并使得下一个导航出现的更晚。

通过在卸载事件处理器中创建一个图片元素并设置它的 src 属性的方法来延迟卸载以保证数据的发送。因为绝大多数用户代理会延迟卸载以保证图片的载入,所以数据可以在卸载事件中发送。

使用 sendBeacon() 方法会使用户代理在有机会时异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能。这就解决了提交分析数据时的所有的问题:数据可靠,传输异步并且不会影响下一页面的加载。

异常监控

前端代码中一般涉及到的业务场景分为JS逻辑处理,外部资源加载,CSS样式处理,其中JS逻辑处理又分为同步和异步代码。

1. JS异常

try...catch即时运行异常局部捕获

try {
  console.log(notdefined);
} catch (error) {
  console.log(error,'error');
}

image.png

⚠️:try...catch不能捕获异步错误,不能捕获语法错误(VSCode可检测语法错误)

try {
  setTimeout(()=>{
    console.log(a);
  },0)
} catch (error) {
  console.log(error,'error'); // 无错误提示
}

window.onerror即时运行错误全局捕获

window.onerror = function(message, source, lineno, colno, error){
    console.log('捕获到异常:',{message, source, lineno, colno, error});
}
console.log(notdefined); // 可以捕获运行错误

setTimeout(()=>{ // 可以捕获异步错误
  console.log(a);
},0)

image.png

⚠️:window.onerror不能捕获资源加载错误,不能捕获语法错误(VSCode可检测语法错误)

2. 资源加载异常

window.addEventListener('error', callback, true)全局捕获资源加载异常

<script>
  window.addEventListener("error",(error) => {
    console.log("捕获到异常:", error);
  },true);
</script>
<script>
  <link href="https://yun.tuia.cn/foundnull.css" rel="stylesheet"/>
</script>

image.png

⚠️:window.addEventListener不能捕获new Image()错误,不能捕获fetch请求错误

<script>
  new Image().src = 'https://yun.tuia.cn/image/lll.png'
  fetch('https://tuia.cn/test')
</script>

3. 异步异常

Promise.catch()局部捕获Promise异常

new Promise((resolve, reject) => {
  JSON.parse("");
  resolve();
}).catch((err) => {
  console.log("捕获到异常:", err);
});

image.png

await+try...catch局部捕获async异常

const getJSON = async () => {
  throw new Error("inner error");
};
try {
  await getJSON();
} catch (error) {
  console.log("捕获到异常:", error);
}

image.png

window.addEventListener('unhandledrejection', callback)全局捕获Promise异常

window.addEventListener("unhandledrejection", function (e) {
  console.log("捕获到异常:", e);
});
fetch("https://tuia.cn/test");
new Promise((resolve, reject) => {
  JSON.parse("");
  resolve();
});

image.png

4. CSS样式异常

window.getComputedStyle(element)捕获特定元素样式

<div id="myElement" class="my-element-style">测试元素</div>
var element = document.getElementById("myElement");
var style = window.getComputedStyle(element);
if (style.display === "block") {
  console.log("error异常:样式丢失,display属性为block");
}

image.png

window.getComputedStyle()函数获取元素的计算样式,然后检查特定样式属性是否存在

window.addEventListener('error', callback, true)全局捕获CSS文件加载异常 略!

框架异常捕获

1. Vue异常捕获

Vue.config.errorHandler全局捕获Vue异常

Vue.config.errorHandler = function (err) {
  setTimeout(() => {
    throw err
  })
}

2. React异常捕获ErrorBoundary组件

componentDidCatch、getDerivedStateFromError生命周期钩子局部捕获React异常

ErrorBoundary 只能捕获子组件的 render 错误,以下错误无法处理:

  • 事件处理函数:如 onClick,onMouseEnter
  • 异步代码:如 requestAnimationFrame,setTimeout,promise
  • 服务端渲染错误
  • ErrorBoundary 组件本身的错误
function ErrorCatch(Component){ 
  return class WrapComponent extends React.Component{
    constructor(props){ 
      super(props); 
      this.state={ isError:false } 
    } 
    static getDerivedStateFromError(){ 
      return{ isError:true } // 无法获取this,但是返回值会合并到state中,作为新的state值向下传递 
    } 
    componentDidCatch(error, errorInfo){ 
      this.setState({ isError:true }) 
    } 
    
    render(){ 
      return this.state.isError ? <div>出现错误</div>
      : <Component {...this.props}/> 
    } 
  } 
}

生命周期钩子不能捕获函数事件处理器中的错误、异步错误、服务端渲染错误、错误边界组件自身抛出的错误

3. catch-react-error

github地址:github.com/x-orpheus/c…

网易云音乐技术团队推出的一款在React中捕获异常的插件,4年前发布,目前已经迭代到3.0.0版本。

catch-react-error底层实现是通过类装饰器和ErrorBoundary组合使用,同时支持React和React Native。由于ErrorBoundary不支持服务端渲染错误,所以通过try...catch处理了这部分。

实现原理:通过ErrorBoundary包裹入口组件,手动包裹成本太大,而且对于已经存在的代码需要重新修改,所以采用类装饰器处理包裹组件功能,ErrorBoundary无法处理服务端渲染错误,所以通过try/catch包裹SSR。

类装饰器定义:类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。

catchreacterror:catchreacterror类装饰器组件返回一个HOC组件,用于包裹内层子组件。

import React, { Component, forwardRef } from "react";

const catchreacterror = (Boundary = DefaultErrorBoundary) => InnerComponent => {
  class WrapperComponent extends Component {
    render() {
      const { forwardedRef } = this.props;
      return (
        <Boundary>
          <InnerComponent {...this.props} ref={forwardedRef} />
        </Boundary>
      );
    }
  }
};

try/catch处理服务端渲染错误

// 判断是否为SSR
function is_server() {
  return !(typeof window !== "undefined" && window.document);
}
// try/catch包裹SSR
if (is_server()) {
  const originalRender = InnerComponent.prototype.render;

  InnerComponent.prototype.render = function() {
    try {
      return originalRender.apply(this, arguments);
    } catch (error) {
      console.error(error);
      return <div>Something is Wrong</div>;
    }
  };
}

项目使用

// 安装插件
npm install catch-react-error
npm install --save-dev @babel/plugin-proposal-decorators
npm install --save-dev @babel/plugin-proposal-class-properties

// 引入
import catchreacterror from "catch-react-error";

// 添加babelplugin配置
{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "loose": true }]
  ]
}

// 类组件使用
@catchreacterror()
class Count extends React.Component {
  render() {
    const { count } = this.props;
    if (count === 3) {
      throw new Error("count is three");
    }
    return <h1>{count}</h1>;
  }
}

// 函数组件使用
function Count({ count }) {
  if (count === 3) {
    throw new Error("count is three");
  }
  return <h1>{count}</h1>;
}
const SaleCount = catchreacterror()(Count);

TS的装饰器官网

link.juejin.cn/?target=htt…