Мобильное приложение может стабильно проходить тесты и всё равно падать у пользователей в сценариях, которые команда не успела предусмотреть. В статье разберём, как связать Firebase Crashlytics, Sentry и Datadog во Flutter‑приложении, чтобы собирать ошибки с контекстом, быстрее находить причины сбоев и видеть проблемы с производительностью до того, как они станут массовыми. Читать далее
Привет, Хабр!
Меня зовут Андрей Бирюков. Я — независимый эксперт в области ИТ и ИБ, преподаю в учебных центрах и пишу статьи и книги.
Существует масса приложений, которые падают в продуктивной среде, а разработчики узнают об этом через гневные отзывы в Google Play. Но, проблема не в том, что ошибки случаются — они случаются всегда. Проблема в том, что стандартные отчёты об ошибках часто бесполезны без контекста. Вы видите стектрейс, но не знаете, что пользователь делал за 10 секунд до падения, какое у него состояние приложения и какие сетевые запросы висели в очереди.
В этой статье мы поговорим о том, как построить гибридную стратегию сбора ошибок: Firebase Crashlytics для быстрой статистики и массовых инцидентов, Sentry для детального анализа с хлебными крошками, и Datadog для корпоративного мониторинга с полной трассировкой. А главное — как заставить их работать вместе, а не мешать друг другу.
Почему один инструмент не решает все задачиКонечно, всем бы хотелось, чтобы было какое‑то одно решение, позволяющее что называется, увидеть все. Но в реальности все несколько сложнее, и мы будем рассматривать сразу три решения. При этом, каждый из трёх сервисов решает свою задачу, и у каждого есть своя слепая зона.
Firebase Crashlytics — это чемпион по скорости и простоте. Он легковесный, мгновенно показывает, какой процент пользователей затронут конкретным крашем, и интегрируется с Firebase Analytics, позволяя видеть, какие события предшествовали падению. Но его кастомизация ограничена, а работа с изолятами и фоновыми ошибками оставляет желать лучшего.
Sentry даёт «хлебные крошки» (breadcrumbs) — вы можете записывать каждое действие пользователя, каждый переход между экранами и каждый сетевой запрос. Ошибка приходит в Sentry уже с контекстом: пользователь был на экране профиля, нажал кнопку «сохранить», перед этим получил ответ от API. Это бесценно для воспроизведения сложных багов.
Datadog — это enterprise‑решение с Real User Monitoring (RUM), которое показывает не только ошибки, но и производительность: долгие задачи, фризы интерфейса, сетевые задержки.
Глобальный перехват ошибокПравильная стратегия — использовать Crashlytics для быстрого реагирования на массовые проблемы, Sentry для глубокого анализа сложных кейсов, а Datadog (если бюджет позволяет) для проактивного мониторинга производительности.
Прежде чем говорить об отправке ошибок в конкретные сервисы, нужно понять, как перехватывать все ошибки в Flutter‑приложении.
Их три типа:
Ошибки в виджетах — перехватываются через FlutterError.onError.
Ошибки вне виджетов (асинхронные, в изолятах) — через PlatformDispatcher.instance.onError.
Ошибки в зонах — если вы используете runZonedGuarded.
Ниже приведена базовая структура инициализации, которая ловит все три типа ошибок:
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Инициализация Firebase — обязательна перед Crashlytics
await Firebase.initializeApp();
// Настройка Sentry
await SentryFlutter.init(
(options) {
options.dsn = 'YOUR_SENTRY_DSN';
options.tracesSampleRate = 0.2; // 20% сессий для производительности
},
appRunner: () => runApp(MyApp()),
);
// Глобальные обработчики ошибок
FlutterError.onError = (FlutterErrorDetails details) {
// Отправляем в Sentry
Sentry.captureException(
details.exception,
stackTrace: details.stack,
hint: SentryHint(details),
);
// Отправляем в Crashlytics как фатальную ошибку
FirebaseCrashlytics.instance.recordFlutterFatalError(details);
// Отправляем в Datadog
DatadogSdk.instance.rum?.handleFlutterError(details);
// Выводим в консоль в дебаге
if (kDebugMode) print(details.toString());
};
PlatformDispatcher.instance.onError = (error, stack) {
// Асинхронные ошибки
Sentry.captureException(error, stackTrace: stack);
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
DatadogSdk.instance.rum?.addErrorInfo(
error.toString(),
RumErrorSource.source,
stackTrace: stack,
);
return true; // Говорим, что обработали
};
runApp(const MyApp());
}Здесь стоит обратить внимание на важное предостережение: если вы используете Zones, убедитесь, что WidgetsFlutterBinding.ensureInitialized() и runApp() выполняются в одной и той же зоне. Начиная с Flutter 3.10, нарушение этого правила вызывает предупреждение в консоли. Если вы работаете с зонами, оборачивайте всё в единую runZonedGuarded:
runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
await SentryFlutter.init(...);
runApp(MyApp());
}, (error, stack) {
// Зонные ошибки сюда не дойдут, если вы правильно настроили PlatformDispatcher
// Но подстраховаться стоит
});Быстрая диагностика массовых проблемНачнем с Crashlytics — это ваш первый рубеж обороны. Он показывает, какие сбои чаще всего происходят и сколько пользователей они задевают. Но его сила не в автоматическом сборе, а в контексте, который вы добавляете руками.

Дело в том, что не все проблемы убивают приложение. Ошибка парсинга JSON или неудачный API‑запрос — это не сбой, но это проблема. Crashlytics позволяет логировать такие события как non‑fatal:
try {
final result = int.parse('invalid_number');
} catch (e, stack) {
FirebaseCrashlytics.instance.recordError(
e,
stack,
fatal: false,
reason: 'Number parsing failed in profile setup',
);
}Флаг fatal: false говорит Crashlytics, что это не сбой, но проблема заслуживает внимания. В дашборде такие ошибки группируются отдельно и не влияют на показатель «Безотказных пользователей».
При этом, главной фишкой Crashlytics являются custom keys. Вы можете привязать к каждому отчёту метаданные о состоянии приложения:
FirebaseCrashlytics.instance.setCustomKey('screen', 'CheckoutScreen');
FirebaseCrashlytics.instance.setCustomKey('cart_items', cartItems.length);
FirebaseCrashlytics.instance.setCustomKey('payment_method', selectedMethod);Теперь, когда на экране оформления заказа произойдёт сбой, вы увидите в Crashlytics, что у пользователя было 3 товара в корзине и выбран метод оплаты «Карта». Это сужает круг поиска с «где‑то в checkout» до конкретного сценария.
Также Crashlytics позволяет с определенными оговорками отслеживать действия пользователей. Так, если вы хотите понимать, затронул ли баг конкретного пользователя, добавьте идентификатор:
FirebaseCrashlytics.instance.setUserIdentifier(userId);Но будьте осторожны с персональными данными и другой нормативкой. Чтобы ничего не нарушить, анонимизируйте данный идентификатор (например, возьмите хеш от email).
Хлебные крошки и кастомные контекстыНаш следующий инструмент это Sentry. Он выигрывает там, где Crashlytics пасует — в сложных, редко воспроизводимых багах. Его главное оружие — breadcrumbs (хлебные крошки) и контексты.

Sentry автоматически добавляет breadcrumbs для навигации, сетевых запросов и событий жизненного цикла при использовании SentryWidget:
await SentryFlutter.init(
(options) {
options.dsn = 'YOUR_DSN';
options.sendDefaultPii = true; // Добавляет IP и заголовки запросов
options.enableLogs = true; // Логи тоже превращаются в breadcrumbs
},
appRunner: () => runApp(
SentryWidget(
child: MyApp(),
),
),
);Но самые ценные данные — те, что вы добавляете руками в ключевые моменты бизнес‑логики:
import 'package:sentry/sentry.dart';
void submitOrder(Order order) async {
Sentry.addBreadcrumb(Breadcrumb(
message: 'Order submitted',
category: 'checkout',
data: {
'order_id': order.id,
'total': order.total,
'items_count': order.items.length,
},
));
try {
await api.submitOrder(order);
} catch (e, stack) {
// При ошибке все breadcrumbs уйдут вместе с отчётом
await Sentry.captureException(e, stackTrace: stack);
}
}Когда через три дня после релиза упадёт оформление заказа, вы откроете Sentry, увидите стектрейс, а под ним — цепочку действий пользователя: «открыл корзину → применил промокод → нажал «Оформить» → отправил заказ → сбой». Именно эта цепочка превращает «где‑то упало» в «упало при применении промокода длиннее 10 символов».
Также Sentry позволяет добавлять глобальный контекст, который будет прикреплён к каждой ошибке:
Sentry.configureScope((scope) {
scope.setTag('app_version', '2.1.0');
scope.setTag('environment', 'production');
scope.setContext('user', {'id': userId, 'plan': 'premium'});
scope.setExtra('device_info', DeviceInfoPlugin().deviceInfo);
});Производительность и мониторинг в реальном времениDatadog RUM (Real User Monitoring) — это не про ошибки, а про состояние приложения в каждый момент времени. Он показывает, сколько времени занимает загрузка экрана, где происходят фризы и какие сетевые запросы тормозят интерфейс.

Далее мы посмотрим несколько примеров использования Datadog. Начнем с инициализации и трассировки.
import 'package:datadog_flutter_plugin/datadog_flutter_plugin.dart';
final configuration = DatadogConfiguration(
clientToken: 'YOUR_CLIENT_TOKEN',
env: 'production',
site: DatadogSite.us1,
nativeCrashReportEnabled: true,
rumConfiguration: DatadogRumConfiguration(
applicationId: 'YOUR_RUM_APP_ID',
sessionSamplingRate: 50.0, // 50% сессий для экономии трафика
traceSampleRate: 20.0, // 20% ресурсов с APM-трассировкой
reportFlutterPerformance: true, // Сбор метрик FPS и времени сборки
),
);
await DatadogSdk.runApp(configuration, TrackingConsent.granted, () async {
runApp(MyApp());
});
Для автоматического отслеживания навигации вы можете указать Datadog, на каком экране находится пользователь, добавив observer, как в примере ниже:
MaterialApp(
navigatorObservers: [
DatadogNavigationObserver(DatadogSdk.instance),
],
//...
);А если вы используете go_router, observer добавляется в конфигурацию роутера:
final router = GoRouter(
routes: [...],
observers: [
DatadogNavigationObserver(datadogSdk: DatadogSdk.instance),
],
);Каждое нажатие кнопки пользователем можно легко превратить в событие RUM:
RumUserActionDetector(
rum: DatadogSdk.instance.rum,
child: Scaffold(
// Все тапы внутри этого дерева будут автоматически залогированы
),
);Наконец, если нужен кастомный трекинг:
DatadogSdk.instance.rum?.addAction('checkout_completed', attributes: {
'order_value': total,
});Гибридная стратегия: когда что использоватьИтак, у вас есть три инструмента. Как в них не запутаться, куда и что отправлять? Здесь можно воспользоваться простым правилом. Firebase Crashlytics предназначен для всего, что прерывает работу приложения. Сбои, фатальные ошибки, и non‑fatal ошибки с высоким приоритетом. Здесь вы смотрите на тренды: «за последний час упало 500 пользователей на экране логина».
В свою очередь, Sentry используется для ошибок, требующих глубокого контекста. Нефатальные ошибки бизнес‑логики, проблемы с API, парсингом данных. Сюда же — все ошибки с тегами и breadcrumbs, чтобы потом воспроизвести сценарий.
А Datadog используем для мониторинга производительности и сетевых запросов. Здесь вы видите, что экран профиля грузится 3 секунды, хотя должен загружаться за 500 мс. И видите, какой именно эндпоинт тормозит.
В коде это выглядит так:
Future<Result<User>> fetchUser(String id) async {
try {
final response = await dio.get('/users/$id');
final user = User.fromJson(response.data);
// Успешный кейс — ничего не отправляем
return Success(user);
} catch (e, stack) {
// 1. В Crashlytics — как non-fatal с важностью
FirebaseCrashlytics.instance.recordError(e, stack, fatal: false);
// 2. В Sentry — с контекстом и breadcrumbs
Sentry.addBreadcrumb(Breadcrumb(
message: 'Failed to fetch user',
category: 'api',
data: {'user_id': id},
));
await Sentry.captureException(e, stackTrace: stack);
// 3. В Datadog — как ошибка в RUM-сессии
DatadogSdk.instance.rum?.addErrorInfo(
'Failed to fetch user: $e',
RumErrorSource.source,
stackTrace: stack,
);
return Failure(mapError(e));
}
}Обработка ошибок на уровне UIПоследняя миля — показать пользователю что‑то вменяемое вместо белого экрана. Для этого переопределяем ErrorWidget.builder:
import 'package:flutter/material.dart';
void main() {
ErrorWidget.builder = (FlutterErrorDetails details) {
// Логируем ошибку (она уже перехвачена глобальным обработчиком)
// Показываем дружелюбный UI
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'Что-то пошло не так',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text(
'Попробуйте перезапустить приложение',
style: TextStyle(color: Colors.grey),
),
],
),
),
);
};
runApp(MyApp());
}Этот кастомный виджет сработает, когда Flutter не может отрендерить рабочий виджет — например, при null в build или ошибке в initState.
В завершении статьи давайте подведем краткий итог. Итак, ни один инструмент не закроет все потребности. Crashlytics даёт скорость и массовость, Sentry — глубину и контекст, Datadog — производительность и полную картину.
Главное правило: ошибки не должны быть безмолвными. Если вы не видите ошибку в своём мониторинге — её не существует для вашей команды. А значит, её не починят.
Настройте глобальный перехват, продумайте, какие ошибки куда отправлять, и добавьте контекст везде, где это возможно. Тогда даже самый редкий баг, пойманный одним пользователем в Таиланде на Android 9, превратится в понятный кейс с хлебными крошками, стектрейсом и информацией об устройстве. И вы сможете починить его до того, как он станет массовым.
Когда ошибки, логи и трассировки живут в разных системах, даже хороший стек мониторинга не всегда помогает быстро найти причину сбоя.
На бесплатных занятиях разберём, как связать наблюдаемость, логирование и архитектурные требования в одну рабочую схему, чтобы замечать проблемы раньше пользователей и быстрее возвращать приложение в нормальное состояние. Присоединяйтесь:
4 августа, 20:00. «OpenTelemetry — наблюдаемость на блюдечке». Записаться
5 августа, 20:00. «Влияние нефункциональных требований на архитектуру». Записаться
17 августа, 20:00. «Системы логирования: ELK, EFK или Graylog?». Записаться
Больше бесплатных уроков июля смотрите в дайджесте.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Мост между SAST и фаззингом: как из сработки SAST получить подтверждённую уязвимость | 0 | 7.35 | 22-07-2026 |
| 2 | Баг-трекинг: почему баги возвращаются на прод и какая система это лечит | 0 | 7 | 24-06-2026 |
| 3 | Почему Docker не всегда лучший выбор и чем его заменить | 1 | 6.36 | 21-07-2026 |
| 4 | Названа фатальная ошибка при хранении данных | 0 | 5 | 30-06-2026 |
| 5 | Актуальность техстека как задача: наш путь от хаоса к регулярным SLA-апдейтам | 5 | 7 | 26-06-2026 |
| 6 | Как избежать ошибок в Claude Fable: совет инженера Anthropic | 0 | 5 | 07-07-2026 |
| 7 | 1. Stellantis продаёт электромобиль Fiat Topolino за $13 995, а ... | 0 | 8 | 13-07-2026 |
| 8 | Mehrere Probleme in ruby (Fedora) | 0 | 5 | 18-07-2026 |
| 9 | В мобильных приложениях для образования нашли более 1000 критических уязвимостей | 0 | 7 | 25-06-2026 |
| 10 | Пользователи в США сообщили о неполадках в работе Twitter | 0 | 0 | 21-03-2023 |