@escapenavigator/utils 1.10.135 → 1.10.137
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/classify-api-error.d.ts +59 -0
- package/dist/classify-api-error.js +133 -0
- package/dist/format-amount/index.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Единая классификация ошибок HTTP для Sentry-репорта во всех фронтах
|
|
3
|
+
* (`app`, `orders`, `widget`, `auth`).
|
|
4
|
+
*
|
|
5
|
+
* Цель: вместо «5xx → шлём, всё остальное → молчим» получить понятную
|
|
6
|
+
* таксономию, по которой можно фильтровать issue в Sentry-UI и группировать
|
|
7
|
+
* однотипные бизнес-инциденты.
|
|
8
|
+
*
|
|
9
|
+
* critical → level=error, tag severity=critical
|
|
10
|
+
* 5xx, network/timeout онлайн, внутренние ошибки бэка
|
|
11
|
+
* без бизнес-кода. Всегда баг — наш или upstream.
|
|
12
|
+
*
|
|
13
|
+
* business → level=warning, tag severity=business
|
|
14
|
+
* 4xx с конкретным `errorCode` (SLOT_TAKEN, PROMO_INVALID,
|
|
15
|
+
* ALREADY_PAID, ...). Не баг сам по себе, но если по одному
|
|
16
|
+
* endpoint+code за день копится много событий — повод проверить
|
|
17
|
+
* UX или валидацию на бэке.
|
|
18
|
+
*
|
|
19
|
+
* debug → level=info, tag severity=debug
|
|
20
|
+
* 4xx с generic кодом (BAD_REQUEST, INTERNAL_ERROR, DEFAULT)
|
|
21
|
+
* без бизнес-смысла, либо валидации форм. Шум, но иногда
|
|
22
|
+
* полезно посмотреть.
|
|
23
|
+
*
|
|
24
|
+
* skip → не репортим
|
|
25
|
+
* 401/403 (handled через onUnauthorized + редирект на /auth),
|
|
26
|
+
* 304 (etag-success), отменённые axios-запросы (ERR_CANCELED).
|
|
27
|
+
*
|
|
28
|
+
* Группировка (`fingerprint`) делается так, чтобы все события с одинаковым
|
|
29
|
+
* `apiKey + errorCode` сливались в один issue. Иначе Sentry плодит отдельный
|
|
30
|
+
* issue под каждое сообщение от бэка («Слот занят на 19:00» vs «Слот занят
|
|
31
|
+
* на 20:00»), и в дашборде каша.
|
|
32
|
+
*/
|
|
33
|
+
export type ApiErrorSeverity = 'critical' | 'business' | 'debug' | 'skip';
|
|
34
|
+
export type SentryLevel = 'fatal' | 'error' | 'warning' | 'info' | 'debug' | 'log';
|
|
35
|
+
export type ApiErrorClassificationContext = {
|
|
36
|
+
apiKey: string;
|
|
37
|
+
method?: string;
|
|
38
|
+
url?: string;
|
|
39
|
+
status?: number;
|
|
40
|
+
/** Сетевой код axios (ECONNABORTED, ERR_CANCELED, ECONNRESET, ...) */
|
|
41
|
+
code?: string;
|
|
42
|
+
/** Бизнес-код из тела 4xx-ответа (ErrorData.errorCode) */
|
|
43
|
+
errorCode?: string;
|
|
44
|
+
/** Изначальный бизнес-код до маппинга (originalErrorCode) */
|
|
45
|
+
originalErrorCode?: string;
|
|
46
|
+
};
|
|
47
|
+
export type ApiErrorClassification = {
|
|
48
|
+
severity: ApiErrorSeverity;
|
|
49
|
+
/** Sentry level: для skip — не используется. */
|
|
50
|
+
level: SentryLevel;
|
|
51
|
+
/**
|
|
52
|
+
* Группа для fingerprint в Sentry. `null` означает «оставить дефолтный
|
|
53
|
+
* fingerprint Sentry» (только для уникальных-навсегда событий).
|
|
54
|
+
*/
|
|
55
|
+
fingerprint: string[] | null;
|
|
56
|
+
/** Удобный shorthand: `severity !== 'skip'`. */
|
|
57
|
+
shouldReport: boolean;
|
|
58
|
+
};
|
|
59
|
+
export declare function classifyApiError(ctx: ApiErrorClassificationContext): ApiErrorClassification;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Единая классификация ошибок HTTP для Sentry-репорта во всех фронтах
|
|
4
|
+
* (`app`, `orders`, `widget`, `auth`).
|
|
5
|
+
*
|
|
6
|
+
* Цель: вместо «5xx → шлём, всё остальное → молчим» получить понятную
|
|
7
|
+
* таксономию, по которой можно фильтровать issue в Sentry-UI и группировать
|
|
8
|
+
* однотипные бизнес-инциденты.
|
|
9
|
+
*
|
|
10
|
+
* critical → level=error, tag severity=critical
|
|
11
|
+
* 5xx, network/timeout онлайн, внутренние ошибки бэка
|
|
12
|
+
* без бизнес-кода. Всегда баг — наш или upstream.
|
|
13
|
+
*
|
|
14
|
+
* business → level=warning, tag severity=business
|
|
15
|
+
* 4xx с конкретным `errorCode` (SLOT_TAKEN, PROMO_INVALID,
|
|
16
|
+
* ALREADY_PAID, ...). Не баг сам по себе, но если по одному
|
|
17
|
+
* endpoint+code за день копится много событий — повод проверить
|
|
18
|
+
* UX или валидацию на бэке.
|
|
19
|
+
*
|
|
20
|
+
* debug → level=info, tag severity=debug
|
|
21
|
+
* 4xx с generic кодом (BAD_REQUEST, INTERNAL_ERROR, DEFAULT)
|
|
22
|
+
* без бизнес-смысла, либо валидации форм. Шум, но иногда
|
|
23
|
+
* полезно посмотреть.
|
|
24
|
+
*
|
|
25
|
+
* skip → не репортим
|
|
26
|
+
* 401/403 (handled через onUnauthorized + редирект на /auth),
|
|
27
|
+
* 304 (etag-success), отменённые axios-запросы (ERR_CANCELED).
|
|
28
|
+
*
|
|
29
|
+
* Группировка (`fingerprint`) делается так, чтобы все события с одинаковым
|
|
30
|
+
* `apiKey + errorCode` сливались в один issue. Иначе Sentry плодит отдельный
|
|
31
|
+
* issue под каждое сообщение от бэка («Слот занят на 19:00» vs «Слот занят
|
|
32
|
+
* на 20:00»), и в дашборде каша.
|
|
33
|
+
*/
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.classifyApiError = classifyApiError;
|
|
36
|
+
/**
|
|
37
|
+
* Бекенд маппит часть ошибок на эти generic-коды (см.
|
|
38
|
+
* `packages/utils/src/get-service-error.ts:INTERNAL_SERVICE_ERROR_CODES`).
|
|
39
|
+
* Если errorCode из этого списка — никакого «бизнеса» в ошибке нет, это
|
|
40
|
+
* generic-ошибка валидации/внутренней проблемы. Шлём как debug.
|
|
41
|
+
*/
|
|
42
|
+
const GENERIC_ERROR_CODES = new Set([
|
|
43
|
+
'BAD_REQUEST',
|
|
44
|
+
'INTERNAL_SERVER_ERROR',
|
|
45
|
+
'INTERNAL_ERROR',
|
|
46
|
+
'WS_ERROR',
|
|
47
|
+
'DEFAULT',
|
|
48
|
+
'VALIDATION_ERROR',
|
|
49
|
+
]);
|
|
50
|
+
/**
|
|
51
|
+
* Сетевые axios-коды, которые в продакшене НЕ означают баг фронта/бэка:
|
|
52
|
+
* ERR_CANCELED — мы сами отменили запрос (см. useApiMethod source.cancel()
|
|
53
|
+
* на каждом новом fetch). Засчитывать как ошибку нельзя — это нормальный
|
|
54
|
+
* lifecycle компонента.
|
|
55
|
+
*/
|
|
56
|
+
const SKIP_AXIOS_CODES = new Set(['ERR_CANCELED', 'CANCELED']);
|
|
57
|
+
/**
|
|
58
|
+
* 401 = expired session, у нас глобальный `onUnauthorized` → редирект на
|
|
59
|
+
* /auth. Sentry от такого не получает никакой полезной инфы.
|
|
60
|
+
*
|
|
61
|
+
* 403 = доступ закрыт. На CRM это тоже expected (роль пользователя), на
|
|
62
|
+
* widget/orders просто не должно случаться — но если случилось, это
|
|
63
|
+
* config issue, нам нужен бэк-лог, а не дубль на фронте.
|
|
64
|
+
*
|
|
65
|
+
* 304 = успешный ответ при if-none-match (etag), axios считает это
|
|
66
|
+
* ошибкой только потому что код != 200.
|
|
67
|
+
*/
|
|
68
|
+
const SKIP_HTTP_STATUSES = new Set([401, 403, 304]);
|
|
69
|
+
function classifyApiError(ctx) {
|
|
70
|
+
const { status, code, apiKey, errorCode, originalErrorCode } = ctx;
|
|
71
|
+
if (code && SKIP_AXIOS_CODES.has(code)) {
|
|
72
|
+
return { severity: 'skip', level: 'info', fingerprint: null, shouldReport: false };
|
|
73
|
+
}
|
|
74
|
+
if (typeof status === 'number' && SKIP_HTTP_STATUSES.has(status)) {
|
|
75
|
+
return { severity: 'skip', level: 'info', fingerprint: null, shouldReport: false };
|
|
76
|
+
}
|
|
77
|
+
// 5xx → critical. Включаем status в fingerprint, чтобы 502/503/504 не
|
|
78
|
+
// схлопывались в один issue с 500 — у них разные источники проблем
|
|
79
|
+
// (proxy/CDN vs само приложение vs gateway-timeout).
|
|
80
|
+
if (typeof status === 'number' && status >= 500) {
|
|
81
|
+
return {
|
|
82
|
+
severity: 'critical',
|
|
83
|
+
level: 'error',
|
|
84
|
+
fingerprint: ['api', 'critical', apiKey, String(status), errorCode || '_'],
|
|
85
|
+
shouldReport: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// 0-status + ECONNABORTED/ETIMEDOUT — наш timeout сработал (по умолчанию
|
|
89
|
+
// 25s для widget, 0 для остальных). Это сигнал «бэк не успел ответить» —
|
|
90
|
+
// critical, чтобы видеть на проде если ручка стала медленной.
|
|
91
|
+
if (!status &&
|
|
92
|
+
(code === 'ECONNABORTED' || code === 'ETIMEDOUT' || code === 'ECONNRESET')) {
|
|
93
|
+
return {
|
|
94
|
+
severity: 'critical',
|
|
95
|
+
level: 'error',
|
|
96
|
+
fingerprint: ['api', 'critical', apiKey, code],
|
|
97
|
+
shouldReport: true,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// 4xx → business либо debug. Решаем по errorCode из тела ответа:
|
|
101
|
+
// - конкретный бизнес-код (SLOT_TAKEN, PROMO_INVALID, ...) → business
|
|
102
|
+
// - generic (BAD_REQUEST, INTERNAL_ERROR) или его отсутствие → debug
|
|
103
|
+
//
|
|
104
|
+
// Фингерпринт по `apiKey + errorCode`: все события «createPreorder
|
|
105
|
+
// упал с SLOT_TAKEN» соберутся в один issue, независимо от того,
|
|
106
|
+
// какой слот занят.
|
|
107
|
+
if (typeof status === 'number' && status >= 400 && status < 500) {
|
|
108
|
+
const effectiveCode = errorCode || originalErrorCode;
|
|
109
|
+
if (effectiveCode && !GENERIC_ERROR_CODES.has(effectiveCode)) {
|
|
110
|
+
return {
|
|
111
|
+
severity: 'business',
|
|
112
|
+
level: 'warning',
|
|
113
|
+
fingerprint: ['api', 'business', apiKey, effectiveCode],
|
|
114
|
+
shouldReport: true,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
severity: 'debug',
|
|
119
|
+
level: 'info',
|
|
120
|
+
fingerprint: ['api', 'debug', apiKey, String(status), effectiveCode || '_'],
|
|
121
|
+
shouldReport: true,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// Сюда падают: NETWORK_ERROR без axios-кода, какие-то странные кейсы
|
|
125
|
+
// без status. Считаем critical, но без агрессивного группирования —
|
|
126
|
+
// пусть Sentry разберётся по stacktrace.
|
|
127
|
+
return {
|
|
128
|
+
severity: 'critical',
|
|
129
|
+
level: 'error',
|
|
130
|
+
fingerprint: null,
|
|
131
|
+
shouldReport: true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -13,7 +13,7 @@ const getCurrency = (currency) => {
|
|
|
13
13
|
[profile_currency_1.ProfileCurrencyEnum.CZK]: 'Kč', // Чешская крона
|
|
14
14
|
[profile_currency_1.ProfileCurrencyEnum.PLN]: 'zł', // Польский злотый
|
|
15
15
|
[profile_currency_1.ProfileCurrencyEnum.HUF]: 'Ft', // Венгерский форинт
|
|
16
|
-
[profile_currency_1.ProfileCurrencyEnum.RUB]: '
|
|
16
|
+
[profile_currency_1.ProfileCurrencyEnum.RUB]: 'руб.',
|
|
17
17
|
[profile_currency_1.ProfileCurrencyEnum.BGN]: 'лв',
|
|
18
18
|
[profile_currency_1.ProfileCurrencyEnum.CAD]: 'C$',
|
|
19
19
|
[profile_currency_1.ProfileCurrencyEnum.AUD]: '$',
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./agreement-config"), exports);
|
|
18
18
|
__exportStar(require("./booking-lead-time"), exports);
|
|
19
|
+
__exportStar(require("./classify-api-error"), exports);
|
|
19
20
|
__exportStar(require("./convert-hhmm-to-minutes"), exports);
|
|
20
21
|
__exportStar(require("./convert-minutes-to-hhmm"), exports);
|
|
21
22
|
__exportStar(require("./convert-to-options"), exports);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@escapenavigator/utils",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.137",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"test": "jest"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@escapenavigator/types": "^1.10.
|
|
17
|
+
"@escapenavigator/types": "^1.10.133",
|
|
18
18
|
"axios": "^0.21.4",
|
|
19
19
|
"class-transformer": "^0.5.1",
|
|
20
20
|
"class-validator": "^0.13.2",
|
|
@@ -28,5 +28,5 @@
|
|
|
28
28
|
"ts-jest": "^29.1.1",
|
|
29
29
|
"typescript": "^5.6"
|
|
30
30
|
},
|
|
31
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "e6562ef1dbf0652659bc037c3574e4f188663bf1"
|
|
32
32
|
}
|