Analytics and Crash Reporting Guide
This project includes built-in analytics and crash reporting services that help track user behavior, monitor app performance, and identify issues in production.
Structure
lib/core/services/
├── analytics/
│ ├── analytics_service.dart # Abstract service interface
│ ├── posthog_analytics_service.dart # Debug implementation
│ └── analytics_navigator_observer.dart # Automatic screen tracking
└── crash_reporting/
├── crash_reporting_service.dart # Abstract service interface
└── debug_crash_reporting_service.dart # Debug implementation1. Analytics Service
The analytics service tracks user interactions, screen views, and custom events to help understand user behavior.
Using Analytics in Your Code
1. User Identification
Track user identity for personalized analytics:
import 'package:flutter_boilerplate/app/di.dart';
import 'package:flutter_boilerplate/core/services/analytics/analytics_service.dart';
class AuthCubit extends Cubit<AuthState> {
Future<void> signIn(String email, String password) async {
// ... authentication logic
if (user != null) {
// ✅ Identify user for analytics
await di<AnalyticsService>().identifyUser(
userId: user.id,
email: user.email,
name: user.displayName,
photoUrl: user.photoUrl,
username: user.username,
);
}
}
}2. Event Tracking
Track user actions and interactions:
class NotesCubit extends Cubit<NotesState> {
Future<void> createNote(String title, String content) async {
// ... note creation logic
final (note, failure) = await _repository.createNote(
userId: userId,
title: title,
content: content,
);
if (failure == null && note != null) {
// ✅ Track note creation event
await di<AnalyticsService>().logEvent(
'note_created',
parameters: {
'note_id': note.id,
'title_length': title.length,
'content_length': content.length,
'creation_method': 'manual',
},
);
}
}
Future<void> deleteNote(String id) async {
// ... deletion logic
// ✅ Track note deletion
await di<AnalyticsService>().logEvent(
'note_deleted',
parameters: {
'note_id': id,
'deletion_method': 'swipe',
},
);
}
}3. Feature Usage Tracking
Track feature adoption and usage patterns:
class SettingsCubit extends Cubit<SettingsState> {
Future<void> changeTheme(ThemeMode themeMode) async {
// ... theme change logic
// ✅ Track theme preference
await di<AnalyticsService>().logEvent(
'theme_changed',
parameters: {
'theme_mode': themeMode.name,
'previous_theme': state.themeMode.name,
},
);
}
Future<void> changeLanguage(Locale locale) async {
// ... language change logic
// ✅ Track language preference
await di<AnalyticsService>().logEvent(
'language_changed',
parameters: {
'language_code': locale.languageCode,
'country_code': locale.countryCode,
},
);
}
}4. Screen Tracking
Screen tracking is handled automatically by AnalyticsNavigatorObserver, but you can also track screens manually:
class FeaturePage extends StatefulWidget {
@override
void initState() {
super.initState();
// ✅ Manual screen tracking (if needed)
WidgetsBinding.instance.addPostFrameCallback((_) {
di<AnalyticsService>().logScreen('feature_page');
});
}
}5. Error and Performance Tracking
Track errors and performance metrics:
class DataService {
Future<List<Item>> loadItems() async {
final stopwatch = Stopwatch()..start();
try {
final items = await _api.getItems();
// ✅ Track successful data load
await di<AnalyticsService>().logEvent(
'data_load_success',
parameters: {
'item_count': items.length,
'load_time_ms': stopwatch.elapsedMilliseconds,
'data_source': 'api',
},
);
return items;
} catch (error) {
// ✅ Track data load failure
await di<AnalyticsService>().logEvent(
'data_load_failure',
parameters: {
'error_type': error.runtimeType.toString(),
'load_time_ms': stopwatch.elapsedMilliseconds,
'data_source': 'api',
},
);
rethrow;
}
}
}Common Analytics Events
Here are some common events you should consider tracking:
// User engagement
await analytics.logEvent('app_opened');
await analytics.logEvent('feature_discovered', parameters: {'feature': 'notes'});
await analytics.logEvent('tutorial_completed', parameters: {'step_count': 5});
// Content interaction
await analytics.logEvent('content_created', parameters: {'type': 'note'});
await analytics.logEvent('content_shared', parameters: {'method': 'link'});
await analytics.logEvent('search_performed', parameters: {'query_length': 10});
// User preferences
await analytics.logEvent('setting_changed', parameters: {'setting': 'notifications'});
await analytics.logEvent('subscription_upgraded', parameters: {'plan': 'premium'});
// Errors and issues
await analytics.logEvent('error_encountered', parameters: {'error_code': '404'});
await analytics.logEvent('feature_unavailable', parameters: {'feature': 'export'});2. Crash Reporting Service
The crash reporting service captures and reports application crashes and errors to help identify and fix issues.
Using Crash Reporting in Your Code
1. User Identification
Link crashes to specific users for better debugging:
class AuthCubit extends Cubit<AuthState> {
Future<void> signIn(String email, String password) async {
// ... authentication logic
if (user != null) {
// ✅ Identify user for crash reporting
await di<CrashReportingService>().identifyUser(userId: user.id);
}
}
Future<void> signOut() async {
// ... sign out logic
// ✅ Clear user identification
await di<CrashReportingService>().identifyUser(userId: 'anonymous');
}
}2. Error Reporting
Report non-fatal errors that don't crash the app:
class NotesRepository {
Future<(List<Note>, Failure?)> loadNotes() async {
try {
final notes = await _api.getNotes();
return (notes, null);
} catch (error, stackTrace) {
// ✅ Report non-fatal error
await di<CrashReportingService>().reportError(
error: error,
stackTrace: stackTrace,
fatal: false,
context: 'Failed to load notes from API',
);
// Return cached data or empty list
final cachedNotes = await _loadCachedNotes();
return (cachedNotes, const Failure(message: 'Failed to load notes'));
}
}
}3. Critical Error Reporting
Report critical errors that might affect app functionality:
class DatabaseService {
Future<void> initializeDatabase() async {
try {
await _database.open();
} catch (error, stackTrace) {
// ✅ Report critical error
await di<CrashReportingService>().reportError(
error: error,
stackTrace: stackTrace,
fatal: true,
context: 'Failed to initialize local database',
);
rethrow;
}
}
}4. Global Error Handling
Set up global error handlers to catch unhandled exceptions:
// In main.dart or bootstrap.dart
void setupErrorHandling() {
// ✅ Handle Flutter framework errors
FlutterError.onError = (FlutterErrorDetails details) {
di<CrashReportingService>().reportError(
error: details.exception,
stackTrace: details.stack,
fatal: false,
context: 'Flutter framework error: ${details.context}',
);
};
// ✅ Handle Dart errors outside Flutter
PlatformDispatcher.instance.onError = (error, stack) {
di<CrashReportingService>().reportError(
error: error,
stackTrace: stack,
fatal: true,
context: 'Unhandled Dart error',
);
return true;
};
}5. Context-Rich Error Reporting
Provide additional context to help with debugging:
class PaymentService {
Future<void> processPayment(PaymentRequest request) async {
try {
await _paymentProvider.charge(request);
} catch (error, stackTrace) {
// ✅ Report with rich context
await di<CrashReportingService>().reportError(
error: error,
stackTrace: stackTrace,
fatal: false,
context: 'Payment processing failed - '
'Amount: ${request.amount}, '
'Currency: ${request.currency}, '
'Method: ${request.paymentMethod}, '
'User: ${request.userId}',
);
rethrow;
}
}
}3. Service Configuration
Dependency Injection Setup
Configure both services in your DI container:
// In lib/app/di.dart
Future<void> _configureCoreServices() async {
// ... other services
// ✅ Register analytics service
if (kDebugMode) {
di.registerSingleton<AnalyticsService>(
DebugAnalyticsService(),
);
} else {
di.registerSingleton<AnalyticsService>(
FirebaseAnalyticsService(), // Your production implementation
);
}
// ✅ Register crash reporting service
if (kDebugMode) {
di.registerSingleton<CrashReportingService>(
DebugCrashReportingService(),
);
} else {
di.registerSingleton<CrashReportingService>(
FirebaseCrashlyticsService(), // Your production implementation
);
}
// ✅ Initialize services
await di<AnalyticsService>().initialize(enabled: !kDebugMode);
await di<CrashReportingService>().initialize(enabled: !kDebugMode);
}Router Integration
Add analytics observer to your router:
// In lib/app/routes.dart
GoRouter createRouter() => GoRouter(
observers: [
// ✅ Automatic screen tracking
di<AnalyticsNavigatorObserver>(),
],
routes: [/* routes */],
);4. Production Implementation
Firebase Analytics
class FirebaseAnalyticsService implements AnalyticsService {
final FirebaseAnalytics _analytics = FirebaseAnalytics.instance;
@override
Future<void> logEvent(String eventName, {Map<String, dynamic>? parameters}) async {
await _analytics.logEvent(
name: eventName,
parameters: parameters,
);
}
@override
Future<void> logScreen(String screenName) async {
await _analytics.logScreenView(screenName: screenName);
}
// ... other implementations
}Firebase Crashlytics
class FirebaseCrashlyticsService implements CrashReportingService {
final FirebaseCrashlytics _crashlytics = FirebaseCrashlytics.instance;
@override
Future<void> reportError({
required dynamic error,
StackTrace? stackTrace,
bool fatal = false,
String? context,
}) async {
await _crashlytics.recordError(
error,
stackTrace,
fatal: fatal,
information: [context ?? 'No additional context'],
);
}
// ... other implementations
}5. Privacy and Compliance
User Consent
Always respect user privacy and obtain consent:
class PrivacyService {
Future<void> requestAnalyticsConsent() async {
final consent = await showConsentDialog();
if (consent) {
await di<AnalyticsService>().initialize(enabled: true);
await di<CrashReportingService>().initialize(enabled: true);
} else {
await di<AnalyticsService>().initialize(enabled: false);
await di<CrashReportingService>().initialize(enabled: false);
}
}
}Data Anonymization
Remove or hash sensitive data before reporting:
Future<void> reportUserError(User user, Exception error) async {
await di<CrashReportingService>().reportError(
error: error,
context: 'User error - ID: ${_hashUserId(user.id)}, '
'Type: ${user.accountType}',
);
}
String _hashUserId(String userId) {
// Hash or anonymize user ID
return userId.hashCode.toString();
}6. Best Practices
Analytics
- Event Naming: Use consistent, descriptive event names
- Parameter Limits: Respect platform limits on parameter count and size
- User Privacy: Always obtain consent and respect privacy settings
- Performance: Don't block UI with analytics calls
- Testing: Use mock services in tests and debug builds
Crash Reporting
- Context Information: Provide rich context for easier debugging
- Error Classification: Distinguish between fatal and non-fatal errors
- User Identification: Link crashes to users while respecting privacy
- Sensitive Data: Never include passwords or personal data in reports
- Error Handling: Don't let crash reporting itself cause crashes
General
- Environment Separation: Use different configurations for debug/release
- Service Abstraction: Use abstract interfaces for easy testing and swapping
- Initialization: Initialize services early in app lifecycle
- Error Handling: Handle service failures gracefully
- Documentation: Document what events and errors you're tracking
Both analytics and crash reporting are essential for understanding user behavior and maintaining app quality. Use them responsibly while respecting user privacy and following platform guidelines.