Flutter apps live in a harsh ecosystem — they run on multiple platforms (iOS and Android), speak to native code, and fail silently when errors escape your try/catch blocks. Without proper instrumentation, a crash in production means a one-star review before you ever see the error in your logs. Flutter error tracking captures those platform-specific exceptions, groups them by root cause, and surfaces them in a dashboard so you can fix crashes before users complain.
This guide sets up production-grade error tracking in a Flutter app using the Sentry SDK, pointed at LightTrace. By the end you'll capture Dart exceptions, platform crashes, native errors from iOS and Android, and monitor crash-free rate across releases. If you're new to error tracking, see our guide to what is error tracking for the bigger picture.
Install the Sentry Flutter SDK
Add the SDK to your pubspec.yaml:
flutter pub add sentry_flutter
Then initialize it before your app runs. Because LightTrace is Sentry-SDK-compatible, you point the standard Sentry SDK at LightTrace by changing only the dsn.
import 'package:flutter/material.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
Future<void> main() async {
await SentryFlutter.init(
(options) {
options.dsn = 'https://<key>@your-lighttrace-host/1';
options.tracesSampleRate = 1.0;
options.environment = 'production';
options.release = 'app@1.0.0+1'; // version@build
},
appRunner: () => runApp(const MyApp()),
);
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const HomePage(),
navigatorObservers: [
SentryNavigatorObserver(),
],
);
}
}
That single init installs a global handler that catches uncaught exceptions. The SentryNavigatorObserver also captures route changes, so breadcrumbs show your user's navigation path leading up to a crash.
The dsn is your project's error endpoint. Each LightTrace project has its own DSN — you'll find it in project settings. Use a placeholder like the one above in development; swap in your real DSN on the next step.
Configure for iOS and Android builds
Flutter error tracking works differently on each platform because iOS and Android speak different crash languages. Sentry handles the plumbing, but you need to enable symbol upload so stack traces become readable. This is similar to how source maps work for JavaScript — without them, a crash points to minified code instead of your real files.
For iOS, add a build phase in Xcode. Open ios/Runner.xcworkspace, select the Runner target, go to Build Phases, and add a new Run Script phase:
"$PODS_ROOT/Sentry/Scripts/sentry-debug-symbols.sh"
This uploads dSYM (debug symbols) after each build so iOS crashes map back to your real source lines.
For Android, add Sentry's Gradle plugin to your app's android/app/build.gradle:
plugins {
id 'com.android.application'
id 'io.sentry.android.gradle' version '3.+'
}
sentry {
uploadProguardMapping = true
autoUploadProguardMapping = true
}
This automatically uploads Proguard mappings so minified Android stack traces become readable. Both steps are one-time setup — from then on, every release uploads symbols automatically.
Catch framework errors with Dart error handlers
Dart can throw errors in places try/catch doesn't reach: during image loading, in stream listeners, in background tasks. The Sentry SDK installs a global FlutterError.onError handler, but you can add your own for app-specific logic:
FlutterError.onError = (FlutterErrorDetails details) {
Sentry.captureException(details.exception, stackTrace: details.stack);
};
PlatformDispatcher.instance.onError = (error, stack) {
Sentry.captureException(error, stackTrace: stack);
return true;
};
The first catches Flutter framework errors (layout issues, asset loading, etc.). The second catches Dart errors that escape the main isolate — these are especially important for background tasks and futures that complete after the main thread has moved on. Combined with the SDK's built-in handlers, this setup captures nearly every crash that could otherwise go undetected.
Add breadcrumbs for fast debugging
Raw stack traces tell you what broke but not how your user got there. Breadcrumbs record navigation, HTTP requests, and user actions leading up to the crash, so you can reproduce the exact path — a critical piece of crash reporting.
// Log navigation
Sentry.addBreadcrumb(
Breadcrumb(
level: SentryLevel.info,
category: 'navigation',
message: 'Navigated to product details',
data: {'product_id': productId},
),
);
// Log network calls
Sentry.addBreadcrumb(
Breadcrumb(
level: SentryLevel.info,
category: 'http',
message: 'GET /api/products',
data: {'status_code': 200},
),
);
// Log user actions
Sentry.addBreadcrumb(
Breadcrumb(
level: SentryLevel.info,
category: 'user-action',
message: 'Tapped checkout button',
),
);
When a crash happens, the dashboard shows these breadcrumbs in order, so you see the exact sequence of events that led to failure. This turns a one-line stack trace into a reproducible story.
Set the user context early, right after login. When a crash arrives, you'll know exactly which user hit it and can correlate with their support ticket if they file one.
Sentry.setUser(SentryUser(
id: userId,
email: userEmail,
name: userName,
));
Monitor crash-free rate per release
The real power of crash-free rate is seeing which release introduced a regression. Tag every event with your app version so the dashboard groups crashes by build:
await SentryFlutter.init(
(options) {
options.release = 'app@1.0.0+5'; // version@build number
// ... other config
},
appRunner: () => runApp(const MyApp()),
);
Every crash from that build now rolls up into one release's crash-free percentage. When you ship 1.0.1 and the crash-free rate drops, you know that release broke something — and you can roll back or hotfix immediately instead of waiting for user reports.
Set up release tracking for fast rollbacks
When you deploy a new version, tell LightTrace about it so errors correlate with specific releases. Add this before your app runs:
final packageInfo = await PackageInfo.fromPlatform();
final version = packageInfo.version;
final buildNumber = packageInfo.buildNumber;
await SentryFlutter.init(
(options) {
options.release = '$version+$buildNumber';
options.dist = buildNumber;
},
appRunner: () => runApp(const MyApp()),
);
Now when a release starts crashing, you see exactly which build introduced the issue. Combine this with release health monitoring to spot regressions in the dashboard before they hit many users.
Common Flutter errors and how to debug them
A few errors show up constantly in Flutter apps. Each has its own root cause:
- Platform channel errors — a Dart call to iOS or Android fails; check the platform-specific logs and the stack trace. These often indicate a mismatch between the method signature on the platform side and the call from Dart.
- Async errors in isolates — background tasks throw but no one catches them; wrap isolate entry points in try/catch and use the global error handler. These are especially common in compute() calls and background services.
- Framework errors — layout constraint violations, asset load failures, or animation errors; the stack trace points to the exact line in your build method. These often happen during rapid rebuilds or when widgets receive unexpected state.
- Native crashes — Objective-C or Kotlin code crashes; symbols must be uploaded for these to be readable. Memory pressure, threading issues, and library incompatibilities are common culprits.
With proper error tracking in place, all of these stop being one-star reviews and become issues with a stack trace, a user count, and a breadcrumb trail showing exactly how the app got into that state. See how to add error tracking to any app for the framework-agnostic setup pattern.
Start tracking errors in minutes
Point the Sentry Flutter SDK at LightTrace and start catching crashes across iOS and Android in minutes — free up to 5,000 events a month.
That's the full setup: SDK initialized, symbols uploading automatically, breadcrumbs tracking the path to failure, and release context so regressions surface immediately. The next time a crash happens, you'll see it in a dashboard with enough context to fix it in minutes, not hours.