Skip to content

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 implementation

1. 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:

dart
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:

dart
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:

dart
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:

dart
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:

dart
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:

dart
// 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:

dart
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:

dart
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:

dart
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:

dart
// 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:

dart
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:

dart
// 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:

dart
// In lib/app/routes.dart
GoRouter createRouter() => GoRouter(
  observers: [
    // ✅ Automatic screen tracking
    di<AnalyticsNavigatorObserver>(),
  ],
  routes: [/* routes */],
);

4. Production Implementation

Firebase Analytics

dart
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

dart
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

Always respect user privacy and obtain consent:

dart
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:

dart
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

  1. Event Naming: Use consistent, descriptive event names
  2. Parameter Limits: Respect platform limits on parameter count and size
  3. User Privacy: Always obtain consent and respect privacy settings
  4. Performance: Don't block UI with analytics calls
  5. Testing: Use mock services in tests and debug builds

Crash Reporting

  1. Context Information: Provide rich context for easier debugging
  2. Error Classification: Distinguish between fatal and non-fatal errors
  3. User Identification: Link crashes to users while respecting privacy
  4. Sensitive Data: Never include passwords or personal data in reports
  5. Error Handling: Don't let crash reporting itself cause crashes

General

  1. Environment Separation: Use different configurations for debug/release
  2. Service Abstraction: Use abstract interfaces for easy testing and swapping
  3. Initialization: Initialize services early in app lifecycle
  4. Error Handling: Handle service failures gracefully
  5. 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.