Sentry原理--收集错误、上报

13,954 阅读11分钟

我们平常收集错误,大部分人、公司都采用的Sentry来收集,当然也有好多公司也有自研的前端监控。趁着最近我们组刚好要接入Sentry,并且要根据自己的调研来做一次分享。所以就总结出了下面的内容。分享给各位大佬们。

1. 上报方式

  1. 自动收集

    • 无感知收集
  2. 手动上报,手动上报又分为:

     Sentry.captureException('captureException');
     Sentry.captureMessage('captureMessage');
     // 设置用户信息:
     scope.setUser({ “email”: “xx@xx.cn”})
     // 给事件定义标签:
     scope.setTags({ ‘api’, ‘api/ list / get’})
     // 设置事件的严重性:
     scope.setLevel(‘error’)
     // 设置附加数据:
     scope.setExtra(‘data’, { request: { a: 1, b: 2 })
     // 添加一个面包屑
     Sentry.addBreadcrumb({
         category: "auth",
         message: "Authenticated user ",
         level: Sentry.Severity.Info,
     });
     // 添加一个scope 标题??? 当前事务名称用于对Performance产品中的事务进行分组 ,并用错误点注释错误事件。
     Sentry.configureScope(scope => scope.setTransactionName("UserListView"));
     // 局部
     Sentry.withScope(function (scope) {
         scope.setTag("my-tag", "my value");
         scope.setLevel("warning");
         // will be tagged with my-tag="my value"
         Sentry.captureException(new Error("my error"));
     });
     // 设置上下文
     Sentry.setContext("character", {
         name: "Mighty Fighter",
         age: 19,
         attack_type: "melee",
     });
    

    captureExceptioncaptureMessage的实现大概是这样的

    // 手动触发throw一个报错,然后把报错的信息重置为用户传入的,然后调用callOnHub来触发一个上报
    function captureException(exception, captureContext) {
         var syntheticException;
         try {
             throw new Error('Sentry syntheticException');
         }
         catch (exception) {
             syntheticException = exception;
         }
         return callOnHub('captureException', exception, {
             captureContext: captureContext,
             originalException: exception,
             syntheticException: syntheticException,
         });
     }
    
     function captureMessage(message, captureContext) {
         var syntheticException;
         try {
             throw new Error(message);
         }
         catch (exception) {
             syntheticException = exception;
         }
         return callOnHub('captureMessage', message, level, tslib_1.__assign({ originalException: message, syntheticException: syntheticException }, context));
     }
    

    2. 上传source map

    Sentry上传source map大概分为三种方式吧

    • 官方cli(sentry-cli)
    • 调用 HTTP API
    • webpack插件 下面我主要讲解一下webpack的实现方式,其他的方式请自行查看。其实sentry主要是使用@sentry/webpack-plugin这个插件进行source map上传的。
     // 使用示例
     yarn add --dev @sentry/webpack-plugin
    
     const SentryWebpackPlugin = require("@sentry/webpack-plugin");
     module.exports = {
       // other configuration
       configureWebpack: {
         plugins: [
           new SentryWebpackPlugin({
             // sentry-cli configuration
             authToken: process.env.SENTRY_AUTH_TOKEN,
             org: "example-org",
             project: "example-project",
             release: process.env.SENTRY_RELEASE,
             // webpack specific configuration
             include: ".",
             ignore: ["node_modules", "webpack.config.js"],
           }),
         ],
       },
     };
    

    2.1 @sentry/webpack-plugin上传的原理

    大概的一个过程其实就是在webpack的afterEmit钩子,获取到打包之后的文件。然后过滤得出文件类型是/\.js$|\.map$/结尾的就上传到sentry的服务器上。然后删除的时候只删除/\.map$/结尾的文件。

     // upload sourcemaps
     apply(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]);
     }
     // 上传文件
     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}`);
       }
     }
    
     // 删除 sourcemaps
     sentryDel(compiler) {
       compiler.hooks.done.tapAsync(this.name, async (stats, callback) => {
         console.log('Whether to delete SourceMaps:', this.isDeleteSourceMaps);
         if (this.isDeleteSourceMaps) {
           await this.deleteFiles(stats);
           console.info('\n\u001b[32mDelete SourceMaps done.\u001b[39m\n');
         }
         callback(null);
       });
     }
     // 删除文件
     async deleteFiles(stats) {
       console.log();
       console.info('\u001b[33mStarting delete SourceMaps...\u001b[39m\n');
    
       Object.keys(stats.compilation.assets)
         .filter((name) => this.deleteRegex.test(name))
         .forEach((name) => {
         const filePath = this.getAssetPath(stats.compilation, name);
         if (filePath) {
           console.log(filePath);
           fs.unlinkSync(filePath);
         } else {
           console.warn(`bss-plugin-sentry: 不能删除 '${name}', 文件不存在, 由于生成错误它可能没有创建`);
         }
       });
     }
    

    但是@sentry/webpack-plugin上传完之后不支持删除source map,所以我们可以使用webpack-sentry-plugin这个插件,好像也是官方出的吧,是支持上传完之后删除source map的。使用的话可以参考文档。

    export const SentryPluginConfig = {
        deleteAfterCompile: true, // 上传完 source-map 文件后要不要删除当前目录下的source-map 文件
        // Sentry options are required
        organization: '', // 组名
        project: '', // 当前项目名
        baseSentryURL: 'https://xxx', // 默认是 https://sentry.io/api/0,也即是上传到 sentry 官网上去,如果是自己搭建的 sentry 系统,那把sentry.io替换成你自己的sentry系统域名就行。
        apiKey: '',
        // Release version name/hash is required
        release: SentryRelease, // 版本
    };
    

3. 原理

3.1 异常捕获

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

  • 全局捕获代码集中,易于管理;
  • 单点捕获作为补充,对某些特殊情况进行捕获,但分散,不利于管理。
  1. 全局捕获
    • 通过全局的接口,将捕获代码集中写在一个地方,可以利用的接口有:
      • window.addEventListener(‘error’) / window.addEventListener(“unhandledrejection”) / document.addEventListener(‘click’)
    • 框架级别的全局监听,
      • 例如aixos中使用interceptor进行拦截,
      • vue、react都有自己的错误采集接口
    • 通过对全局函数进行封装包裹,实现在在调用该函数时自动捕获异常
    • 对实例方法重写(Patch),在原有功能基础上包裹一层,
      • 例如对setTimeout进行重写,在使用方法不变的情况下也可以异常捕获
  2. 全局捕获
    • 在业务代码中对单个代码块进行包裹,或在逻辑流程中打点,实现有针对性的异常捕获:
    • try…catch
    • 专门写一个函数来收集异常信息,在异常发生时,调用该函数
    • 专门写一个函数来包裹其他函数,得到一个新函数,该新函数运行结果和原函数一模一样,只是在发生异常时可以捕获异常

3.2 详细讲解

  • window.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对象(对象

大家可以看到 JS 错误监控里面有个 window.onEerror,
又用了 window.addEventLisenter'error'),
其实两者并不能互相代替。
window.onError 是一个标准的错误捕获接口,它可以拿到对应的这种 JS 错误;
window.addEventLisenter'error')也可以捕获到错误,
但是它拿到的 JS 报错堆栈往往是不完整的。
同时 window.onError 无法获取到资源加载失败的一个情况,
必须使用 window.addEventLisenter'error')来捕获资源加载失败的情况。
  • Promise
    • Promise的话主要是unhandledrejection事件,也就是未被catchreject状态的promise
window.addEventListener("unhandledrejection", event => {
  console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
});
  • 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);
}
  • 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;
  • ReactErrorBoundary
    • 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>
  • 那么Sentry是怎么实现的呢?
// 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 });
  });
};
  • 请求
    • XHR通过重写(拦截)send和open
    • fetch通过拦截整个方法(需要讨论,reject的情况)
    • axios通过请求/响应拦截器 注意:sentry支持自动收集和手动收集两种错误收集方法,但是还不能捕捉到异步操作、接口请求中的错误,比如接口返回404、500等信息,此时我们可以通过Sentry.caputureException()进行主动上报。
    1. xhr的实现
    function fill(source, name, replacementFactory) {
     var original = source[name];
     var wrapped = replacementFactory(original);
     source[name] = wrapped;
    }
    // xhr
    function instrumentXHR(): void {
       // 保存真实的xhr的原型
       const xhrproto = XMLHttpRequest.prototype;
       // 拦截open方法
       fill(xhrproto, 'open', function (originalOpen: () => void): () => void {
           return function (this: SentryWrappedXMLHttpRequest, ...args: any[]): void {
             const xhr = this;
             const onreadystatechangeHandler = function (): void {
               if (xhr.readyState === 4) {
                 if (xhr.__sentry_xhr__) {
                     xhr.__sentry_xhr__.status_code = xhr.status;
                   }
                 // // 上报sentry
                 triggerHandlers('xhr', {
                   args,
                   endTimestamp: Date.now(),
                   startTimestamp: Date.now(),
                   xhr,
                 });
               }
             };
    
         if ('onreadystatechange' in xhr && typeof xhr.onreadystatechange === 'function') {
           // 拦截onreadystatechange方法
           fill(xhr, 'onreadystatechange', function (original: WrappedFunction): Function {
             return function (...readyStateArgs: any[]): void {
               onreadystatechangeHandler();
               // 返回原来的方法
               return original.apply(xhr, readyStateArgs);
             };
           });
         } else {
           xhr.addEventListener('readystatechange', onreadystatechangeHandler);
         }
         // 调用原来的方法
         return originalOpen.apply(xhr, args);
       };
    });
         // fill其实就是拦截的一个封装originalSend就是原来的send方法
       fill(xhrproto, 'send', function (originalSend: () => void): () => void {
           return function (this: SentryWrappedXMLHttpRequest, ...args: any[]): void {
             // 上报sentry
             triggerHandlers('xhr', {
               args,
               startTimestamp: Date.now(),
               xhr: this,
             });
             // 返回原来方法
             return originalSend.apply(this, args);
           };
       });
    }
    
    • Fetch
    // 重写fetch
    function instrumentFetch() {
      if (!supportsNativeFetch()) {
        return;
      }
      fill(global$2, 'fetch', function (originalFetch) {
        return function () {
          var args = [];
          for (var _i = 0; _i < arguments.length; _i++) {
            args[_i] = arguments[_i];
          }
          var handlerData = {
            args: args,
            fetchData: {
              method: getFetchMethod(args),
              url: getFetchUrl(args),
            },
            startTimestamp: Date.now(),
          };
          triggerHandlers('fetch', __assign({}, handlerData));
          // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
          return originalFetch.apply(global$2, args).then(function (response) {
            triggerHandlers('fetch', __assign(__assign({}, handlerData), { endTimestamp: Date.now(), response: response }));
            return response;
          }, function (error) {
            triggerHandlers('fetch', __assign(__assign({}, handlerData), { endTimestamp: Date.now(), error: error }));
            throw error;
          });
        };
      });
    }
    
    • console.xxx
    function instrumentConsole() {
      if (!('console' in global$2)) {
        return;
      }
      ['debug', 'info', 'warn', 'error', 'log', 'assert'].forEach(function (level) {
        if (!(level in global$2.console)) {
          return;
        }
        fill(global$2.console, level, function (originalConsoleLevel) {
          return function () {
            var args = [];
            for (var _i = 0; _i < arguments.length; _i++) {
              args[_i] = arguments[_i];
            }
            // 上报sentry
            triggerHandlers('console', { args: args, level: level });
            // this fails for some browsers. :(
            if (originalConsoleLevel) {
              Function.prototype.apply.call(originalConsoleLevel, global$2.console, args);
            }
          };
        });
      });
    }
    

4. 性能监控(建议阅读Web Vitals

0f507f65-b6ab-484a-bb63-9673a02e1c85.png 0f7e810b-3b2c-43ba-9f39-c85b164a2998.png

4.1 关键数据获取(仅供参考)

  • 首屏时间:页面开始展示的时间点 - 开始请求的时间点
  • 白屏时间:responseEnd - navigationStart
  • 页面总下载时间:loadEventEnd - navigationStart
  • DNS解析耗时:domainLookupEnd - domainLookupStart
  • TCP链接耗时:connectEnd - connectStart
  • 首包请求耗时:responseEnd - responseStart
  • dom解释耗时:domComplete - domInteractive
  • 用户可操作时间:domContentLoadedEventEnd - navigationStart 注意: 由于window.preformance.timing是一个在不同阶段,被不停修正的一个参数对象,所以,建议在window.onload中进行性能数据读取和上报。
// 收集性能信息
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;
};

sentry的性能监控其实主要依靠web-vitals这个包,主要依靠PerformanceObserver()实现

5. 上报方式

  • 通过xhr上报(Sentry采用的方式)
    • // 判断使用什么方式上报,首先判断支不支持fetch,不支持的话就使用xhr。
BrowserBackend.prototype._setupTransport = function () {
        if (!this._options.dsn) {
                // We return the noop transport here in case there is no Dsn.
                // 没有设置dsn,调用BaseBackend.prototype._setupTransport 返回空函数
                return _super.prototype._setupTransport.call(this);
        }
        var transportOptions = __assign({}, this._options.transportOptions, { dsn: this._options.dsn });
        if (this._options.transport) {
                return new this._options.transport(transportOptions);
        }
        // 支持Fetch则返回 FetchTransport 实例,否则返回 XHRTransport实例,
        // 这两个构造函数具体代码在开头已有提到。
        if (supportsFetch()) {
                return new FetchTransport(transportOptions);
        }
        return new XHRTransport(transportOptions);
};
  • fetch方式发送请求
FetchTransport.prototype.sendEvent = function (event) {
    var defaultOptions = {
            body: JSON.stringify(event),
            method: 'POST',
            referrerPolicy: (supportsReferrerPolicy() ? 'origin' : ''),
    };
    return this._buffer.add(global$2.fetch(this.url, defaultOptions).then(function (response) { return ({
            status: exports.Status.fromHttpCode(response.status),
    }); }));
};
  • xhr发送请求
class XHRTransport extends BaseTransport {
  sendEvent(event) {
    return this._sendRequest(eventToSentryRequest(event, this._api), event);
  }
  _sendRequest(sentryRequest, originalPayload) {
    // 如果超过限制的数量%就不发送
    if (this._isRateLimited(sentryRequest.type)) {
      return Promise.reject({
        event: originalPayload,
        type: sentryRequest.type,
        reason: `Transport locked till ${this._disabledUntil(sentryRequest.type)} due to too many requests.`,
        status: 429,
      });
    }
    return this._buffer.add(new SyncPromise((resolve, reject) => {
      const request = new XMLHttpRequest();
      request.onreadystatechange = () => {
        if (request.readyState === 4) {
          const headers = {
            'x-sentry-rate-limits': request.getResponseHeader('X-Sentry-Rate-Limits'),
            'retry-after': request.getResponseHeader('Retry-After'),
          };
          this._handleResponse({ requestType: sentryRequest.type, response: request, headers, resolve, reject });
        }
      };
      request.open('POST', sentryRequest.url);
      for (const header in this.options.headers) {
        if (this.options.headers.hasOwnProperty(header)) {
          request.setRequestHeader(header, this.options.headers[header]);
        }
      }
      request.send(sentryRequest.body);
    }));
  }
}

this._buffer.add(task) {
  if (!this.isReady()) {
    return SyncPromise.reject(new SentryError('Not adding Promise due to buffer limit reached.'));
  }
  if (this._buffer.indexOf(task) === -1) {
    this._buffer.push(task);
  }
  task
    .then(() => this.remove(task))
    	.then(null, () => this.remove(task).then(null, () => {
      // We have to add this catch here otherwise we have an unhandledPromiseReject      
			// because it's a new Promise chain      
  		})
		);
  return task;
}
  • new Image()上报
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
// 或者
<img src="images/bg.png" onerror="javascript:this.src='images/bgError.png';">
  • 拓展 比如我们的一些打点之类的需求,我们有些数据上报都是采用new Image()的方式去上报的,那么真的就只是随便一个资源就可以吗?
  1. 为什么不能用请求其他的文件资源(js/css/ttf)的方式进行上报?

    • 这和浏览器的特性有关。通常,创建资源节点后只有将对象注入到浏览器DOM树后,浏览器才会实际发送资源请求。反复操作DOM不仅会引发性能问题,而且载入js/css资源还会阻塞页面渲染,影响用户体验。
    • 但是图片请求例外。图片打点不仅不用插入DOM,只要在js中new出Image对象就能发起请求,而且还没有阻塞问题,在没有js的浏览器环境中也能通过img标签正常打点,这是其他类型的资源请求所做不到的。所以,在所有通过请求文件资源进行打点的方案中,使用图片是最好的解决方案。
  2. 同样都是图片,上报时选用了1x1的透明GIF,而不是其他的PNG/JEPG/BMP文件?

    • 原因其实不太好想,需要分开来看。首先,1x1像素是最小的合法图片。而且,因为是通过图片打点,所以图片最好是透明的,这样一来不会影响页面本身展示效果,二者表示图片透明只要使用一个二进制位标记图片是透明色即可,不用存储色彩空间数据,可以节约体积。
    • 因为需要透明色,所以可以直接排除JEPG(BMP32格式可以支持透明色)。然后还剩下BMP、PNG和GIF,但是为什么会选GIF呢?因为体积小!同样的响应,GIF可以比BMP节约41%的流量,比PNG节约35%的流量。 总结: 使用img元素发送不会影响页面的展示、性能和体验这些,就是不会带来副作用,还有一个使用GIF的格式是因为体积小。

6. more

6.1 录制

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

6.2 sentry获取url的方式(通过dsn)

  • 这里我把sentry获取上报的url的代码给精简逻辑拼接起来了,这样也方便大家理解。
// 匹配DSN的正则
const DSN_REGEX = /^(?:(\w+):)\/\/(?:(\w+)(?::(\w+))?@)([\w.-]+)(?::(\d+))?\/(.+)/;
const dsn = 'https://3543756743567437634345@sentry.com/430'; // 假的dsn
// 获取到正则匹配到的分组信息
const match = DSN_REGEX.exec(dsn);
const [protocol, publicKey, pass = '', host, port = '', lastPath] = match.slice(1);
const obj = {
  'protocol': protocol,
  'publicKey': publicKey,
  'pass': pass,
  'host': host,
  'port': port,
  'lastPath': lastPath,
  'projectId': lastPath.split('/')[0],
}
// 打印信息
console.log('obj', JSON.stringify(obj));
// {
//     protocol: "https",
//     publicKey: "3543756743567437634345",
//     pass: "",
//     host: "sentry.com",
//     port: "",
//     lastPath: "430",
//     projectId: "430",
// }

function getBaseApiEndpoint() {
  const dsn = obj;
  const protocol = dsn.protocol ? `${dsn.protocol}:` : '';
  const port = dsn.port ? `:${dsn.port}` : '';
  return `${protocol}//${dsn.host}${port}${dsn.path ? `/${dsn.path}` : ''}/api/`;
}
console.log('getBaseApiEndpoint', getBaseApiEndpoint());
// https://sentry.com/api/

// 不同的事件类型发送到不同的域名
function getIngestEndpoint(target) {
  const eventType = event.type || 'event';
  const useEnvelope = eventType === 'transaction';
  target = useEnvelope ? 'envelope' : 'store';
  const base = this.getBaseApiEndpoint();
  const dsn = obj;
  return `${base}${dsn.projectId}/${target}/`;
}
console.log('getIngestEndpoint', getIngestEndpoint());
// https://sentry.com/api/430/store/

function getStoreOrEnvelopeEndpointWithUrlEncodedAuth() {
  return `${this.getIngestEndpoint()}?${this.encodedAuth()}`;
}
// 获取到认证的信息,可以理解为一个凭证
function encodedAuth() {
  const dsn = obj;
  const auth = {
    // We send only the minimum set of required information. See
    // https://github.com/getsentry/sentry-javascript/issues/2572.
    sentry_key: dsn.publicKey,
    sentry_version: SENTRY_API_VERSION = 7, // 写死的7,不同的版本不一样
  };
  return urlEncode(auth);
}

// 格式化url
function urlEncode(object) {
  return Object.keys(object)
    .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(object[key])}`)
    .join('&');
}
// 最后获取到的完整的url
console.log('getStoreOrEnvelopeEndpointWithUrlEncodedAuth', getStoreOrEnvelopeEndpointWithUrlEncodedAuth());
// https://sentry.com/api/430/store/?sentry_key=3543756743567437634345&sentry_version=7
  • store和envelopes的不同我也不是很理解,按照表象来看的话就是上报的数据格式不一样,而且store的有响应返回envelopes的无响应返回
  • 具体可以参考:storeenvelopes 有了解的可以在评论区告诉我哦~

6.3 Vue2.x中对errorHandle的处理src/core/util/error.js

/* @flow */
import config from '../config'
import { warn } from './debug'
import { inBrowser, inWeex } from './env'
import { isPromise } from 'shared/util'
import { pushTarget, popTarget } from '../observer/dep'

// 初始化
function initGlobalAPI (Vue) {
    // config
    var configDef = {};
    const config = {
    	errorHandler: null,
    	warnHandler: null,
    };
    configDef.get = function () { return config; };
    {
      configDef.set = function () {
        warn(
          'Do not replace the Vue.config object, set individual fields instead.'
        );
      };
    }
    // 定义全局的Vue.prototype.config的初始值
    Object.defineProperty(Vue, 'config', configDef);
}

export function handleError (err: Error, vm: any, info: string) {
  // Deactivate deps tracking while processing error handler to avoid possible infinite rendering.
  // See: https://github.com/vuejs/vuex/issues/1505
  pushTarget()
  try {
    if (vm) {
      let cur = vm
      while ((cur = cur.$parent)) {
        // 获取全局以及所有子级的errorCaptured hook
        const hooks = cur.$options.errorCaptured
        if (hooks) {
          for (let i = 0; i < hooks.length; i++) {
            try {
              // 调用errorCaptured事件
              // 钩子可以返回 false 以阻止该错误继续向上传播
              const capture = hooks[i].call(cur, err, vm, info) === false
              if (capture) return
            } catch (e) {
              // 如果这个hook发生了错误也会上报
              globalHandleError(e, cur, 'errorCaptured hook')
            }
          }
        }
      }
    }
    // errorHandle上报
    globalHandleError(err, vm, info)
  } finally {
    popTarget()
  }
}

function globalHandleError (err, vm, info) {
  if (config.errorHandler) {
    try {
      return config.errorHandler.call(null, err, vm, info)
    } catch (e) {
      // if the user intentionally throws the original error in the handler,
      // do not log it twice
      // 防止两次上报
      if (e !== err) {
        logError(e, null, 'config.errorHandler')
      }
    }
  }
  logError(err, vm, info)
}

// 判断是否输出到控制台
function logError (err, vm, info) {
  if (process.env.NODE_ENV !== 'production') {
    warn(`Error in ${info}: "${err.toString()}"`, vm)
  }
  /* istanbul ignore else */
  if ((inBrowser || inWeex) && typeof console !== 'undefined') {
    console.error(err)
  } else {
    throw err
  }
}
  • 那么这个事件具体在哪里用的呢?
// 初始化数据的时候 src/core/instance/state.js
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
}
export function getData (data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}

// 执行nextTick回调的时候 src/core/util/next-tick.js
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
}

// 执行指令的一些声明周期的时候 src/core/vdom/modules/directives.js
callHook(dir, 'bind', vnode, oldVnode)
callHook(dir, 'update', vnode, oldVnode)
callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
function callHook (dir, hook, vnode, oldVnode, isDestroy) {
  const fn = dir.def && dir.def[hook]
  if (fn) {
    try {
      fn(vnode.elm, dir, vnode, oldVnode, isDestroy)
    } catch (e) {
      handleError(e, vnode.context, `directive ${dir.name} ${hook} hook`)
    }
  }
}

// watch中通过配置 src/core/observer/watcher.js
get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    // 在这个条件下才会上报
    // 这个我没有在代码中和api中有看到配置user,有了解的可以在评论区告诉我哦~
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    // 从这里我们也可以看出vue的deep watch是怎么实现的
    // 其实就是监听这个对象的所有元素
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}

6.4 sentry中检测是否支持fetch

 // 是否支持fetch 通过在window中能不能找到fetch来判断
 function supportsFetch() {
   if (!('fetch' in getGlobalObject())) {
     return false;
   }
   try {
     // 如果支持这些属性,就说明支持fetch
     // MDN https://developer.mozilla.org/zh-CN/docs/Web/API/Headers
     new Headers(); //  Fetch API 的 Headers 接口呈现了对一次请求的响应数据。
     new Request(''); //  Fetch API 的 Request 接口呈现了对一次请求的响应数据
     new Response(); //  Fetch API 的 Response 接口呈现了对一次请求的响应数据
     return true;
   }
   catch (e) {
     return false;
   }
 }
// 是否是原生的就是浏览器的fetch
function isNativeFetch(func) {
  return func && /^function fetch\(\)\s+\{\s+\[native code\]\s+\}$/.test(func.toString());
}
// 是否支持原生的fetch而不是被代理的或者prolyFill对的
function supportsNativeFetch() {
  if (!supportsFetch()) {
    return false;
  }
  const global = getGlobalObject();
  // Fast path to avoid DOM I/O
  // eslint-disable-next-line @typescript-eslint/unbound-method
  if (isNativeFetch(global.fetch)) {
    return true;
  }
  // window.fetch is implemented, but is polyfilled or already wrapped (e.g: by a chrome extension)
  // so create a "pure" iframe to see if that has native fetch
  let result = false;
  const doc = global.document;
  // eslint-disable-next-line deprecation/deprecation
  // 如果当前的fetch不是原生的,那么就通过创建一个iframe来判断支不支持fetch
  if (doc && typeof doc.createElement === `function`) {
    try {
      const sandbox = doc.createElement('iframe');
      sandbox.hidden = true;
      doc.head.appendChild(sandbox);
      // 查看window上有没有fetch方法
      if (sandbox.contentWindow && sandbox.contentWindow.fetch) {
        // eslint-disable-next-line @typescript-eslint/unbound-method
        result = isNativeFetch(sandbox.contentWindow.fetch);
      }
      doc.head.removeChild(sandbox);
    }
    catch (err) {
      logger.warn('Could not create sandbox iframe for pure fetch check, bailing to window.fetch: ', err);
    }
  }
  return result;
}

6.5 路由和组件性能

  • 其实这一块我没有看懂具体是用来干什么的,有知道的可以在评论区告诉我哦~
import { matchPath } from 'react-router-dom';
const routes = [{ path: '/' }, { path: '/report' }];
// https://docs.sentry.io/platforms/javascript/guides/react/configuration/integrations/react-router/
Sentry.init({
  dsn: 'xxx',
  integrations: [
    new Integrations.BrowserTracing({
      // Can also use reactRouterV4Instrumentation
      // 上报的时候可以上报具体的路由地址
      routingInstrumentation: Sentry.reactRouterV5Instrumentation(HashHistory, routes, matchPath),
    }),
  ],
})

// https://docs.sentry.io/platforms/javascript/guides/react/components/profiler/
// withProfiler高阶组件用于检测App组件
class App extends React.Component {
  render() {
    return (
        <NestedComponent someProp={2} />
    );
  }
}

export default Sentry.withProfiler(App);

6.6 扩展

6.7 很大概的一个流程

  • 按照我很笼统的一个理解 2e90f688ba8347ba9d66ffe82150766e1487.png

6.8 初始化的流程

import * as Sentry from '@ipalfish/sentry/react';

Sentry.init({
  dsn: SENTRY_KEY || DSN,
});

// Sentry.init
function init(options) {
    options._metadata = options._metadata || {};
    if (options._metadata.sdk === undefined) {
        options._metadata.sdk = {
            name: 'sentry.javascript.react',
            packages: [
                {
                    name: 'npm:@sentry/react',
                    version: browser_1.SDK_VERSION,
                },
            ],
            version: browser_1.SDK_VERSION,
        };
    }
    browser_1.init(options);
}

// browser_1.init
export function init(options) {
    if (options === void 0) { options = {}; }
    if (options.defaultIntegrations === undefined) {
        options.defaultIntegrations = defaultIntegrations;
    }
    if (options.release === undefined) {
        var window_1 = getGlobalObject();
        // This supports the variable that sentry-webpack-plugin injects
        if (window_1.SENTRY_RELEASE && window_1.SENTRY_RELEASE.id) {
            options.release = window_1.SENTRY_RELEASE.id;
        }
    }
    if (options.autoSessionTracking === undefined) {
        options.autoSessionTracking = true;
    }
    initAndBind(BrowserClient, options);
    if (options.autoSessionTracking) {
        startSessionTracking();
    }
}

// 默认集成
const defaultIntegrations = [
  new InboundFilters(),
  new FunctionToString(),
  new TryCatch(), // setTimeout, setInterval, requestAnimationFrame, xhr
  new Breadcrumbs(), // console, dom, xhr, fetch, history
  new GlobalHandlers(), // window.onerror,unhandledrejection
  new LinkedErrors(),
  new UserAgent(),
];

function initAndBind(clientClass, options) {
    if (options.debug === true) {
        utils_1.logger.enable();
    }
    var hub = hub_1.getCurrentHub();
    var client = new clientClass(options);
    hub.bindClient(client);
}

var BrowserClient = /** @class */ (function (_super) {
	// `BrowserClient` 继承自`BaseClient`
	__extends(BrowserClient, _super);

	function BrowserClient(options) {
		if (options === void 0) { options = {}; }
		// 把`BrowserBackend`,`options`传参给`BaseClient`调用。
		return _super.call(this, BrowserBackend, options) || this;
	}
	return BrowserClient;
}(BaseClient));

class BaseClient {
      constructor(backendClass, options) {
          this._backend = new backendClass(options); // 获取上报的方式
          this._options = options;
          if (options.dsn) {
              this._dsn = new Dsn(options.dsn);
          }
      }
}

var BrowserBackend = /** @class */ (function (_super) {
    __extends(BrowserBackend, _super);
	/**
	 * 设置请求
	 */
	BrowserBackend.prototype._setupTransport = function () {
		if (!this._options.dsn) {
			// 没有设置dsn,调用BaseBackend.prototype._setupTransport 返回空函数noop
			return _super.prototype._setupTransport.call(this);
		}
		var transportOptions = __assign({}, this._options.transportOptions, { dsn: this._options.dsn });
		if (this._options.transport) {
			return new this._options.transport(transportOptions);
		}
		// 支持Fetch则返回 FetchTransport 实例,否则返回 XHRTransport实例,
		if (supportsFetch()) {
			return new FetchTransport(transportOptions);
		}
		return new XHRTransport(transportOptions);
	};
}(BaseBackend));

7. 名词解释