Глобальное логирование ошибок с отправкой в Telegram
от @Valerka_P
Давно хотел добавить красивый логер. Внедрить просто - добавляете кастомные файл и правите main. А вот такой красивый экран с ошибками появится у вас если потрясти устройство ,)
А вот такой красивый экран с ошибками появится у вас если потрясти устройство
Глобальное логирование ошибок с отправкой в Telegram
Описание решения
Система логирования на базе пакета talker_flutter, которая:
- Перехватывает все ошибки приложения (синхронные и асинхронные)
- Автоматически отправляет критические ошибки (exceptions) в Telegram бот
- Предоставляет экран просмотра логов (TalkerScreen) для отладки
- Ограничивает доступ к TalkerScreen по списку разрешённых user ID
- Открывает TalkerScreen по жесту "тряска телефона" (shake)
Архитектура
┌─────────────────────────────────────────────────────────────────┐
│ main.dart │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ runZonedGuarded │ │
│ │ (перехват асинхронных ошибок) │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ FlutterError.onError │ │ │
│ │ │ (перехват ошибок Flutter) │ │ │
│ │ │ ┌───────────────────────────────────────────────┐ │ │ │
│ │ │ │ ShakeGesture │ │ │ │
│ │ │ │ (открытие TalkerScreen при тряске) │ │ │ │
│ │ │ │ ┌─────────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ MaterialApp │ │ │ │ │
│ │ │ │ │ (всё приложение) │ │ │ │ │
│ │ │ │ └─────────────────────────────────────────┘ │ │ │ │
│ │ │ └───────────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ TalkerService │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Talker │ │
│ │ (логирование в консоль) │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ TelegramTalkerObserver │ │
│ │ (отправка exceptions в Telegram) │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ Telegram Bot │
│ API │
└─────────────────┘
Что отправляется в Telegram
| Тип | Отправка | Описание |
|---|---|---|
exception |
✅ Да | Критические ошибки (try-catch exceptions) |
error |
✅ Да | FlutterError (ошибки рендеринга и т.д.) |
warning |
❌ Нет | Только в локальные логи |
info |
❌ Нет | Только в локальные логи |
debug |
❌ Нет | Только в локальные логи |
Формат сообщения в Telegram
🚨 EXCEPTION в Hoof Master
👤 Пользователь:
• ID: 541c2bfe-bddb-4cb7-8308-90a054961fa6
• Имя: Иван Иванов
• Email: ivan@example.com
❌ Ошибка:
FormatException: Invalid date format
📝 Сообщение:
Ошибка при парсинге даты
📍 Stack Trace:
#0 DateParser.parse (package:app/utils/date.dart:42:10)
#1 _FormWidgetState.submit (package:app/widgets/form.dart:123:15)
...
🕐 Время: 2026-01-09T12:30:45.123456
Требования
Зависимости в pubspec.yaml
dependencies:
# Логирование и отображение логов
talker_flutter: ^4.6.1
# Распознавание жеста тряски телефона
shake_gesture: ^2.0.0
# HTTP запросы (обычно уже есть)
http: ^1.4.0
Полный код решения
Файл: lib/custom_code/TalkerService.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:talker_flutter/talker_flutter.dart';
import '/app_state.dart';
/// ============================================================================
/// КОНФИГУРАЦИЯ - Telegram бот и доступы
/// ============================================================================
/// Токен Telegram бота для отправки ошибок
const String _telegramBotToken = 'YOUR_BOT_TOKEN_HERE';
/// Chat ID куда отправлять ошибки (группа/канал/личный чат)
const String _telegramChatId = 'YOUR_CHAT_ID_HERE';
/// Список user ID которым разрешён доступ к TalkerScreen
/// Добавляйте сюда UUID пользователей из вашей базы данных
const List<String> _allowedUserIds = [
'user-uuid-1',
'user-uuid-2',
];
/// ============================================================================
/// TalkerService - синглтон сервис логирования
/// ============================================================================
/// Предоставляет:
/// - Глобальный экземпляр Talker для логирования
/// - Автоматическую отправку критических ошибок в Telegram
/// - Доступ к TalkerScreen для разрешённых пользователей
/// ============================================================================
class TalkerService {
/// Приватный конструктор для паттерна Singleton
TalkerService._internal();
/// Единственный экземпляр сервиса
static final TalkerService _instance = TalkerService._internal();
/// Фабричный конструктор возвращает единственный экземпляр
factory TalkerService() => _instance;
/// Экземпляр Talker - основной логгер
late final Talker _talker;
/// Геттер для доступа к Talker из любого места приложения
Talker get talker => _talker;
/// Флаг инициализации
bool _isInitialized = false;
/// ============================================================================
/// Инициализация сервиса
/// ============================================================================
/// Вызывается один раз при старте приложения в main()
/// Создаёт Talker с TelegramObserver для отправки ошибок
/// ============================================================================
void initialize() {
if (_isInitialized) return;
_isInitialized = true;
/// Создаём observer для отправки ошибок в Telegram
final telegramObserver = TelegramTalkerObserver();
/// Инициализируем Talker с настройками
_talker = TalkerFlutter.init(
/// Observer для отправки ошибок в Telegram
observer: telegramObserver,
/// Настройки логирования
settings: TalkerSettings(
/// Включаем логирование в консоль
enabled: true,
/// Используем цветной вывод в консоль
useConsoleLogs: true,
/// Максимальное количество записей в истории
maxHistoryItems: 1000,
),
);
/// Логируем успешную инициализацию
_talker.info('TalkerService инициализирован');
/// ========================================================================
/// ТЕСТОВАЯ ОШИБКА - раскомментировать для проверки отправки в Telegram
/// ========================================================================
// Future.delayed(const Duration(seconds: 3), () {
// logException(
// Exception('Тестовая ошибка для проверки Telegram бота'),
// StackTrace.current,
// 'Тест отправки в Telegram',
// );
// });
}
/// ============================================================================
/// Проверка доступа пользователя к TalkerScreen
/// ============================================================================
/// Возвращает true если текущий пользователь есть в списке _allowedUserIds
/// ============================================================================
bool hasAccess() {
final currentUserId = FFAppState().user.id;
return _allowedUserIds.contains(currentUserId);
}
/// ============================================================================
/// Открыть TalkerScreen (экран логов)
/// ============================================================================
/// Проверяет права доступа и открывает экран если пользователь разрешён.
/// Возвращает true если экран был открыт, false если доступ запрещён.
/// ============================================================================
bool openTalkerScreen(BuildContext context) {
/// Проверяем права доступа
if (!hasAccess()) {
_talker.warning('Попытка доступа к TalkerScreen без прав');
return false;
}
/// Открываем экран логов
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => TalkerScreen(
talker: _talker,
/// Кастомизация темы экрана
theme: TalkerScreenTheme(
backgroundColor: Colors.grey[900]!,
cardColor: Colors.grey[850] ?? Colors.grey[800]!,
textColor: Colors.white,
/// Цвета для разных типов логов
logColors: {
TalkerLogType.error.key: Colors.redAccent,
TalkerLogType.critical.key: Colors.red,
TalkerLogType.exception.key: Colors.deepOrange,
TalkerLogType.warning.key: Colors.orange,
TalkerLogType.info.key: Colors.blue,
TalkerLogType.debug.key: Colors.grey,
TalkerLogType.verbose.key: Colors.grey[600]!,
},
),
),
),
);
_talker.info('TalkerScreen открыт');
return true;
}
}
/// ============================================================================
/// TelegramTalkerObserver - отправка ошибок в Telegram
/// ============================================================================
/// Observer который слушает все события Talker и отправляет
/// критические ошибки (exceptions) в Telegram бот.
/// ============================================================================
class TelegramTalkerObserver extends TalkerObserver {
/// ============================================================================
/// Обработка Exception (критических ошибок)
/// ============================================================================
/// Вызывается когда происходит exception в приложении.
/// Отправляет подробную информацию в Telegram.
/// ============================================================================
void onException(TalkerException exception) {
_sendToTelegram(
type: 'EXCEPTION',
message: exception.message ?? 'Нет сообщения',
error: exception.exception?.toString() ?? 'Неизвестная ошибка',
stackTrace: exception.stackTrace?.toString(),
);
}
/// ============================================================================
/// Обработка Error (ошибок Flutter)
/// ============================================================================
/// Вызывается при FlutterError и других критических ошибках.
/// ============================================================================
void onError(TalkerError err) {
_sendToTelegram(
type: 'ERROR',
message: err.message ?? 'Нет сообщения',
error: err.error?.toString() ?? 'Неизвестная ошибка',
stackTrace: err.stackTrace?.toString(),
);
}
/// ============================================================================
/// Отправка сообщения в Telegram
/// ============================================================================
/// Формирует сообщение с информацией об ошибке и отправляет
/// через Telegram Bot API.
/// ============================================================================
Future<void> _sendToTelegram({
required String type,
required String message,
required String error,
String? stackTrace,
}) async {
try {
/// Получаем информацию о пользователе из состояния приложения
final userId = FFAppState().user.id;
final userName = FFAppState().user.name;
final userEmail = FFAppState().user.email;
/// Формируем текст сообщения
/// Ограничиваем длину stackTrace чтобы не превысить лимит Telegram (4096 символов)
final truncatedStackTrace = stackTrace != null && stackTrace.length > 1500
? '${stackTrace.substring(0, 1500)}...\n[обрезано]'
: stackTrace;
/// Экранируем специальные символы Markdown
final safeError = _escapeMarkdown(error);
final safeMessage = _escapeMarkdown(message);
final safeName = _escapeMarkdown(userName.isNotEmpty ? userName : 'Неизвестно');
final safeEmail = _escapeMarkdown(userEmail.isNotEmpty ? userEmail : 'Неизвестно');
final text = '''
🚨 *$type* в MyApp
👤 *Пользователь:*
• ID: `$userId`
• Имя: $safeName
• Email: $safeEmail
❌ *Ошибка:*
$safeError
📝 *Сообщение:*
$safeMessage
📍 *Stack Trace:*
${truncatedStackTrace ?? 'Нет stacktrace'}
🕐 *Время:* ${DateTime.now().toIso8601String()}
''';
/// URL Telegram Bot API
final url = Uri.parse(
'https://api.telegram.org/bot$_telegramBotToken/sendMessage',
);
/// Создаём клиент с таймаутом
final client = http.Client();
try {
/// Отправляем POST запрос с таймаутом
final response = await client
.post(
url,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: {
'chat_id': _telegramChatId,
'text': text,
'parse_mode': 'Markdown',
},
)
.timeout(const Duration(seconds: 10));
/// Логируем результат для отладки
if (response.statusCode != 200) {
print('Telegram API error: ${response.statusCode} - ${response.body}');
} else {
print('Telegram: сообщение отправлено успешно');
}
} finally {
client.close();
}
} catch (e) {
/// Если не удалось отправить в Telegram - логируем локально
/// Не выбрасываем ошибку чтобы не создать бесконечный цикл
print('Ошибка отправки в Telegram: $e');
}
}
/// Экранирование специальных символов Markdown для Telegram
String _escapeMarkdown(String text) {
return text
.replaceAll('_', '\\_')
.replaceAll('*', '\\*')
.replaceAll('[', '\\[')
.replaceAll(']', '\\]')
.replaceAll('`', '\\`');
}
}
/// ============================================================================
/// Глобальные функции-хелперы для удобного логирования
/// ============================================================================
/// Логирование информационного сообщения
void logInfo(String message) => TalkerService().talker.info(message);
/// Логирование предупреждения
void logWarning(String message) => TalkerService().talker.warning(message);
/// Логирование ошибки (НЕ отправляется в Telegram)
void logError(String message) => TalkerService().talker.error(message);
/// Логирование и обработка exception (отправляется в Telegram)
void logException(Object exception, [StackTrace? stackTrace, String? message]) {
TalkerService().talker.handle(exception, stackTrace, message);
}
/// Логирование debug сообщения
void logDebug(String message) => TalkerService().talker.debug(message);
Файл: lib/main.dart
ВАЖНО: При использовании
MaterialApp.routerс GoRouter НЕ добавляйте
дополнительный Navigator вbuilder. Это блокирует доступ к GoRouter
из дочерних виджетов и вызывает ошибку "No GoRouter found in context".
import 'dart:async';
import 'dart:ui' as ui;
import 'package:provider/provider.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:shake_gesture/shake_gesture.dart';
// Ваши импорты...
import '/app_state.dart';
import '/custom_code/TalkerService.dart';
void main() async {
/// ============================================================================
/// runZonedGuarded оборачивает всё приложение для перехвата
/// асинхронных ошибок которые не ловятся через try-catch
/// ============================================================================
runZonedGuarded<Future<void>>(
() async {
WidgetsFlutterBinding.ensureInitialized();
/// ========================================================================
/// Инициализация TalkerService - должен быть первым!
/// ========================================================================
/// Это позволяет логировать все последующие этапы инициализации
/// и перехватывать ошибки с самого начала
/// ========================================================================
TalkerService().initialize();
/// ========================================================================
/// Настройка глобального обработчика ошибок Flutter
/// ========================================================================
/// FlutterError.onError перехватывает ошибки рендеринга,
/// widget lifecycle и другие Flutter-специфичные ошибки
/// ========================================================================
FlutterError.onError = (FlutterErrorDetails details) {
/// Логируем ошибку в Talker (будет отправлена в Telegram)
TalkerService().talker.handle(
details.exception,
details.stack,
'FlutterError: ${details.context?.toString() ?? "Нет контекста"}',
);
};
// Ваша инициализация...
// await YourService.initialize();
final appState = FFAppState();
await appState.initializePersistedState();
/// Логируем успешный запуск приложения
logInfo('Приложение успешно инициализировано');
runApp(ChangeNotifierProvider(
create: (context) => appState,
child: MyApp(),
));
},
/// ========================================================================
/// Обработчик асинхронных ошибок
/// ========================================================================
/// Перехватывает ошибки в Future, Stream, Timer и других
/// асинхронных операциях которые не были обработаны
/// ========================================================================
(error, stackTrace) {
/// Логируем ошибку в Talker (будет отправлена в Telegram)
TalkerService().talker.handle(
error,
stackTrace,
'Необработанная асинхронная ошибка',
);
},
);
}
class MyApp extends StatefulWidget {
State<MyApp> createState() => _MyAppState();
static _MyAppState of(BuildContext context) =>
context.findAncestorStateOfType<_MyAppState>()!;
}
/// Кастомный ScrollBehavior для поддержки скроллинга стилусом на планшетах
class MyAppScrollBehavior extends MaterialScrollBehavior {
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
PointerDeviceKind.stylus, // Поддержка стилуса для скролла
};
}
class _MyAppState extends State<MyApp> {
late GoRouter _router;
void initState() {
super.initState();
// Инициализация роутера (в реальном коде используется createRouter)
_router = createRouter(AppStateNotifier.instance);
}
/// ============================================================================
/// Обработчик жеста Shake (тряска телефона)
/// ============================================================================
/// Открывает TalkerScreen если у пользователя есть права доступа.
///
/// ВАЖНО: Используем _router.routerDelegate.navigatorKey.currentContext
/// для получения контекста, а НЕ отдельный Navigator с GlobalKey.
/// Дополнительный Navigator блокирует доступ к GoRouter!
/// ============================================================================
void _onShake() {
final context = _router.routerDelegate.navigatorKey.currentContext;
if (context != null) {
final opened = TalkerService().openTalkerScreen(context);
if (!opened) {
/// Если доступ запрещён - можно показать snackbar (опционально)
logDebug('Shake detected, но доступ к TalkerScreen запрещён');
}
}
}
Widget build(BuildContext context) {
/// ========================================================================
/// ShakeGesture оборачивает приложение для открытия TalkerScreen
/// при тряске телефона (только для разрешённых пользователей)
/// ========================================================================
return ShakeGesture(
onShake: _onShake,
child: MaterialApp.router(
// Ваши настройки MaterialApp.router...
scrollBehavior: MyAppScrollBehavior(),
routerConfig: _router,
/// ВАЖНО: НЕ используйте builder с дополнительным Navigator!
/// Это заблокирует GoRouter и вызовет ошибку
/// "No GoRouter found in context" в дочерних виджетах.
///
/// ❌ НЕПРАВИЛЬНО:
/// builder: (context, child) {
/// return Navigator(
/// key: _navigatorKey,
/// onGenerateRoute: (_) => MaterialPageRoute(
/// builder: (_) => child ?? const SizedBox.shrink(),
/// ),
/// );
/// },
///
/// ✅ ПРАВИЛЬНО: не использовать builder вообще,
/// или использовать для других целей (например, баннер):
/// builder: (context, child) {
/// return Stack(children: [child!, const NoInternetBanner()]);
/// },
),
);
}
}
Инструкция по интеграции
Шаг 1: Создать Telegram бота
-
Откройте Telegram и найдите @BotFather
-
Отправьте команду
/newbot -
Следуйте инструкциям - укажите имя и username бота
-
Получите токен бота (формат:
123456789:ABCdefGHI...) -
Создайте группу/канал и добавьте туда бота
-
Получите chat_id:
- отправьте сообщение в группу и откройте:
https://api.telegram.org/bot<TOKEN>/getUpdates
- отправьте сообщение в группу и откройте:
Шаг 2: Добавить зависимости
flutter pub add talker_flutter shake_gesture
Или в pubspec.yaml:
dependencies:
talker_flutter: ^4.6.1
shake_gesture: ^2.0.0
Шаг 3: Создать TalkerService.dart
Скопируйте код выше в lib/custom_code/TalkerService.dart и замените:
YOUR_BOT_TOKEN_HEREна токен вашего ботаYOUR_CHAT_ID_HEREна ваш chat_id- Список
_allowedUserIdsна UUID ваших пользователей
Шаг 4: Интегрировать в main.dart
- Добавьте импорт TalkerService
- Оберните
main()вrunZonedGuarded - Инициализируйте TalkerService первым
- Настройте
FlutterError.onError - Оберните приложение в
ShakeGesture
Шаг 5: Тестирование
Раскомментируйте тестовый код в TalkerService.initialize():
Future.delayed(const Duration(seconds: 3), () {
logException(
Exception('Тестовая ошибка для проверки Telegram бота'),
StackTrace.current,
'Тест отправки в Telegram',
);
});
Запустите приложение и проверьте Telegram - через 3 секунды должно прийти сообщение.
Использование в коде
Логирование (не отправляется в Telegram)
import '/custom_code/TalkerService.dart';
// Информационное сообщение
logInfo('Пользователь вошёл в систему');
// Предупреждение
logWarning('Кэш устарел, обновляем...');
// Ошибка (без отправки в Telegram)
logError('Не удалось загрузить изображение');
// Debug (только для разработки)
logDebug('API response: $response');
Логирование с отправкой в Telegram
try {
await someRiskyOperation();
} catch (e, stackTrace) {
// Отправится в Telegram!
logException(e, stackTrace, 'Ошибка в someRiskyOperation');
}
Прямой доступ к Talker
final talker = TalkerService().talker;
// Все методы Talker доступны
talker.info('Info');
talker.warning('Warning');
talker.error('Error');
talker.handle(exception, stackTrace, 'Message');
// Кастомные логи
talker.logTyped(YourCustomLog('message'));
Открытие TalkerScreen программно
// Откроет только если пользователь в списке разрешённых
TalkerService().openTalkerScreen(context);
// Проверка доступа
if (TalkerService().hasAccess()) {
// Показать кнопку "Логи" в меню
}
Кастомизация
Изменить тему TalkerScreen
TalkerScreen(
talker: _talker,
theme: TalkerScreenTheme(
backgroundColor: Colors.black,
cardColor: Colors.grey[900]!,
textColor: Colors.white,
logColors: {
TalkerLogType.error.key: Colors.red,
TalkerLogType.warning.key: Colors.amber,
TalkerLogType.info.key: Colors.lightBlue,
},
),
)
Добавить больше данных в Telegram
В методе _sendToTelegram можно добавить:
- Версию приложения
- Модель устройства
- Версию ОС
- Текущий экран/роут
import 'package:package_info_plus/package_info_plus.dart';
import 'package:device_info_plus/device_info_plus.dart';
// В _sendToTelegram:
final packageInfo = await PackageInfo.fromPlatform();
final deviceInfo = await DeviceInfoPlugin().androidInfo; // или iosInfo
final text = '''
🚨 *$type*
📱 *Устройство:*
• Модель: ${deviceInfo.model}
• Android: ${deviceInfo.version.release}
• App: ${packageInfo.version}+${packageInfo.buildNumber}
// остальное...
''';
Изменить жест открытия
Вместо shake можно использовать:
- Long press на определённом элементе
- Секретная комбинация тапов
- Скрытая кнопка в настройках
// Пример: long press на версии приложения
GestureDetector(
onLongPress: () => TalkerService().openTalkerScreen(context),
child: Text('v1.0.0'),
)
Безопасность
Что НЕ должно попадать в логи
- Пароли пользователей
- Токены авторизации
- Номера карт и CVV
- Персональные данные (по GDPR)
Рекомендации
- Не логируйте тела запросов с чувствительными данными
- Маскируйте токены в логах:
token: "eyJ...***" - Ограничьте список
_allowedUserIdsтолько разработчиками - Храните токен бота безопасно (не в публичных репозиториях)
Troubleshooting
Сообщения не приходят в Telegram
- Проверьте токен бота - он должен быть актуальным
- Проверьте chat_id - для групп он отрицательный
- Убедитесь что бот добавлен в группу/канал
- Проверьте логи:
Telegram: сообщение отправлено успешноили ошибка
TalkerScreen не открывается при тряске
- Проверьте что user ID в списке
_allowedUserIds - Убедитесь что ShakeGesture обёрнут правильно
- На эмуляторе тряска может не работать - используйте реальное устройство
Ошибки не логируются
- Убедитесь что
TalkerService().initialize()вызывается первым вmain() - Проверьте что
FlutterError.onErrorнастроен - Проверьте что
runZonedGuardedоборачивает весь код
Ошибка "No GoRouter found in context"
Симптомы: При навигации из popup/modal или после закрытия диалога
появляется ошибка No GoRouter found in context.
Причина: В MaterialApp.router использован builder с дополнительным
Navigator, который перехватывает контекст и блокирует доступ к GoRouter.
Решение:
- Удалите дополнительный Navigator из builder
- Используйте
_router.routerDelegate.navigatorKey.currentContextдля
получения контекста вместо собственного GlobalKey
// ❌ НЕПРАВИЛЬНО - блокирует GoRouter:
builder: (context, child) {
return Navigator(
key: _navigatorKey,
onGenerateRoute: (_) => MaterialPageRoute(
builder: (_) => child ?? const SizedBox.shrink(),
),
);
},
// ✅ ПРАВИЛЬНО - используем контекст из GoRouter:
void _onShake() {
final context = _router.routerDelegate.navigatorKey.currentContext;
if (context != null) {
TalkerService().openTalkerScreen(context);
}
}
Автор
Решение разработано на базе пакета talker_flutter.
Дата: Январь 2026
