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",
});
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
而captureException
和captureMessage
的实现大概是这样的;
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,
});
}
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));
}
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);
};
});
}
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;
});
};
});
}
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);
}
};
});
});
}
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"],
}),
],
},
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 上传的原理【核心】
在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}', 文件不存在, 由于生成错误它可能没有创建`); } }); }
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, // 版本
};
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);
};
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),
}); }));
};
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;
}
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';">
2
3
4
5
6
7
8
# 拓展
比如我们的一些打点之类的需求,我们有些数据上报都是采用new Image()
的方式去上报的,那么真的就只是随便一个资源就可以吗?
- 为什么不能用请求其他的文件资源
(js/css/ttf)
的方式进行上报?- 这和浏览器的特性有关。通常,创建资源节点后只有将对象注入到浏览器DOM树后,浏览器才会实际发送资源请求。反复操作DOM不仅会引发性能问题,而且载入js/css资源还会阻塞页面渲染,影响用户体验。
- 但是图片请求例外。图片打点不仅不用插入DOM,只要在js中new出Image对象就能发起请求,而且还没有阻塞问题,在没有js的浏览器环境中也能通过img标签正常打点,这是其他类型的资源请求所做不到的。所以,在所有通过请求文件资源进行打点的方案中,使用图片是最好的解决方案。
- 同样都是图片,上报时选用了1x1的透明GIF,而不是其他的
PNG/JEPG/BMP
文件?- 原因其实不太好想,需要分开来看。首先,1x1像素是最小的合法图片。而且,因为是通过图片打点,所以图片最好是透明的,这样一来不会影响页面本身展示效果,二者表示图片透明只要使用一个二进制位标记图片是透明色即可,不用存储色彩空间数据,可以节约体积。
- 因为需要透明色,所以可以直接排除JEPG(BMP32格式可以支持透明色)。然后还剩下BMP、PNG和GIF,但是为什么会选GIF呢?因为体积小!同样的响应,GIF可以比BMP节约41%的流量,比PNG节约35%的流量。
总结: 使用img
元素发送不会影响页面的展示、性能和体验这些,就是不会带来副作用,还有一个使用GIF
的格式是因为体积小。
# 性能监控(建议阅读Web Vitals (opens new window))
# 关键数据获取(仅供参考)
- 首屏时间:页面开始展示的时间点 - 开始请求的时间点
- 白屏时间:
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;
};
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
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
}
}
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
}
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;
}
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);
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
# 初始化的流程
图示流程:
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));
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
# 扩展
# 名词解释
- docs.sentry.io/product/per… (opens new window)
- docs.sentry.io/product/per… (opens new window)
- 性能指标 (opens new window)
# 参考链接
- https://juejin.cn/post/6957475955858210823
- https://juejin.cn/post/7020555250608111646