Flutter Crash Manager

A comprehensive Flutter crash reporting package that provides a unified interface for multiple crash reporting services including Firebase Crashlytics and Sentry.

Features

  • Unified Interface: Single API for multiple crash reporting services
  • Firebase Crashlytics Support: Complete integration with Firebase Crashlytics
  • Sentry Support: Full Sentry integration for cross-platform error tracking
  • Multi-Service Support: Use multiple crash reporting services simultaneously
  • Rich Context: User context, device information, and custom attributes
  • Breadcrumb Logging: Detailed debugging trails
  • Crash Filtering: Custom filters to control which crashes are reported
  • Privacy Controls: Built-in sensitive data filtering
  • Type Safety: Comprehensive type safety with proper null handling
  • Production Ready: Built for high-performance production applications

Installation

Add this package to your pubspec.yaml:

dependencies:
  crash_manager:
    git:
      url: https://github.com/bahricanyesil/flutter-crash-manager.git

Breaking Changes in v1.0.2

⚠️ IMPORTANT: Version 1.0.2 introduces breaking changes to follow Clean Architecture principles. The Firebase Crash Manager now requires proper dependency injection.

Migration from v1.0.1 to v1.0.2

Before (v1.0.1):

// Old factory-based initialization
final crashManager = await FirebaseCrashManager.create();

After (v1.0.2):

// 1. Initialize Firebase at application level
await Firebase.initializeApp();

// 2. Create and inject dependencies
final crashManager = FirebaseCrashManager(
  crashlytics: FirebaseCrashlytics.instance,
  logger: LogManagerFactory.createDefault(),
  deviceManager: DeviceInfoManagerImpl(),
  packageManager: PackageInfoManagerImpl(),
  connectivityManager: await ConnectivityManagerFactory.createForProduction(),
);

// 3. Initialize the manager
await crashManager.initialize();

Why the Breaking Change?

The new architecture provides:

  • Better Testability: Easy dependency injection and mocking
  • Cleaner Code: Clear separation of concerns
  • Better Performance: Reduced initialization overhead
  • Easier Maintenance: Dependencies managed at application level
  • Type Safety: Improved error handling and null safety

Quick Start

1. Basic Setup

Important Note: Starting from version 1.0.2, Firebase Crash Manager follows Clean Architecture principles and requires proper dependency injection.

import 'package:crash_manager/crash_manager.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:app_logger_manager/app_logger_manager.dart';
import 'package:device_package_info_manager/device_package_info_manager.dart';
import 'package:flutter_connectivity_manager/flutter_connectivity_manager.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 1. Initialize Firebase at application level
  await Firebase.initializeApp();

  // 2. Create dependencies
  final crashlytics = FirebaseCrashlytics.instance;
  final logger = LogManagerFactory.createDefault();
  final deviceManager = DeviceInfoManagerImpl();
  final packageManager = PackageInfoManagerImpl();
  final connectivityManager = await ConnectivityManagerFactory.createForProduction();

  // 3. Create crash manager with dependency injection
  final crashManager = FirebaseCrashManager(
    crashlytics: crashlytics,
    logger: logger,
    deviceManager: deviceManager,
    packageManager: packageManager,
    connectivityManager: connectivityManager,
  );

  // 4. Initialize the crash manager
  await crashManager.initialize();

  // 5. Set user context
  await crashManager.setUserContext(UserContext(
    userId: 'user123',
    email: 'user@example.com',
    name: 'John Doe',
  ));

  runApp(MyApp());
}

2. Recording Crashes

try {
  // Your code that might throw
  riskyOperation();
} catch (error, stackTrace) {
  // Record fatal crash
  await crashManager.recordFatalCrash(error, stackTrace);

  // Or record non-fatal error
  await crashManager.recordNonFatalError(error, stackTrace);
}

3. Adding Context and Breadcrumbs

// Add custom attributes
await crashManager.setCustomAttribute('feature', 'user_profile');
await crashManager.setCustomAttribute('experiment_id', 'exp_123');

// Add breadcrumbs for debugging
await crashManager.addBreadcrumb('User clicked login button');
await crashManager.addBreadcrumb('API call started', data: {'endpoint': '/login'});

Firebase Crashlytics Setup

Android Setup

  1. Add Firebase to your Android app:

    • Go to Firebase Console
    • Create a new project or use existing one
    • Add your Android app to the project
    • Download google-services.json and place it in android/app/
  2. Configure build.gradle files:

    Project-level android/build.gradle:

    buildscript {
      dependencies {
        classpath 'com.google.gms:google-services:4.4.0'
        classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9'
      }
    }
    

    App-level android/app/build.gradle:

    apply plugin: 'com.google.gms.google-services'
    apply plugin: 'com.google.firebase.crashlytics'
    
    android {
      compileSdkVersion 34
    
      defaultConfig {
        multiDexEnabled true
      }
    }
    
    dependencies {
      implementation 'com.google.firebase:firebase-crashlytics'
      implementation 'com.google.firebase:firebase-analytics'
    }
    

iOS Setup

  1. Add Firebase to your iOS app:

    • In Firebase Console, add your iOS app
    • Download GoogleService-Info.plist
    • Add the file to your Xcode project in ios/Runner/
  2. Configure Xcode:

    • Open ios/Runner.xcworkspace
    • Ensure GoogleService-Info.plist is added to the Runner target
    • No additional configuration needed for Crashlytics

Firebase Initialization Scenarios

The Firebase crash manager handles multiple initialization scenarios automatically:

import 'package:crash_manager/crash_manager.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize Firebase at application level
  await Firebase.initializeApp();

  // Create crash manager with proper dependency injection
  final crashManager = FirebaseCrashManager(
    crashlytics: FirebaseCrashlytics.instance,
    logger: LogManagerFactory.createDefault(),
    deviceManager: DeviceInfoManagerImpl(),
    packageManager: PackageInfoManagerImpl(),
    connectivityManager: await ConnectivityManagerFactory.createForProduction(),
  );

  await crashManager.initialize();
  runApp(MyApp());
}

Scenario 2: Firebase with Other Services (Most Common)

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:crash_manager/crash_manager.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize Firebase once for all services
  await Firebase.initializeApp();

  // Initialize other Firebase services
  final auth = FirebaseAuth.instance;
  final firestore = FirebaseFirestore.instance;

  // Create crash manager using same Firebase instance
  final crashManager = FirebaseCrashManager(
    crashlytics: FirebaseCrashlytics.instance,
    logger: LogManagerFactory.createDefault(),
    deviceManager: DeviceInfoManagerImpl(),
    packageManager: PackageInfoManagerImpl(),
    connectivityManager: await ConnectivityManagerFactory.createForProduction(),
  );

  await crashManager.initialize();
  runApp(MyApp());
}

Scenario 3: Custom Firebase Options

import 'package:firebase_core/firebase_core.dart';
import 'package:crash_manager/crash_manager.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize Firebase with custom options
  await Firebase.initializeApp(
    options: const FirebaseOptions(
      apiKey: 'your-api-key',
      appId: 'your-app-id',
      messagingSenderId: 'your-sender-id',
      projectId: 'your-project-id',
      // ... other options
    ),
  );

  // Create crash manager with injected dependencies
  final crashManager = FirebaseCrashManager(
    crashlytics: FirebaseCrashlytics.instance,
    logger: LogManagerFactory.createDefault(),
    deviceManager: DeviceInfoManagerImpl(),
    packageManager: PackageInfoManagerImpl(),
    connectivityManager: await ConnectivityManagerFactory.createForProduction(),
  );

  await crashManager.initialize();
  runApp(MyApp());
}

Scenario 4: Multiple Firebase Apps

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:crash_manager/crash_manager.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize multiple Firebase apps
  await Firebase.initializeApp(); // Default app
  await Firebase.initializeApp(
    name: 'secondary',
    options: secondaryOptions,
  );

  // Get Crashlytics from specific Firebase app
  final secondaryApp = Firebase.app('secondary');
  final secondaryCrashlytics = FirebaseCrashlytics.instanceFor(app: secondaryApp);

  // Create crash manager using specific Firebase app
  final crashManager = FirebaseCrashManager(
    crashlytics: secondaryCrashlytics,
    logger: LogManagerFactory.createDefault(),
    deviceManager: DeviceInfoManagerImpl(),
    packageManager: PackageInfoManagerImpl(),
    connectivityManager: await ConnectivityManagerFactory.createForProduction(),
  );

  await crashManager.initialize();
  runApp(MyApp());
}

Scenario 5: Testing and Mocking

import 'package:crash_manager/crash_manager.dart';
import 'package:mockito/mockito.dart';

class MockCrashlytics extends Mock implements FirebaseCrashlytics {}
class MockLogger extends Mock implements AppLogger {}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // For testing, inject mock dependencies
  final crashManager = FirebaseCrashManager(
    crashlytics: MockCrashlytics(),
    logger: MockLogger(),
    deviceManager: MockDeviceManager(),
    packageManager: MockPackageManager(),
    connectivityManager: MockConnectivityManager(),
  );

  await crashManager.initialize();
  runApp(MyApp());
}

Error Handling for Missing Configuration

If Firebase configuration files are missing, you'll get a helpful error:

Firebase configuration files not found. Please ensure you have:
• Android: google-services.json in android/app/
• iOS: GoogleService-Info.plist in ios/Runner/
Or provide FirebaseOptions manually.
See: https://firebase.flutter.dev/docs/overview#installation

The new architecture also provides better error handling:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  try {
    await Firebase.initializeApp();

    final crashManager = FirebaseCrashManager(
      crashlytics: FirebaseCrashlytics.instance,
      logger: LogManagerFactory.createDefault(),
      deviceManager: DeviceInfoManagerImpl(),
      packageManager: PackageInfoManagerImpl(),
      connectivityManager: await ConnectivityManagerFactory.createForProduction(),
    );

    await crashManager.initialize();
    runApp(MyApp());
  } catch (e) {
    // Handle initialization errors gracefully
    runApp(ErrorApp(error: e.toString()));
  }
}

Sentry Setup

1. Create Sentry Account and Project

  1. Go to Sentry.io and create an account
  2. Create a new Flutter project
  3. Copy your DSN from the project settings

2. Initialize Sentry with Configuration

Unlike Firebase, Sentry requires configuration to be passed directly to the crash manager:

import 'package:crash_manager/crash_manager.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Create and initialize Sentry crash manager with configuration (already initialized!)
  final crashManager = await SentryCrashManager.create(
    config: SentryConfig(
      dsn: 'YOUR_SENTRY_DSN_HERE', // Required - get from Sentry dashboard
      environment: 'production', // or 'development', 'staging'
      release: '1.0.0', // Your app version
      debug: kDebugMode, // Enable debug logs in debug mode
      tracesSampleRate: 1.0, // Performance monitoring sample rate
      maxBreadcrumbs: 100, // Maximum breadcrumbs to keep
      enableAutoSessionTracking: true, // Track user sessions
      attachScreenshot: true, // Attach screenshots on crashes (iOS/Android)
      attachViewHierarchy: true, // Attach view hierarchy (iOS/Android)
      tags: {
        'team': 'mobile',
        'platform': 'flutter',
      },
      inAppIncludes: ['com.yourcompany.yourapp'], // Filter stack traces
    ),
  );

  runApp(MyApp());
}

3. Advanced Configuration Options

The SentryConfig class provides comprehensive configuration options:

const SentryConfig(
  dsn: 'YOUR_SENTRY_DSN_HERE', // Required

  // Environment settings
  environment: 'production', // deployment environment
  release: '1.0.0+1', // app version/build

  // Performance monitoring
  tracesSampleRate: 1.0, // 0.0 to 1.0 (0% to 100%)

  // Debug settings
  debug: false, // enable debug logging

  // Breadcrumbs
  maxBreadcrumbs: 100, // max breadcrumbs to keep in memory

  // Session tracking
  enableAutoSessionTracking: true, // automatically track sessions

  // Screenshots and view hierarchy (mobile only)
  attachScreenshot: false, // attach screenshots on crashes
  attachViewHierarchy: false, // attach view hierarchy on crashes

  // Stack trace filtering
  inAppIncludes: ['com.yourcompany.yourapp'], // include patterns
  inAppExcludes: ['flutter', 'dart'], // exclude patterns

  // Global tags
  tags: {
    'team': 'mobile',
    'platform': 'flutter',
    'version': '1.0.0',
  },
);

Multi-Service Setup

Use both Firebase Crashlytics and Sentry simultaneously:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize Firebase first
  await Firebase.initializeApp();

  // Create multi crash manager
  final crashManager = await MultiCrashManager.create();

  // Create and add Firebase Crashlytics with dependency injection
  final firebaseManager = FirebaseCrashManager(
    crashlytics: FirebaseCrashlytics.instance,
    logger: LogManagerFactory.createDefault(),
    deviceManager: DeviceInfoManagerImpl(),
    packageManager: PackageInfoManagerImpl(),
    connectivityManager: await ConnectivityManagerFactory.createForProduction(),
  );
  await firebaseManager.initialize();
  crashManager.addManager(firebaseManager);

  // Create and add Sentry with configuration
  final sentryManager = await SentryCrashManager.create(
    config: SentryConfig(
      dsn: 'YOUR_SENTRY_DSN_HERE',
      environment: 'production',
      debug: kDebugMode,
      tracesSampleRate: 0.1, // Lower sample rate for production
      enableAutoSessionTracking: true,
      tags: {
        'service': 'multi_crash_manager',
        'platform': 'flutter',
      },
    ),
  );
  crashManager.addManager(sentryManager);

  runApp(MyApp());
}

Advanced Usage

Custom Crash Filtering

// Set up custom crash filter
crashManager.setCrashFilter((crashReport) {
  // Don't report network errors in debug mode
  if (kDebugMode && crashReport.message.contains('SocketException')) {
    return false;
  }

  // Don't report crashes from specific users
  if (crashReport.userId == 'test_user') {
    return false;
  }

  return true;
});

Comprehensive Context Setup

// Set device information
final deviceInfo = await DeviceInfoCollector.collect();
await crashManager.setDeviceInfo(deviceInfo);

// Set user context with custom attributes
await crashManager.setUserContext(UserContext(
  userId: 'user123',
  email: 'user@example.com',
  name: 'John Doe',
  role: 'premium',
  customAttributes: {
    'subscription_tier': 'premium',
    'feature_flags': ['new_ui', 'beta_features'],
    'last_login': DateTime.now().toIso8601String(),
  },
));

// Set session ID
await crashManager.setSessionId('session_abc123');

Manual Crash Reporting

// Create custom crash report
final crashReport = CrashUtils.createCrashReport(
  error: Exception('Custom error occurred'),
  stackTrace: StackTrace.current,
  isFatal: false,
  customAttributes: {
    'user_action': 'button_click',
    'screen': 'profile_page',
  },
  userId: 'user123',
  sessionId: 'session_abc123',
);

await crashManager.recordCrashReport(crashReport);

Performance Monitoring

// Add performance breadcrumbs
await crashManager.addBreadcrumb(
  'API call started',
  data: {
    'endpoint': '/api/users',
    'method': 'GET',
    'timestamp': DateTime.now().toIso8601String(),
  },
  level: CrashLogLevel.info,
);

// Measure operation performance
final stopwatch = Stopwatch()..start();
try {
  await performOperation();
  await crashManager.addBreadcrumb(
    'Operation completed successfully',
    data: {'duration_ms': stopwatch.elapsedMilliseconds},
  );
} catch (error, stackTrace) {
  await crashManager.recordNonFatalError(
    error,
    stackTrace,
    customAttributes: {
      'operation_duration_ms': stopwatch.elapsedMilliseconds,
      'operation_type': 'api_call',
    },
  );
}

API Reference

CrashManager Interface

abstract class CrashManager {
  Future<void> initialize();
  Future<void> setUserContext(UserContext userContext);
  Future<void> recordFatalCrash(Object error, StackTrace stackTrace);
  Future<void> recordNonFatalError(Object error, StackTrace? stackTrace);
  Future<void> addBreadcrumb(String message, {Map<String, Object?>? data});
  Future<void> setCustomAttribute(String key, Object? value);
  void setCrashFilter(bool Function(CrashReport report) filter);
  // ... and more
}

Data Models

CrashReport

class CrashReport extends BaseDataModel<CrashReport> {
  final String id;
  final String message;
  final String stackTrace;
  final String errorType;
  final DateTime timestamp;
  final bool isFatal;
  final Map<String, Object?> userContext;
  final Map<String, Object?> customAttributes;
  // ... and more
}

UserContext

class UserContext extends BaseDataModel<UserContext> {
  final String? userId;
  final String? email;
  final String? name;
  final String? role;
  final Map<String, Object?> customAttributes;
  final bool isAuthenticated;
  // ... and more
}

DeviceInfo

class DeviceInfo extends BaseDataModel<DeviceInfo> {
  final String platform;
  final String model;
  final String manufacturer;
  final String osVersion;
  final String appVersion;
  final String buildNumber;
  // ... and more
}

Best Practices

1. Privacy and Security

  • Always sanitize sensitive data before reporting
  • Use the built-in data sanitization features
  • Implement custom filters for sensitive operations

2. Performance

  • Use non-fatal error reporting for non-critical issues
  • Limit breadcrumb frequency for high-traffic operations
  • Implement rate limiting for error reporting

3. Testing

  • Use test crashes sparingly and only in development
  • Implement proper error boundaries in your app
  • Test crash reporting in staging environments

4. Monitoring

  • Regularly review crash reports and patterns
  • Set up alerts for critical crash thresholds
  • Monitor crash-free user percentages

Troubleshooting

Firebase Crashlytics Issues

Problem: Crashes not appearing in Firebase Console

  • Solution: Ensure debug builds are configured to send crashes
  • Check: Verify google-services.json and GoogleService-Info.plist are correctly placed
  • Wait: It can take up to 15 minutes for crashes to appear

Problem: Build errors with Crashlytics

  • Solution: Ensure all gradle plugins are up to date
  • Check: Verify minimum SDK versions are met

Sentry Issues

Problem: Events not appearing in Sentry

  • Solution: Verify DSN is correct and project is active
  • Check: Ensure network connectivity and no firewall blocking

Problem: High event volume

  • Solution: Implement proper sampling rates and filters
  • Configure: Set appropriate tracesSampleRate values

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests for new functionality
  5. Submit a pull request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Support

For support and questions:

Changelog

See CHANGELOG.md for a detailed list of changes and updates.

Libraries

crash_manager
A library for managing crash reports and handling crashes.