@escapenavigator/utils 2.1.3 → 2.1.5
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,41 @@ 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(['api', 'client-env', 'xhr-blocked']);
|
|
127
|
+
});
|
|
128
|
+
it('ответ сервера важнее: со status классификация обычная', () => {
|
|
129
|
+
const r = (0, classify_api_error_1.classifyApiError)({ ...xhrBlocked, status: 500 });
|
|
130
|
+
expect(r.severity).toBe('critical');
|
|
131
|
+
});
|
|
132
|
+
it('чужой InvalidStateError без XHR в сообщении не перехватываем', () => {
|
|
133
|
+
const r = (0, classify_api_error_1.classifyApiError)({
|
|
134
|
+
...base,
|
|
135
|
+
code: 'InvalidStateError',
|
|
136
|
+
message: 'The object is in an invalid state',
|
|
137
|
+
});
|
|
138
|
+
expect(r.fingerprint).toBeNull();
|
|
139
|
+
expect(r.severity).toBe('critical');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
107
142
|
describe('business (4xx с бизнес-кодом — то, ради чего всё затевалось)', () => {
|
|
108
143
|
it('4xx + конкретный errorCode → business, fingerprint по apiKey+code', () => {
|
|
109
144
|
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.
|
|
3
|
+
"version": "2.1.5",
|
|
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.
|
|
17
|
+
"@escapenavigator/types": "^2.1.5",
|
|
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": "
|
|
32
|
+
"gitHead": "d8214323f0f1fc011d7c57a80e1d148ddad821bc"
|
|
33
33
|
}
|