Обычно мы собираем ошибки, большинство людей и компаний используютSentryДля сбора, конечно же, у многих компаний есть и самостоятельная разработка фронтенд-мониторинга. Воспользовавшись тем, что наша группа вот-вот присоединитсяSentry, и сделайте акцию на основе ваших собственных исследований. Итак, резюмируем следующим образом. Поделитесь с вами, ребята.
1. Как сообщить
-
автоматический сбор
- Бессенсорная коллекция
-
Ручной отчет, ручной отчет делится на:
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", });а также
captureExceptionа такжеcaptureMessageРеализация наверное такая// 手动触发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. Загрузить исходную карту
Существует три способа загрузки исходных карт в Sentry.
- Официальный cli (sentry-cli)
- Вызов HTTP API
- плагин веб-пакета
Далее я в основном объясню метод реализации веб-пакета. Пожалуйста, проверьте другие методы самостоятельно. Фактически, часовой в основном использует
@sentry/webpack-pluginЭтот плагин выполняет загрузку исходной карты.// 使用示例 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
Вероятно, процесс на самом деле находится в веб-пакете
afterEmitХук, чтобы получить упакованный файл. Затем отфильтруйте, чтобы получить тип файла/\.js$|\.map$/В конце он загружается на сторожевой сервер. Затем, когда вы удаляете его, просто удалите/\.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Удаление исходной карты после загрузки не поддерживается, поэтому мы можем использоватьwebpack-sentry-pluginПохоже, этот плагин официально выпущен, он поддерживает удаление исходной карты после загрузки. Вы можете обратиться к документации, если вы ее используете.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 Перехват исключений
Исключения перехвата внешнего интерфейса делятся на глобальные перехваты и одноточечные перехваты.
- Код глобального захвата централизован и прост в управлении;
- В качестве дополнения одноточечный захват используется для захвата некоторых особых случаев, но он разрознен и не способствует управлению.
- глобальный захват
- Через глобальный интерфейс код захвата пишется в одном месте.Интерфейсы, которые можно использовать:
-
window.addEventListener(‘error’) / window.addEventListener(“unhandledrejection”) / document.addEventListener(‘click’)Ждать
-
- Глобальный мониторинг рамочного уровня,
- Например
aixosиспользуется вinterceptorперехватить, -
vue、reactИмеет собственный интерфейс сбора ошибок
- Например
- Инкапсулируя глобальную функцию, исключение автоматически перехватывается при вызове функции.
- Переопределить метод экземпляра (Patch), обернув слой на основе исходной функции,
- Например, чтобы
setTimeoutПерепишите его, и вы также можете ловить исключения, когда метод использования остается неизменным
- Например, чтобы
- Через глобальный интерфейс код захвата пишется в одном месте.Интерфейсы, которые можно использовать:
- глобальный захват
- Оберните один блок кода в бизнес-код или управляйте им в логическом потоке, чтобы добиться целевого захвата исключений:
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событие, то есть неcatchизrejectсостояние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);
}
-
VueизVue.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;
-
ReactизErrorBoundary-
ErrorBoundaryОпределение:еслиclassопределяется в компонентеstatic getDerivedStateFromError()илиcomponentDidCatch()Когда любой из этих двух методов жизненного цикла становится границей ошибки.. Когда выдается ошибка, используйтеstatic getDerivedStateFromError()Чтобы отобразить альтернативный пользовательский интерфейс, используйте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 путем перезаписи (перехвата) отправки и открытия
- Fetch перехватывает весь метод путем перехвата (нужно обсудить, случай отклонения)
- axios через перехватчик запроса/ответа
-
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)
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)- // Чтобы определить, какой метод использовать для отчета, сначала определите, поддерживает ли ветвь выборку, если нет, используйте 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);
};
- Отправить запрос в методе выборки
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()Способ сообщить об этом, это действительно просто любой ресурс?
-
Почему я не могу использовать для запроса другие файловые ресурсы
(js/css/ttf)способ сообщить?- Это связано с особенностями браузера. Обычно браузер фактически не отправляет запрос ресурса до тех пор, пока объект не будет внедрен в дерево DOM браузера после создания узла ресурса. Повторяющиеся манипуляции с DOM не только вызовут проблемы с производительностью, но и загрузят ресурсы js/css, которые заблокируют отрисовку страницы и повлияют на взаимодействие с пользователем.
- Но исключение запроса изображения. Управление изображением не только не нужно вставлять в DOM, но нужно только создать новый объект изображения в js для инициации запроса, и нет проблемы с блокировкой.В среде браузера без js им также можно нормально управлять через тег img, что является причиной других типов запросов ресурсов невозможно. Поэтому среди всех решений по управлению с помощью запроса файловых ресурсов использование картинок — лучшее решение.
-
Так же все картинки, а для репортажа выбран прозрачный GIF 1х1, вместо других
PNG/JEPG/BMPдокумент?- Причину на самом деле не так просто придумать, и ее нужно рассматривать отдельно. Во-первых, 1x1 пиксель — это самое маленькое разрешенное изображение. Кроме того, поскольку изображение прочерчено точками, изображение предпочтительно должно быть прозрачным, чтобы оно не влияло на эффект отображения самой страницы.Двойка указывает на то, что изображение прозрачно, если для маркировки используется двоичный бит. изображение в виде прозрачного цвета, и нет необходимости хранить данные цветового пространства, что может сэкономить объем.
- Поскольку требуется прозрачный цвет, JPEG можно исключить напрямую (формат BMP32 может поддерживать прозрачный цвет). Тогда остаются BMP, PNG и GIF, но зачем выбирать GIF? Из-за своего маленького размера! Для одного и того же ответа GIF может сэкономить 41% трафика по сравнению с BMP и 35% трафика по сравнению с PNG.
Суммировать:использоватьimgОтправка элемента не повлияет на отображение, производительность и опыт страницы, но не принесет побочных эффектов.GIFФормат из-за его небольшого размера.
6. more
6.1 Запись
- В дополнение к сообщениям об ошибках и производительности, на самом деле
sentryВы также можете записывать информацию об экране, чтобы помочь разработчикам быстрее находить ошибки.официальная документация,sentryНеправильная запись на самом деле в основном зависит отrrwebЭтот пакет реализует- Общий процесс заключается в том, чтобы сначала сохранить снимок всего DOM в начале, а затем создать уникальный идентификатор для каждого узла.
- Когда DOM переходит через
MutationObserverследить за тем, какиеDOMКакое свойство какого изменилось, сохраните его. - Отслеживайте события взаимодействия мыши и клавиатуры со страницей, чтобы записывать информацию о положении и взаимодействии и, наконец, использовать ее для имитации действий пользователя.
- Затем проанализируйте внутренне написанный метод синтаксического анализа (я понимаю, что этот шаг самый сложный)
- Отрисовывая дом и используя
RAFИграть так, как будто смотреть видео.
6.2 часовой путь получения 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Если вы что-то знаете, дайте мне знать в разделе комментариев~
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 通过在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 Очень грубый процесс
- По моему очень общему пониманию
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));