The principle of Sentry and how to use custom reporting

The last article talks about how to connect the front-end project to the sentry monitoring platform?

This article mainly summarizes the principle of Sentry and the use of custom reporting

1. Common exception types in the front end

  1. ECMAScript Execeptions

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

  1. DOMException

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

  1. Resource loading error cdn, img, script, link, audio, video, iframe...
  • performance.getEntries to find unloaded resources
  • Script adds crossorigin attribute / server supports cross-domain
  1. Promise error

Second, the front-end common exception capture method

  1. try catch is synchronous/locally intrusive, cannot handle syntax errors and async
// 能捕获
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 can globally capture / only capture runtime errors, not resource loading errors
// 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.addEventListener('error') catches resource load failures, but the error stack is incomplete
window.addEventListener('error', function(event) { 
    if(!e.message){
        // 网络资源加载错误
    }
}, true)
复制代码
  1. unhandledrejection
    1. PromiseWhen rejected and there is no reject handler, it will triggerunhandledrejection
    2. PromiseWhen rejected and there is a reject handler, it will triggerrejectionhandled
window.addEventListener("unhandledrejection", event => {
  console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
});
复制代码
  1. setTimeout、setInterval、requestAnimationFrameetc., use method interception to rewrite
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

ErrorBoundaryDefinition: **If a class component defines either (or both) of these two lifecycle methods, static getDerivedStateFromError() or ****componentDidCatch()** , then it becomes an error border . When an error is thrown, use static getDerivedStateFromError()Render Alternate UI, use componentDidCatch()Print Error Message

// 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. request interception
    1. XHR rewrite intercepts send and open
    2. fetch interception
    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;
  }
});

复制代码

6. Sentry manual report

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

Sentry.setTag("page_locale", "de-at"); // 全局
复制代码
  1. Manual capture

Sentry does not automatically catch caught exceptions : if a try/catch is written without rethrowing the exception, the exception will never appear in 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. Interface exception report
// 正常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. Crash handling: If the program closes unexpectedly, it can be handled in the close event
Sentry.close(2000).then(function() {
  // perform something after close
});
复制代码

Seven, performance monitoring

  • Time above the fold: the time when the page starts showing - the time when the request starts
  • White screen time:responseEnd - navigationStart
  • Total page download time:loadEventEnd - navigationStart
  • Time-consuming DNS resolution:domainLookupEnd - domainLookupStart
  • TCP connection time:connectEnd - connectStart
  • Time-consuming for the first packet request:responseEnd - responseStart
  • dom explains the time-consuming:domComplete - domInteractive
  • User operation time:domContentLoadedEventEnd - navigationStart

Sentry is mainly implemented through 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;
};
复制代码

Reference documentation

Guess you like

Origin juejin.im/post/7078980068302651400
Recommended