// Читаемый лог необработанных отклонений промисов. // // Зачем: SPFx вешает на window свой обработчик unhandledrejection, и в консоль // попадает только его собственный стек внутри минифицированного бандла — // примерно такой: // // Error // at vp (...request-portal-web-part_.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); }); }