🐛 v1.7.8: Добавлена система багрепортов в Telegram
Some checks failed
CI / run (push) Has been cancelled
Some checks failed
CI / run (push) Has been cancelled
- Автоматический сбор диагностики (статус, пинг, протокол, логи) - Кнопка 'Сообщить о проблеме' в настройках - Анонимная отправка багрепортов в Telegram - Telegram бот настроен и готов к работе
This commit is contained in:
10
.env.example
Normal file
10
.env.example
Normal file
@@ -0,0 +1,10 @@
|
||||
# Конфигурация для разработки (пример)
|
||||
# Скопируй в .env.local и заполни реальными значениями
|
||||
|
||||
# Sentry DSN для сбора crash reports
|
||||
# Получи из: https://umbrix-dj.sentry.io/ → Projects → umbrix-app → Settings → Client Keys
|
||||
# Формат: https://публичный_ключ@o123.ingest.sentry.io/456
|
||||
SENTRY_DSN=
|
||||
|
||||
# Пример (ЗАМЕНИ на свой):
|
||||
# SENTRY_DSN=https://abc123def456@o4510744763170816.ingest.sentry.io/789456
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -59,5 +59,9 @@ app.*.map.json
|
||||
/data
|
||||
|
||||
# FVM Version Cache
|
||||
.fvm/lib/core/telegram_config.dart
|
||||
.fvm/
|
||||
|
||||
# Secrets
|
||||
lib/core/telegram_config.dart
|
||||
android/key.properties
|
||||
.env.local
|
||||
|
||||
359
BUG_REPORT_INTEGRATION.md
Normal file
359
BUG_REPORT_INTEGRATION.md
Normal file
@@ -0,0 +1,359 @@
|
||||
# 🔗 Интеграция системы багрепортов с существующими логами
|
||||
|
||||
## Архитектура логирования в Umbrix
|
||||
|
||||
### Файлы логов
|
||||
|
||||
```
|
||||
~/.local/share/umbrix/logs/ (или другая директория на основе платформы)
|
||||
├── box.log # Логи ядра (singbox core)
|
||||
└── app.log # Логи приложения (Flutter)
|
||||
```
|
||||
|
||||
### Ротация логов
|
||||
|
||||
Файлы автоматически ротируются при достижении 5MB:
|
||||
- `box.log` → `box.log.1` → `box.log.2`
|
||||
- Максимум 2 backup файла
|
||||
|
||||
## Как подключиться к логам
|
||||
|
||||
### 1. LogRepository (Основной источник)
|
||||
|
||||
```dart
|
||||
// Получить репозиторий логов
|
||||
final logRepository = await ref.read(logRepositoryProvider.future);
|
||||
|
||||
// Подписаться на поток логов в реальном времени
|
||||
logRepository.watchLogs().listen((either) {
|
||||
either.fold(
|
||||
(failure) => print('Error: $failure'),
|
||||
(logs) {
|
||||
for (final log in logs) {
|
||||
print('${log.time} [${log.level}] ${log.message}');
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### 2. LogPathResolver (Пути к файлам)
|
||||
|
||||
```dart
|
||||
// Получить пути к файлам логов
|
||||
final logPathResolver = ref.read(logPathResolverProvider);
|
||||
|
||||
// Core logs
|
||||
final coreLogFile = logPathResolver.coreFile();
|
||||
print('Core logs: ${coreLogFile.path}');
|
||||
|
||||
// App logs
|
||||
final appLogFile = logPathResolver.appFile();
|
||||
print('App logs: ${appLogFile.path}');
|
||||
|
||||
// Директория логов
|
||||
final logsDir = logPathResolver.directory;
|
||||
print('Logs directory: ${logsDir.path}');
|
||||
```
|
||||
|
||||
### 3. Чтение файлов напрямую
|
||||
|
||||
```dart
|
||||
import 'dart:io';
|
||||
|
||||
// Прочитать все логи
|
||||
final coreLogFile = logPathResolver.coreFile();
|
||||
if (await coreLogFile.exists()) {
|
||||
final content = await coreLogFile.readAsString();
|
||||
print(content);
|
||||
}
|
||||
|
||||
// Прочитать последние N строк
|
||||
Future<String> readLastLines(File file, int maxLines) async {
|
||||
final lines = await file.readAsLines();
|
||||
final lastLines = lines.length > maxLines
|
||||
? lines.sublist(lines.length - maxLines)
|
||||
: lines;
|
||||
return lastLines.join('\n');
|
||||
}
|
||||
|
||||
final last100Lines = await readLastLines(coreLogFile, 100);
|
||||
```
|
||||
|
||||
## Что собирает BugReportService
|
||||
|
||||
### Диагностическая информация
|
||||
|
||||
```dart
|
||||
class DiagnosticInfo {
|
||||
// 1. Информация об устройстве (анонимно)
|
||||
final String deviceInfo; // "Android 12" / "iOS 15" / "Windows"
|
||||
|
||||
// 2. Статус подключения
|
||||
final String connectionStatus; // "CONNECTED" / "DISCONNECTED" / etc
|
||||
final String? connectionError; // Текст ошибки если есть
|
||||
|
||||
// 3. Информация о прокси
|
||||
final String? activeProxyInfo; // "Тип: vmess, Тег: server-1, Пинг: 120ms"
|
||||
final int? pingDelay; // Задержка в мс (120, 450, etc)
|
||||
|
||||
// 4. Логи (последние 100 строк)
|
||||
final String coreLogsPreview; // Из box.log
|
||||
final String appLogsPreview; // Из app.log
|
||||
|
||||
// 5. Метаданные
|
||||
final DateTime timestamp; // Время создания отчёта
|
||||
}
|
||||
```
|
||||
|
||||
### Источники данных
|
||||
|
||||
| Данные | Источник | Provider/Service |
|
||||
|--------|----------|------------------|
|
||||
| **Device Info** | `Platform.operatingSystem` | `TelegramLogger.getAnonymousDeviceInfo()` |
|
||||
| **Connection Status** | `ConnectionNotifier` | `connectionNotifierProvider` |
|
||||
| **Active Proxy** | `ActiveProxyNotifier` | `activeProxyNotifierProvider` |
|
||||
| **Ping Delay** | `ProxyItemEntity.urlTestDelay` | Из активного прокси |
|
||||
| **Core Logs** | `box.log` файл | `logPathResolver.coreFile()` |
|
||||
| **App Logs** | `app.log` файл | `logPathResolver.appFile()` |
|
||||
|
||||
## Как добавить дополнительную информацию
|
||||
|
||||
### 1. Расширить DiagnosticInfo
|
||||
|
||||
```dart
|
||||
// В bug_report_service.dart
|
||||
class DiagnosticInfo {
|
||||
// Добавьте новое поле
|
||||
final String? vpnStatus;
|
||||
final int? memoryUsage;
|
||||
final String? networkType;
|
||||
|
||||
// ... остальное
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Обновить метод collectDiagnostics()
|
||||
|
||||
```dart
|
||||
Future<DiagnosticInfo> collectDiagnostics() async {
|
||||
// ... существующий код
|
||||
|
||||
// Добавьте новый сбор данных
|
||||
String? vpnStatus;
|
||||
try {
|
||||
// Ваша логика получения VPN статуса
|
||||
vpnStatus = await getVpnStatus();
|
||||
} catch (e) {
|
||||
vpnStatus = 'Ошибка: $e';
|
||||
}
|
||||
|
||||
return DiagnosticInfo(
|
||||
// ... существующие поля
|
||||
vpnStatus: vpnStatus,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Обновить форматирование
|
||||
|
||||
```dart
|
||||
String _formatBugReport(...) {
|
||||
// ... существующий код
|
||||
|
||||
// Добавьте в отчёт
|
||||
if (diagnostics.vpnStatus != null) {
|
||||
buffer.writeln('📡 VPN:');
|
||||
buffer.writeln(diagnostics.vpnStatus);
|
||||
buffer.writeln();
|
||||
}
|
||||
|
||||
// ... остальное
|
||||
}
|
||||
```
|
||||
|
||||
## Подключение к метрикам Singbox
|
||||
|
||||
### Получить статистику трафика
|
||||
|
||||
```dart
|
||||
// Singbox предоставляет метрики
|
||||
final singbox = ref.read(singboxServiceProvider);
|
||||
|
||||
// В будущем можно добавить:
|
||||
// - Общий трафик (upload/download)
|
||||
// - Количество подключений
|
||||
// - Ошибки DNS
|
||||
// - etc
|
||||
```
|
||||
|
||||
### Пример расширенной диагностики
|
||||
|
||||
```dart
|
||||
Future<DiagnosticInfo> collectDiagnostics() async {
|
||||
// ... существующий код
|
||||
|
||||
// Дополнительные данные из singbox
|
||||
String? trafficInfo;
|
||||
try {
|
||||
// Если singbox предоставляет API для статистики
|
||||
final stats = await singbox.getStats();
|
||||
trafficInfo = 'Upload: ${stats.upload}, Download: ${stats.download}';
|
||||
} catch (e) {
|
||||
trafficInfo = 'Недоступно';
|
||||
}
|
||||
|
||||
return DiagnosticInfo(
|
||||
// ... остальные поля
|
||||
trafficInfo: trafficInfo,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Логирование в приложении
|
||||
|
||||
### Использование логгера
|
||||
|
||||
```dart
|
||||
import 'package:umbrix/utils/custom_loggers.dart';
|
||||
|
||||
class MyService with InfraLogger { // или AppLogger, CoreLogger
|
||||
void someMethod() {
|
||||
loggy.debug('Debug message');
|
||||
loggy.info('Info message');
|
||||
loggy.warning('Warning message');
|
||||
loggy.error('Error message', error, stackTrace);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Типы логгеров
|
||||
|
||||
- `InfraLogger` — инфраструктурные логи (network, IO, etc)
|
||||
- `AppLogger` — логи приложения (UI, бизнес-логика)
|
||||
- `CoreLogger` — логи ядра (singbox)
|
||||
|
||||
Все они пишутся в соответствующие файлы через `LogRepository`.
|
||||
|
||||
## Расширенная интеграция с Telegram
|
||||
|
||||
### Форматирование для Telegram
|
||||
|
||||
```dart
|
||||
// Telegram поддерживает HTML разметку
|
||||
final message = '''
|
||||
<b>🐛 БАГРЕПОРТ</b>
|
||||
<i>Устройство:</i> $deviceInfo
|
||||
<code>Статус: $connectionStatus</code>
|
||||
|
||||
<pre>
|
||||
Логи:
|
||||
$logs
|
||||
</pre>
|
||||
''';
|
||||
|
||||
await telegramLogger.sendLogsAsText(message);
|
||||
```
|
||||
|
||||
### Отправка нескольких файлов
|
||||
|
||||
```dart
|
||||
// Можно отправить Core и App логи отдельно
|
||||
await telegramLogger.sendLogsAsFile(
|
||||
logPathResolver.coreFile(),
|
||||
deviceInfo: 'Core Logs - $deviceInfo',
|
||||
);
|
||||
|
||||
await telegramLogger.sendLogsAsFile(
|
||||
logPathResolver.appFile(),
|
||||
deviceInfo: 'App Logs - $deviceInfo',
|
||||
);
|
||||
```
|
||||
|
||||
## Дебаг и тестирование
|
||||
|
||||
### Посмотреть текущие логи
|
||||
|
||||
```dart
|
||||
// В debug режиме можно вывести текущие логи
|
||||
void debugPrintLogs() async {
|
||||
final logPathResolver = ref.read(logPathResolverProvider);
|
||||
|
||||
print('=== CORE LOGS ===');
|
||||
final coreFile = logPathResolver.coreFile();
|
||||
if (await coreFile.exists()) {
|
||||
final lines = await coreFile.readAsLines();
|
||||
lines.take(10).forEach(print); // Первые 10 строк
|
||||
}
|
||||
|
||||
print('=== APP LOGS ===');
|
||||
final appFile = logPathResolver.appFile();
|
||||
if (await appFile.exists()) {
|
||||
final lines = await appFile.readAsLines();
|
||||
lines.take(10).forEach(print); // Первые 10 строк
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Тест диагностики
|
||||
|
||||
```dart
|
||||
void testDiagnostics() async {
|
||||
final service = ref.read(bugReportServiceProvider);
|
||||
final diagnostics = await service.collectDiagnostics();
|
||||
|
||||
print('Device: ${diagnostics.deviceInfo}');
|
||||
print('Connection: ${diagnostics.connectionStatus}');
|
||||
print('Proxy: ${diagnostics.activeProxyInfo}');
|
||||
print('Ping: ${diagnostics.pingDelay}ms');
|
||||
print('Core logs (first 5 lines):');
|
||||
print(diagnostics.coreLogsPreview.split('\n').take(5).join('\n'));
|
||||
}
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q: Где физически хранятся логи?
|
||||
**A:** Зависит от платформы:
|
||||
- Android: `/data/data/com.hiddify.umbrix/files/logs/`
|
||||
- iOS: `Application Support/logs/`
|
||||
- Desktop: `~/.local/share/umbrix/logs/` (Linux), `~/Library/Application Support/umbrix/logs/` (macOS)
|
||||
|
||||
### Q: Как очистить логи?
|
||||
**A:**
|
||||
```dart
|
||||
final logRepository = await ref.read(logRepositoryProvider.future);
|
||||
await logRepository.clearLogs();
|
||||
```
|
||||
|
||||
### Q: Можно ли собирать логи без отправки?
|
||||
**A:** Да:
|
||||
```dart
|
||||
final service = ref.read(bugReportServiceProvider);
|
||||
final diagnostics = await service.collectDiagnostics();
|
||||
// diagnostics теперь содержит всю информацию
|
||||
```
|
||||
|
||||
### Q: Как добавить скриншот к багрепорту?
|
||||
**A:** Нужно расширить `TelegramLogger`:
|
||||
```dart
|
||||
Future<bool> sendPhotoWithCaption(File photo, String caption) async {
|
||||
// Используйте Telegram Bot API endpoint sendPhoto
|
||||
final formData = FormData.fromMap({
|
||||
'chat_id': TelegramConfig.chatId,
|
||||
'photo': await MultipartFile.fromFile(photo.path),
|
||||
'caption': caption,
|
||||
});
|
||||
|
||||
final response = await _dio.post(
|
||||
'https://api.telegram.org/bot${TelegramConfig.botToken}/sendPhoto',
|
||||
data: formData,
|
||||
);
|
||||
|
||||
return response.statusCode == 200;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Система готова и полностью интегрирована!** 🎉
|
||||
225
BUG_REPORT_SYSTEM.md
Normal file
225
BUG_REPORT_SYSTEM.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# 🐛 Система отправки багрепортов в техподдержку
|
||||
|
||||
## 📋 Обзор
|
||||
|
||||
Реализована полноценная система для сбора и отправки багрепортов в Telegram с автоматическим сбором диагностической информации.
|
||||
|
||||
## ✨ Что реализовано
|
||||
|
||||
### 1. **Автоматический сбор диагностики** (`BugReportService`)
|
||||
|
||||
Система автоматически собирает:
|
||||
- ✅ **Информация об устройстве** (анонимно): OS, версия
|
||||
- ✅ **Статус подключения**: connected/disconnected/connecting
|
||||
- ✅ **Ошибки подключения**: если есть
|
||||
- ✅ **Активный прокси**: тип протокола, тег сервера
|
||||
- ✅ **Пинг/задержка**: в миллисекундах + оценка качества
|
||||
- ✅ **Логи**: последние 100 строк из `core.log` и `app.log`
|
||||
|
||||
### 2. **UI для отправки багрепортов** (`BugReportDialog`)
|
||||
|
||||
Диалоговое окно с:
|
||||
- Поле для описания проблемы пользователем
|
||||
- Чекбокс "Включить логи" (по умолчанию включён)
|
||||
- Информация о конфиденциальности
|
||||
- Кнопка "Отправить" с индикатором загрузки
|
||||
|
||||
### 3. **Интеграция с Telegram**
|
||||
|
||||
Использует существующий `TelegramLogger`:
|
||||
- Отправка как текст (если помещается в 4KB)
|
||||
- Или как файл (если логи большие)
|
||||
- Красиво форматированный отчёт с эмодзи
|
||||
|
||||
## 📍 Где находится
|
||||
|
||||
### Код:
|
||||
```
|
||||
lib/features/bug_report/
|
||||
├── data/
|
||||
│ └── bug_report_service.dart # Сервис сбора и отправки
|
||||
└── widget/
|
||||
└── bug_report_dialog.dart # UI диалога
|
||||
```
|
||||
|
||||
### Кнопки в UI:
|
||||
1. **Настройки → Логи и отладка → "Сообщить о проблеме"**
|
||||
- Всегда доступна
|
||||
- Основной способ отправки багрепорта
|
||||
|
||||
## 🔧 Как использовать
|
||||
|
||||
### Для пользователя:
|
||||
|
||||
1. Откройте **Настройки**
|
||||
2. Раздел **"Логи и отладка"**
|
||||
3. Нажмите **"Сообщить о проблеме"** 🐛
|
||||
4. Опишите проблему
|
||||
5. Нажмите **"Отправить"**
|
||||
|
||||
### Для разработчика:
|
||||
|
||||
```dart
|
||||
// Показать диалог программно
|
||||
await BugReportDialog.show(context);
|
||||
|
||||
// Или получить сервис напрямую
|
||||
final service = ref.read(bugReportServiceProvider);
|
||||
final result = await service.sendBugReport(
|
||||
userDescription: 'Описание проблемы',
|
||||
includeLogs: true,
|
||||
);
|
||||
```
|
||||
|
||||
## 📊 Формат отчёта
|
||||
|
||||
Отчёт выглядит так:
|
||||
|
||||
```
|
||||
🐛 БАГРЕПОРТ UMBRIX
|
||||
═══════════════════════════════
|
||||
|
||||
📝 ОПИСАНИЕ ПРОБЛЕМЫ:
|
||||
Не могу подключиться к серверу, таймаут
|
||||
|
||||
═══════════════════════════════
|
||||
💻 УСТРОЙСТВО:
|
||||
Android 12
|
||||
|
||||
🔌 СТАТУС ПОДКЛЮЧЕНИЯ:
|
||||
DISCONNECTED
|
||||
Ошибка: Connection timeout
|
||||
|
||||
🌐 ПРОКСИ:
|
||||
Тип: vmess, Тег: server-1, Пинг: 450ms
|
||||
Задержка: 450ms (🟠 Медленно)
|
||||
|
||||
🕐 ВРЕМЯ:
|
||||
2026-01-22T15:30:00.000Z
|
||||
|
||||
═══════════════════════════════
|
||||
📋 ЛОГИ (ПОСЛЕДНИЕ 20 СТРОК):
|
||||
|
||||
Core:
|
||||
[INFO] Starting connection...
|
||||
[ERROR] Connection failed: timeout
|
||||
...
|
||||
|
||||
App:
|
||||
[DEBUG] User clicked connect
|
||||
[ERROR] Failed to establish connection
|
||||
...
|
||||
```
|
||||
|
||||
## 🔐 Безопасность и конфиденциальность
|
||||
|
||||
✅ **Мы НЕ собираем:**
|
||||
- Личные данные
|
||||
- IP адреса
|
||||
- Конфигурацию серверов (URL, пароли)
|
||||
- Историю посещений
|
||||
|
||||
✅ **Мы собираем ТОЛЬКО:**
|
||||
- Тип ОС (Android/iOS/Windows...)
|
||||
- Статус подключения
|
||||
- Задержку пинга
|
||||
- Технические логи (без личных данных)
|
||||
|
||||
## 🎯 Как это предотвращает негативные отзывы в Play Store
|
||||
|
||||
### Проблема:
|
||||
Пользователи оставляют плохие отзывы когда:
|
||||
- Не могут подключиться
|
||||
- Приложение "не работает"
|
||||
- Не понимают, что делать
|
||||
|
||||
### Решение:
|
||||
1. **Перехватываем недовольство** — кнопка "Сообщить о проблеме" даёт альтернативу отзыву
|
||||
2. **Собираем контекст** — автоматически видим ЧТО именно не работает
|
||||
3. **Быстро реагируем** — получаем отчёт в Telegram → можем помочь
|
||||
4. **Показываем заботу** — пользователь видит, что есть поддержка
|
||||
|
||||
## 📱 Дополнительные места размещения (опционально)
|
||||
|
||||
Можно добавить кнопку в:
|
||||
|
||||
### 1. Экран ошибки подключения
|
||||
```dart
|
||||
// В connection_failure_screen.dart
|
||||
FilledButton.icon(
|
||||
onPressed: () => BugReportDialog.show(context),
|
||||
icon: const Icon(FluentIcons.bug_20_regular),
|
||||
label: const Text('Сообщить о проблеме'),
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Меню приложения (три точки)
|
||||
```dart
|
||||
// В app_bar_actions.dart
|
||||
PopupMenuItem(
|
||||
child: const Text('Сообщить о проблеме'),
|
||||
onTap: () => BugReportDialog.show(context),
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Диалог обновления (при ошибке)
|
||||
```dart
|
||||
// Если обновление провалилось
|
||||
if (updateFailed) {
|
||||
TextButton(
|
||||
child: const Text('Сообщить о проблеме'),
|
||||
onPressed: () => BugReportDialog.show(context),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 Настройка Telegram
|
||||
|
||||
1. Создайте бота через @BotFather
|
||||
2. Получите токен
|
||||
3. Создайте приватный канал/группу
|
||||
4. Получите Chat ID
|
||||
5. Настройте в `lib/core/telegram_config.dart`:
|
||||
|
||||
```dart
|
||||
class TelegramConfig {
|
||||
static const String botToken = 'YOUR_BOT_TOKEN';
|
||||
static const String chatId = 'YOUR_CHAT_ID';
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 Тестирование
|
||||
|
||||
```dart
|
||||
// Протестировать сбор диагностики
|
||||
final service = ref.read(bugReportServiceProvider);
|
||||
final diagnostics = await service.collectDiagnostics();
|
||||
print(diagnostics.connectionStatus);
|
||||
print(diagnostics.pingDelay);
|
||||
|
||||
// Протестировать отправку
|
||||
final result = await service.sendBugReport(
|
||||
userDescription: 'Test report',
|
||||
includeLogs: true,
|
||||
);
|
||||
print(result.isSuccess);
|
||||
```
|
||||
|
||||
## 📈 Метрики для отслеживания
|
||||
|
||||
- Количество отправленных багрепортов
|
||||
- Среднее время ответа поддержки
|
||||
- Процент решённых проблем
|
||||
- Соотношение багрепортов к негативным отзывам
|
||||
|
||||
## 🎉 Результат
|
||||
|
||||
Вместо:
|
||||
> ⭐☆☆☆☆ "Не работает, не подключается" — в Play Store
|
||||
|
||||
Получаем:
|
||||
> 🐛 Багрепорт в Telegram → быстрая помощь → довольный пользователь → ⭐⭐⭐⭐⭐
|
||||
|
||||
---
|
||||
|
||||
**Готово к использованию!** 🚀
|
||||
254
TELEGRAM_BOT_SETUP_RU.md
Normal file
254
TELEGRAM_BOT_SETUP_RU.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# 🤖 Настройка Telegram бота для багрепортов
|
||||
|
||||
## Шаг 1: Создать бота
|
||||
|
||||
1. Откройте Telegram
|
||||
2. Найдите **@BotFather**
|
||||
3. Отправьте команду: `/newbot`
|
||||
4. Введите имя бота: `Umbrix Bug Report Bot`
|
||||
5. Введите username: `@Dorod_bug_bot` (или любой свободный)
|
||||
6. **Скопируйте TOKEN** — это строка типа `7987728101:AAGYUWTeYfFANhBA9-C3dZCjGOSwByAWCaA`
|
||||
|
||||
```
|
||||
📋 Пример токена:
|
||||
7987728101:AAGYUWTeYfFANhBA9-C3dZCjGOSwByAWCaA
|
||||
```
|
||||
|
||||
## Шаг 2: Создать ПРИВАТНЫЙ канал для логов
|
||||
|
||||
### 📱 В мобильном приложении Telegram:
|
||||
|
||||
1. **Откройте Telegram** на телефоне
|
||||
2. **Нажмите на ☰ меню** (три полоски слева вверху)
|
||||
3. **Выберите "Новый канал"**
|
||||
- Если не видите — нажмите на карандаш ✏️ справа внизу, там будет "Новый канал"
|
||||
4. **Введите название:** `Umbrix Bug Reports` (или любое)
|
||||
5. **Введите описание** (необязательно)
|
||||
6. **Нажмите "Создать"**
|
||||
7. **‼️ ВАЖНО: Выберите тип канала:**
|
||||
- ❌ НЕ нажимайте "Публичный канал"
|
||||
- ✅ Просто нажмите "Пропустить" или "Далее"
|
||||
- Это сделает канал **ПРИВАТНЫМ** по умолчанию
|
||||
8. **Нажмите "Сохранить"**
|
||||
|
||||
### 💻 В десктопном приложении Telegram:
|
||||
|
||||
1. **Откройте Telegram** на компьютере
|
||||
2. **Нажмите ☰ меню** (слева вверху)
|
||||
3. **Выберите "Новый канал"**
|
||||
4. **Введите название:** `Umbrix Bug Reports`
|
||||
5. **Нажмите "Далее"**
|
||||
6. **‼️ ВАЖНО:** На вопросе "Публичный или приватный?"
|
||||
- ✅ Выберите **"Приватный канал"**
|
||||
- ❌ НЕ выбирайте "Публичный"
|
||||
7. **Нажмите "Сохранить"**
|
||||
|
||||
### 🤖 Добавить бота как администратора:
|
||||
|
||||
1. **Откройте ваш новый канал** `Umbrix Bug Reports`
|
||||
2. **Нажмите на название канала** вверху
|
||||
3. **Выберите "Администраторы"** (или ⚙️ → "Управление каналом" → "Администраторы")
|
||||
4. **Нажмите "Добавить администратора"**
|
||||
5. **В поиске введите:** `@Dorod_bug_bot` (имя вашего бота)
|
||||
6. **Выберите бота** из списка
|
||||
7. **Дайте права:**
|
||||
- ✅ **"Публикация сообщений"** — ОБЯЗАТЕЛЬНО включите!
|
||||
- Остальное можно оставить выключенным
|
||||
8. **Нажмите "Сохранить"** или "Готово"
|
||||
|
||||
✅ **Готово!** Теперь ваш бот может отправлять сообщения в канал.
|
||||
|
||||
## Шаг 3: Получить Chat ID
|
||||
|
||||
### Способ 1 (Простой):
|
||||
|
||||
1. Отправьте любое сообщение в канал
|
||||
2. Перешлите это сообщение боту **@userinfobot**
|
||||
3. Он покажет Chat ID канала
|
||||
|
||||
### Способ 2 (Через API):
|
||||
|
||||
1. Отправьте любое сообщение в канал
|
||||
2. Откройте в браузере:
|
||||
```
|
||||
https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates
|
||||
```
|
||||
Замените `<YOUR_BOT_TOKEN>` на токен из Шага 1
|
||||
|
||||
3. Найдите в ответе `"chat":{"id":-1001234567890}`
|
||||
4. Это ваш Chat ID (обычно начинается с `-100`)
|
||||
|
||||
```json
|
||||
📋 Пример Chat ID для канала:
|
||||
-1001234567890
|
||||
```
|
||||
|
||||
## Шаг 4: Настроить в приложении
|
||||
|
||||
Откройте файл и замените значения:
|
||||
|
||||
```dart
|
||||
// lib/core/telegram_config.dart
|
||||
|
||||
class TelegramConfig {
|
||||
/// Токен из @BotFather
|
||||
static const String botToken = '6789012345:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw';
|
||||
|
||||
/// Chat ID вашего приватного канала
|
||||
static const String chatId = '-1001234567890';
|
||||
|
||||
static bool get isConfigured {
|
||||
return botToken != 'YOUR_BOT_TOKEN_HERE' && chatId != 'YOUR_CHAT_ID_HERE';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ⚠️ ВАЖНО для безопасности!
|
||||
|
||||
### ✅ ПРАВИЛЬНО:
|
||||
- Используйте **ПРИВАТНЫЙ** канал (никто не видит кроме вас)
|
||||
- Добавьте `.gitignore` чтобы не залить токен в git:
|
||||
```
|
||||
lib/core/telegram_config.dart
|
||||
```
|
||||
|
||||
### ❌ НЕПРАВИЛЬНО:
|
||||
- Публичный канал (все смогут читать баги)
|
||||
- Коммитить токен в git (украдут бота)
|
||||
|
||||
## 🔐 Про анонимность
|
||||
|
||||
### Что мы НЕ собираем автоматически:
|
||||
|
||||
❌ Имя пользователя
|
||||
❌ Email
|
||||
❌ Телефон
|
||||
❌ IP адрес
|
||||
❌ Конфигурацию серверов (URL, пароли)
|
||||
❌ Историю посещений
|
||||
|
||||
### Что собираем:
|
||||
|
||||
✅ Тип ОС (Android/iOS/Windows) — **анонимно**
|
||||
✅ Версия ОС — **анонимно**
|
||||
✅ Статус подключения
|
||||
✅ Задержка пинга (число)
|
||||
✅ Технические логи (без IP, без паролей)
|
||||
|
||||
### Код для проверки:
|
||||
|
||||
Посмотрите в [bug_report_service.dart](lib/features/bug_report/data/bug_report_service.dart#L120):
|
||||
|
||||
```dart
|
||||
/// Получить информацию об устройстве для логов (анонимно)
|
||||
static String getAnonymousDeviceInfo() {
|
||||
// Только общая информация без идентификаторов
|
||||
if (Platform.isAndroid) {
|
||||
return 'Android ${Platform.operatingSystemVersion}';
|
||||
} else if (Platform.isIOS) {
|
||||
return 'iOS ${Platform.operatingSystemVersion}';
|
||||
}
|
||||
// ... и т.д.
|
||||
// НЕТ никаких device ID, IMEI, или других идентификаторов!
|
||||
}
|
||||
```
|
||||
|
||||
## 📱 Как сделать ЕЩЁ более анонимным?
|
||||
|
||||
Если хотите убрать даже версию ОС:
|
||||
|
||||
```dart
|
||||
static String getAnonymousDeviceInfo() {
|
||||
if (Platform.isAndroid) return 'Android';
|
||||
if (Platform.isIOS) return 'iOS';
|
||||
if (Platform.isWindows) return 'Windows';
|
||||
if (Platform.isMacOS) return 'macOS';
|
||||
if (Platform.isLinux) return 'Linux';
|
||||
return 'Unknown';
|
||||
}
|
||||
```
|
||||
|
||||
Тогда будет только `"Android"` без версии.
|
||||
|
||||
## 🧪 Тестирование
|
||||
|
||||
После настройки проверьте:
|
||||
|
||||
```dart
|
||||
import 'package:umbrix/core/telegram_config.dart';
|
||||
|
||||
void main() {
|
||||
print('Configured: ${TelegramConfig.isConfigured}');
|
||||
print('Bot: ${TelegramConfig.botToken}');
|
||||
print('Chat: ${TelegramConfig.chatId}');
|
||||
}
|
||||
```
|
||||
|
||||
Должно вывести:
|
||||
```
|
||||
Configured: true
|
||||
Bot: 6789012345:AAHdqTcvCH...
|
||||
Chat: -1001234567890
|
||||
```
|
||||
|
||||
## 📨 Пример отчёта в Telegram
|
||||
|
||||
Вот что придёт в ваш канал:
|
||||
|
||||
```
|
||||
📱 Umbrix Logs
|
||||
Device: Android 12
|
||||
📅 2026-01-22T15:30:00.000Z
|
||||
━━━━━━━━━━━━━━━━
|
||||
🐛 БАГРЕПОРТ UMBRIX
|
||||
═══════════════════════════════
|
||||
|
||||
📝 ОПИСАНИЕ ПРОБЛЕМЫ:
|
||||
Не могу подключиться, постоянно таймаут
|
||||
|
||||
💻 УСТРОЙСТВО:
|
||||
Android 12
|
||||
|
||||
🔌 СТАТУС ПОДКЛЮЧЕНИЯ:
|
||||
DISCONNECTED
|
||||
Ошибка: Connection timeout
|
||||
|
||||
🌐 ПРОКСИ:
|
||||
Тип: vmess, Тег: server-1, Пинг: 450ms
|
||||
Задержка: 450ms (🟠 Медленно)
|
||||
|
||||
📋 ЛОГИ:
|
||||
[ERROR] Failed to connect...
|
||||
[WARN] Timeout exceeded...
|
||||
```
|
||||
|
||||
## ❓ FAQ
|
||||
|
||||
**Q: Обязательно ли канал? Может группу?**
|
||||
A: Лучше канал — он проще и не спамит уведомлениями других участников.
|
||||
|
||||
**Q: Можно несколько каналов для разных устройств?**
|
||||
A: Да, создайте несколько конфигов или используйте разные билды.
|
||||
|
||||
**Q: Что если не хочу использовать Telegram?**
|
||||
A: Можно заменить `TelegramLogger` на отправку email, webhook, или другой сервис.
|
||||
|
||||
**Q: Бот будет постить от имени пользователя?**
|
||||
A: Нет, бот постит от своего имени. Пользователи останутся анонимными.
|
||||
|
||||
**Q: Можно добавить больше информации?**
|
||||
A: Да, смотрите [BUG_REPORT_INTEGRATION.md](BUG_REPORT_INTEGRATION.md)
|
||||
|
||||
## ✅ Готово!
|
||||
|
||||
Теперь запустите приложение:
|
||||
1. Зайдите в Настройки → Логи
|
||||
2. Нажмите "Сообщить о проблеме"
|
||||
3. Напишите тестовый отчёт
|
||||
4. Отправьте
|
||||
|
||||
Отчёт должен прийти в ваш канал! 🎉
|
||||
|
||||
---
|
||||
|
||||
**Безопасно. Анонимно. Работает.** 🔐
|
||||
53
build_with_sentry.sh
Executable file
53
build_with_sentry.sh
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
# Скрипт сборки Umbrix с поддержкой Sentry crash reports
|
||||
|
||||
set -e
|
||||
|
||||
# Загружаем переменные из .env.local (если существует)
|
||||
if [ -f .env.local ]; then
|
||||
echo "📝 Загружаем конфигурацию из .env.local..."
|
||||
export $(grep -v '^#' .env.local | xargs)
|
||||
else
|
||||
echo "⚠️ Файл .env.local не найден. Sentry будет отключён."
|
||||
echo " Создай .env.local из .env.example и добавь SENTRY_DSN"
|
||||
fi
|
||||
|
||||
# Проверяем наличие DSN
|
||||
if [ -z "$SENTRY_DSN" ]; then
|
||||
echo "⚠️ SENTRY_DSN не задан → crash reports будут отключены"
|
||||
echo " Для включения: добавь SENTRY_DSN в .env.local"
|
||||
else
|
||||
echo "✅ Sentry включён (DSN найден)"
|
||||
fi
|
||||
|
||||
# Выбор платформы
|
||||
PLATFORM=${1:-android}
|
||||
|
||||
case $PLATFORM in
|
||||
android)
|
||||
echo "🤖 Сборка Android APK..."
|
||||
flutter build apk --release --dart-define sentry_dsn="$SENTRY_DSN"
|
||||
echo "✅ APK: build/app/outputs/flutter-apk/app-release.apk"
|
||||
;;
|
||||
|
||||
linux)
|
||||
echo "🐧 Сборка Linux..."
|
||||
flutter build linux --release --dart-define sentry_dsn="$SENTRY_DSN"
|
||||
echo "✅ Linux: build/linux/x64/release/bundle/"
|
||||
;;
|
||||
|
||||
windows)
|
||||
echo "🪟 Сборка Windows..."
|
||||
flutter build windows --release --dart-define sentry_dsn="$SENTRY_DSN"
|
||||
echo "✅ Windows: build/windows/x64/runner/Release/"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "❌ Неизвестная платформа: $PLATFORM"
|
||||
echo "Использование: ./build_with_sentry.sh [android|linux|windows]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "🎉 Сборка завершена!"
|
||||
@@ -163,15 +163,6 @@ Future<void> _performBootstrap(
|
||||
);
|
||||
}
|
||||
|
||||
// Автопроверка обновлений для Android
|
||||
if (Platform.isAndroid) {
|
||||
_safeInit(
|
||||
"auto check updates",
|
||||
() => container.read(appUpdateNotifierProvider.notifier).checkSilently(),
|
||||
timeout: 5000,
|
||||
);
|
||||
}
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
await _safeInit(
|
||||
"android display mode",
|
||||
|
||||
@@ -7,11 +7,11 @@ library;
|
||||
class TelegramConfig {
|
||||
/// Токен вашего Telegram бота от @BotFather
|
||||
/// Пример: '1234567890:ABCdefGHIjklMNOpqrsTUVwxyz123456789'
|
||||
static const String botToken = 'YOUR_BOT_TOKEN_HERE';
|
||||
static const String botToken = '7987728101:AAGYUWTeYfFANhBA9-C3dZCjGOSwByAWCaA';
|
||||
|
||||
/// Chat ID группы/канала куда отправлять логи
|
||||
/// Пример: '-1001234567890'
|
||||
static const String chatId = 'YOUR_CHAT_ID_HERE';
|
||||
static const String chatId = '-1003546852118';
|
||||
|
||||
/// Проверка что конфиг настроен
|
||||
static bool get isConfigured {
|
||||
|
||||
@@ -9,10 +9,7 @@ import 'package:umbrix/core/model/constants.dart';
|
||||
import 'package:umbrix/core/router/router.dart';
|
||||
import 'package:umbrix/core/theme/app_theme.dart';
|
||||
import 'package:umbrix/core/theme/theme_preferences.dart';
|
||||
import 'package:umbrix/core/notification/in_app_notification_controller.dart';
|
||||
import 'package:umbrix/features/app_update/notifier/app_update_notifier.dart';
|
||||
import 'package:umbrix/features/app_update/notifier/app_update_state.dart';
|
||||
import 'package:umbrix/features/app_update/widget/new_version_dialog.dart';
|
||||
import 'package:umbrix/features/connection/widget/connection_wrapper.dart';
|
||||
import 'package:umbrix/features/profile/notifier/profiles_update_notifier.dart';
|
||||
import 'package:umbrix/features/shortcut/shortcut_wrapper.dart';
|
||||
@@ -37,31 +34,6 @@ class App extends HookConsumerWidget with PresLogger {
|
||||
|
||||
ref.listen(foregroundProfilesUpdateNotifierProvider, (_, __) {});
|
||||
|
||||
// Слушаем состояние обновлений и показываем диалог
|
||||
ref.listen(appUpdateNotifierProvider, (previous, next) {
|
||||
if (next is AppUpdateStateAvailable) {
|
||||
// Получаем BuildContext через router
|
||||
final context = router.routerDelegate.navigatorKey.currentContext;
|
||||
if (context != null && context.mounted) {
|
||||
// Показываем диалог обновления
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (context.mounted) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => NewVersionDialog(
|
||||
newVersion: next.versionInfo,
|
||||
onIgnore: () {
|
||||
ref.read(appUpdateNotifierProvider.notifier).ignoreRelease(next.versionInfo);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return WindowWrapper(
|
||||
TrayWrapper(
|
||||
ShortcutWrapper(
|
||||
|
||||
@@ -79,7 +79,7 @@ class NewVersionDialog extends HookConsumerWidget with PresLogger {
|
||||
fileExt = '.zip';
|
||||
final zipUrl = newVersion.findAssetByExtension('.zip');
|
||||
if (zipUrl == null) {
|
||||
fileExt = '.exe'; // Fallback на .exe если нет .zip
|
||||
fileExt = '.exe'; // Fallback на .exe если нет .zip
|
||||
}
|
||||
} else if (Platform.isMacOS)
|
||||
fileExt = '.dmg';
|
||||
@@ -135,13 +135,23 @@ class NewVersionDialog extends HookConsumerWidget with PresLogger {
|
||||
if (context.mounted) {
|
||||
CustomToast('Установка обновления...', type: AlertType.info).show(context);
|
||||
}
|
||||
|
||||
|
||||
// Запускаем установщик в тихом режиме с правами администратора
|
||||
// /VERYSILENT - без UI, /SUPPRESSMSGBOXES - без диалогов
|
||||
// /NORESTART - не перезагружать систему
|
||||
final result = await Process.run(
|
||||
'powershell',
|
||||
['-Command', 'Start-Process', '-FilePath', '"$savePath"', '-ArgumentList', '"/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART"', '-Verb', 'RunAs', '-Wait'],
|
||||
[
|
||||
'-Command',
|
||||
'Start-Process',
|
||||
'-FilePath',
|
||||
'"$savePath"',
|
||||
'-ArgumentList',
|
||||
'"/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART"',
|
||||
'-Verb',
|
||||
'RunAs',
|
||||
'-Wait'
|
||||
],
|
||||
);
|
||||
|
||||
if (result.exitCode == 0) {
|
||||
@@ -172,25 +182,33 @@ class NewVersionDialog extends HookConsumerWidget with PresLogger {
|
||||
// Получить путь к исполняемому файлу приложения
|
||||
final exePath = Platform.resolvedExecutable;
|
||||
final appDir = Directory(exePath).parent.path;
|
||||
|
||||
|
||||
// Распаковать во временную папку
|
||||
final tempDir = Directory('${Directory.systemTemp.path}\\umbrix_update_${DateTime.now().millisecondsSinceEpoch}');
|
||||
await tempDir.create(recursive: true);
|
||||
|
||||
|
||||
loggy.info('Extracting ZIP to: ${tempDir.path}');
|
||||
|
||||
|
||||
// Распаковка через PowerShell
|
||||
final extractResult = await Process.run(
|
||||
'powershell',
|
||||
['-Command', 'Expand-Archive', '-Path', '"$savePath"', '-DestinationPath', '"${tempDir.path}"', '-Force'],
|
||||
[
|
||||
'-Command',
|
||||
'Expand-Archive',
|
||||
'-Path',
|
||||
'"$savePath"',
|
||||
'-DestinationPath',
|
||||
'"${tempDir.path}"',
|
||||
'-Force'
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
if (extractResult.exitCode != 0) {
|
||||
throw Exception('Failed to extract ZIP: ${extractResult.stderr}');
|
||||
}
|
||||
|
||||
|
||||
loggy.info('ZIP extracted successfully');
|
||||
|
||||
|
||||
// Скрипт для замены файлов после закрытия приложения
|
||||
final updateScript = '''
|
||||
@echo off
|
||||
@@ -209,23 +227,23 @@ start "" "$exePath"
|
||||
echo Update complete!
|
||||
del "%~f0"
|
||||
''';
|
||||
|
||||
|
||||
final scriptPath = '${Directory.systemTemp.path}\\umbrix_update.bat';
|
||||
await File(scriptPath).writeAsString(updateScript);
|
||||
|
||||
|
||||
if (context.mounted) {
|
||||
CustomToast.success('Обновление установлено! Приложение перезагрузится...').show(context);
|
||||
context.pop();
|
||||
}
|
||||
|
||||
|
||||
// Запустить скрипт и закрыть приложение
|
||||
await Process.start('cmd', ['/c', scriptPath], mode: ProcessStartMode.detached);
|
||||
|
||||
|
||||
// Задержка перед выходом
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
exit(0);
|
||||
});
|
||||
|
||||
|
||||
return;
|
||||
} catch (e) {
|
||||
loggy.warning('Failed to install from ZIP: $e');
|
||||
|
||||
285
lib/features/bug_report/data/bug_report_service.dart
Normal file
285
lib/features/bug_report/data/bug_report_service.dart
Normal file
@@ -0,0 +1,285 @@
|
||||
import 'dart:io';
|
||||
import 'package:umbrix/core/telegram/telegram_logger.dart';
|
||||
import 'package:umbrix/features/log/data/log_path_resolver.dart';
|
||||
import 'package:umbrix/features/log/data/log_data_providers.dart';
|
||||
import 'package:umbrix/features/connection/notifier/connection_notifier.dart';
|
||||
import 'package:umbrix/features/connection/model/connection_status.dart';
|
||||
import 'package:umbrix/features/proxy/active/active_proxy_notifier.dart';
|
||||
import 'package:umbrix/utils/utils.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
/// Сервис для отправки багрепортов в техподдержку
|
||||
class BugReportService with InfraLogger {
|
||||
BugReportService({
|
||||
required this.telegramLogger,
|
||||
required this.logPathResolver,
|
||||
required this.ref,
|
||||
});
|
||||
|
||||
final TelegramLogger telegramLogger;
|
||||
final LogPathResolver logPathResolver;
|
||||
final Ref ref;
|
||||
|
||||
/// Собрать диагностическую информацию
|
||||
Future<DiagnosticInfo> collectDiagnostics() async {
|
||||
try {
|
||||
// 1. Информация о системе (анонимно)
|
||||
final deviceInfo = TelegramLogger.getAnonymousDeviceInfo();
|
||||
|
||||
// 2. Статус подключения
|
||||
String connectionStatus = 'Неизвестно';
|
||||
String? connectionError;
|
||||
try {
|
||||
final connection = await ref.read(connectionNotifierProvider.future);
|
||||
connectionStatus = connection.format();
|
||||
if (connection is Disconnected && connection.connectionFailure != null) {
|
||||
connectionError = connection.connectionFailure.toString();
|
||||
}
|
||||
} catch (e) {
|
||||
connectionStatus = 'Ошибка получения статуса';
|
||||
connectionError = e.toString();
|
||||
}
|
||||
|
||||
// 3. Активный прокси и его задержка
|
||||
String? activeProxyInfo;
|
||||
int? pingDelay;
|
||||
try {
|
||||
final activeProxy = await ref.read(activeProxyNotifierProvider.future);
|
||||
if (activeProxy != null) {
|
||||
pingDelay = activeProxy.urlTestDelay;
|
||||
activeProxyInfo = 'Тип: ${activeProxy.type}, Тег: ${activeProxy.tag}, Пинг: ${pingDelay}ms';
|
||||
} else {
|
||||
activeProxyInfo = 'Нет активного прокси';
|
||||
}
|
||||
} catch (e) {
|
||||
activeProxyInfo = 'Ошибка получения прокси: $e';
|
||||
}
|
||||
|
||||
// 4. Последние логи (последние 100 строк из каждого файла)
|
||||
final coreLogs = await _readLastLines(logPathResolver.coreFile(), 100);
|
||||
final appLogs = await _readLastLines(logPathResolver.appFile(), 100);
|
||||
|
||||
return DiagnosticInfo(
|
||||
deviceInfo: deviceInfo,
|
||||
connectionStatus: connectionStatus,
|
||||
connectionError: connectionError,
|
||||
activeProxyInfo: activeProxyInfo,
|
||||
pingDelay: pingDelay,
|
||||
coreLogsPreview: coreLogs,
|
||||
appLogsPreview: appLogs,
|
||||
timestamp: DateTime.now(),
|
||||
);
|
||||
} catch (e, st) {
|
||||
loggy.error('Ошибка сбора диагностики', e, st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Отправить багрепорт в Telegram
|
||||
Future<BugReportResult> sendBugReport({
|
||||
required String userDescription,
|
||||
bool includeLogs = true,
|
||||
}) async {
|
||||
try {
|
||||
final diagnostics = await collectDiagnostics();
|
||||
|
||||
// Формируем текст репорта
|
||||
final reportText = _formatBugReport(userDescription, diagnostics);
|
||||
|
||||
// Отправляем как текст (если влезает в лимит)
|
||||
bool success = false;
|
||||
if (reportText.length < 3500) {
|
||||
success = await telegramLogger.sendLogsAsText(
|
||||
reportText,
|
||||
deviceInfo: diagnostics.deviceInfo,
|
||||
);
|
||||
}
|
||||
|
||||
// Если текст слишком большой или отправка не удалась - отправляем файлы
|
||||
if (!success && includeLogs) {
|
||||
success = await _sendLogsAsFiles(diagnostics, userDescription);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
return BugReportResult.success(
|
||||
message: 'Отчёт отправлен в техподдержку',
|
||||
);
|
||||
} else {
|
||||
return BugReportResult.failure(
|
||||
error: 'Не удалось отправить отчёт. Telegram бот не настроен?',
|
||||
);
|
||||
}
|
||||
} catch (e, st) {
|
||||
loggy.error('Ошибка отправки багрепорта', e, st);
|
||||
return BugReportResult.failure(
|
||||
error: 'Ошибка: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Отправить логи как файлы
|
||||
Future<bool> _sendLogsAsFiles(DiagnosticInfo diagnostics, String userDescription) async {
|
||||
try {
|
||||
// Создаём временный файл с полным отчётом
|
||||
final tempDir = Directory.systemTemp;
|
||||
final reportFile = File('${tempDir.path}/umbrix_bug_report_${DateTime.now().millisecondsSinceEpoch}.txt');
|
||||
|
||||
final fullReport = _formatBugReport(userDescription, diagnostics, includeFullLogs: true);
|
||||
await reportFile.writeAsString(fullReport);
|
||||
|
||||
final success = await telegramLogger.sendLogsAsFile(
|
||||
reportFile,
|
||||
deviceInfo: diagnostics.deviceInfo,
|
||||
);
|
||||
|
||||
// Удаляем временный файл
|
||||
try {
|
||||
await reportFile.delete();
|
||||
} catch (_) {}
|
||||
|
||||
return success;
|
||||
} catch (e, st) {
|
||||
loggy.error('Ошибка отправки файлов логов', e, st);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Прочитать последние N строк из файла
|
||||
Future<String> _readLastLines(File file, int maxLines) async {
|
||||
try {
|
||||
if (!await file.exists()) {
|
||||
return '(Файл не существует)';
|
||||
}
|
||||
|
||||
final lines = await file.readAsLines();
|
||||
final lastLines = lines.length > maxLines ? lines.sublist(lines.length - maxLines) : lines;
|
||||
|
||||
return lastLines.join('\n');
|
||||
} catch (e) {
|
||||
return '(Ошибка чтения: $e)';
|
||||
}
|
||||
}
|
||||
|
||||
/// Форматировать багрепорт для отправки
|
||||
String _formatBugReport(
|
||||
String userDescription,
|
||||
DiagnosticInfo diagnostics, {
|
||||
bool includeFullLogs = false,
|
||||
}) {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
buffer.writeln('🐛 БАГРЕПОРТ UMBRIX');
|
||||
buffer.writeln('═══════════════════════════════');
|
||||
buffer.writeln();
|
||||
|
||||
// Описание пользователя
|
||||
buffer.writeln('📝 ОПИСАНИЕ ПРОБЛЕМЫ:');
|
||||
buffer.writeln(userDescription.trim());
|
||||
buffer.writeln();
|
||||
buffer.writeln('═══════════════════════════════');
|
||||
|
||||
// Система
|
||||
buffer.writeln('💻 УСТРОЙСТВО:');
|
||||
buffer.writeln(diagnostics.deviceInfo);
|
||||
buffer.writeln();
|
||||
|
||||
// Подключение
|
||||
buffer.writeln('🔌 СТАТУС ПОДКЛЮЧЕНИЯ:');
|
||||
buffer.writeln(diagnostics.connectionStatus);
|
||||
if (diagnostics.connectionError != null) {
|
||||
buffer.writeln('Ошибка: ${diagnostics.connectionError}');
|
||||
}
|
||||
buffer.writeln();
|
||||
|
||||
// Прокси
|
||||
buffer.writeln('🌐 ПРОКСИ:');
|
||||
buffer.writeln(diagnostics.activeProxyInfo ?? 'Не определён');
|
||||
if (diagnostics.pingDelay != null) {
|
||||
final delayMs = diagnostics.pingDelay!;
|
||||
final quality = delayMs < 100
|
||||
? '✅ Отлично'
|
||||
: delayMs < 300
|
||||
? '🟡 Нормально'
|
||||
: delayMs < 1000
|
||||
? '🟠 Медленно'
|
||||
: '🔴 Очень медленно';
|
||||
buffer.writeln('Задержка: ${delayMs}ms ($quality)');
|
||||
}
|
||||
buffer.writeln();
|
||||
|
||||
// Время создания
|
||||
buffer.writeln('🕐 ВРЕМЯ:');
|
||||
buffer.writeln(diagnostics.timestamp.toIso8601String());
|
||||
buffer.writeln();
|
||||
|
||||
// Логи (preview или full)
|
||||
if (includeFullLogs) {
|
||||
buffer.writeln('═══════════════════════════════');
|
||||
buffer.writeln('📋 CORE LOGS (ПОЛНЫЕ):');
|
||||
buffer.writeln(diagnostics.coreLogsPreview);
|
||||
buffer.writeln();
|
||||
buffer.writeln('📋 APP LOGS (ПОЛНЫЕ):');
|
||||
buffer.writeln(diagnostics.appLogsPreview);
|
||||
} else {
|
||||
buffer.writeln('═══════════════════════════════');
|
||||
buffer.writeln('📋 ЛОГИ (ПОСЛЕДНИЕ 20 СТРОК):');
|
||||
buffer.writeln();
|
||||
buffer.writeln('Core:');
|
||||
final coreLines = diagnostics.coreLogsPreview.split('\n');
|
||||
buffer.writeln(coreLines.length > 20 ? coreLines.sublist(coreLines.length - 20).join('\n') : diagnostics.coreLogsPreview);
|
||||
buffer.writeln();
|
||||
buffer.writeln('App:');
|
||||
final appLines = diagnostics.appLogsPreview.split('\n');
|
||||
buffer.writeln(appLines.length > 20 ? appLines.sublist(appLines.length - 20).join('\n') : diagnostics.appLogsPreview);
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// Диагностическая информация для багрепорта
|
||||
class DiagnosticInfo {
|
||||
final String deviceInfo;
|
||||
final String connectionStatus;
|
||||
final String? connectionError;
|
||||
final String? activeProxyInfo;
|
||||
final int? pingDelay;
|
||||
final String coreLogsPreview;
|
||||
final String appLogsPreview;
|
||||
final DateTime timestamp;
|
||||
|
||||
DiagnosticInfo({
|
||||
required this.deviceInfo,
|
||||
required this.connectionStatus,
|
||||
this.connectionError,
|
||||
this.activeProxyInfo,
|
||||
this.pingDelay,
|
||||
required this.coreLogsPreview,
|
||||
required this.appLogsPreview,
|
||||
required this.timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
/// Результат отправки багрепорта
|
||||
class BugReportResult {
|
||||
final bool isSuccess;
|
||||
final String message;
|
||||
final String? error;
|
||||
|
||||
BugReportResult.success({required this.message})
|
||||
: isSuccess = true,
|
||||
error = null;
|
||||
|
||||
BugReportResult.failure({required this.error})
|
||||
: isSuccess = false,
|
||||
message = '';
|
||||
}
|
||||
|
||||
/// Provider для сервиса багрепортов
|
||||
final bugReportServiceProvider = Provider<BugReportService>((ref) {
|
||||
return BugReportService(
|
||||
telegramLogger: TelegramLogger(),
|
||||
logPathResolver: ref.watch(logPathResolverProvider),
|
||||
ref: ref,
|
||||
);
|
||||
});
|
||||
162
lib/features/bug_report/widget/bug_report_dialog.dart
Normal file
162
lib/features/bug_report/widget/bug_report_dialog.dart
Normal file
@@ -0,0 +1,162 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:umbrix/features/bug_report/data/bug_report_service.dart';
|
||||
import 'package:umbrix/core/localization/translations.dart';
|
||||
import 'package:umbrix/utils/utils.dart';
|
||||
|
||||
/// Диалог для отправки багрепорта в техподдержку
|
||||
class BugReportDialog extends HookConsumerWidget {
|
||||
const BugReportDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final descriptionController = useTextEditingController();
|
||||
final isSending = useState(false);
|
||||
final includeLogs = useState(true);
|
||||
|
||||
Future<void> sendReport() async {
|
||||
if (descriptionController.text.trim().isEmpty) {
|
||||
CustomToast.error('Пожалуйста, опишите проблему').show(context);
|
||||
return;
|
||||
}
|
||||
|
||||
isSending.value = true;
|
||||
|
||||
try {
|
||||
final service = ref.read(bugReportServiceProvider);
|
||||
final result = await service.sendBugReport(
|
||||
userDescription: descriptionController.text,
|
||||
includeLogs: includeLogs.value,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
if (result.isSuccess) {
|
||||
CustomToast.success(result.message).show(context);
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
CustomToast.error(result.error ?? 'Неизвестная ошибка').show(context);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
CustomToast.error('Ошибка: $e').show(context);
|
||||
}
|
||||
} finally {
|
||||
isSending.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
FluentIcons.bug_20_regular,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
const Gap(12),
|
||||
const Text('Сообщить о проблеме'),
|
||||
],
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 500,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Опишите проблему, с которой вы столкнулись. Мы автоматически соберём диагностическую информацию (логи, статус подключения, задержку).',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
TextField(
|
||||
controller: descriptionController,
|
||||
maxLines: 6,
|
||||
enabled: !isSending.value,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Описание проблемы *',
|
||||
hintText: 'Например: "Не могу подключиться к серверу, ошибка таймаута"',
|
||||
border: const OutlineInputBorder(),
|
||||
helperText: 'Чем подробнее, тем лучше',
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
CheckboxListTile(
|
||||
title: const Text('Включить логи'),
|
||||
subtitle: Text(
|
||||
'Отправить файлы логов для диагностики',
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
value: includeLogs.value,
|
||||
onChanged: isSending.value
|
||||
? null
|
||||
: (value) {
|
||||
includeLogs.value = value ?? true;
|
||||
},
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
const Gap(8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
FluentIcons.shield_checkmark_20_regular,
|
||||
size: 20,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Мы не собираем личные данные. Только техническая информация.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: isSending.value ? null : () => Navigator.of(context).pop(),
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: isSending.value ? null : sendReport,
|
||||
icon: isSending.value
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(FluentIcons.send_20_regular),
|
||||
label: Text(isSending.value ? 'Отправка...' : 'Отправить'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Показать диалог багрепорта
|
||||
static Future<void> show(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => const BugReportDialog(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:umbrix/core/localization/translations.dart';
|
||||
import 'package:umbrix/core/router/router.dart';
|
||||
import 'package:umbrix/features/bug_report/widget/bug_report_dialog.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
/// Секция "Логи и отладка" в настройках
|
||||
@@ -14,6 +15,20 @@ class LogsSettingTiles extends HookConsumerWidget {
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Кнопка "Сообщить о проблеме"
|
||||
ListTile(
|
||||
title: const Text('Сообщить о проблеме'),
|
||||
subtitle: const Text('Отправить багрепорт в техподдержку с автоматическими логами'),
|
||||
leading: Icon(
|
||||
FluentIcons.bug_20_regular,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
trailing: const Icon(FluentIcons.send_20_regular),
|
||||
onTap: () {
|
||||
BugReportDialog.show(context);
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
// Переход на страницу логов
|
||||
ListTile(
|
||||
title: Text(t.logs.pageTitle),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: umbrix
|
||||
description: Cross Platform Multi Protocol Proxy Frontend.
|
||||
publish_to: "none"
|
||||
version: 1.7.6+176
|
||||
version: 1.7.8+178
|
||||
|
||||
environment:
|
||||
sdk: ">=3.3.0 <4.0.0"
|
||||
|
||||
Reference in New Issue
Block a user