feat: Добавление компонентов ErrorBoundary и NetworkErrorHandler для улучшения обработки ошибок в приложении. Увеличение таймаута запросов до 30 секунд и улучшение логики повторных попыток с детализированными уведомлениями об ошибках.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m43s

This commit is contained in:
2025-10-03 15:49:28 +07:00
parent 8954c58744
commit cd6a971991
12 changed files with 2694 additions and 33 deletions
+672
View File
@@ -0,0 +1,672 @@
# 🛡️ Руководство по обработке ошибок
Комплексная система обработки ошибок для Router Lists UI, включающая автоматический retry, user-friendly сообщения и мониторинг состояния сети.
## 📚 Оглавление
- [Компоненты системы](#компоненты-системы)
- [Типы ошибок](#типы-ошибок)
- [Автоматический Retry](#автоматический-retry)
- [Использование в компонентах](#использование-в-компонентах)
- [API Reference](#api-reference)
- [Примеры](#примеры)
- [Лучшие практики](#лучшие-практики)
---
## 🧩 Компоненты системы
### 1. **ErrorBoundary**
Глобальный обработчик ошибок React для ловли ошибок рендеринга.
**Возможности:**
- ✅ Ловит ошибки рендеринга и показывает fallback UI
- ✅ Логирует ошибки в консоль и систему мониторинга
- ✅ Показывает детали ошибки в dev режиме
- ✅ Автоматически очищает кэш при повторяющихся ошибках
- ✅ Кнопки восстановления: "Попробовать снова", "Перезагрузить", "На главную"
**Использование:**
```jsx
// Уже встроен в App.jsx, оборачивает все приложение
<ErrorBoundary>
<App />
</ErrorBoundary>
```
### 2. **NetworkErrorHandler**
Компонент для мониторинга состояния сети.
**Возможности:**
- ✅ Детектирует offline/online события
- ✅ Показывает баннер при потере соединения
- ✅ Уведомляет о восстановлении соединения
- ✅ Интегрируется с глобальной системой уведомлений
**Использование:**
```jsx
// Уже встроен в App.jsx
<NetworkErrorHandler />
```
### 3. **apiErrorHandler.js**
Утилиты для классификации и обработки ошибок API.
**Функции:**
- `getErrorType(error)` - определяет тип ошибки
- `isRetriableError(error, method)` - проверяет возможность retry
- `getRetryDelay(attemptNumber)` - вычисляет задержку для retry
- `getMaxRetries(errorType, method)` - возвращает максимум попыток
- `formatErrorMessage(error)` - форматирует user-friendly сообщение
- `getErrorDetails(error)` - извлекает детали ошибки
- `getErrorAction(error)` - возвращает рекомендации по устранению
- `isCriticalError(error)` - проверяет критичность ошибки
- `logError(error, context)` - логирует ошибку
### 4. **useErrorHandler**
React хук для обработки ошибок в компонентах.
**API:**
```javascript
const {
handleError, // Обработать ошибку
handleSuccess, // Показать успех
handleWarning, // Показать предупреждение
handleInfo, // Показать инфо
withErrorHandler // Обёртка для async функций
} = useErrorHandler();
```
### 5. **RetryButton**
Кнопка для повторной попытки с индикацией загрузки.
**Props:**
- `onRetry` - async функция для выполнения
- `loading` - внешнее состояние загрузки
- `disabled` - отключить кнопку
- `className` - CSS классы
- `showIcon` - показывать иконку
- `children` - текст кнопки
---
## 🎭 Типы ошибок
Система классифицирует ошибки на следующие типы:
| Тип | Код | Описание | Retry |
|-----|-----|----------|-------|
| **NETWORK** | - | Проблемы с сетью (нет соединения) | ✅ Да (3x) |
| **TIMEOUT** | ECONNABORTED | Превышено время ожидания | ✅ Да (2x) |
| **SERVER** | 5xx | Ошибка на сервере | ✅ Да (2x GET) |
| **CLIENT** | 4xx | Ошибка клиента (общая) | ❌ Нет |
| **VALIDATION** | 400 | Ошибка валидации данных | ❌ Нет |
| **AUTH** | 401 | Требуется аутентификация | ❌ Нет |
| **PERMISSION** | 403 | Нет прав доступа | ❌ Нет |
| **NOT_FOUND** | 404 | Ресурс не найден | ❌ Нет |
| **CONFLICT** | 409 | Конфликт данных (ETag) | ❌ Нет |
| **RATE_LIMIT** | 429 | Превышен лимит запросов | ❌ Нет |
| **UNKNOWN** | - | Неизвестная ошибка | ❌ Нет |
---
## 🔄 Автоматический Retry
### Стратегия Retry
**GET запросы:**
- Network Error: до 3 попыток
- Timeout: до 2 попыток
- Server Error (5xx): до 2 попыток
**POST/PUT/PATCH/DELETE:**
- Network Error: до 1 попытки
- Timeout: до 1 попытки
- Без retry для 5xx (чтобы не создать дубликаты)
### Exponential Backoff
Задержки между попытками растут экспоненциально:
```
Попытка 1: 300ms (± 20% jitter)
Попытка 2: 600ms (± 20% jitter)
Попытка 3: 1200ms (± 20% jitter)
```
**Jitter** (±20%) добавляется для избежания thundering herd problem.
### Максимальная задержка
Максимальная задержка ограничена **10 секундами** для предотвращения бесконечного ожидания.
---
## 💻 Использование в компонентах
### Вариант 1: Ручная обработка
```jsx
import { useState } from 'react';
import { useErrorHandler } from '../hooks/useErrorHandler';
import api from '../lib/api';
function MyComponent() {
const [data, setData] = useState(null);
const { handleError, handleSuccess } = useErrorHandler();
const fetchData = async () => {
try {
const response = await api.get('/domains-new');
setData(response.data);
handleSuccess('Данные загружены');
} catch (error) {
handleError(error, { component: 'MyComponent' });
}
};
return (
<button onClick={fetchData}>
Загрузить данные
</button>
);
}
```
### Вариант 2: Автоматическая обработка
```jsx
import { useState } from 'react';
import { useErrorHandler } from '../hooks/useErrorHandler';
import api from '../lib/api';
function MyComponent() {
const [data, setData] = useState(null);
const { withErrorHandler } = useErrorHandler();
const fetchData = withErrorHandler(
async () => {
const response = await api.get('/domains-new');
setData(response.data);
},
{
successMessage: 'Данные загружены',
context: { component: 'MyComponent' }
}
);
return (
<button onClick={fetchData}>
Загрузить данные
</button>
);
}
```
### Вариант 3: С RetryButton
```jsx
import RetryButton from '../components/RetryButton';
import api from '../lib/api';
function MyComponent() {
const saveData = async () => {
await api.post('/domains-new', { domains: [...] });
};
return (
<RetryButton onRetry={saveData}>
Сохранить
</RetryButton>
);
}
```
### Вариант 4: React Query интеграция
```jsx
import { useQuery } from '@tanstack/react-query';
import { useErrorHandler } from '../hooks/useErrorHandler';
import api from '../lib/api';
function MyComponent() {
const { handleError } = useErrorHandler();
const { data, isLoading, refetch } = useQuery({
queryKey: ['domains'],
queryFn: async () => {
const response = await api.get('/domains-new');
return response.data;
},
onError: (error) => {
handleError(error, { component: 'MyComponent' });
}
});
// React Query автоматически делает retry,
// но мы добавляем user-friendly уведомления
return <div>{/* ... */}</div>;
}
```
---
## 📖 API Reference
### useErrorHandler()
```typescript
interface ErrorHandlerHook {
// Обработать ошибку
handleError: (error: Error, context?: object) => void;
// Показать успех
handleSuccess: (message?: string) => void;
// Показать предупреждение
handleWarning: (message: string, details?: object) => void;
// Показать информацию
handleInfo: (message: string, details?: object) => void;
// Обёртка для async функций
withErrorHandler: (
asyncFn: Function,
options?: {
successMessage?: string;
context?: object;
silent?: boolean;
rethrow?: boolean;
defaultValue?: any;
}
) => Function;
}
```
### getErrorType()
```javascript
import { getErrorType, ErrorType } from '../lib/api';
const error = new Error('Network error');
const type = getErrorType(error);
if (type === ErrorType.NETWORK) {
console.log('Проблемы с сетью');
}
```
### formatErrorMessage()
```javascript
import { formatErrorMessage } from '../lib/api';
try {
await api.get('/endpoint');
} catch (error) {
const message = formatErrorMessage(error);
// "Не удалось подключиться к серверу. Проверьте соединение."
alert(message);
}
```
### getErrorAction()
```javascript
import { getErrorAction } from '../lib/api';
try {
await api.post('/endpoint', data);
} catch (error) {
const action = getErrorAction(error);
// "Проверьте правильность введённых данных."
console.log('Рекомендация:', action);
}
```
---
## 🎯 Примеры
### Пример 1: Загрузка данных с обработкой ошибок
```jsx
import { useState } from 'react';
import { useErrorHandler } from '../hooks/useErrorHandler';
import RetryButton from '../components/RetryButton';
import api from '../lib/api';
function DataLoader() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const { withErrorHandler } = useErrorHandler();
const loadData = withErrorHandler(
async () => {
setLoading(true);
try {
const response = await api.get('/domains-new');
setData(response.data);
} finally {
setLoading(false);
}
},
{
successMessage: 'Данные успешно загружены',
context: { component: 'DataLoader' }
}
);
return (
<div className="card">
<div className="card-body">
{!data ? (
<RetryButton
onRetry={loadData}
loading={loading}
>
Загрузить данные
</RetryButton>
) : (
<div>
<p>Загружено: {data.items.length} записей</p>
<button onClick={loadData}>Обновить</button>
</div>
)}
</div>
</div>
);
}
```
### Пример 2: Форма с валидацией
```jsx
import { useState } from 'react';
import { useErrorHandler } from '../hooks/useErrorHandler';
import api from '../lib/api';
function DomainForm() {
const [domain, setDomain] = useState('');
const { handleError, handleSuccess } = useErrorHandler();
const handleSubmit = async (e) => {
e.preventDefault();
try {
await api.post('/domains-new', {
domains: [{ domain, community: '65000:100' }]
});
handleSuccess('Домен успешно добавлен');
setDomain('');
} catch (error) {
// Система автоматически покажет user-friendly сообщение
// "Ошибка валидации данных. Проверьте введённые значения."
handleError(error, { form: 'DomainForm', domain });
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={domain}
onChange={(e) => setDomain(e.target.value)}
placeholder="example.com"
/>
<button type="submit">Добавить</button>
</form>
);
}
```
### Пример 3: Обработка критичных ошибок
```jsx
import { useEffect } from 'react';
import { useErrorHandler } from '../hooks/useErrorHandler';
import { isCriticalError } from '../lib/api';
import api from '../lib/api';
function CriticalDataLoader() {
const { handleError } = useErrorHandler();
useEffect(() => {
const loadCriticalData = async () => {
try {
await api.get('/server-configs');
} catch (error) {
if (isCriticalError(error)) {
// Критичная ошибка - показываем модальное окно
handleError(error);
// Можно также перенаправить на страницу ошибки
// window.location.href = '/error';
}
}
};
loadCriticalData();
}, [handleError]);
return <div>...</div>;
}
```
---
## ✅ Лучшие практики
### 1. Всегда используйте useErrorHandler
```jsx
// ✅ Хорошо
import { useErrorHandler } from '../hooks/useErrorHandler';
function MyComponent() {
const { handleError } = useErrorHandler();
// ...
}
// ❌ Плохо
function MyComponent() {
const handleError = (error) => {
alert(error.message); // Не user-friendly
};
}
```
### 2. Передавайте контекст в handleError
```jsx
// ✅ Хорошо
handleError(error, {
component: 'DomainsManager',
action: 'delete',
itemId: domain.id
});
// ❌ Плохо
handleError(error);
```
### 3. Используйте withErrorHandler для простых случаев
```jsx
// ✅ Хорошо - лаконично
const loadData = withErrorHandler(
async () => {
const res = await api.get('/data');
setData(res.data);
},
{ successMessage: 'Загружено' }
);
// ❌ Избыточно
const loadData = async () => {
try {
const res = await api.get('/data');
setData(res.data);
handleSuccess('Загружено');
} catch (error) {
handleError(error);
}
};
```
### 4. Не дублируйте обработку ошибок
```jsx
// ✅ Хорошо - API автоматически показывает уведомление
try {
await api.post('/data', payload);
} catch (error) {
// Ошибка уже показана пользователю
// Здесь только локальная обработка
setLoading(false);
}
// ❌ Плохо - двойное уведомление
try {
await api.post('/data', payload);
} catch (error) {
handleError(error); // Уведомление показано дважды!
}
```
### 5. Используйте RetryButton для операций
```jsx
// ✅ Хорошо
<RetryButton onRetry={saveData}>
Сохранить
</RetryButton>
// ❌ Плохо - ручная реализация retry
<button onClick={async () => {
let retries = 0;
while (retries < 3) {
try {
await saveData();
break;
} catch {
retries++;
}
}
}}>
Сохранить
</button>
```
### 6. Логируйте критичные ошибки
```jsx
// ✅ Хорошо
import { isCriticalError, logError } from '../lib/api';
catch (error) {
if (isCriticalError(error)) {
logError(error, { critical: true, user: currentUser });
}
handleError(error);
}
```
---
## 🔧 Настройка
### Изменение таймаута
```javascript
// frontend/src/lib/api.js
const api = axios.create({
timeout: 30000, // 30 секунд (по умолчанию)
});
```
### Настройка retry параметров
```javascript
// frontend/src/lib/apiErrorHandler.js
// Изменить базовую задержку
export function getRetryDelay(attemptNumber, baseDelay = 300) {
// Меняем baseDelay для другой стратегии
}
// Изменить количество попыток
export function getMaxRetries(errorType, method = 'GET') {
if (method === 'GET') {
return errorType === ErrorType.NETWORK ? 5 : 3; // Больше попыток
}
// ...
}
```
### Интеграция с Sentry
```javascript
// frontend/src/lib/apiErrorHandler.js
export function logError(error, context = {}) {
// ...
// Добавить отправку в Sentry
if (window.Sentry && isCriticalError(error)) {
window.Sentry.captureException(error, {
contexts: {
api: getErrorDetails(error),
custom: context
},
level: 'error'
});
}
}
```
---
## 🎓 Дополнительные ресурсы
- [Примеры использования](./frontend/src/examples/ErrorHandlingExample.jsx)
- [Axios документация](https://axios-http.com/docs/handling_errors)
- [React Error Boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)
- [Exponential Backoff](https://en.wikipedia.org/wiki/Exponential_backoff)
---
## 📝 Changelog
### v1.0.0 (2025-10-03)
- ✅ Добавлен ErrorBoundary для React ошибок
- ✅ Добавлен NetworkErrorHandler для offline/online
- ✅ Улучшен retry механизм с exponential backoff
- ✅ Добавлена типизация ошибок (11 типов)
- ✅ User-friendly сообщения вместо технических
- ✅ Хук useErrorHandler для компонентов
- ✅ Компонент RetryButton
- ✅ Интеграция с системой уведомлений
- ✅ Логирование критичных ошибок
- ✅ Увеличен таймаут до 30 секунд
---
## 🤝 Поддержка
Если у вас возникли вопросы или проблемы с системой обработки ошибок:
1. Проверьте консоль браузера на наличие ошибок
2. Проверьте вкладку Network в DevTools
3. Убедитесь что backend сервер запущен
4. Проверьте настройки CORS
**Полезные команды для отладки:**
```javascript
// В консоли браузера
localStorage.clear(); // Очистить кэш
window.notify.clear(); // Очистить уведомления
console.log(window.notify); // Проверить систему уведомлений
```