Sentry系列一——React接入Sentry完整教程
Sentry介绍与使用
简介
Sentry 是跨平台的应用程序监控,专注于错误报告。GitHub、官方网站
Sentry 翻译过来是「哨兵」的意思,可以监控程序代码中出现的报错问题,生成issue,通过查看issue快速定位到问题发生的位置。
使用
- 安装
npm install --save @sentry/react @sentry/tracing- 配置
-
在入口文件index.tsx中添加配置
/* 报错监控 */if (process.env.NODE_ENV === 'production') {Sentry.init({dsn: 'https://aee8343989d243d682d58a5b0cd0b4d7@sentry-monitor.quickcep.com/2',integrations: [new Integrations.BrowserTracing()],maxBreadcrumbs: 50,environment: env.isProd ? 'prod' : 'dev',tracesSampleRate: 1.0,});}- dsn:Data Source Name错误报告发送的地址,获取Settings -> Projects -> Client Keys (DSN)
- integrations
- maxBreadcrumbs:这个变量控制应该捕获的面包屑总数。默认值为 100。
- environment:环境,在sentry中生成不同的环境
- tracesSampleRate:0-1,控制给定事务发送到 Sentry 的概率百分比。0 表示 0%,1 表示 100%
- 更多配置
-
在组件中增加ErrorBoundary组件,会自动将报错信息发送到Sentry,并回退UI
import {ErrorBoundary} from '@sentry/react'const DefaultErrorBoundary = ({children}) => {return <ErrorBoundary fallback={<><h2>请刷新页面重新操作</h2></>}>{children}</ErrorBoundary>}export default DefaultErrorBoundary;-
fallback (React.ReactNode or Function):当错误边界捕获错误时要呈现的 React 元素
-
beforeCapture:在将错误发送到 Sentry 之前调用的函数,允许将额外的标签或上下文添加到错误中
beforeCapture={(scope) => {scope.setTag('storeId', Number(ls.getItem('quick-storeId')));scope.setTag('userId', user?.staffInfo?.userId);scope.setTag('currentTime', moment().format('YYYY-MM-DD HH:mm:ss'));scope.setTag('pathname', window.location.pathname);}}
-
原理
Javascript代码发生错误后,Javascript引擎会抛出一个Error对象,并触发window.onerror事件,Sentry正是对window.onerror进行重写,实现错误监控的逻辑,并添加了很多信息帮助错误定位,并对错误进行滚跨浏览器的兼容等
-
window.onerror
捕获基本的js错误,但不能获取到资源加载失败的情况,必须使用window.addEventListener(‘error’)才行
-
Promise
Promise 如果reject没被catch的话,不能被window.onerror捕获,需要使用unhandledrejection捕获
window.addEventListener("unhandledrejection", event => {console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);}); -
Vue的Vue.config.errorHandler
Sentry对Vue.config.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; -
React的ErrorBoundary
/*** 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;// 获取到配置的propsvar _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 });});}; -
请求
Sentry不能捕获异步操作、接口请求中的错误,比如404、500等,此时需要通过Sentry.caputureException()主动上报
- XHR通过拦截send和open
- fetch通过拦截整个方法
- axios通过请求/响应拦截器
接入SourceMap
使用Sentry的webpack插件配置sourceMap,在构建的时候自动上传到Sentry,如果不上传SourceMap有些问题不好定位
Sentry一共提供了三种上传source map的方式
Sentry-cli
使用API上传
使用webpack plugins
- 安装@sentry/webpack-plugin
npm install --save-dev @sentry/webpack-plugin- 项目根目录添加.sentryclirc文件
[auth]token = ef5b031a00964f3c8f8ccbca07bb03c1d950be4b33f24ff
[defaults]url = https://sentry-monitor.xxx.com/org = xxxproject = xxx- token:sentry的Auth Token,settings -> account -> api -> auth-tokens
- url:sentry地址
- org:组织settings -> settings/organization-slug
- project:项目名称
- 配置webpack
const SentryWebpackPlugin = require("@sentry/webpack-plugin");
module.exports = { // other webpack configuration devtool: 'source-map', // 将 Webpack 插件设置为最后运行的插件 否则插件收到的 source maps 可能不是最终的 plugins: [ new SentryWebpackPlugin({ release:"v1.0.1", include: ".", ignore: ["node_modules", "webpack.config.js"], configFile: "sentry.properties", }), ],};- release:每次上传sourcemap是一次release的过程,如果init时没有配置release属性,sentry会自动生成一个随机数作为release版本;配置的话需要init和webpack配置中的一致
- include:指定路径让sentry-cli来检测有没有.map与.js文件,如果有就会上传到sentry
- ignore: 忽略文件夹或文件不要被检测
- configFile: 用来替代第二步的.sentryclirc文件 需要有对应的文件 默认不配置即可
在sentry上就会有对应项目的 Source Map 文件;(在settings -> projects -> xxx -> Source Maps)
- 测试
- @sentry/webpack-plugin原理 源码
在webpack的afterEmit钩子(在生成文件到output目录之后执行)中获取打包后的文件,过滤出文件类型为/\.js$|\.map$/结尾的文件上传到对应的sentry服务器
// upload sourcemapsapply(compiler) { // afterEmit在生成文件到output目录之后执行 compiler.hooks.afterEmit.tapAsync(this.name, async (compilation, callback) => { const files = this.getFiles(compilation); try { await this.createRelease(); await this.uploadFiles(files); console.info('\n\u001b[32mUpload successfully.\u001b[39m\n'); } catch (error) { // todo } callback(null); });} // 获取需要上传的文件 getFiles(compilation) { // 通过 compilation.assets 获取我们需要的文件信息,格式信息 // compilation.assets { // 'bundle.js': SizeOnlySource { _size: 212 }, // 'bundle.js.map': SizeOnlySource { _size: 162 } // } return Object.keys(compilation.assets) .map((name) => { if (this.isIncludeOrExclude(name)) { return { name, filePath: this.getAssetPath(compilation, name) }; } return null; }) .filter(Boolean);} // 获取文件的绝对路径 getAssetPath(compilation, name) { return path.join(compilation.getPath(compilation.compiler.outputPath), name.split('?')[0]); } // 获取文件的绝对路径 getAssetPath(compilation, name) { return path.join(compilation.getPath(compilation.compiler.outputPath), name.split('?')[0]); } // 上传文件 async uploadFile({ filePath, name }) { console.log(filePath); try { await request({ url: `${this.sentryReleaseUrl()}/${this.release}/files/`, // 上传的sentry路径 method: 'POST', auth: { bearer: this.apiKey, }, headers: {}, formData: { file: fs.createReadStream(filePath), name: this.filenameTransform(name), }, }); } catch (e) { console.error(`uploadFile failed ${filePath}`); } }webpack钩子
- done: 编译完成后
- beforeRun: 在编译器执行前
- run: 在编译器开始读取记录前执行
- emit: 在生成文件到output目录之前执行
- afterEmit: 在生成文件到output目录之后执行
- compilation: 创建compilation后
- beforeCompile: 在编译前
- compile: 创建compilation前
- make:编译完成前
对比
与其他的npm下载量对比
- 100%开源
- 私有化部署
- 支持大多数编程语言