feat(TEAMMSBMOB-20426): подключение Sentry

This commit is contained in:
Онуфрийчук Егор
2025-11-19 09:49:47 +03:00
parent 732bfc84cb
commit 7eeb56a6b4
35 changed files with 4614 additions and 8337 deletions
+2 -1
View File
@@ -13,7 +13,8 @@ module.exports = {
'react/destructuring-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'sonarjs/no-identical-functions': 'off',
"prettier/prettier": ["error", { "endOfLine": "auto" }]
'jsdoc/check-indentation': 'off',
'prettier/prettier': ['error', { endOfLine: 'auto' }],
},
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
+3877 -8332
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -40,7 +40,7 @@
"start:stage:fea": "lerna run start:stage --scope=msb-host --stream & lerna run start --scope=msb-fea --stream",
"start:partner-check": "lerna run start --scope=msb-host --scope=msb-partner-check --stream",
"start:stage:partner-check": "lerna run start:stage --scope=msb-host --stream & lerna run start --scope=msb-partner-check --stream",
"build": "lerna run build --scope=msb-* --stream --concurrency=2",
"build": "cross-env IB_MODULE_VERSION=$npm_package_version lerna run build --scope=msb-* --stream --concurrency=2",
"build:webpack-config": "lerna run build --scope=@msb/mf-builder --stream",
"start:prod": "npm run build && docker-compose up",
"lint": "lerna run lint --scope=msb-* --stream",
+1 -1
View File
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
import { captureException } from '@msb/sentry';
import type { AxiosError } from 'axios';
import { captureException } from '@eco/monitoring';
import { PLUGIN_NETWORK_ERROR_MESSAGE, VERSION_ERROR_MESSAGE } from '../stream-constants/crypto';
const CRYPTO_MODULE_ERROR_TYPE_TAG = 'crypto-module-error-type';
+1
View File
@@ -13,6 +13,7 @@
"i18next": "22.5.1",
"@msb/http": "1.0.0",
"@msb/shared": "1.0.0",
"@msb/sentry": "1.0.0",
"@msb/localization": "1.0.0",
"axios": "1.12.2",
"yup": "0.32.9",
+3
View File
@@ -4,6 +4,9 @@
"private": true,
"description": "",
"main": "src/index.ts",
"dependencies": {
"@msb/sentry": "1.0.0"
},
"devDependencies": {
"react": "17.0.2",
"react-dom": "17.0.2",
+10 -1
View File
@@ -1,5 +1,6 @@
import type { ReactElement, ReactNode } from 'react';
import type { ReactElement, ReactNode, ErrorInfo } from 'react';
import { Component } from 'react';
import { captureException } from '@msb/sentry';
export interface IErrorFallbackProps {
// Ошибка, возникшая в одном из дочерних компонентов
@@ -32,6 +33,14 @@ export class ErrorBoundary extends Component<IErrorBoundaryProps, IErrorBoundary
status: 'ok',
};
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
captureException(error, hint => {
hint.setExtra('errorInfo', errorInfo);
return hint;
});
}
reload(): void {
this.setState({ status: 'ok' });
this.props.onReload?.();
+3
View File
@@ -0,0 +1,3 @@
export { captureException, captureMessage, captureEvent, setContext, setExtra, setExtras, setTag, setTags, setUser } from '@sentry/react';
export { TAGS, generateUniqueException } from './model';
export { useSentry } from './ui';
+1
View File
@@ -0,0 +1 @@
export * from './use-token';
+73
View File
@@ -0,0 +1,73 @@
import { useCallback, useEffect, useRef } from 'react';
import { useQuery } from '@msb/http';
import axios from 'axios';
import type { SentryState } from '../store/types';
import { isExcludedPath } from '../utils';
import { sentryConfig, tokenHeader } from '../utils/constants';
interface Props extends SentryState {
enabled: boolean;
}
const TOKEN_REQUEST_KEY = 'tokenRequest';
/** Хук механизма генерации токена. */
const useToken = ({ enabled, attempts, isSendingAllowed, setState }: Props) => {
const reconnectTimer = useRef<NodeJS.Timeout>();
const { enabledTokenGeneration, url, timeout, retry, retryDelay, attemptsBeforeDisable, reconnectionTime } = sentryConfig;
// Признак активности механизма генерации токена для отправляемых данных
const isTokenGenerationAvailable = Boolean(enabledTokenGeneration && url);
// GET-запрос токена для отправляемых данных
const { refetch, isLoading } = useQuery([TOKEN_REQUEST_KEY], () => axios.get(url!, { timeout }), {
enabled: enabled && isTokenGenerationAvailable && !isExcludedPath(),
retry,
retryDelay,
refetchOnWindowFocus: false,
onSuccess: response => {
const token = response?.headers?.[tokenHeader];
if (token) {
const newAttemptsCount = attempts + 1;
let newProps: Partial<SentryState> = {};
// Если количество перезапросов токена не превышает максимально допустимое,
// то включаем логирование в Sentry, иначе выключаем
newProps =
newAttemptsCount < attemptsBeforeDisable ? { isSendingAllowed: true, token: Number(token) } : { isSendingAllowed: false };
setState?.({ ...newProps, attempts: newAttemptsCount });
}
},
onError: () => {
setState?.({ isSendingAllowed: false, token: null });
},
onSettled: () => {
clearTimeout(reconnectTimer.current);
if (!isSendingAllowed) {
reconnectTimer.current = setTimeout(async () => {
setState?.({ attempts: 0 });
await refetch();
}, reconnectionTime);
}
},
});
useEffect(() => () => clearTimeout(reconnectTimer.current), []);
/** Функция перезапроса токена. */
const renewToken = useCallback(async () => {
if (isLoading) return;
setState?.({ isSendingAllowed: false });
await refetch();
}, [isLoading, setState, refetch]);
return isTokenGenerationAvailable ? renewToken : undefined;
};
export { useToken };
+3
View File
@@ -0,0 +1,3 @@
export * from './hooks';
export * from './store';
export * from './utils';
+10
View File
@@ -0,0 +1,10 @@
import type { SentryState } from './types';
/** Состояние стейта по умолчанию. */
const initialSentryState: SentryState = {
attempts: 0,
token: null,
isSendingAllowed: false,
};
export { initialSentryState };
+10
View File
@@ -0,0 +1,10 @@
import create from 'zustand';
import { initialSentryState } from './constants';
import type { SentryState } from './types';
const useSentryStore = create<SentryState>(set => ({
...initialSentryState,
setState: (newState: Partial<SentryState>) => set(state => ({ ...state, ...newState })),
}));
export { useSentryStore };
+12
View File
@@ -0,0 +1,12 @@
interface SentryState {
// Признак разрешения отправки событий.
isSendingAllowed: boolean;
// Количество попыток перезапроса токена.
attempts: number;
// Токен для запросов.
token: number | null;
// Функция обновления cтора.
setState?(newState: Partial<SentryState>): void;
}
export type { SentryState };
+106
View File
@@ -0,0 +1,106 @@
import { getClient } from '@sentry/react';
import type { ErrorEvent, Event, Breadcrumb, BreadcrumbHint } from '@sentry/types';
import { TAGS } from './constants';
import { clearData, isExcludedPath } from './helpers';
/**
* Функция вызывается перед добавлением breadcrumb в Sentry. Позволяет фильтровать и модифицировать breadcrumbs.
*
* @param breadcrumb - Объект breadcrumb, который будет отправлен в Sentry.
* @param hint - Объект, содержащий дополнительную информацию о событии.
* @returns Модифицированный breadcrumb, или null, если breadcrumb не должен быть отправлен.
*/
const beforeBreadcrumb = (breadcrumb: Breadcrumb, hint: BreadcrumbHint | undefined, isSendingAllowed: boolean) => {
// Не отправляем breadcrumbs, если страница в исключениях или отправка запрещена.
if (isExcludedPath() || !isSendingAllowed) {
return null;
}
const augmentedData = { ...breadcrumb.data };
// По 400+ кодам ответов в нетворке дополняем payload, response и traceId в объект Breadcrumb
if (breadcrumb.category === 'xhr' && augmentedData?.status_code >= 400) {
const payload = clearData(hint?.input);
const traceId = hint?.xhr instanceof XMLHttpRequest ? hint.xhr.getResponseHeader(TAGS.traceIdHeader) : undefined;
if (traceId) {
augmentedData[TAGS.traceIdHeader] = traceId;
}
if (payload) {
augmentedData.payload = payload;
}
const response = clearData(hint?.xhr?.responseText);
if (response) {
augmentedData.response = response;
}
return { ...breadcrumb, data: augmentedData };
} else if (breadcrumb.category === 'navigation') {
// убираем хэш-часть урлов редиректов, т.к. в ней может быть токен
if (typeof augmentedData.from === 'string') {
augmentedData.from = augmentedData.from.replace(/#.*/, '');
}
if (typeof augmentedData.to === 'string') {
augmentedData.to = augmentedData.to.replace(/#.*/, '');
}
return { ...breadcrumb, data: augmentedData };
}
return breadcrumb;
};
/**
* Функция вызывается перед отправкой события в Sentry. Позволяет фильтровать и модифицировать события.
*
* @param event - Объект события, который будет отправлен в Sentry.
* @returns Модифицированный event, или null, если событие не должно быть отправлено.
*/
const beforeSend = (event: ErrorEvent, isSendingAllowed: boolean): Event | PromiseLike<Event | null> | null => {
const { breadcrumbs, contexts, exception: eventException, tags } = event;
const exception = eventException?.values?.[0];
if (
// Выключаем логирование для случаев:
// страница из исключений
isExcludedPath() ||
// стоит флаг запрета отправки событий
!isSendingAllowed
) {
return null;
}
const mechanismType = exception?.mechanism?.type;
if (mechanismType === 'http.client') {
const errData = breadcrumbs?.[breadcrumbs.length - 1]?.data;
const traceId = contexts?.response?.headers?.[TAGS.traceIdHeader];
// Добавляем тег с traceId от бэкенда, если получили его в заголовках
if (traceId) {
return { ...event, tags: { ...tags, [TAGS.traceIdHeader]: traceId } };
}
try {
const traceIdResp = errData?.response ? JSON.parse(errData?.response)?.errorInfo?.traceId : null;
// Добавляем тег с traceId от бэкенда, если получили его в теле ответа
return traceIdResp ? { ...event, tags: { ...tags, [TAGS.traceIdHeader]: traceIdResp } } : event;
} catch {
return event;
}
}
return event;
};
/** Отключение клиента Sentry. */
const clientShutdown = () => getClient()?.close(0);
export { beforeBreadcrumb, beforeSend, clientShutdown };
+28
View File
@@ -0,0 +1,28 @@
import type { MODE, PADDING } from 'egoroof-blowfish';
import { Blowfish } from 'egoroof-blowfish';
/** Шифрует данные по Blowfish.
*
* @param key - Ключ шифрования.
* @param iv - Вектор инициализации.
* @param data - Данные для шифрования.
* @param mode - Режим шифрования.
* @default MODE.CBC
* @param padding - Режим дополнения.
* @default PADDING.PKCS5
*/
const encryptData = (
key: string,
iv: string,
data: Uint8Array | string,
mode: MODE = Blowfish.MODE.CBC,
padding: PADDING = Blowfish.PADDING.PKCS5
): Uint8Array => {
const bf = new Blowfish(key, mode, padding);
bf.setIv(iv);
return bf.encode(data);
};
export { encryptData };
+64
View File
@@ -0,0 +1,64 @@
import type { SentryConfig } from './types';
/** Конфигурация Sentry. */
const sentryConfig: SentryConfig = {
enabledAdvancedLogging: true,
enabledAuthoritiesCollecting: true,
enabledCompressing: true,
enabledCustomHeaders: true,
enabledEncoding: true,
enabledTokenGeneration: true,
vector: '58741936',
msg: 'KratosAresSiriusMSB',
timeout: 30_000,
retry: 1,
retryDelay: 10_000,
attemptsBeforeDisable: 3,
reconnectionTime: 1_800_000,
disabledForStreams: ['/web-dealing'],
dsn: process.env.SENTRY_DSN,
url: process.env.SENTRY_URI,
};
/** Заголовок с токеном. */
const tokenHeader = 'x-csrf-token';
/** Общие теги. */
const TAGS = {
/** Запрос. */
request: 'request',
/** Модуль. */
module: 'module',
/** Id апросd. */
requestId: 'request-id',
/** Тип криптоплагина. */
cryptoPlugin: 'crypto-plugin',
/** Версия криптоплагина. */
cryptoPluginVersion: 'crypto-plugin-version',
/** Код события. */
eventCode: 'event-code',
/** Заголовок с traceId от backend'а. */
traceIdHeader: 'x-b3-traceid',
} as const;
/** Исключаемые поля в payload и response запросов. Взят у экосистемы. */
const excludedDataFields = [
'authData',
'deviceDigitalPrint',
'digest',
'mfa_certificate',
'mfa_password',
'mfa_signed_data',
'mfa_token',
'newPassword',
'newPasswordConfirm',
'oldPassword',
'password',
'redirect_uri',
'signature',
'token',
'user_environment',
'X509data',
];
export { sentryConfig, excludedDataFields, tokenHeader, TAGS };
+105
View File
@@ -0,0 +1,105 @@
import { captureException } from '@sentry/react';
import { gzip } from 'pako';
import { encryptData } from './bf-crypto';
import { excludedDataFields, sentryConfig } from './constants';
import type { ConvertRequestBodyProps, GenerateUniqueExceptionProps, NetworkBody } from './types';
/**
* Преобразует тело запроса Sentry: шифрует и сжимает gzip .
*
* @param params - Параметры преобразования.
* @param params.body - Тело запроса, которое нужно преобразовать.
* @param params.enabledCompressing - Флаг, указывающий, нужно ли сжимать тело запроса с использованием gzip.
* @param params.enabledEncoding - Флаг, указывающий, нужно ли шифровать тело запроса.
* @param params.msg - Сообщение (ключ) для шифрования.
* @param params.vector - Вектор инициализации (IV) для шифрования.
*
* @returns Преобразованное тело запроса.
*/
const convertRequestBody = ({ body, enabledCompressing, enabledEncoding, msg, vector }: ConvertRequestBodyProps) => {
let result = body;
if (enabledEncoding && msg && vector) {
result = encryptData(msg, vector, body);
}
if (enabledCompressing) {
return gzip(result);
}
return result;
};
/** Генерирует новый токен для POST-запросов.
*
* @param token - Токен для преобразования.
*/
const generateToken = (token: number): number => {
// Преобразуем токен в 32-битное беззнаковое целое число.
const unsigned = token >>> 0;
// Преобразуем беззнаковое целое число в двоичную строку, дополняя нулями до 32 символов.
const binary = unsigned.toString(2).padStart(32, '0');
// Сдвигаем беззнаковое целое число вправо на 1 бит (эквивалентно делению на 2).
// Затем преобразуем результат в двоичную строку, дополняя нулями до 32 символов.
const shifted = ((unsigned >> 1) >>> 0).toString(2).padStart(32, '0');
const res = `${binary.slice(-1)}${shifted.slice(1)}`;
return parseInt(res, 2);
};
/** Исключает из объекта данных лишние поля.
*
* @param data - Объект данных.
*/
const clearData = (data: NetworkBody) => {
try {
const dataObj: NetworkBody = typeof data === 'string' ? JSON.parse(data) : data;
if (Array.isArray(dataObj)) {
dataObj.forEach(subData => clearData(subData));
} else if (dataObj && typeof dataObj === 'object') {
Object.entries(dataObj).forEach(([key, value]) => {
if (excludedDataFields.includes(key)) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete dataObj[key];
} else if (Array.isArray(value)) {
value.forEach(subData => clearData(subData));
} else if (value && typeof value === 'object') {
clearData(value);
}
});
}
return typeof data === 'string' ? JSON.stringify(dataObj) : dataObj;
} catch {
return data;
}
};
/**
* Генерирует уникальное событие при каждом вызове, объединяя в один exception в Sentry.
*
* @param params - Параметры генерации исключения.
* @param params.name - Имя исключения (используется для fingerprint).
* @param params.tags - Объект с тегами, которые будут добавлены к событию в Sentry.
*
*/
const generateUniqueException = ({ name, tags }: GenerateUniqueExceptionProps) => {
const err = new Error(`${name}_${Date.now()}`);
err.name = name;
captureException(err, e => {
tags && e.setTags(tags);
// одинаковый fingerprint, чтобы группировать события в один issue
e.setFingerprint([name]);
return e;
});
};
/** Проверяет, находится ли текущая страница в исключениях. */
const isExcludedPath = (): boolean => sentryConfig.disabledForStreams.some(url => window.location.pathname.startsWith(url));
export { convertRequestBody, generateToken, generateUniqueException, isExcludedPath, clearData };
+6
View File
@@ -0,0 +1,6 @@
export * from './addons';
export * from './bf-crypto';
export * from './constants';
export * from './helpers';
export * from './transport';
export * from './types';
+84
View File
@@ -0,0 +1,84 @@
import { createTransport } from '@sentry/core';
import type {
BaseTransportOptions,
Transport,
TransportMakeRequestResponse,
TransportRequest,
TransportRequestExecutor,
} from '@sentry/types';
import axios from 'axios';
import type { SentryState } from '../store/types';
import { sentryConfig, tokenHeader } from './constants';
import { convertRequestBody, generateToken, isExcludedPath } from './helpers';
import type { TransportProps } from './types';
const axiosInstance = axios.create();
axiosInstance.interceptors.request.use(config => {
// Блокируем отправку запросов для страниц из исключений, или если стоит флаг запрета отправки событий
if (isExcludedPath()) {
return Promise.reject(new Error('Request blocked due to excluded path.'));
}
return config;
});
/** Кастомный транспорт для отправки запросов в Sentry. */
const customTransport =
({ token, attempts, setState, renewToken }: TransportProps) =>
(options: BaseTransportOptions): Transport => {
const { enabledCustomHeaders, customHeaders, url, attemptsBeforeDisable, enabledCompressing, enabledEncoding, vector, msg } =
sentryConfig;
const makeRequest: TransportRequestExecutor = ({ body: data }) => {
const sendRequest = ({
body,
url: requestUrl,
}: Pick<BaseTransportOptions, 'url'> & TransportRequest): PromiseLike<TransportMakeRequestResponse> =>
new Promise((resolve, reject) => {
let newToken: SentryState['token'] = null;
if (renewToken) {
newToken = token ? generateToken(token) : null;
setState?.({ token: newToken });
}
axiosInstance
.post(requestUrl, convertRequestBody({ body, enabledCompressing, enabledEncoding, vector, msg }), {
headers: {
// Добавляем кастомные хедеры для запросов в Sentry
...(enabledCustomHeaders
? {
...(newToken ? { [tokenHeader]: newToken } : {}),
...customHeaders,
}
: {}),
},
})
.then(response => {
// При успешной отправке сбрасываем счётчик неудачных попыток
setState?.({ attempts: 0 });
resolve({
...response,
headers: response.headers as TransportMakeRequestResponse['headers'],
});
})
.catch(error => {
// Перезапрашиваем токен, если получили код 400 в ответ на отправку данных в Sentry
// и ещё не превышено допустимое количество попыток
if (error?.response?.status === 400 && attempts < attemptsBeforeDisable) {
renewToken?.();
}
reject(error);
});
});
return sendRequest({ body: data, url: url ?? options.url });
};
return createTransport(options, makeRequest);
};
export { customTransport };
+62
View File
@@ -0,0 +1,62 @@
import type { Scope, TransportRequest } from '@sentry/types';
import type { SentryState } from '../store/types';
/** Интерфейс конфига Sentry. */
interface SentryConfig {
// Дополнительные заголовки.
customHeaders?: Record<string, string>;
// Источник данных Sentry.
dsn?: string;
// Включение механизма расширенного логирования.
enabledAdvancedLogging?: boolean;
// Включение отправки привилегий.
enabledAuthoritiesCollecting: boolean;
// Включение сжатия (gzip).
enabledCompressing: boolean;
// Включение пользовательских заголовков.
enabledCustomHeaders: boolean;
// Включение шифрования.
enabledEncoding: boolean;
// Включение механизма генерации токена.
enabledTokenGeneration: boolean;
// Ключ шифрования.
msg?: string;
// Вектор инициализации.
vector: string;
// Ссылка для отправки запросов.
url?: string;
// Таймаут для запроса токена.
timeout: number;
// Количество попыток перезапроса токена в случае ошибки.
retry: number;
// Задержка перезапроса токена.
retryDelay: number;
// Максимальное количество попыток запроса токена за сессию.
attemptsBeforeDisable: number;
// Интервал восстановления подключения.
reconnectionTime?: number;
// Список url'ов, для которых отключено логирование.
disabledForStreams: string[];
}
/** Свойства функции convertRequestBody. */
type ConvertRequestBodyProps = Pick<SentryConfig, 'enabledCompressing' | 'enabledEncoding' | 'msg' | 'vector'> & TransportRequest;
/** Свойства функции ecoTransport. */
interface TransportProps extends Omit<SentryState, 'isSendingAllowed'> {
/** Функция перезапроса токена. */
renewToken?(): void;
}
/** Свойства функции generateUniqueException. */
interface GenerateUniqueExceptionProps {
// Название exception.
name: string;
// Теги для добавления к событию.
tags?: Parameters<Scope['setTags']>[0];
}
/** Payload запросов. */
type NetworkBody = NetworkBody[] | string | { [key: string]: NetworkBody | number };
export type { SentryConfig, NetworkBody, GenerateUniqueExceptionProps, TransportProps, ConvertRequestBodyProps };
+21
View File
@@ -0,0 +1,21 @@
{
"name": "@msb/sentry",
"version": "1.0.0",
"private": true,
"main": "index.ts",
"description": "",
"dependencies": {
"@msb/http": "1.0.0",
"@sentry/integrations": "7.98.0",
"@sentry/react": "7.98.0",
"@sentry/replay": "7.98.0",
"@sentry/tracing": "7.98.0",
"egoroof-blowfish": "4.0.1",
"nanoid": "3.3.11",
"pako": "2.0.4",
"zustand": "^3.5.10"
},
"devDependencies": {
"@types/pako": "^2.0.3"
}
}
+1
View File
@@ -0,0 +1 @@
export * from './useSentry';
+67
View File
@@ -0,0 +1,67 @@
import { useEffect } from 'react';
import { httpClientIntegration } from '@sentry/integrations';
import { init } from '@sentry/react';
import { beforeBreadcrumb, beforeSend, clientShutdown, customTransport, useToken, useSentryStore, sentryConfig } from '../model';
/**
* Инициализирует Sentry SDK и настраивает интеграции, transport и другие параметры.
*
* @param releaseVersion - Версия релиза приложения.
*/
const useSentry = (releaseVersion: string): void => {
const { token, attempts, isSendingAllowed, setState } = useSentryStore(state => ({
token: state.token,
attempts: state.attempts,
isSendingAllowed: state.isSendingAllowed,
setState: state.setState,
}));
const { dsn, enabledTokenGeneration, url } = sentryConfig;
// Признак активности механизма генерации токена для отправляемых данных
const isTokenGenerationAvailable = Boolean(enabledTokenGeneration && url);
// Включаем Sentry только в production среде и при доступности генерации токена.
const enabled = Boolean(process.env.NODE_ENV === 'production' && releaseVersion && isTokenGenerationAvailable);
const renewToken = useToken({ enabled, token, isSendingAllowed, attempts, setState });
useEffect(() => {
if (!enabled) return;
// Инициализация подключения к Sentry
init({
dsn,
integrations: [
httpClientIntegration({
// Логируем ошибки запросов с кодами ответов 400-599
failedRequestStatusCodes: [[400, 599]],
// Не логируем ошибки запросов к Sentry, если указан URL для генерации токена
failedRequestTargets: url ? [new RegExp(`^((?!${url}).)*$`)] : undefined,
}),
],
sendDefaultPii: true,
tracesSampleRate: 1.0,
release: releaseVersion,
environment: process.env.NODE_ENV,
ignoreErrors: [/ResizeObserver/],
transport: customTransport({
token,
attempts,
setState,
renewToken,
}),
// Коллбэк для обработки breadcrumbs перед отправкой в Sentry.
beforeBreadcrumb: (breadcrumb, hint) => beforeBreadcrumb(breadcrumb, hint, isSendingAllowed),
// Коллбэк для обработки событий перед отправкой в Sentry.
beforeSend: event => beforeSend(event, isSendingAllowed),
});
return () => {
// Завершение работы Sentry
clientShutdown();
};
}, [dsn, enabled, releaseVersion, renewToken, url]);
};
export { useSentry };
+2
View File
@@ -4,3 +4,5 @@ OMNI_FEATURE_ADAPTER_ENDPOINT="https://omni.psi.dmz.gazprombank.ru"
AB_PAYMENTS_ENDPOINT="https://app.stage.p.ab-payments.ru"
YM_ID=103631136
CLIENT_ID=13c910a3-cda5-4dee-978d-08062604ef75
SENTRY_DSN="https://139b953279d38cb6ce0a52b123abb8cf@dbo-msb-tst3.dmz.gazprombank.ru/32"
SENTRY_URI="/api/32/envelope/"
+1
View File
@@ -31,6 +31,7 @@
"@fractal-ui/overlays": "30.15.0",
"@fractal-ui/styling": "30.14.2",
"@msb/crypto": "1.0.0",
"@msb/sentry": "1.0.0",
"@msb/fractal-ui-composites": "30.15.1",
"@msb/http": "^1.0.0",
"@msb/localization": "1.0.0",
+8
View File
@@ -0,0 +1,8 @@
{
"dsn": "https://139b953279d38cb6ce0a52b123abb8cf@dbo-msb-tst3.dmz.gazprombank.ru/32",
"msg": "KratosAresSiriusMSB",
"customHeaders": {
"Content-Type": "application/octet-stream"
},
"url": "/api/32/envelope/"
}
+4
View File
@@ -1,12 +1,16 @@
import { type ReactElement } from 'react';
import type { RemoteRouteConfigDto } from '@msb/http';
import { useSentry } from '@msb/sentry';
import { MEDIA, YM_GOALS, useMediaQuery, YaMetrikaReachGoal } from '@msb/shared';
import { AppProvider } from './providers';
import { Layout } from '@/pages/Layout';
const App = ({ modules }: { modules: RemoteRouteConfigDto[] }): ReactElement => {
const releaseVersion = process.env.IB_MODULE_VERSION ?? '';
const isDesktop = useMediaQuery(MEDIA.desktop);
useSentry(releaseVersion);
return (
<AppProvider>
<YaMetrikaReachGoal goalType={YM_GOALS.VISIT} params={{ [YM_GOALS.VISIT]: { ext_type: isDesktop ? 'desktop' : 'adaptive' } }} />
@@ -1,5 +1,6 @@
import { useMemo } from 'react';
import { useQuery, type OrganizationDto } from '@msb/http';
import { captureException, TAGS } from '@msb/sentry';
import { fetchClientOrganizations, QUERY_KEYS_ORGANIZATIONS } from '../../api';
import { mapUniqClientOrganizations } from './mapUniqClientOrganizations';
@@ -11,6 +12,13 @@ const useOrganizations = (enabled = true) => {
refetch,
} = useQuery<OrganizationDto[], Error | undefined>({
queryKey: [QUERY_KEYS_ORGANIZATIONS],
onError: err => {
captureException(err, hint => {
hint.setTag(TAGS.request, QUERY_KEYS_ORGANIZATIONS);
return hint;
});
},
queryFn: fetchClientOrganizations,
enabled,
});
@@ -1,5 +1,6 @@
import type { AuthoritiesResponseDto } from '@msb/http';
import { useQuery } from '@msb/http';
import { captureException, TAGS } from '@msb/sentry';
import { fetchUserAuthorities, QUERY_KEY_FETCH_USER_AUTHORITIES } from '@/app/api';
const useUserAuthorities = () => {
@@ -11,6 +12,13 @@ const useUserAuthorities = () => {
} = useQuery<AuthoritiesResponseDto, Error | undefined>({
queryKey: [QUERY_KEY_FETCH_USER_AUTHORITIES],
queryFn: fetchUserAuthorities,
onError: err => {
captureException(err, hint => {
hint.setTag(TAGS.request, QUERY_KEY_FETCH_USER_AUTHORITIES);
return hint;
});
},
});
return {
@@ -1,6 +1,7 @@
import { useRef } from 'react';
import type { UserProfileDto } from '@msb/http';
import { useQuery } from '@msb/http';
import { captureException, TAGS } from '@msb/sentry';
import { fetchUserProfile, QUERY_KEY_USER_PROFILE } from '@/app/api';
const useUserProfile = (enabled = true) => {
@@ -16,6 +17,12 @@ const useUserProfile = (enabled = true) => {
queryFn: fetchUserProfile,
onSettled: (data, error) => {
if (error) {
captureException(error, hint => {
hint.setTag(TAGS.request, QUERY_KEY_USER_PROFILE);
return hint;
});
isLastErrorRef.current = true;
} else if (data) {
isLastErrorRef.current = false;
@@ -1,5 +1,6 @@
import { useRef } from 'react';
import { useQuery, type AgreementsPageResponseDto } from '@msb/http';
import { captureException, TAGS } from '@msb/sentry';
import { fetchAgreementPage, QUERY_KEY_AGREEMENT_PAGE } from '../api';
import { AGREEMENTS_MAX_FETCH_COUNT } from '../constant';
@@ -30,6 +31,12 @@ const useAgreementPage = (enabled = true) => {
const isLastErrorRef = useRef<boolean>(false);
if (agreementsError) {
captureException(agreementsError, hint => {
hint.setTag(TAGS.request, QUERY_KEY_AGREEMENT_PAGE);
return hint;
});
isLastErrorRef.current = true;
} else if (agreements) {
isLastErrorRef.current = false;
@@ -1,5 +1,6 @@
import { useRef } from 'react';
import { useQuery, type UserProfileDto } from '@msb/http';
import { captureException, TAGS } from '@msb/sentry';
import { fetchUserProfile, QUERY_KEY_USER_PROFILE } from '../../api';
const useUserProfile = () => {
@@ -14,6 +15,12 @@ const useUserProfile = () => {
queryFn: fetchUserProfile,
onSettled: (data, error) => {
if (error) {
captureException(error, hint => {
hint.setTag(TAGS.request, QUERY_KEY_USER_PROFILE);
return hint;
});
isLastErrorRef.current = true;
} else if (data) {
isLastErrorRef.current = false;
@@ -1,5 +1,6 @@
import { useQuery } from '@msb/http';
import type { DigitalSignatureToolDto } from '@msb/http/signature';
import { captureException, TAGS } from '@msb/sentry';
import { fetchSignatureList, QUERY_KEY_SIGNATURES_LIST } from '../api';
const useSignatureList = (id: string) => {
@@ -11,6 +12,13 @@ const useSignatureList = (id: string) => {
} = useQuery<DigitalSignatureToolDto[], Error | undefined>({
queryFn: () => fetchSignatureList(id),
queryKey: [QUERY_KEY_SIGNATURES_LIST, id],
onError: err => {
captureException(err, hint => {
hint.setTags({ [TAGS.request]: QUERY_KEY_SIGNATURES_LIST, [TAGS.requestId]: id });
return hint;
});
},
});
return {
+2
View File
@@ -1,6 +1,7 @@
import type { IWebpackAppConfig } from '@msb/mf-builder';
import CopyPlugin from 'copy-webpack-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import webpack from 'webpack';
import path from 'node:path';
import packageJson from './package.json';
@@ -75,6 +76,7 @@ const config: IWebpackAppConfig = {
],
},
plugins: [
new webpack.DefinePlugin({ 'process.env.IB_MODULE_VERSION': JSON.stringify(process.env.IB_MODULE_VERSION) }),
new HtmlWebpackPlugin({
template: path.resolve(publicPath, 'index.html'),
filename: 'index.html',