@escapenavigator/utils 1.10.170 → 1.10.172

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const classify_api_error_1 = require("./classify-api-error");
4
+ /**
5
+ * Триаж API-ошибок — это «входной фильтр» всего, что попадает в Sentry как
6
+ * `severity=business`. Если он сломается, бизнес-инциденты либо утонут в шуме
7
+ * (debug вместо business), либо наоборот зафлудят дашборд (business на каждый
8
+ * отменённый axios-запрос). Поэтому фиксируем каждую ветку классификатора.
9
+ */
10
+ describe('classifyApiError', () => {
11
+ const base = { apiKey: 'createPreorder', method: 'POST', url: '/openapi/orders' };
12
+ describe('skip (не репортим)', () => {
13
+ it('отменённый axios-запрос (ERR_CANCELED) → skip', () => {
14
+ const r = (0, classify_api_error_1.classifyApiError)({ ...base, code: 'ERR_CANCELED' });
15
+ expect(r.severity).toBe('skip');
16
+ expect(r.shouldReport).toBe(false);
17
+ });
18
+ it('304 (etag-success) → skip', () => {
19
+ const r = (0, classify_api_error_1.classifyApiError)({ ...base, status: 304 });
20
+ expect(r.shouldReport).toBe(false);
21
+ });
22
+ it('401/403 на app/auth → skip (ожидаемый session/role flow)', () => {
23
+ expect((0, classify_api_error_1.classifyApiError)({ ...base, status: 401, app: 'app' }).shouldReport).toBe(false);
24
+ expect((0, classify_api_error_1.classifyApiError)({ ...base, status: 403, app: 'auth' }).shouldReport).toBe(false);
25
+ });
26
+ it('401/403 на widget/orders → НЕ skip (сигнал мониторинга)', () => {
27
+ const r = (0, classify_api_error_1.classifyApiError)({ ...base, status: 403, app: 'widget' });
28
+ expect(r.shouldReport).toBe(true);
29
+ expect(r.severity).toBe('debug');
30
+ });
31
+ });
32
+ describe('critical', () => {
33
+ it.each([500, 502, 503, 504])('%s → critical с status в fingerprint', (status) => {
34
+ const r = (0, classify_api_error_1.classifyApiError)({ ...base, status });
35
+ expect(r.severity).toBe('critical');
36
+ expect(r.level).toBe('error');
37
+ expect(r.fingerprint).toContain(String(status));
38
+ });
39
+ it('таймаут без status (ECONNABORTED) → critical', () => {
40
+ const r = (0, classify_api_error_1.classifyApiError)({ ...base, code: 'ECONNABORTED' });
41
+ expect(r.severity).toBe('critical');
42
+ expect(r.fingerprint).toContain('ECONNABORTED');
43
+ });
44
+ it('совсем странный кейс (нет status/code/message) → critical, дефолтный fingerprint', () => {
45
+ const r = (0, classify_api_error_1.classifyApiError)({ apiKey: 'x' });
46
+ expect(r.severity).toBe('critical');
47
+ expect(r.fingerprint).toBeNull();
48
+ });
49
+ });
50
+ describe('business (4xx с бизнес-кодом — то, ради чего всё затевалось)', () => {
51
+ it('4xx + конкретный errorCode → business, fingerprint по apiKey+code', () => {
52
+ const r = (0, classify_api_error_1.classifyApiError)({ ...base, status: 400, errorCode: 'slotAlreadyOccupied' });
53
+ expect(r.severity).toBe('business');
54
+ expect(r.level).toBe('warning');
55
+ expect(r.fingerprint).toEqual([
56
+ 'api',
57
+ 'business',
58
+ 'createPreorder',
59
+ 'slotAlreadyOccupied',
60
+ ]);
61
+ });
62
+ it('тот же код на разных слотах схлопывается в один issue (стабильный fingerprint)', () => {
63
+ const a = (0, classify_api_error_1.classifyApiError)({ ...base, status: 400, errorCode: 'slotAlreadyOccupied' });
64
+ const b = (0, classify_api_error_1.classifyApiError)({ ...base, status: 409, errorCode: 'slotAlreadyOccupied' });
65
+ expect(a.fingerprint).toEqual(b.fingerprint);
66
+ });
67
+ it('падает на originalErrorCode, если errorCode пуст', () => {
68
+ const r = (0, classify_api_error_1.classifyApiError)({
69
+ ...base,
70
+ status: 400,
71
+ originalErrorCode: 'bookingWindowClosed',
72
+ });
73
+ expect(r.severity).toBe('business');
74
+ expect(r.fingerprint).toContain('bookingWindowClosed');
75
+ });
76
+ it('мобильный обрыв (timeout of 0ms) → business/info', () => {
77
+ const r = (0, classify_api_error_1.classifyApiError)({
78
+ ...base,
79
+ code: 'ECONNABORTED',
80
+ message: 'timeout of 0ms exceeded',
81
+ });
82
+ expect(r.severity).toBe('business');
83
+ expect(r.level).toBe('info');
84
+ expect(r.fingerprint).toContain('mobile-abort');
85
+ });
86
+ it('чистый network error → business', () => {
87
+ expect((0, classify_api_error_1.classifyApiError)({ ...base, code: 'ERR_NETWORK' }).severity).toBe('business');
88
+ expect((0, classify_api_error_1.classifyApiError)({ ...base, message: 'Network Error' }).severity).toBe('business');
89
+ });
90
+ });
91
+ describe('debug (generic 4xx — шум, но репортим info-level)', () => {
92
+ it.each([
93
+ 'BAD_REQUEST',
94
+ 'INTERNAL_ERROR',
95
+ 'VALIDATION_ERROR',
96
+ 'DEFAULT',
97
+ ])('generic-код %s → debug, НЕ business', (errorCode) => {
98
+ const r = (0, classify_api_error_1.classifyApiError)({ ...base, status: 400, errorCode });
99
+ expect(r.severity).toBe('debug');
100
+ expect(r.level).toBe('info');
101
+ });
102
+ it('4xx вообще без кода → debug', () => {
103
+ const r = (0, classify_api_error_1.classifyApiError)({ ...base, status: 422 });
104
+ expect(r.severity).toBe('debug');
105
+ });
106
+ });
107
+ });
108
+ describe('isOfflineNetworkRelatedEvent', () => {
109
+ it('ловит network/timeout по message', () => {
110
+ expect((0, classify_api_error_1.isOfflineNetworkRelatedEvent)('Network Error')).toBe(true);
111
+ expect((0, classify_api_error_1.isOfflineNetworkRelatedEvent)('timeout of 25000ms exceeded')).toBe(true);
112
+ });
113
+ it('ловит по original exception code', () => {
114
+ expect((0, classify_api_error_1.isOfflineNetworkRelatedEvent)('boom', { originalException: { code: 'ERR_NETWORK' } })).toBe(true);
115
+ });
116
+ it('обычную ошибку не считает сетевой', () => {
117
+ expect((0, classify_api_error_1.isOfflineNetworkRelatedEvent)('slot already occupied')).toBe(false);
118
+ });
119
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escapenavigator/utils",
3
- "version": "1.10.170",
3
+ "version": "1.10.172",
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.163",
17
+ "@escapenavigator/types": "^1.10.164",
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": "ad28ed8f74f9a00d33dffd883d7f9ed26d596629"
31
+ "gitHead": "ec475db7eb7e2365207a19ac0e6cd15f86be35ac"
32
32
  }