@escapenavigator/utils 2.1.2 → 2.1.4

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.
@@ -87,8 +87,37 @@ const isNetworkErrorMessage = (message) => typeof message === 'string' && /netwo
87
87
  * («timeout of 25000ms exceeded») этому не соответствует и остаётся critical.
88
88
  */
89
89
  const isClientAbortMessage = (message) => typeof message === 'string' && /timeout of 0ms|request aborted/i.test(message);
90
+ /**
91
+ * Запрос не смог даже открыться, потому что `XMLHttpRequest` на странице
92
+ * пропатчен сторонним кодом: расширение-блокировщик, CMP или аналитика
93
+ * хост-сайта подменяют `open`, тихо не открывают запрос, и следующий
94
+ * `setRequestHeader` в xhr-адаптере axios бросает `InvalidStateError`
95
+ * (`DOMException.code === 11`).
96
+ *
97
+ * Своего бага здесь нет — ни фронтового, ни бэкового, поэтому `critical` (куда
98
+ * это падало раньше как «странный кейс без status и сетевого кода») врёт.
99
+ * Но и молчать нельзя: на таком хосте гость не может забронировать, пока
100
+ * виджет не уйдёт на fetch-транспорт. Отдаём отдельный business-бакет со
101
+ * стабильным fingerprint'ом, чтобы все embed-домены собирались в один issue.
102
+ */
103
+ const isXhrBlockedError = (ctx) => {
104
+ const isInvalidState = ctx.code === 'InvalidStateError' ||
105
+ ctx.code === 11 ||
106
+ (typeof ctx.message === 'string' && /InvalidStateError/i.test(ctx.message));
107
+ if (!isInvalidState)
108
+ return false;
109
+ return typeof ctx.message === 'string' && /XMLHttpRequest/i.test(ctx.message);
110
+ };
90
111
  function classifyApiError(ctx) {
91
112
  const { status, code, apiKey, errorCode, originalErrorCode, message, app } = ctx;
113
+ if (!status && isXhrBlockedError(ctx)) {
114
+ return {
115
+ severity: 'business',
116
+ level: 'warning',
117
+ fingerprint: ['api', 'client-env', 'xhr-blocked'],
118
+ shouldReport: true,
119
+ };
120
+ }
92
121
  if (code && SKIP_AXIOS_CODES.has(code)) {
93
122
  return { severity: 'skip', level: 'info', fingerprint: null, shouldReport: false };
94
123
  }
@@ -104,6 +104,45 @@ describe('classifyApiError', () => {
104
104
  expect(r.fingerprint).toBeNull();
105
105
  });
106
106
  });
107
+ describe('сломанный хостом XMLHttpRequest (WIDGET-8D)', () => {
108
+ // Расширение/CMP на странице партнёра патчит XHR.open, запрос не
109
+ // открывается, и axios падает на setRequestHeader. Раньше это уезжало
110
+ // в critical как «странный кейс».
111
+ const xhrBlocked = {
112
+ ...base,
113
+ apiKey: 'getWidgetData',
114
+ method: 'GET',
115
+ url: '/openapi/widget/info',
116
+ message: "Failed to execute 'setRequestHeader' on 'XMLHttpRequest': " +
117
+ 'The object\u2019s state must be OPENED.',
118
+ };
119
+ it('DOMException.code 11 → business, а не critical', () => {
120
+ const r = (0, classify_api_error_1.classifyApiError)({ ...xhrBlocked, code: 11 });
121
+ expect(r.severity).toBe('business');
122
+ expect(r.level).toBe('warning');
123
+ expect(r.shouldReport).toBe(true);
124
+ });
125
+ it('все embed-домены схлопываются в один issue', () => {
126
+ expect((0, classify_api_error_1.classifyApiError)({ ...xhrBlocked, code: 'InvalidStateError' }).fingerprint).toEqual([
127
+ 'api',
128
+ 'client-env',
129
+ 'xhr-blocked',
130
+ ]);
131
+ });
132
+ it('ответ сервера важнее: со status классификация обычная', () => {
133
+ const r = (0, classify_api_error_1.classifyApiError)({ ...xhrBlocked, status: 500 });
134
+ expect(r.severity).toBe('critical');
135
+ });
136
+ it('чужой InvalidStateError без XHR в сообщении не перехватываем', () => {
137
+ const r = (0, classify_api_error_1.classifyApiError)({
138
+ ...base,
139
+ code: 'InvalidStateError',
140
+ message: 'The object is in an invalid state',
141
+ });
142
+ expect(r.fingerprint).toBeNull();
143
+ expect(r.severity).toBe('critical');
144
+ });
145
+ });
107
146
  describe('business (4xx с бизнес-кодом — то, ради чего всё затевалось)', () => {
108
147
  it('4xx + конкретный errorCode → business, fingerprint по apiKey+code', () => {
109
148
  const r = (0, classify_api_error_1.classifyApiError)({ ...base, status: 400, errorCode: 'slotAlreadyOccupied' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escapenavigator/utils",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
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": "^2.1.2",
17
+ "@escapenavigator/types": "^2.1.4",
18
18
  "axios": "^0.21.4",
19
19
  "class-transformer": "^0.5.1",
20
20
  "class-validator": "^0.13.2",
@@ -29,5 +29,5 @@
29
29
  "ts-jest": "^29.1.1",
30
30
  "typescript": "^5.6"
31
31
  },
32
- "gitHead": "dd6e8ce621abae35e9e462a7a1e1a3cf54cdbf6f"
32
+ "gitHead": "e389cd613c0b7b746d6bf65b24e86c4ce25ce400"
33
33
  }