diff --git a/src/webparts/requestPortal/shared/diagnostics.ts b/src/webparts/requestPortal/shared/diagnostics.ts index 58552bb..69a44e4 100644 --- a/src/webparts/requestPortal/shared/diagnostics.ts +++ b/src/webparts/requestPortal/shared/diagnostics.ts @@ -52,16 +52,26 @@ export function describeReason(reason: unknown): string { } } +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) => { - const reason: unknown = e.reason; - console.error(`${PREFIX} Необработанная ошибка: ${describeReason(reason)}`); - // Стек и объект — отдельными аргументами: так консоль браузера даёт - // раскрыть объект и перейти по стеку, а не показывает «[object Object]». - if (reason instanceof Error && reason.stack) console.error(`${PREFIX} Стек:`, reason.stack); - console.error(`${PREFIX} Причина целиком:`, reason); + logError('Необработанная ошибка', e.reason); + }); + + // Синхронные ошибки страницы. Раньше их ловил ErrorOverlay ради углового + // уведомления; уведомление убрано (чужая ошибка со страницы SharePoint + // выглядела как поломка портала), а разбор нужен — поэтому он переехал сюда. + window.addEventListener('error', (e: ErrorEvent) => { + logError('Ошибка на странице', e.error || e.message); }); } diff --git a/src/webparts/requestPortal/shared/ui/ErrorOverlay/ErrorOverlay.module.scss b/src/webparts/requestPortal/shared/ui/ErrorOverlay/ErrorOverlay.module.scss index 8563c27..97dc0c7 100644 --- a/src/webparts/requestPortal/shared/ui/ErrorOverlay/ErrorOverlay.module.scss +++ b/src/webparts/requestPortal/shared/ui/ErrorOverlay/ErrorOverlay.module.scss @@ -83,73 +83,3 @@ border-color: $gold-400; } } - -// Уведомление об АСИНХРОННОЙ ошибке: в углу, поверх содержимого, но ничего не -// перекрывает и не блокирует — портал остаётся рабочим (см. ErrorOverlay.tsx о -// том, почему такие ошибки больше не схлопывают вебпарт). -.toast { - position: fixed; - right: $sp-4; - bottom: $sp-4; - z-index: 999998; - box-sizing: border-box; - width: 360px; - max-width: calc(100vw - #{$sp-4} * 2); - padding: $sp-3 $sp-4; - border-radius: $r-md; - border-left: 4px solid $red-500; - background: $white; - box-shadow: $sh-modal; - font-family: $font-sans; -} - -.toastHead { - display: flex; - align-items: center; - justify-content: space-between; - gap: $sp-2; -} - -.toastTitle { - font-size: $fs-sm; - font-weight: 600; - color: $navy-950; -} - -.toastClose { - padding: 0 4px; - border: 0; - background: none; - color: $gray-500; - font-size: 18px; - line-height: 1; - cursor: pointer; - - &:hover { - color: $navy-800; - } -} - -.toastText { - margin-top: $sp-1; - max-height: 96px; - overflow: auto; - font-size: $fs-caption; - color: $gray-900; - word-break: break-word; -} - -.toastCopy { - margin-top: $sp-2; - padding: 4px 10px; - border: 1px solid $gray-300; - border-radius: $r-sm; - background: $white; - color: $navy-800; - font-size: $fs-caption; - cursor: pointer; - - &:hover { - border-color: $gold-400; - } -} diff --git a/src/webparts/requestPortal/shared/ui/ErrorOverlay/ErrorOverlay.tsx b/src/webparts/requestPortal/shared/ui/ErrorOverlay/ErrorOverlay.tsx index e7a9b35..e01f753 100644 --- a/src/webparts/requestPortal/shared/ui/ErrorOverlay/ErrorOverlay.tsx +++ b/src/webparts/requestPortal/shared/ui/ErrorOverlay/ErrorOverlay.tsx @@ -5,14 +5,12 @@ import styles from './ErrorOverlay.module.scss'; interface ICaughtError { message: string; stack: string; - source: string; // откуда поймана: React-рендер, window.onerror, unhandledrejection + source: string; } interface IErrorOverlayState { /** Падение самого рендера — дерево сломано, показывать нечего. */ fatal: ICaughtError | undefined; - /** Асинхронная ошибка со страницы — портал работает, это просто уведомление. */ - notice: ICaughtError | undefined; } function formatError(err: unknown, source: string): ICaughtError { @@ -24,7 +22,7 @@ function formatError(err: unknown, source: string): ICaughtError { // at vp (request-portal-web-part_.js:9:397946) // at t.onUnhandledRejection (...) // - // Место, где промис реально отклонился, при этом терялось полностью. + // Место, где ошибка реально возникла, при этом терялось полностью. if (err instanceof Error) { return { message: err.message || describeReason(err), @@ -41,112 +39,46 @@ function copyToClipboard(text: string): void { navigator.clipboard.writeText(text).catch(() => undefined); } -function fullTextOf(error: ICaughtError): string { - return `[${error.source}] ${error.message}\n\n${error.stack}`; -} - /** - * Ловит падения, которые иначе схлопывают вебпарт в SPFx workbench. + * Граница ошибок React: ловит падение РЕНДЕРА, из-за которого вебпарт иначе + * схлопывается в пустое место, и показывает вместо него текст ошибки со стеком. * - * Разделение на fatal и notice — не косметика. Слушатели `error` и - * `unhandledrejection` вешаются на window, то есть срабатывают на ВСЮ страницу - * SharePoint: на другие вебпарты, на скрипты самого SharePoint, на любые - * фоновые промисы. Раньше любое такое отклонение — даже постороннее и - * безобидное — заменяло собой весь портал: вместо открытой заявки пользователь - * получал экран «Необработанное исключение». Именно так это и проявлялось при - * переходе по ссылке на заявку. - * - * Теперь портал заменяется только при падении РЕНДЕРА (там показывать - * действительно нечего), а асинхронные ошибки — угловое уведомление, которое - * можно закрыть; работать оно не мешает. Полный разбор причины при этом всё - * равно уходит в консоль — см. shared/diagnostics.ts. + * Асинхронные ошибки страницы (`unhandledrejection`, `window.onerror`) здесь + * НЕ ловятся намеренно. Слушатели на window срабатывают на всю страницу + * SharePoint — на другие вебпарты, на скрипты самого SharePoint, на любые + * фоновые промисы, — и показывать по ним что-то пользователю смысла нет: портал + * при этом работает, а чужая ошибка выглядит как поломка портала. Разбор таких + * ошибок уходит в консоль, см. shared/diagnostics.ts. */ export class ErrorOverlay extends React.Component, IErrorOverlayState> { - public state: IErrorOverlayState = { fatal: undefined, notice: undefined }; + public state: IErrorOverlayState = { fatal: undefined }; - private onWindowError = (event: ErrorEvent): void => { - this.showNotice(formatError(event.error || event.message, 'window.onerror')); - }; - - private onUnhandledRejection = (event: PromiseRejectionEvent): void => { - this.showNotice(formatError(event.reason, 'unhandledrejection')); - }; - - private showNotice(notice: ICaughtError): void { - // Первое уведомление не затираем: за одной ошибкой обычно летит несколько - // производных, а разбираться нужно с исходной. - this.setState((prev) => (prev.notice ? prev : { ...prev, notice })); - } - - public componentDidMount(): void { - window.addEventListener('error', this.onWindowError); - window.addEventListener('unhandledrejection', this.onUnhandledRejection); - } - - public componentWillUnmount(): void { - window.removeEventListener('error', this.onWindowError); - window.removeEventListener('unhandledrejection', this.onUnhandledRejection); - } - - public static getDerivedStateFromError(err: unknown): Partial { + public static getDerivedStateFromError(err: unknown): IErrorOverlayState { return { fatal: formatError(err, 'React render') }; } public render(): React.ReactNode { - const { fatal, notice } = this.state; + const { fatal } = this.state; + if (!fatal) return this.props.children; - if (fatal) { - const fullText = fullTextOf(fatal); - return ( -
-
-
Необработанное исключение
-
{fatal.source}
-
{fullText}
-
- - -
-
-
- ); - } + const fullText = `[${fatal.source}] ${fatal.message}\n\n${fatal.stack}`; return ( - <> - {this.props.children} - {notice && ( -
-
- Что-то пошло не так - -
-
{notice.message}
- +
- )} - + + ); } }