常见监控问题汇总

# 异常捕获原理【核心】

# 监控方式分类

前端捕获异常分为全局捕获和单点捕获。

  • 全局捕获:代码集中,易于管理;
  • 单点捕获:作为补充,对某些特殊情况进行捕获,但分散,不利于管理。

# 全局捕获

  • 通过全局的接口,将捕获代码集中写在一个地方,可以利用的接口有:

    • window.addEventListener(‘error’) / window.addEventListener(“unhandledrejection”) / document.addEventListener(‘click’)
  • 框架级别的全局监听;

    • 例如aixos中使用interceptor进行拦截,
    • vue、react都有自己的错误采集接口
  • 通过对全局函数进行封装包裹,实现在在调用该函数时自动捕获异常

  • 对实例方法重写(Patch),在原有功能基础上包裹一层,

    例如对setTimeout进行重写,在使用方法不变的情况下也可以异常捕获

# 单点捕获

  • 在业务代码中对单个代码块进行包裹,或在逻辑流程中打点,实现有针对性的异常捕获:
  • try…catch
  • 专门写一个函数来收集异常信息,在异常发生时,调用该函数
  • 专门写一个函数来包裹其他函数,得到一个新函数,该新函数运行结果和原函数一模一样,只是在发生异常时可以捕获异常

# window.onerror 异常处理

window.onerror 无论是异步还是非异步错误,onerror 都能捕获到运行时错误。

监控JavaScript运行时错误(包括语法错误)和 资源加载错误;

window.onerror = function(message, source, lineno, colno, error) { ... }
window.addEventListener('error', function(event) { ... }, true)
// 函数参数:
    // message:错误信息(字符串)。可用于HTML onerror=""处理程序中的event。
    // source:发生错误的脚本URL(字符串)
    // lineno:发生错误的行号(数字)
    // colno:发生错误的列号(数字)
    // error:Error对象(对象
1
2
3
4
5
6
7
8
window.onerror = function (msg, url, row, col, error) {
    console.log('我知道错误了');
    console.log({
        msg,  url,  row, col, error
    })
    return true;
};
1
2
3
4
5
6
7

注意:

  • 1)window.onerror 函数只有在返回 true 的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示 Uncaught Error: xxxxx。
  • 2)window.onerror 是无法捕获到网络异常的错误。由于网络请求异常不会事件冒泡,因此必须在捕获阶段将其捕捉到才行,但是这种方式虽然可以捕捉到网络请求的异常,但是无法判断 HTTP 的状态是 404 还是其他比如 500 。还需要配合服务端日志才进行排查分析才可以。
  • 可以看到 JS 错误监控里面有个 window.onEerror,又用了 window.addEventLisenter('error'),其实两者并不能互相代替。 window.onError 是一个标准的错误捕获接口,它可以拿到对应的这种 JS 错误; window.addEventLisenter('error')也可以捕获到错误,但是它拿到的 JS 报错堆栈往往是不完整的。
  • 同时 window.onError 无法获取到资源加载失败的一个情况,必须使用 window.addEventLisenter('error')来捕获资源加载失败的情况。
window.addEventListener('error', (msg, url, row, col, error) => {
    console.log('我知道错误了');
    console.log(
        msg, url, row, col, error
    );
    return true;
}, true);
1
2
3
4
5
6
7

# Promise错误

Promise 实例抛出异常而你没有用 catch 去捕获的话,onerror 或 try-catch 也无能为力,无法捕捉到错误。 如果用到很多 Promise 实例的话,特别是你在一些基于 promise 的异步库比如 axios 等一定要小心,因为你不知道什么时候这些异步请求会抛出异常而你并没有处理它,所以你最好添加一个 Promise 全局异常捕获事件 unhandledrejection。

``Promise的话主要是unhandledrejection事件,也就是未被catchreject状态的promise`;

window.addEventListener("unhandledrejection", function(e){
    e.preventDefault()
    console.log('我知道 promise 的错误了');
    console.log(e.reason);
    return true;
});
1
2
3
4
5
6
window.addEventListener("unhandledrejection", event => {
  console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
});
1
2
3

# iframe错误

父窗口直接使用 window.onerror 是无法直接捕获,如果你想要捕获 iframe 的异常的话,有分好几种情况。

1) 如果你的 iframe 页面和你的主站是同域名的话,直接给 iframe 添加 onerror 事件即可。

<iframe src="./iframe.html" frameborder="0"></iframe>
<script>
  window.frames[0].onerror = function (msg, url, row, col, error) {
    console.log('我知道 iframe 的错误了,也知道错误信息');
    console.log({
      msg,  url,  row, col, error
    })
    return true;
  };
</script>
1
2
3
4
5
6
7
8
9
10

2)如果你嵌入的 iframe 页面和你的主站不是同个域名的,但是 iframe 内容不属于第三方

可以通过与 iframe 通信的方式将异常信息抛给主站接收。与 iframe 通信的方式有很多,常用的如: postMessage,hash 或者 name字段跨域等等 (opens new window)

3)如果是非同域且网站不受自己控制的话,除了通过控制台看到详细的错误信息外,没办法捕获

# 异步定时相关

setTimeout、setInterval、requestAnimationFrame等:其实就是通过代理的方式把原来的方法拦截一下在调用真实的方法之前做一些自己的事情;【拦截劫持】

const prevSetTimeout = window.setTimeout;

window.setTimeout = function(callback, timeout) {
  const self = this;
  return prevSetTimeout(function() {
    try {
      callback.call(this);
    } catch (e) {
      // 捕获到详细的错误,在这里处理日志上报等了逻辑
      // ...
      throw e;
    }
  }, timeout);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# Vue.config.errorHandler

VueVue.config.errorHandler跟上面的同理;

// sentry中对Vue errorHandler的处理
function vuePlugin(Raven, Vue) {
  var _oldOnError = Vue.config.errorHandler;
  Vue.config.errorHandler = function VueErrorHandler(error, vm, info) {
    // 上报
    Raven.captureException(error, {
      extra: metaData
    });

    if (typeof _oldOnError === 'function') {
      // 为什么这么做?
      _oldOnError.call(this, error, vm, info);
    }
  };
}
module.exports = vuePlugin;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# React的 ErrorBoundary

ErrorBoundary的定义:如果一个class组件中定义了static getDerivedStateFromError()componentDidCatch()这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用static getDerivedStateFromError()渲染备用 UI ,使用componentDidCatch()打印错误信息

// ErrorBoundary的示例
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  componentDidCatch(error, info) {
    this.setState({ hasError: true });
    // 在这里可以做异常的上报
    logErrorToMyService(error, info);
  }

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

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# Sentry中react的实现

// ts声明的类型,可以看到sentry大概实现的方法
/**
 * A ErrorBoundary component that logs errors to Sentry.
 * Requires React >= 16
 */
declare class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
    state: ErrorBoundaryState;
    componentDidCatch(error: Error, { componentStack }: React.ErrorInfo): void;
    componentDidMount(): void;
    componentWillUnmount(): void;
    resetErrorBoundary: () => void;
    render(): React.ReactNode;
}

// 真实上报的地方
ErrorBoundary.prototype.componentDidCatch = function (error, _a) {
  var _this = this;
  var componentStack = _a.componentStack;
  // 获取到配置的props
  var _b = this.props, beforeCapture = _b.beforeCapture, onError = _b.onError, showDialog = _b.showDialog, dialogOptions = _b.dialogOptions;
  withScope(function (scope) {
    // 上报之前做一些处理,相当于axios的请求拦截器
    if (beforeCapture) {
      beforeCapture(scope, error, componentStack);
    }
    // 上报
    var eventId = captureException(error, { contexts: { react: { componentStack: componentStack } } });
    // 开发者的回调
    if (onError) {
      onError(error, componentStack, eventId);
    }
    // 是否显示sentry的错误反馈组件(也是一种收集错误的方式)
    if (showDialog) {
      showReportDialog(__assign(__assign({}, dialogOptions), { eventId: eventId }));
    }
    // componentDidCatch is used over getDerivedStateFromError
    // so that componentStack is accessible through state.
    _this.setState({ error: error, componentStack: componentStack, eventId: eventId });
  });
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 监控上报发送

监控拿到报错信息之后,接下来就需要将捕捉到的错误信息发送到信息收集平台上,常用的发送形式主要有两种:

  • 通过 Ajax 发送数据
  • 动态创建 img 标签的形式
function error(msg,url,line){
   var REPORT_URL = "xxxx/cgi"; // 收集上报数据的信息
   var m =[msg, url, line, navigator.userAgent, +new Date];// 收集错误信息,发生错误的脚本文件网络地址,用户代理信息,时间
   var url = REPORT_URL + m.join('||');// 组装错误上报信息内容URL
   var img = new Image;
   img.onload = img.onerror = function(){
      img = null;
   };
   img.src = url;// 发送数据到后台cgi
}
// 监听错误上报
window.onerror = function(msg,url,line){
   error(msg,url,line);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# JS代码压缩后,如何定位

该部分原文出处:https://github.com/joeyguo/blog/issues/14

# 源代码(存在错误)

function test() {
    noerror // <- 报错
}
test();
1
2
3
4

# 经 webpack 打包压缩后产生如下代码

!function(n){function r(e){if(t[e])return t[e].exports;var o=t[e]={i:e,l:!1,exports:{}};return n[e].call(o.exports,o,o.exports,r),o.l=!0,o.exports}var t={};r.m=n,r.c=t,r.i=function(n){return n},r.d=function(n,t,e){r.o(n,t)||Object.defineProperty(n,t,{configurable:!1,enumerable:!0,get:e})},r.n=function(n){var t=n&&n.__esModule?function(){return n.default}:function(){return n};return r.d(t,"a",t),t},r.o=function(n,r){return Object.prototype.hasOwnProperty.call(n,r)},r.p="",r(r.s=0)}([function(n,r){function t(){noerror}t()}]);
1

# 代码如期报错,并上报相关信息

{ msg: 'Uncaught ReferenceError: noerror is not defined',
  url: 'http://127.0.0.1:8077/main.min.js',
  row: '1',
  col: '515' }
1
2
3
4

此时,错误信息中行列数为 1 和 515。 结合压缩后的代码,肉眼观察很难定位出具体问题。

# 将压缩代码中分号变成换行

uglifyjs 有一个叫 semicolons 配置参数,设置为 false 时,会将压缩代码中的分号替换为换行符,提高代码可读性, 如

!function(n){function r(e){if(t[e])return t[e].exports
var o=t[e]={i:e,l:!1,exports:{}}
return n[e].call(o.exports,o,o.exports,r),o.l=!0,o.exports}var t={}
r.m=n,r.c=t,r.i=function(n){return n},r.d=function(n,t,e){r.o(n,t)||Object.defineProperty(n,t,{configurable:!1,enumerable:!0,get:e})},r.n=function(n){var t=n&&n.__esModule?function(){return n.default}:function(){return n}
return r.d(t,"a",t),t},r.o=function(n,r){return Object.prototype.hasOwnProperty.call(n,r)},r.p="",r(r.s=0)}([function(n,r){function t(){noerror}t()}])
1
2
3
4
5

此时,错误信息中行列数为 5 和 137,查找起来比普通压缩方便不少。但仍会出现一行中有很多代码,不容易定位的问题。

# js 代码半压缩 · 保留空格和换行

uglifyjs 的另一配置参数 beautify 设置为 true 时,最终代码将呈现压缩后进行格式化的效果(保留空格和换行),如

!function(n) {
    // ...
    // ...
}([ function(n, r) {
    function t() {
        noerror;
    }
    t();
} ]);
1
2
3
4
5
6
7
8
9

此时,错误信息中行列数为 32 和 9,能够快速定位到具体位置,进而对应到源代码。但由于增加了换行和空格,所以文件大小有所增加。

# SourceMap 快速定位

SourceMap 是一个信息文件,存储着源文件的信息及源文件与处理后文件的映射关系。 在定位压缩代码的报错时,可以通过错误信息的行列数与对应的 SourceMap 文件,处理后得到源文件的具体错误信息。

SourceMap 文件中的 sourcesContent 字段对应源代码内容,不希望将 SourceMap 文件发布到外网上,而是将其存储到脚本错误处理平台上,只用在处理脚本错误中。 通过 SourceMap 文件可以得到源文件的具体错误信息,结合 sourcesContent 上源文件的内容进行可视化展示,让报错信息一目了然!

sourceMap

# 开源方案 sentry

sentry (opens new window) 是一个实时的错误日志追踪和聚合平台,包含了上面 sourcemap 方案,并支持更多功能,如:错误调用栈,log 信息,issue管理,多项目,多用户,提供多种语言客户端等,具体介绍可以查看 getsentry/sentry,sentry.io,这里暂不展开。

sentry

# 参考文章

上次更新: 2022/04/15, 05:41:29
×