103 lines
6.3 KiB
TypeScript
103 lines
6.3 KiB
TypeScript
// Читаемый лог необработанных отклонений промисов.
|
||
//
|
||
// Зачем: 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);
|
||
});
|
||
}
|