убрал erroroverlay
This commit is contained in:
@@ -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 {
|
export function installErrorLogging(): void {
|
||||||
if (installed) return;
|
if (installed) return;
|
||||||
installed = true;
|
installed = true;
|
||||||
|
|
||||||
window.addEventListener('unhandledrejection', (e: PromiseRejectionEvent) => {
|
window.addEventListener('unhandledrejection', (e: PromiseRejectionEvent) => {
|
||||||
const reason: unknown = e.reason;
|
logError('Необработанная ошибка', e.reason);
|
||||||
console.error(`${PREFIX} Необработанная ошибка: ${describeReason(reason)}`);
|
});
|
||||||
// Стек и объект — отдельными аргументами: так консоль браузера даёт
|
|
||||||
// раскрыть объект и перейти по стеку, а не показывает «[object Object]».
|
// Синхронные ошибки страницы. Раньше их ловил ErrorOverlay ради углового
|
||||||
if (reason instanceof Error && reason.stack) console.error(`${PREFIX} Стек:`, reason.stack);
|
// уведомления; уведомление убрано (чужая ошибка со страницы SharePoint
|
||||||
console.error(`${PREFIX} Причина целиком:`, reason);
|
// выглядела как поломка портала), а разбор нужен — поэтому он переехал сюда.
|
||||||
|
window.addEventListener('error', (e: ErrorEvent) => {
|
||||||
|
logError('Ошибка на странице', e.error || e.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,73 +83,3 @@
|
|||||||
border-color: $gold-400;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ import styles from './ErrorOverlay.module.scss';
|
|||||||
interface ICaughtError {
|
interface ICaughtError {
|
||||||
message: string;
|
message: string;
|
||||||
stack: string;
|
stack: string;
|
||||||
source: string; // откуда поймана: React-рендер, window.onerror, unhandledrejection
|
source: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IErrorOverlayState {
|
interface IErrorOverlayState {
|
||||||
/** Падение самого рендера — дерево сломано, показывать нечего. */
|
/** Падение самого рендера — дерево сломано, показывать нечего. */
|
||||||
fatal: ICaughtError | undefined;
|
fatal: ICaughtError | undefined;
|
||||||
/** Асинхронная ошибка со страницы — портал работает, это просто уведомление. */
|
|
||||||
notice: ICaughtError | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatError(err: unknown, source: string): ICaughtError {
|
function formatError(err: unknown, source: string): ICaughtError {
|
||||||
@@ -24,7 +22,7 @@ function formatError(err: unknown, source: string): ICaughtError {
|
|||||||
// at vp (request-portal-web-part_<hash>.js:9:397946)
|
// at vp (request-portal-web-part_<hash>.js:9:397946)
|
||||||
// at t.onUnhandledRejection (...)
|
// at t.onUnhandledRejection (...)
|
||||||
//
|
//
|
||||||
// Место, где промис реально отклонился, при этом терялось полностью.
|
// Место, где ошибка реально возникла, при этом терялось полностью.
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
return {
|
return {
|
||||||
message: err.message || describeReason(err),
|
message: err.message || describeReason(err),
|
||||||
@@ -41,112 +39,46 @@ function copyToClipboard(text: string): void {
|
|||||||
navigator.clipboard.writeText(text).catch(() => undefined);
|
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.onerror`) здесь
|
||||||
* `unhandledrejection` вешаются на window, то есть срабатывают на ВСЮ страницу
|
* НЕ ловятся намеренно. Слушатели на window срабатывают на всю страницу
|
||||||
* SharePoint: на другие вебпарты, на скрипты самого SharePoint, на любые
|
* SharePoint — на другие вебпарты, на скрипты самого SharePoint, на любые
|
||||||
* фоновые промисы. Раньше любое такое отклонение — даже постороннее и
|
* фоновые промисы, — и показывать по ним что-то пользователю смысла нет: портал
|
||||||
* безобидное — заменяло собой весь портал: вместо открытой заявки пользователь
|
* при этом работает, а чужая ошибка выглядит как поломка портала. Разбор таких
|
||||||
* получал экран «Необработанное исключение». Именно так это и проявлялось при
|
* ошибок уходит в консоль, см. shared/diagnostics.ts.
|
||||||
* переходе по ссылке на заявку.
|
|
||||||
*
|
|
||||||
* Теперь портал заменяется только при падении РЕНДЕРА (там показывать
|
|
||||||
* действительно нечего), а асинхронные ошибки — угловое уведомление, которое
|
|
||||||
* можно закрыть; работать оно не мешает. Полный разбор причины при этом всё
|
|
||||||
* равно уходит в консоль — см. shared/diagnostics.ts.
|
|
||||||
*/
|
*/
|
||||||
export class ErrorOverlay extends React.Component<React.PropsWithChildren<unknown>, IErrorOverlayState> {
|
export class ErrorOverlay extends React.Component<React.PropsWithChildren<unknown>, IErrorOverlayState> {
|
||||||
public state: IErrorOverlayState = { fatal: undefined, notice: undefined };
|
public state: IErrorOverlayState = { fatal: undefined };
|
||||||
|
|
||||||
private onWindowError = (event: ErrorEvent): void => {
|
public static getDerivedStateFromError(err: unknown): IErrorOverlayState {
|
||||||
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<IErrorOverlayState> {
|
|
||||||
return { fatal: formatError(err, 'React render') };
|
return { fatal: formatError(err, 'React render') };
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
const { fatal, notice } = this.state;
|
const { fatal } = this.state;
|
||||||
|
if (!fatal) return this.props.children;
|
||||||
|
|
||||||
if (fatal) {
|
const fullText = `[${fatal.source}] ${fatal.message}\n\n${fatal.stack}`;
|
||||||
const fullText = fullTextOf(fatal);
|
|
||||||
return (
|
|
||||||
<div className={styles.overlay}>
|
|
||||||
<div className={styles.card}>
|
|
||||||
<div className={styles.title}>Необработанное исключение</div>
|
|
||||||
<div className={styles.source}>{fatal.source}</div>
|
|
||||||
<pre className={styles.stack}>{fullText}</pre>
|
|
||||||
<div className={styles.actions}>
|
|
||||||
<button type="button" className={styles.copyBtn} onClick={() => copyToClipboard(fullText)}>
|
|
||||||
Скопировать
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.reloadBtn}
|
|
||||||
onClick={() => window.location.reload()}
|
|
||||||
>
|
|
||||||
Перезагрузить страницу
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.overlay}>
|
||||||
{this.props.children}
|
<div className={styles.card}>
|
||||||
{notice && (
|
<div className={styles.title}>Необработанное исключение</div>
|
||||||
<div className={styles.toast} role="alert">
|
<div className={styles.source}>{fatal.source}</div>
|
||||||
<div className={styles.toastHead}>
|
<pre className={styles.stack}>{fullText}</pre>
|
||||||
<span className={styles.toastTitle}>Что-то пошло не так</span>
|
<div className={styles.actions}>
|
||||||
<button
|
<button type="button" className={styles.copyBtn} onClick={() => copyToClipboard(fullText)}>
|
||||||
type="button"
|
Скопировать
|
||||||
className={styles.toastClose}
|
</button>
|
||||||
aria-label="Закрыть уведомление"
|
<button type="button" className={styles.reloadBtn} onClick={() => window.location.reload()}>
|
||||||
onClick={() => this.setState({ notice: undefined })}
|
Перезагрузить страницу
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className={styles.toastText}>{notice.message}</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.toastCopy}
|
|
||||||
onClick={() => copyToClipboard(fullTextOf(notice))}
|
|
||||||
>
|
|
||||||
Скопировать детали
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user