sentry收集错误上报的原理

# 上报方式

# 自动收集

无感知收集

# 手动上报

 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",
 });
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

captureExceptioncaptureMessage的实现大概是这样的;

capturemessage在sentry上相当于多了一条日志,captureError相当于触发了一次报警。可以理解为一个warning,一个error。或者一个对应console.log一个是console.error

captureException

// 手动触发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,
     });
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

captureMessage

 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));
 }
1
2
3
4
5
6
7
8
9
10

# sentry请求上报原理

  • XHR通过重写(拦截)send和open
  • fetch通过拦截整个方法(需要讨论,reject的情况)
  • axios通过请求/响应拦截器

注意:sentry支持自动收集和手动收集两种错误收集方法,但是还不能捕捉到异步操作、接口请求中的错误,比如接口返回404、500等信息,此时我们可以通过Sentry.caputureException()进行主动上报。

# 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);
       };
   });
}
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

# 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;
      });
    };
  });
}
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

# 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);
        }
      };
    });
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 上传source map

# 三种方式

# webpack插件方式详解

下面主要讲解一下webpack的实现方式,其他的方式请自行查看。其实sentry主要是使用@sentry/webpack-plugin这个插件进行source map上传的。

 // 使用示例
 yarn add --dev @sentry/webpack-plugin

 const SentryWebpackPlugin = require("@sentry/webpack-plugin");
 module.exports = {
   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"],
       }),
     ],
   },
 };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 上传的原理【核心】

  1. 在webpack的afterEmit钩子,获取到打包之后的文件;

  2. 然后过滤得出文件类型是/\.js$|\.map$/结尾的就上传到sentry的服务器上;

  3. 然后删除的时候只删除/\.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}', 文件不存在, 由于生成错误它可能没有创建`);
         }
       });
     }
    
    
    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
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87

# 其他情况

但是@sentry/webpack-plugin上传完之后不支持删除source map,所以我们可以使用webpack-sentry-plugin (opens new window)这个插件,好像也是官方出的吧,是支持上传完之后删除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, // 版本
};
1
2
3
4
5
6
7
8
9
10

# 上报原理

# 通过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);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 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),
    }); }));
};
1
2
3
4
5
6
7
8
9
10

# 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;
}
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
42
43
44
45
46
47
48
49
50
51
52

# 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';">
1
2
3
4
5
6
7
8

# 拓展

比如我们的一些打点之类的需求,我们有些数据上报都是采用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的格式是因为体积小。

# 性能监控(建议阅读Web Vitals (opens new window)

image-20211112235240405

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

  • 首屏时间:页面开始展示的时间点 - 开始请求的时间点
  • 白屏时间: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;
};
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

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

# 相关知识扩展

# 录制

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

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

# 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
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
  • store和envelopes的不同我也不是很理解,按照表象来看的话就是上报的数据格式不一样,而且store的有响应返回envelopes的无响应返回
  • 具体可以参考:store (opens new window)envelopes (opens new window) 有了解的可以在评论区告诉我哦~

# Vue2.x中对errorHandle的处理src/core/util/error.js (opens new window)

/* @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
  }
}

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
  • 那么这个事件具体在哪里用的呢?
// 初始化数据的时候 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
}

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

# 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;
}

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

# 路由和组件性能

  • 其实这一块我没有看懂具体是用来干什么的,有知道的可以在评论区告诉我哦~
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);

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

# 初始化的流程

图示流程:

image-20211112235311054

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));

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110

# 扩展

# 名词解释

# 参考链接

  • https://juejin.cn/post/6957475955858210823
  • https://juejin.cn/post/7020555250608111646
上次更新: 2022/04/15, 05:41:29
×