一、前言
最近对前端监控很有兴趣,所以去使用了前端监控里优秀的开源库最近对前端监控很有兴趣,所以去使用了前端监控里优秀的开源库Sentry,并且研究了下Sentry源码,整理出这一系列的文章,希望能帮助大家更好地了解前端监控原理。
这一系列的文章我将结合官网api与流程图的方式,通俗易懂地讲解整个流程。
以下是我已完成和计划下一篇文章的主题:
- 搞懂Sentry初始化
- Sentry如何处理错误数据
- Sentry拿到错误数据后,又是如何上报呢(本文)?
- 计划:Sentry如何集成面包屑Breadcrumbs
- 计划:Sentry如何处理session
- ...
这里给觉得看源码很困难的小伙伴一些建议:
- 推荐先看一些文章或者官网文档,并且相应的api要熟悉。然后根据主线走,支线可以大概了解是做什么的,别深究。
- 有时候可能分不清主次,这个时候可以画流程图,这也是我快速了解源码的方法。你画着画着,原本一开始看不懂的源码,可能在后续的某个地方就关联了起来。
- 最后就是要去多思考,比如:从源码中我学到了哪些优秀的写法,用到了什么设计模式,我如何应用到实际项目中,碰到bug是不是能够通过自己知道的运行流程快速定位到bug大概可能出现的位置等等。
而关于Sentry源码如何调试大家可以按照如下:
- git clone
git@github.com:getsentry/sentry-javascript.git
- 进入到 packages/browser 进行npm i 下载依赖
- 进入到 packages/browser/examples 打开index.html就可以开心调试了(这里建议用Live Server打开)
- 说明:packages/browser/examples 下的bundle.js就是打包后的源码,他指向了packages/browser/build/bundle.js。这时候你会发现build目录下还有bundle.es6.js,如果你想使用es6的方式去阅读可以将文件替换成bundle.es6.js
二、导读
通过我们上一篇文章错误处理后,我们得到了经过Sentry处理好的错误数据,这时候需要调用currentHub.captureEvent
进行数据上报。
我们先来看看主流的数据上报方式有:
-
采用
Ajax
通信的方式上报(Sentry采用的方式) -
img请求上报, url参数带上错误信息
比如:(new Image()).src = 'https://docs.sentry.io/error?error=error’
而Sentry上报的方式是采用Ajax
通信。我在Sentry会在初始化的7.1 BrowserClient中的有提到
为了兼容低版本浏览器不支持fetch,所以在初始化的时候就确定ajax通信采用的是fetch还是xhr。
我们来看看源码怎么写的:
class BaseBackend {
constructor(options) {
// ...
this._transport = this._setupTransport();
}
_setupTransport() {
// ...
if (supportsFetch()) {
return new FetchTransport(transportOptions);
}
return new XHRTransport(transportOptions);
}
}
supportsFetch:
/**
* Tells whether current environment supports Fetch API
* {@link supportsFetch}.
*
* @returns Answer to the given question.
*/
function supportsFetch() {
if (!('fetch' in getGlobalObject())) {
return false;
}
try {
new Headers();
new Request('');
new Response();
return true;
} catch (e) {
return false;
}
}
分析:
- getGlobalObject是获取全局对象
- 代码很简单就看全局上是不是有fetch方法
- 最终
this._transport
上就有了对应发送请求的方法
接下来就是详细的步骤分析:
三、数据上报
我们先看captureEvent源码:
captureEvent(event, hint) {
const eventId = (this._lastEventId = uuid4());
this._invokeClient('captureEvent', event, Object.assign(Object.assign({}, hint), { event_id: eventId }));
return eventId;
}
分析:
- 可以看到captureEvent实际上是调用_invokeClient
3.1 _invokeClient
/**
* Internal helper function to call a method on the top client if it exists.
*
* @param method The method to call on the client.
* @param args Arguments to pass to the client function.
*/
_invokeClient(method, ...args) {
const { scope, client } = this.getStackTop();
if (client && client[method]) {
client[method](...args, scope);
}
}
分析:
- _invokeClient 是个
统一的调度方法
,取到scope,执行captureEvent方法。
3.2 captureEvent
captureEvent(event, hint, scope) {
let eventId = hint && hint.event_id;
this._process(
this._captureEvent(event, hint, scope).then(result => {
eventId = result;
}),
);
return eventId;
}
分析:
- _process 就是流程控制器,记录当前的步骤
3.3 _captureEvent
_captureEvent(event, hint, scope) {
return this._processEvent(event, hint, scope).then(
finalEvent => {
return finalEvent.event_id;
},
reason => {
logger.error(reason);
return undefined;
},
);
}
分析:
- _processEvent是
实现的重点
- 最后会返回event_id 事件id
3.4 重点:_processEvent
_processEvent是数据上报的关键,这里主要处理事件(错误或信息),并将其发送给Sentry,同时也可以为事件添加面包屑breadcrumbs
和context上下文信息
,当然前提是有平台的信息比如用户ip地址
等。
_processEvent(event, hint, scope) {
const { beforeSend, sampleRate } = this.getOptions();
if (!this._isEnabled()) {
return SyncPromise.reject(new SentryError('SDK not enabled, will not send event.'));
}
const isTransaction = event.type === 'transaction';
// 1.0 === 100% events are sent
// 0.0 === 0% events are sent
// Sampling for transaction happens somewhere else
if (!isTransaction && typeof sampleRate === 'number' && Math.random() > sampleRate) {
return SyncPromise.reject(new SentryError('This event has been sampled, will not send event.'));
}
return this._prepareEvent(event, scope, hint)
.then(prepared => {
if (prepared === null) {
throw new SentryError('An event processor returned null, will not send event.');
}
const isInternalException = hint && hint.data && hint.data.__sentry__ === true;
if (isInternalException || isTransaction || !beforeSend) {
return prepared;
}
const beforeSendResult = beforeSend(prepared, hint);
if (typeof beforeSendResult === 'undefined') {
throw new SentryError('`beforeSend` method has to return `null` or a valid event.');
} else if (isThenable(beforeSendResult)) {
return beforeSendResult.then(
event => event,
e => {
throw new SentryError(`beforeSend rejected with ${e}`);
},
);
}
return beforeSendResult;
})
.then(processedEvent => {
if (processedEvent === null) {
throw new SentryError('`beforeSend` returned `null`, will not send event.');
}
const session = scope && scope.getSession && scope.getSession();
if (!isTransaction && session) {
this._updateSessionFromEvent(session, processedEvent);
}
this._sendEvent(processedEvent);
return processedEvent;
})
.then(null, reason => {
if (reason instanceof SentryError) {
throw reason;
}
this.captureException(reason, {
data: {
__sentry__: true,
},
originalException: reason,
});
throw new SentryError(
`Event processing pipeline threw an error, original event will not be sent. Details have been sent as a new event.\nReason: ${reason}`,
);
});
}
因为涉及内容很多,所以我分成几个大块详细讲解分析
(1) 校验
const { beforeSend, sampleRate } = this.getOptions();
if (!this._isEnabled()) {
return SyncPromise.reject(new SentryError('SDK not enabled, will not send event.'));
}
const isTransaction = event.type === 'transaction';
// 1.0 === 100% events are sent
// 0.0 === 0% events are sent
// Sampling for transaction happens somewhere else
if (!isTransaction && typeof sampleRate === 'number' && Math.random() > sampleRate) {
return SyncPromise.reject(new SentryError('This event has been sampled, will not send event.'));
这一段主要是去判断是否满足上报的条件:
-
参数中event代表发送给Sentry的事件,hint代表包含有关原始异常的其他信息,scope包含事件元数据的作用域
-
_isEnabled这里主要是为了判断用户传入的参数里是不是设置了
enabled为false或者dsn为空的情况
,这会导致SDK无法使用,无法发送。所以如果当客户端接受不到信息的时候,不要慌看看自己Sentry.init都传了什么值_isEnabled() { return this.getOptions().enabled !== false && this._dsn !== undefined; }
-
SyncPromise其实就是模拟了一个Promise
-
sampled 这一块是与
Performance性能挂钩的,它其实就是采样
。我们在Sentry.init的时候其实可以传入tracesSampleRate去控制每个事务都有几个百分比的机会被发送到 Sentry。(例如,如果你将 tracesSampleRate 设置为0.2,大约20% 的事务将被记录并发送。):
Sentry.init({ // ... tracesSampleRate: 0.2, });
sampled这里判断是不是number类型是因为,sampled也可以设置为
boolean值
比如在创建事务时知道是否希望将事务发送到 Sentry,于是可以采用Sentry.startTransaction方法直接给事务构造函数。这时候,事务就不会受到 tracesSampleRate 的约束,也不会运行 tracesSampler,发送的事务也不会被覆盖。
Sentry.startTransaction({ name: "Search from navbar", sampled: true, });
更多相关内容可参考官网
(2) _prepareEvent添加通用的信息
return this._prepareEvent(event, scope, hint)
.then(prepared => {
if (prepared === null) {
throw new SentryError('An event processor returned null, will not send event.');
}
分析:
-
_prepareEvent 主要是为event事件
添加通用的信息
,包含了从options里获取的发布的版本号release,和环境environment,从作用域scope获取的面包屑breadcrumbs和上下文context等等 -
在_prepareEvent返回事件前,会有一个
self._shouldDropEvent进行判断
,如果在Sentry.init中设置了ignoreErrors,denyUrls,allowUrls等数据过滤并且命中
的时候,此时会返回null
,因此prepared此时也会返回null,会退出事件,不进行事件上报
if (self._shouldDropEvent(event, options)) { return null; }
-
_prepareEvent
其实涉及了很多细节,这里与本文没有太多关联,如果有兴趣我再专门讲解。
(3) beforeSend 数据上报前的回调函数
const beforeSendResult = beforeSend(prepared, hint);
if (typeof beforeSendResult === 'undefined') {
throw new SentryError('`beforeSend` method has to return `null` or a valid event.');
} else if (isThenable(beforeSendResult)) {
return beforeSendResult.then(
event => event,
e => {
throw new SentryError(`beforeSend rejected with ${e}`);
},
);
}
return beforeSendResult;
beforeSend
会在事件发送到服务器之前立即调用,而这里的beforeSend其实就是用户传入beforeSend方法
。
比如:避免发送邮箱信息
Sentry.init({
beforeSend(event) {
// Modify the event here
if (event.user) {
// Don't send user's email address
delete event.user.email;
}
return event;
},
});
(4) 序列化错误数据
const session = scope && scope.getSession && scope.getSession();
if (!isTransaction && session) {
this._updateSessionFromEvent(session, processedEvent);
}
this._sendEvent(processedEvent);
return processedEvent;
分析:
-
有session就调用
_updateSessionFromEvent
就是从事件event 获取信息,更新session。具体就看之后的session专题 -
_sendEvent就是告诉
backend后端去发送事件
。_sendEvent(event) { const integration = this.getIntegration(Breadcrumbs); if (integration) { integration.addSentryBreadcrumb(event); } super._sendEvent(event); } ------------------------------------------------------------------ _sendEvent(event) { this._getBackend().sendEvent(event); } ------------------------------------------------------------------- sendEvent(event) { this._transport.sendEvent(event).then(null, reason => { logger.error(`Error while sending event: ${reason}`); }); } -------------------------------------------------------------------- sendEvent(event) { return this._sendRequest(eventToSentryRequest(event, this._api), event); }
分析:
-
发送的时候会
获取到Breadcrumbs面包屑
-
然后获取后端也就是
BrowserBackend去执行sendEvent
(说明一下backend也就是BrowserClient传入的BrowserBackend,可以看这初始化那篇文章) -
_transport 其实就是之前初始化已经辨别是走xhr还是fetch,这里走fetch(也就是导读分析的部分)
-
eventToSentryRequest:
eventToSentryRequest主要是处理
event序列化,生成body,url,type
function eventToSentryRequest(event, api) { // ... const req = { body: JSON.stringify(event), type: event.type || 'event', url: useEnvelope ? api.getEnvelopeEndpointWithUrlEncodedAuth() : api.getStoreEndpointWithUrlEncodedAuth(), }; // ... return req; }
最后我们看看,经过我们多篇文章探索所得出的最终
上报数据的格式
如下:body: "{ exception: { values: [ { type: 'Error', value: 'externalLibrary method broken: 1610509422407', stacktrace: { frames: [ { colno: 1, filename: 'https://rawgit.com/kamilogorek/cfbe9f92196c6c61053b28b2d42e2f5d/raw/3aef6ff5e2fd2ad4a84205cd71e2496a445ebe1d/external-lib.js', function: '?', in_app: true, lineno: 5, }, { colno: 9, filename: 'https://rawgit.com/kamilogorek/cfbe9f92196c6c61053b28b2d42e2f5d/raw/3aef6ff5e2fd2ad4a84205cd71e2496a445ebe1d/external-lib.js', function: 'externalLibrary', in_app: true, lineno: 2, }, ], }, mechanism: { handled: false, type: 'onerror' }, }, ], }, platform: 'javascript', sdk: { name: 'sentry.javascript.browser', packages: [{ name: 'npm:@sentry/browser', version: '5.29.2' }], version: '5.29.2', integrations: [ 'InboundFilters', 'FunctionToString', 'TryCatch', 'Breadcrumbs', 'GlobalHandlers', 'LinkedErrors', 'UserAgent', ], }, event_id: 'aec2b5cdf4b34efa92c4766ea76a2f4b', timestamp: 1610509422.9, environment: 'staging', release: '1537345109360', breadcrumbs: [ { timestamp: 1610509411.46, category: 'console', data: { arguments: [ 'currentHub', { _version: 3, _stack: '[Array]', _lastEventId: 'aec2b5cdf4b34efa92c4766ea76a2f4b' }, ], logger: 'console', }, level: 'log', message: 'currentHub [object Object]', }, { timestamp: 1610509411.462, category: 'console', data: { arguments: ['Time Hooker Works!'], logger: 'console' }, level: 'log', message: 'Time Hooker Works!', }, { timestamp: 1610509411.52, category: 'ui.click', message: 'body > button#plainObject' }, { timestamp: 1610509415.083, category: 'ui.click', message: 'body > button#deny-url' }, { timestamp: 1610509416.768, category: 'ui.click', message: 'body > button#deny-url' }, { timestamp: 1610509422.405, category: 'sentry.event', event_id: 'b91c3bbff53047b7b6b40cd87a82c88e', message: 'Error: externalLibrary method broken: 1610509417092', }, ], request: { url: 'http://127.0.0.1:5500/packages/browser/examples/index.html', headers: { Referer: 'http://127.0.0.1:5500/packages/browser/examples/index.html', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36', }, }, tags: {}, }", type: 'event', url: 'https://sentry.io/api/297378/store/?sentry_key=363a337c11a64611be4845ad6e24f3ac&sentry_version=7',
分析:
- body里的exception就是上一篇文章讲过的错误内容
- sdk就是我们具体使用sdk的版本,还有集成
- breadcrumbs面包屑内容,之后专题会讲解如何生成
- request 就是当前发起请求的路径内容
- url是发向后端的api
-
(5)_sendRequest发送请求
/**
* @param sentryRequest Prepared SentryRequest to be delivered
* @param originalPayload Original payload used to create SentryRequest
*/
_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,
});
}
const options = {
body: sentryRequest.body,
method: 'POST',
// Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default
// https://caniuse.com/#feat=referrer-policy
// It doesn't. And it throw exception instead of ignoring this parameter...
// REF: https://github.com/getsentry/raven-js/issues/1233
referrerPolicy: supportsReferrerPolicy() ? 'origin' : '',
};
if (this.options.fetchParameters !== undefined) {
Object.assign(options, this.options.fetchParameters);
}
if (this.options.headers !== undefined) {
options.headers = this.options.headers;
}
return this._buffer.add(
new SyncPromise((resolve, reject) => {
global$3
.fetch(sentryRequest.url, options)
.then(response => {
const headers = {
'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'),
'retry-after': response.headers.get('Retry-After'),
};
this._handleResponse({
requestType: sentryRequest.type,
response,
headers,
resolve,
reject,
});
})
.catch(reject);
}),
);
}
分析:
-
_isRateLimited就是
防止一瞬间太多相同的错误发生
-
然后就是对options的一些处理,
该合并该赋值赋值
-
this._buffer.add 就是把
promise加入buffer队列中
-
之后等待向服务器发起请求,下面是请求的截图
(6)_handleResponse处理返回的请求
我们先看看返回的数据
接着看看_handleResponse对数据都做了哪些处理
/**
* Handle Sentry repsonse for promise-based transports.
*/
_handleResponse({ requestType, response, headers, resolve, reject }) {
const status = exports.Status.fromHttpCode(response.status);
/**
* "The name is case-insensitive."
* https://developer.mozilla.org/en-US/docs/Web/API/Headers/get
*/
const limited = this._handleRateLimit(headers);
if (limited) logger.warn(`Too many requests, backing off until: ${this._disabledUntil(requestType)}`);
if (status === exports.Status.Success) {
resolve({ status });
return;
}
reject(response);
}
分析:
- fromHttpCode就是返回状态200到300是Success,429就是被限制RateLimit,400到500为Invalid,500以上就是Failed,其他Unknown,所以这里的status是Success
- 然后成功就是resolve
(6) 请求失败时
我们回过头看_processEvent 中的
.then(null, reason => {
if (reason instanceof SentryError) {
throw reason;
}
this.captureException(reason, {
data: {
__sentry__: true,
},
originalException: reason,
});
throw new SentryError(
`Event processing pipeline threw an error, original event will not be sent. Details have been sent as a new event.\nReason: ${reason}`,
);
});
分析:
- 第一个参数传入null,如果成功发送其实是不会执行到这里会直接退出
- 而出错就会调用
captureException去上报错误
到此,整个自动上报的过程就完成了,接下来我们看看主动上报
四、captureException和captureMessage主动上报数据
captureException是上传一个错误对象
captureMessage则上传递一个消息
,这个消息即可以包含错误信息
,也可以是普通消息
然后我们看看它们的源码
captureException:
BaseClient.prototype.captureException(exception, hint, scope) {
let eventId = hint && hint.event_id;
this._process(this._getBackend()
.eventFromException(exception, hint)
.then(event => this._captureEvent(event, hint, scope))
.then(result => {
eventId = result;
}));
return eventId;
}
captureMessage:
BaseClient.prototype.captureMessage = function (message, level, hint, scope) {
var _this = this;
var eventId = hint && hint.event_id;
var promisedEvent = utils_1.isPrimitive(message)
? this._getBackend().eventFromMessage(String(message), level, hint)
: this._getBackend().eventFromException(message, hint);
this._process(promisedEvent
.then(function (event) { return _this._captureEvent(event, hint, scope); })
.then(function (result) {
eventId = result;
}));
return eventId;
};
这里的关于如何处理消息已经在上一篇讲解过了,所以这里重点是讲解_captureEvent方法
分析:
-
对于错误消息captureMessage和captureException都会调用eventFromException去处理消息
-
而captureMessage需要判断信息message是不是基本类型,基本类型走eventFromMessage,引用类型走eventFromException 去处理message。
-
这里的关于如何处理消息已经在上一篇讲解过了,所以这里重点是讲解
_captureEvent
方法_captureEvent(event, hint, scope) { return this._processEvent(event, hint, scope).then( finalEvent => { return finalEvent.event_id; }, reason => { logger.error(reason); return undefined; }, ); }
其实就是调用
_processEvent
方法
到此,整个数据上报的内容就完成了
五、总结
最后我们通过一张流程图来看看整个数据上报的过程:
六、参考资料
- Sentry官网
- Sentry仓库
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!