Files
request-portal/src/webparts/requestPortal/shared/diagnostics.ts
T
gorbenko.pavel bf706907de правки
2026-08-21 09:45:20 +05:00

103 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Читаемый лог необработанных отклонений промисов.
//
// Зачем: SPFx вешает на window свой обработчик unhandledrejection, и в консоль
// попадает только его собственный стек внутри минифицированного бандла —
// примерно такой:
//
// Error
// at vp (...request-portal-web-part_<hash>.js:9:397946)
// at t.onUnhandledRejection (...)
//
// Ни причины, ни места, где промис отклонился, там нет: SPFx логирует САМ факт,
// а не `reason`. Поэтому вешаем свой слушатель — он срабатывает наравне с
// SPFx-овским и печатает то, что действительно нужно: сообщение, стек и сам
// объект причины (у ошибок PnPjs/SharePoint внутри лежит ответ сервера).
//
// Ничего не подавляем (preventDefault не зовём) — поведение портала не
// меняется, добавляется только диагностика.
const PREFIX = '[Портал заявок]';
// Слушатель вешается один раз на страницу: на странице может быть несколько
// экземпляров веб-части, и каждый вызвал бы install() в onInit — без флага
// одна и та же ошибка печаталась бы столько раз, сколько их размещено.
let installed = false;
// Причиной отклонения может быть что угодно, не только Error — у PnPjs это
// бывает объект с полями ответа. Разбираем по факту, а не по ожиданию.
//
// Отдельно разобраны случаи, из-за которых раньше в отчёт уходила пустая
// строка. Промис можно отклонить вообще без причины (`Promise.reject()`), и
// тогда reason === undefined: JSON.stringify возвращает на нём НЕ строку, а
// undefined, из-за чего сообщение схлопывалось в пустоту и по отчёту нельзя
// было понять даже того, что причины не было вовсе.
export function describeReason(reason: unknown): string {
if (reason === undefined) return 'промис отклонён без причины (reject без аргумента)';
if (reason === null) return 'промис отклонён значением null';
if (reason instanceof Error) {
return `${reason.name}: ${reason.message || '(пустое сообщение)'}`;
}
if (typeof reason === 'string') return reason || '(пустая строка)';
if (typeof reason !== 'object') return `${typeof reason}: ${String(reason)}`;
// Тип объекта указываем всегда: у PnPjs/SharePoint по одному имени класса
// (HttpRequestError и т.п.) уже понятно, куда смотреть.
const kind = (reason as { constructor?: { name?: string } }).constructor?.name || 'Object';
try {
return `${kind}: ${JSON.stringify(reason)}`;
} catch {
// Циклические ссылки (внутри ошибок PnPjs лежит Response) — тогда хотя бы
// перечислим ключи: по ним видно, что это за объект.
return `${kind}: {${Object.keys(reason as object).join(', ')}}`;
}
}
// PnPjs/SharePoint кладёт в Error.message текст ответа сервера целиком —
// например: `Error making HttpClient request in queryable [404] ::>
// {"odata.error":{"message":{"value":"Элемент не существует..."}}}`. Пока это
// строка, а не распарсенный JSON, русские буквы в ней остаются как литеральные
// escape-последовательности `Эл...`, и в UI вместо кириллицы видно
// набор из "\u". Достаём odata.error.message.value через JSON.parse — сам
// парсер раскодирует escape-последовательности обратно в текст.
export function friendlyErrorMessage(e: unknown): string {
const message = e instanceof Error ? e.message : String(e);
const start = message.indexOf('{');
const end = message.lastIndexOf('}');
if (start !== -1 && end > start) {
try {
const parsed = JSON.parse(message.slice(start, end + 1)) as {
'odata.error'?: { message?: { value?: string } };
};
const value = parsed['odata.error']?.message?.value;
if (value) return value;
} catch {
// Тело не JSON (или не то, что ожидали) — отдаём исходное сообщение.
}
}
return message;
}
function logError(kind: string, reason: unknown): void {
console.error(`${PREFIX} ${kind}: ${describeReason(reason)}`);
// Стек и объект — отдельными аргументами: так консоль браузера даёт
// раскрыть объект и перейти по стеку, а не показывает «[object Object]».
if (reason instanceof Error && reason.stack) console.error(`${PREFIX} Стек:`, reason.stack);
console.error(`${PREFIX} Причина целиком:`, reason);
}
export function installErrorLogging(): void {
if (installed) return;
installed = true;
window.addEventListener('unhandledrejection', (e: PromiseRejectionEvent) => {
logError('Необработанная ошибка', e.reason);
});
// Синхронные ошибки страницы. Раньше их ловил ErrorOverlay ради углового
// уведомления; уведомление убрано (чужая ошибка со страницы SharePoint
// выглядела как поломка портала), а разбор нужен — поэтому он переехал сюда.
window.addEventListener('error', (e: ErrorEvent) => {
logError('Ошибка на странице', e.error || e.message);
});
}