Sentry的原理以及自定义上报的使用方式

上一篇文章讲一下如何前端项目如何接入sentry监控平台?

这篇主要总结一下Sentry的原理以及自定义上报的使用方式

一、前端常见异常类型

  1. ECMAScript Execeptions

developer.mozilla.org/zh-CN/docs/…

  1. DOMException

developer.mozilla.org/zh-CN/docs/…

  1. 资源加载报错 cdn、 img、script、link、audio、video、iframe ...
  • performance.getEntries 可查找未加载的资源
  • script添加crossorigin属性/ 服务端支持跨域
  1. Promise错误

二、前端常见异常的捕获方式

  1. try catch 同步/局部侵入式, 无法处理语法错误和异步
// 能捕获
try{
    a // 未定义变量
} catch(e){
    console.log(e)
}
// 不能捕获--语法错误
try{
    var a = \ 'a'
}catch(e){
   console.log(e)
}
// 不能捕获--异步
try{
   setTimeout(()=>{
        a
    })
} catch(e){
    console.log(e)
}
// 在setTimeout里面再加一层try catch才会生效
复制代码
  1. window.onerror 能全局捕获/ 只能捕获运行时错误,不能捕获资源加载错误
// message:错误信息(字符串)。可用于HTML onerror=""处理程序中的event。
// source:发生错误的脚本URL(字符串)
// lineno:发生错误的行号(数字)
// colno:发生错误的列号(数字)
// error:Error对象(对象)
window.onerror = function(message, source, lineno, colno, error) { ... }

// 对于不同域的js文件,window.onerror不能捕获到有效信息。出于安全原因,不同浏览器返回的错误信息参数可能不一致。跨域之后window.onerror在很多浏览器中是无法捕获异常信息的,统一会返回脚本错误(script error)。所以需要对脚本进行设置
// crossorigin="anonymous"
复制代码
  1. window.addEventLisenter('error') 捕获资源加载失败的情况,但错误堆栈不完整
window.addEventListener('error', function(event) { 
    if(!e.message){
        // 网络资源加载错误
    }
}, true)
复制代码
  1. unhandledrejection
    1. Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection
    2. Promise 被 reject 且有 reject 处理器的时候,会触发 rejectionhandled
window.addEventListener("unhandledrejection", event => {
  console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
});
复制代码
  1. 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. Vue.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. 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. 请求拦截
    1. XHR重写拦截send和open
    2. fetch拦截
    3. axios请求/响应拦截器
  2. 日志拦截 console.XXX 重写
  3. 页面崩溃处理
window.addEventListener('load',()=>{
    sessionStorage.setTitem('page_exit','pending')
})
window.addEventListener('beforeunload',()=>{
    sessionStorage.setTitem('page_exit','true')
})

sessionStorage.getTitem('page_exit')!='true' // 页面崩溃
复制代码

三、错误上报方式

  1. xhr上报
  2. fetch
  3. img
var REPORT_URL = 'xxx'	//数据上报接口
var img = new Image; //创建img标签
img.onload = img.onerror = function(){	//img加载完成或加载src失败时的处理
    img = null;	//img置空,不会循环触发onload/onerror
};
img.src = REPORT_URL + Build._format(params); //数据上报接口地址拼接上报参数作为img的src

复制代码
  1. navigator.sendBeacon

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

window.addEventListener('unload', logData, false);
function logData() {
    navigator.sendBeacon("/log", analyticsData);
}
复制代码

四、Sentry实现原理

  • Init初始化,配置release和项目dsn等信息,然后将sentry对象挂载到全局对象上。
  • 重写window.onerror方法。

当代码在运行时发生错误时,js会抛出一个Error对象,这个error对象可以通过window.onerror方法获取。Sentry利用TraceKit对window.onerror方法进行了重写,对不同的浏览器差异性进行了统一封装处理。

  • 重写window.onunhandledrejection方法。

因为window.onerror事件无法获取promise未处理的异常,这时需要通过利用window.onunhandledrejection方法进行捕获异常进行上报。在这个方法里根据接收到的错误对象类型进行不同方式的处理。

  1. 如果接收到的是一个ErrorEvent对象,那么直接取出它的error属性即可,这就是对应的error对象。
  2. 如果接收到的是一个DOMError或者DOMException,那么直接解析出name和message即可,因为这类错误通常是使用了已经废弃的DOMAPI导致的,并不会附带上错误堆栈信息。
  3. 如果接收到的是一个标准的错误对象,不做处理
  4. 如果接收到的是一个普通的JavaScript对象
  5. 使用Ajax上传

当前端发生异常时,sentry会采集内容

  • 异常信息:抛出异常的 error 信息,Sentry 会自动捕捉。

  • 用户信息:用户的信息(登录名、id、level 等),所属机构信息,所属公司信息。

  • 行为信息:用户的操作过程,例如登陆后进入 xx 页面,点击 xx 按钮,Sentry 会自动捕捉。

  • 版本信息:运行项目的版本信息(测试、生产、灰度),以及版本号。

  • 设备信息:项目使用的平台,web 项目包括运行设备信息及浏览器版本信息。小程序项目包括运行手机的手机型号、系统版本及微信版本。

  • 时间戳:记录异常发生时间,Sentry 会自动捕捉。

  • 异常等级:Sentry将异常划分等级 "fatal", "error", "warning", "log", "info", "debug", "critical"

  • 平台信息:记录异常发生的项目。

最终会调用Fetch请求上报到对应的Sentry服务器上

五、Sentry配置

注意:如果是Vue项目,请不要在开发环境使用sentry。 因为Vue项目使用sentry时,需要配置@sentry/integrations。而@sentry/integrations是通过自定义Vue的errorHandler hook实现的,这将会停止激活Vue原始logError。 会导致Vue组件中的错误不被打印在控制台中。所以vue项目不要在开发环境使用sentry。

// main.js
// 配置参考
// https://docs.sentry.io/platforms/javascript/guides/vue/configuration/options/
Sentry.init({
  Vue,
  dsn: 'https://[email protected]/4',
  tracesSampleRate: 1.0, // 采样率
  environment: process.env.NODE_ENV, // 环境
  release: process.env.SENTRY_RELEASE, // 版本号
  allowUrls:[], // 是否只匹配url
  initialScope: {}, // 设置初始化数据
  trackComponents: true, // 如果要跟踪子组件的渲染信息
  hooks: ["mount", "update", "destroy"], // 可以知道记录的生命周期
  beforeSend: (event, hint) => event, // 在上报日志前预处理
  // 集成配置
  // 默认集成了
  // InboundFilters
  // FunctionToString
  // TryCatch
  // Breadcrumbs 这里集成了对console日志、dom操作、fetch请求、xhr请求、路由history、sentry上报的日志处理
  // GlobalHandlers 这里拦截了 onerror、onunhandledrejection 事件
  // LinkedErrors 链接报错层级,默认为5
  // UserAgent
  // Dedupe 重复数据删除
  // 修改系统集成  integrations: [new Sentry.Integrations.Breadcrumbs({ console: false })].
  integrations: [
    new BrowserTracing({
      routingInstrumentation: Sentry.vueRouterInstrumentation(router),
      // tracingOrigins: ['localhost', 'my-site-url.com', /^\//],
    }),
  ],
})

复制代码
集成插件SentryRRWeb

sentry还可以录制屏幕的信息,来更快的帮助开发者定位错误官方文档sentry的错误录制其实主要依靠rrweb这个包实现

  • 大概的流程就是首先保存一个一开始完整的dom的快照,然后为每一个节点生成一个唯一的id。
  • 当dom变化的时候通过MutationObserver来监听具体是哪个DOM的哪个属性发生了什么变化,保存起来。
  • 监听页面的鼠标和键盘的交互事件来记录位置和交互信息,最后用来模拟实现用户的操作。
  • 然后通过内部写的解析方法来解析(我理解的这一步是最难的)
  • 通过渲染dom,并用RAF来播放,就好像在看一个视频一样

在beforeSend等hook中添加过滤事件和自定义逻辑
 Sentry.init({
dsn:'https://[email protected]/5706930',
  beforeSend(event, hint) {
  // Check if it is an exception, and if so, show the report dialog
    if (event.exception) {
      Sentry.showReportDialog({ eventId: event.event_id });
    }
    return event;
  }
});

复制代码

六、Sentry 手动上报

  1. 设置全局属性
Sentry.setUser({ email: '[email protected]' }) // 设置全局变量
Sentry.configureScope((scope) => scope.clear()); // 清除所有全局变量
Sentry.configureScope((scope) => scope.setUser(null)); // 清除 user 变量

Sentry.setTag("page_locale", "de-at"); // 全局
复制代码
  1. 手动捕获

Sentry 不会自动捕获捕获的异常:如果编写了 try/catch 而不重新抛出异常,则该异常将永远不会出现在 Sentry

import * as Sentry from "@sentry/vue";

// 异常捕获
try {
  aFunctionThatMightFail();
} catch (err) {
  Sentry.captureException(err);
}
// 消息捕获
Sentry.captureMessage("Something went wrong");
// 设置级别
// 级别枚举 ["fatal", "error", "warning", "log", "info", "debug", "critical"]
Sentry.captureMessage("this is a debug message", "debug");
// 设置自定义内容
Sentry.captureMessage("Something went fundamentally wrong", {
  contexts: {
    text: {
      hahah: 22,
    },
  },
  level: Sentry.Severity.Info,
});
// 异常设置级别
Sentry.withScope(function(scope) {
  scope.setLevel("info");
  Sentry.captureException(new Error("custom error"));
});
// 在scope外,继承前面的外置级别
Sentry.captureException(new Error("custom error 2"));

// 设置其他参数 tags, extra, contexts, user, level, fingerprint
// https://docs.sentry.io/platforms/javascript/guides/vue/enriching-events/context/
// Object / Function
Sentry.captureException(new Error("something went wrong"), {
  tags: {
    section: "articles",
  },
});
Sentry.captureException(new Error("clean as never"), scope => {
    scope.clear();
    // 设置用户信息: 
    scope.setUser({ “email”: “[email protected]”})
    // 给事件定义标签: 
    scope.setTags({ ‘api’, ‘api/ list / get’})
    // 设置事件的严重性:
    scope.setLevel(‘error’)
    // 设置事件的分组规则: 
    scope.setFingerprint(['{{ default }}', url])
    // 设置附加数据: 
    scope.setExtra(‘data’, { request: { a: 1, b: 2 })

  return scope;
});

// 事件分组
Sentry.configureScope(scope => scope.setTransactionName("UserListView"));

// 事件自定义 
// 全局
Sentry.configureScope(function(scope) {
  scope.addEventProcessor(function(event, hint) {
    // TODO
    // returning null 会删除这个事件
    return event;
  });
});

// 局部事件
Sentry.withScope(function(scope) {
  scope.addEventProcessor(function(event, hint) {
    // TODO
    // returning null 会删除这个事件
    return event;
  });
  Sentry.captureMessage("Test");
});
复制代码
  1. 接口异常上报
// 正常axios sentry是会捕获的
axios.interceptors.response.use(function (response) {
    return response;
  }, function (error) {
    return Promise.reject(error);
  });

// 1. 手动上报网络异常
axios.interceptors.response.use(
  (response: AxiosResponse) => response,
  (error: AxiosError) => {
    Sentry.captureException(error)
    return Promise.reject(error)
  }
)

axios({
  url,
  method,
})
  .then(async (response: AxiosResponse) => {
    resolve(response.data)
  })
  .catch(async (error: AxiosError) => {
    Sentry.captureException(error)
    reject(error)
  })

// 2. 在异常处上报
Sentry.captureException(error, {
  contexts: {
    message: {
      url: error.response.config.baseURL + error.response.config.url,
      data: error.response.config.data,
      method: error.response.config.method,
      status: error.response.status,
      statusText: error.response.statusText,
      responseData: JSON.stringify(error.response.data),
    },
  },
});
复制代码
  1. 崩溃处理:如果程序意外关闭,可以在close事件处理
Sentry.close(2000).then(function() {
  // perform something after close
});
复制代码

七、性能监控

  • 首屏时间:页面开始展示的时间点 - 开始请求的时间点
  • 白屏时间:responseEnd - navigationStart
  • 页面总下载时间:loadEventEnd - navigationStart
  • DNS解析耗时:domainLookupEnd - domainLookupStart
  • TCP链接耗时:connectEnd - connectStart
  • 首包请求耗时:responseEnd - responseStart
  • dom解释耗时:domComplete - domInteractive
  • 用户可操作时间:domContentLoadedEventEnd - navigationStart

Sentry主要通过window.performance实现

// 收集性能信息
export const getPerformance = () => {
    if (!window.performance) return null;
    const {timing} = window.performance
    if ((getPerformance as any).__performance__) {
        return (getPerformance as any).__performance__;
    }
    const performance = {
        // 重定向耗时
        redirect: timing.redirectEnd - timing.redirectStart,
        // 白屏时间 html head script 执行开始时间
        whiteScreen: window.__STARTTIME__ - timing.navigationStart,
        // DOM 渲染耗时
        dom: timing.domComplete - timing.domLoading,
        // 页面加载耗时
        load: timing.loadEventEnd - timing.navigationStart,
        // 页面卸载耗时
        unload: timing.unloadEventEnd - timing.unloadEventStart,
        // 请求耗时
        request: timing.responseEnd - timing.requestStart,
        // 获取性能信息时当前时间
        time: new Date().getTime(),
    };
    (getPerformance as any).__performance__ = performance;
    return performance;
};
复制代码

参考文档

猜你喜欢

转载自juejin.im/post/7078980068302651400