SaFi Bank Space : Firebase Analytics & Firebase Crashlytics

FIREBASE ANALYTICS

  1. Overview.

  2. Setup.

  3. Implementation Firebase Analytics In Flutter App.

  4. Debug Firebase Analytics.

OVERVIEW

FIREBASE ANALYTICS - GOOGLE ANALYTICS FOR FIREBASE

Google Analytics is a free app measurement solution that provides insight on app usage and user engagement. Analytics reports help you understand clearly how your users behave, which enables you to make informed decisions regarding app marketing and performance optimizations.

Google Analytics for Firebase provides free, unlimited reporting on up to 500 distinct events. The SDK automatically captures certain key events and user properties, and you can define your own custom events to measure the things that uniquely matter to your business.

SETUP

Install firebase_core plugin in flutter app.

For any use of Firebase plugins in Flutter, Firebase has provide setup guidelines and instruction to make sure the configuration of all its plugin in your Flutter app works seamlessly. To achieve that we must follow the step by step guide explained in the FlutterFire site.

Before any Firebase services can be used, we must first install the firebase_core plugin, which is responsible for connecting our application to Firebase.

Install the plugin by running the following command from the project root:

flutter pub add firebase_core

Once firebase_core install, we have to configure FlutterFire to automatically generate firebase_options.dart which containing all the options required for initialization.

use flutterfire_cli to configure it.

# Install the CLI if not already done so
dart pub global activate flutterfire_cli

# Run the `configure` command, select a Firebase project and platforms
flutterfire configure

Configuring flutterfire would also automatically applied Android Google Services Gradle plugin.

Before any of the Firebase services can be used, FlutterFire needs to be initialized. The initialization step is asynchronous, meaning you'll need to prevent any FlutterFire related usage until the initialization is completed.

in the app module → main.dart and inside the main function, add below code to initialize firebase in our app.

await Firebase.initializeApp();

implementation example:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(MyApp());
}

the DefaultFirebaseOptions.currentPlatform is imported from firebase_options.dart file.

On its own, the firebase_core plugin provides basic functionality for usage with Firebase. FlutterFire is broken down into individual, installable plugins that allow you to integrate with a specific Firebase service.

Setup firebase_analytics plugin in flutter app.

// Add the analytics sdk from the root of your flutter project
flutter pub add firebase_analytics

//rebuild Flutter application
flutter run

we can access firebase_analytics plugin by importing it in our Dart code, as follow:

import 'package:firebase_analytics/firebase_analytics.dart';

Implementation Firebase Analytics In Flutter App.

CodeBase

In our flutter app development, Firebase Analytics will be considered as one of multiple analytics platform that will be used inside flutter app. Therefore, there will be stand alone module to setup these platforms and how they are going to be used inside the app.

As the structure of SaFi app architecture was build under the principal of modular system with relational hierarchy between modules, and we consider that analytics was one of the core functionality that the app must have. Therefore, we must place the analytics module under the group of generic modules of the app.

The analytics module was setup with regards to the clean architecture pattern. Therefore, inside this module there will be domain and data layer to implement analytics in the app.

Re

domain layer consist of repository , usecase and entities.

Domain layer

  • Repository is responsible for setup behavior that any analytics platform must have to perform its functionality. This setup was written in the analytics_platform.dart file.

    abstract class AnalyticsPlatform {
      Future<void> init();
    
      Future<void> setUserID(String id);
    
      Future<void> setUserProperties({
        required Map<String, dynamic> properties,
        bool global = true,
      });
    
      Future<void> setCommonEventProperties(Map<String, Object> mapProperties);
    
      Future<bool> trackEvent(AnalyticsEventEntity event,
          {bool isBatch = true, bool isGlobal = true});
    
      RouteObserver getNavigationObserver();
    
      bool get isEnabled => true;
    }

the AnalyticsPlatform class will be implemented in data layer by any specific analytics platform or solution that are going to be used by the app.

  • The analytics_usecase.dart is responsible as base logics that manage multiple analytics platform/solution used by the app.

    abstract class AnalyticsUseCase {
      List<AnalyticsPlatform> getPlatforms();
    
      FutureOr setup();
    
      FutureOr setUserID(String id) async {
        for (final platform in getPlatforms()) {
          try {
            await platform.setUserID(id);
          } catch (e) {
            continue;
          }
        }
      }
    
      FutureOr setUserProperties({
        required Map<String, dynamic> properties,
        bool global = true,
      }) async {
        for (final platform in getPlatforms()) {
          try {
            await platform.setUserProperties(
              properties: properties,
              global: global,
            );
          } catch (e) {
            continue;
          }
        }
      }
    
      FutureOr setCommonEventProperties(Map<String, Object> mapProperties) async {
        for (final platform in getPlatforms()) {
          try {
            await platform.setCommonEventProperties(mapProperties);
          } catch (e) {
            continue;
          }
        }
      }
    
      RouteObserver getNavigationObserver();
    
      FutureOr trackEvent(AnalyticsEventEntity event,
          {bool isBatch = true, bool isGlobal = true}) async {
        for (final platform in getPlatforms()) {
          try {
            await platform.trackEvent(
              event,
              isBatch: isBatch,
              isGlobal: isGlobal,
            );
          } catch (e) {
            continue;
          }
        }
      }
    }
    

The implementation of this abstract class will be placed under the app module.

  • Entities is just any custom class that are required to be sent to the platform when a method triggered. In this case, AnalyticsEventEntity is needed by trackEvent method.

    class AnalyticsEventEntity {
      final String eventName;
      final Map<String, dynamic> properties;
    
      AnalyticsEventEntity({
        required this.eventName,
        required this.properties,
      });
    }

Data layer

  • Repository in data layer consist of analytics specific implementation solution of related specific analytics platform used in our app. In this case, we only provide analytics implementation by firebase_analytics_platform.dart.

    class FirebaseAnalyticsPlatform extends AnalyticsPlatform {
      late FirebaseAnalytics _analytics;
      late FirebaseAnalyticsObserver _observer;
      bool _initialized = false;
    
      @override
      Future<void> init() async {
        try {
          // LOG.debug('Initialize Firebase Analytics');
          _analytics = FirebaseAnalytics.instance;
          _observer = FirebaseAnalyticsObserver(analytics: _analytics);
          await _analytics.setConsent(
            adStorageConsentGranted: true,
            analyticsStorageConsentGranted: true,
          );
          await _analytics.setAnalyticsCollectionEnabled(true);
          _initialized = true;
        } catch (e) {
          // LOG.error('Error:: Can not initialize Firebase Analytics', e);
        }
      }
    
      @override
      Future<void> setCommonEventProperties(
          Map<String, Object> mapProperties) async {
        try {
          await _analytics.setDefaultEventParameters(mapProperties);
        } catch (e) {
          // LOG.error('Error:: Can not set Firebase Analytics user', e);
        }
      }
    
      @override
      Future<void> setUserID(String id) async {
        try {
          await _analytics.setUserId(id: id);
        } catch (e) {
          // LOG.error('Error:: Can not set Firebase Analytics user', e);
        }
      }
    
      @override
      Future<void> setUserProperties(
          {required Map<String, dynamic> properties, bool global = true}) async {
        try {
          await _analytics.setUserProperty(
            name: properties.keys.first,
            value: properties.values.first,
            callOptions: AnalyticsCallOptions(global: global),
          );
        } catch (e) {
          // LOG.error('Error:: Can not set Firebase Analytics user property', e);
        }
      }
    
      @override
      Future<bool> trackEvent(AnalyticsEventEntity event,
          {bool isBatch = true, bool isGlobal = true}) async {
        final AnalyticsCallOptions analyticsCallOptions = AnalyticsCallOptions(
          global: isGlobal,
        );
        if (isEnabled) {
          // await setUserID(event.userId!);
          // LOG.debug('Track Event ${event.eventName}');
          await _analytics.logEvent(
            name: event.eventName,
            parameters: event.properties,
            callOptions: analyticsCallOptions,
          );
          return true;
        }
        return false;
      }
    
      @override
      RouteObserver getNavigationObserver() {
        if (_initialized) {
          return _observer;
        } else {
          return RouteObserver();
        }
      }
    }
    
    

we could add any other class of analytics solution in this layer, as long as it is extends from AnalyticsPlatform abstract class.

  • Model in data layer, is consist of object parsing class from entities in domain layer. In this case, AnalyticsEventModel is extended from AnalyticsEventEntity and it is consist of method fromJson that will be responsible to parse inputs into related entity object.

    class AnalyticsEventModel extends AnalyticsEventEntity {
      AnalyticsEventModel({
        required String eventName,
        required Map<String, dynamic> properties,
      }) : super(
              eventName: eventName,
              properties: properties,
            );
    
      factory AnalyticsEventModel.fromJson(Map<String, dynamic> json) {
        return AnalyticsEventModel(
          eventName: json['eventName'],
          properties: json['properties'],
        );
      }
    }

App Level Implementation

In the app_safi module, inside the repository in the data layer, we write the implementation of AnalyticsUseCase in the analytics_usecase_impl.dart file.

class AnalyticsUseCaseImpl extends AnalyticsUseCase {
  final FirebaseAnalyticsPlatform firebaseAnalyticsPlatform;

  AnalyticsUseCaseImpl(this.firebaseAnalyticsPlatform);

  @override
  List<AnalyticsPlatform> getPlatforms() => [
        firebaseAnalyticsPlatform,
      ].where((platform) => platform.isEnabled).toList();

  @override
  Future<void> setup() async {
    for (final platform in getPlatforms()) {
      await platform.init();
    }
  }

  @override
  RouteObserver getNavigationObserver() {
    for (final platform in getPlatforms()) {
      if (platform is FirebaseAnalyticsPlatform) {
        return platform.getNavigationObserver();
      }
    }
    return RouteObserver();
  }
}

Instantiate this implementation in the main.dart by invoking below method.

  Future<void> setupAnalytics({
    required AnalyticsUseCaseImpl analytics,
  }) async {
    await Future.wait([
      analytics.setup(),
    ]);
  }

The method getNavigationObserver() in the AnalyticsUseCaseImpl is meant to capture/observe user navigation throughout their journey within the app. Flutter provides built-in callback function for this purpose as part of MaterialApp arguments. Therefore inside the App widget which called from main.dart, add the analytics.getNavigationObserver() into navigatorObservers of MaterialApp.

(see line 10-12 of below code).

  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: _getProviders(),
      child: MaterialApp(
        title: 'Skeleton Multimodule',
        theme: ThemeData(primarySwatch: Colors.blue),
        initialRoute: Routes.initial,
        routes: Routes.all,
        navigatorObservers: [
          analytics.getNavigationObserver(),
        ],
      ),
    );
  }

Still in the app_safi module scope, under the presentation layer, add business logic for later implementation on analytics, named analytics_bloc.dart which listen to the eventBus that handles widget on clicked.

class AnalyticsBloc
    extends BaseBlocWithEventBusListener<AnalyticsEvent, AnalyticsState> {
  final AnalyticsUseCase analyticsUseCase;
  final EventBusCubit eventBusCubit;

  AnalyticsBloc({required this.analyticsUseCase, required this.eventBusCubit})
      : super(initialState: AnalyticsInitial(), eventBusCubit: eventBusCubit) {
    addEventBusSubscription<OnWidgetClickedEvent>(add);
    on<OnWidgetClickedEvent>(_sendAnalyticsEvent);
  }

  FutureOr<void> _sendAnalyticsEvent(
    OnWidgetClickedEvent event,
    Emitter<AnalyticsState> emit,
  ) {
    if (event.isTrackEvent) {
      final json = <String, dynamic>{
        'eventName': event.name,
        'properties': event.buttonProperties,
      };

      final analyticsEvent = AnalyticsEventModel.fromJson(json);
      analyticsUseCase.trackEvent(analyticsEvent);
    }
  }
}

Feature Level Implementation.

The outcome of the analytics implementation is to capture user experience and engagement throughout the app. Therefore we must setup ways on how to capture user action in their journey throughout the app. Beside the implementation of the NavigatorObserver which already setup in the App.dart to capture any screen user’s encountered, we also must provide functionality to capture user’s interactive action in the app, such as when user click a widget.

To achieve this outcome, there must be a base custom widget that will be used to interact with user as a child widget in each screen with user action behavior. This widget could be a button, icon button, text button and any other kind of action widget. This custom widget will also have to provide callback to triggered analytics event of each action.

Therefore, in the ui module under generic directory we create action_widget.dart.

typedef ActionButtonAnalyticsCallback = void Function(
    String buttonName, Map<String, dynamic> buttonProperties);

class ActionWidget extends StatelessWidget {
  final Widget button;
  final EdgeInsetsGeometry? padding;
  final bool safeBottom;
  final double? width, height;

  factory ActionWidget({
    required Key buttonKey,
    VoidCallback? onPress,
    required ActionButtonAnalyticsCallback initiateAnalyticsEvent,
    required EdgeInsetsGeometry padding,
    required String buttonText,
    TextStyle? activeStyle,
    TextStyle? inactiveStyle,
    Color? color,
    bool safeBottom = true,
    bool isTrackEvent = true,
    double? width,
    height,
    borderRadius,
    Size? minimumSize,
  }) =>
      ActionWidget._(
        padding: padding,
        button: InkWell(
          child: ElevatedButton(
            key: buttonKey,
            style: ElevatedButton.styleFrom(
              elevation: UIConstant.buttonNoElevation,
              primary: color ?? Colors.blue,
              onPrimary: Colors.white,
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(
                  borderRadius ?? UIConstant.buttonBorderRadius,
                ),
              ),
              onSurface: Colors.grey,
              padding: padding,
              minimumSize: minimumSize ?? UIConstant.buttonMinimumSize,
            ),
            onPressed: onPress != null
                ? () => _onClickWidget(
                      onPress,
                      initiateAnalyticsEvent,
                      buttonKey,
                    )
                : null,
            child: Text(buttonText),
          ),
        ),
        safeBottom: safeBottom,
        width: width ?? UIConstant.buttonMinimumWidth,
        height: height ?? UIConstant.buttonMinimumHeight,
      );

  factory ActionWidget.buttonPrimary({
    required Key buttonKey,
    VoidCallback? onPress,
    required ActionButtonAnalyticsCallback initiateAnalyticsEvent,
    EdgeInsetsGeometry? padding,
    required String buttonText,
    TextStyle? activeStyle,
    TextStyle? inactiveStyle,
    Color? color,
    bool safeBottom = true,
    bool isTrackEvent = true,
    double? width,
    height,
    borderRadius,
    Size? minimumSize,
  }) =>
      ActionWidget._(
        padding: padding,
        button: ElevatedButton(
          key: buttonKey,
          style: ElevatedButton.styleFrom(
            elevation: UIConstant.buttonNoElevation,
            primary: color ?? Colors.blue,
            onPrimary: Colors.white,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(
                borderRadius ?? UIConstant.buttonBorderRadius,
              ),
            ),
            onSurface: Colors.grey,
            padding: padding ?? UIConstant.buttonStandardPadding,
            minimumSize: minimumSize ?? UIConstant.buttonMinimumSize,
          ),
          onPressed: onPress != null
              ? () => _onClickWidget(
                    onPress,
                    initiateAnalyticsEvent,
                    buttonKey,
                  )
              : null,
          child: Text(buttonText),
        ),
        safeBottom: safeBottom,
        width: width ?? UIConstant.buttonMinimumWidth,
        height: height ?? UIConstant.buttonMinimumHeight,
      );

  factory ActionWidget.buttonText({
    required Key buttonKey,
    VoidCallback? onPress,
    required ActionButtonAnalyticsCallback initiateAnalyticsEvent,
    EdgeInsetsGeometry? padding,
    required String buttonText,
    TextStyle? activeStyle,
    TextStyle? inactiveStyle,
    Color? color,
    bool safeBottom = true,
    bool isTrackEvent = true,
    double? width,
    height,
    borderRadius,
    Size? minimumSize,
  }) =>
      ActionWidget._(
        padding: padding,
        button: TextButton(
          key: buttonKey,
          style: TextButton.styleFrom(
            elevation: UIConstant.buttonNoElevation,
            primary: color ?? Colors.blue,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(
                borderRadius ?? UIConstant.buttonBorderRadius,
              ),
            ),
            onSurface: Colors.grey,
            padding: padding ?? UIConstant.buttonStandardPadding,
            minimumSize: minimumSize ?? UIConstant.buttonMinimumSize,
          ),
          onPressed: onPress != null
              ? () => _onClickWidget(
                    onPress,
                    initiateAnalyticsEvent,
                    buttonKey,
                  )
              : null,
          child: Text(buttonText),
        ),
        safeBottom: safeBottom,
        width: width ?? UIConstant.buttonMinimumWidth,
        height: height ?? UIConstant.buttonMinimumHeight,
      );

  factory ActionWidget.circularTextButton({
    required Key buttonKey,
    VoidCallback? onPress,
    required initiateAnalyticsEvent,
    EdgeInsetsGeometry? padding,
    required String buttonText,
    TextStyle? activeStyle,
    TextStyle? inactiveStyle,
    Color? textColor,
    Color? backgroundColor,
    Color? borderColor,
    bool safeBottom = true,
    bool isTrackEvent = true,
    double? borderWidth,
    double? iconSize,
    double? fontSize,
    required BuildContext context,
  }) =>
      ActionWidget._(
        padding: padding,
        safeBottom: safeBottom,
        width: iconSize ?? UIConstant.circularIconSize,
        height: iconSize ?? UIConstant.circularIconSize,
        button: InkWell(
          onTap: onPress != null
              ? () => _onClickWidget(
                    onPress,
                    initiateAnalyticsEvent,
                    buttonKey,
                  )
              : null,
          customBorder: const CircleBorder(),
          splashColor: Colors.white,
          child: Padding(
            padding: const EdgeInsets.all(7.0),
            child: Container(
              decoration: BoxDecoration(
                color: backgroundColor ?? Colors.blue,
                shape: BoxShape.circle,
                border: Border.all(
                  // color: borderColor ?? AppColor.primaryColor,
                  color: borderColor ?? Colors.blue,
                  // width: borderWidth ?? LayoutConstants.dimen_1,
                  width: borderWidth ?? 1.0,
                ),
              ),
              child: Center(
                child: Padding(
                  padding: const EdgeInsets.all(2.0),
                  child: Text(
                    buttonText,
                    maxLines: 3,
                    textAlign: TextAlign.center,
                    style: Theme.of(context).textTheme.subtitle2?.copyWith(
                          color: textColor ?? Colors.white,
                          fontWeight: FontWeight.w600,
                          fontSize: fontSize ?? 13.0,
                        ),
                  ),
                ),
              ),
            ),
          ),
        ),
      );

  const ActionWidget._({
    required this.button,
    this.padding,
    this.safeBottom = true,
    this.height,
    this.width,
  });

  @override
  SafeArea build(BuildContext context) => SafeArea(
        top: false,
        bottom: safeBottom,
        child: Container(
          margin: padding ??
              const EdgeInsets.only(
                left: 10.0,
                right: 10.0,
                bottom: 10.0,
              ),
          width: width ?? ScreenUtil.screenWidthDp,
          height: height ?? UIConstant.heightButton(context),
          child: button,
        ),
      );

  static void _onClickWidget(
    VoidCallback? onPress,
    ActionButtonAnalyticsCallback initiateAnalyticsEvent,
    Key buttonKey,
  ) {
    final eventName = buttonKey
        .toString()
        .trim()
        .replaceAll('[<', '')
        .replaceAll("'", '')
        .replaceAll('>]', '')
        .replaceAll(' ', '_');

    initiateAnalyticsEvent.call(eventName, {});
    onPress?.call();
  }
}

This action widget with its related various form, should be used by any developer who writes code in each journey under the feature module whenever the screen on that specific journey required user interaction.

An example of the use of ActionWidget is as follow:

// place below code in any related screen or page
          ActionWidget.buttonPrimary(
            buttonText: 'Start onboarding',
            buttonKey: const Key('start_onboarding_button'),
            initiateAnalyticsEvent: (buttonName, buttonProperties) {
              context.emitEventBus(OnWidgetClickedEvent(
                buttonProperties: buttonProperties,
                name: buttonName,
              ));
            },
            color: Colors.blue,
            onPress: () => Injector.resolve<LoginInteractionNavigation>()
                .navigateToOnboarding(context),
          ),

The callback of the analytic functionality is the value of initiateAnalyticsEvent argument in the widget.

Debug Firebase Analytics In Flutter App.

To enable Analytics Debug mode on an mobile device, execute the following commands:

on Android:

adb shell setprop debug.firebase.analytics.app ph.safibank.app.dev

on iOS

FIRDebugEnabled

This behavior persists until you explicitly disable Debug mode by executing the following command:

on Android:

adb shell setprop debug.firebase.analytics.app .none.

on iOS

-FIRDebugDisabled

We can view debug process in firebase console on the Debug View menu.

this will display the last 30 minutes user interaction with the app. Any user action through our custom widget will be displayed in the console.

Analytics Dashboard in google console shows the overall activity captured by analytics in our app.

FIREBASE CRASHLYTICS

  1. Overview.

  2. Setup.

  3. Implementation Firebase Crashlytics In Flutter App.

  4. Debug Firebase Crashlytics.

OVERVIEW

FIREBASE CRASHLYTICS

Firebase Crashlytics is a lightweight, realtime crash reporter that helps you track, prioritize, and fix stability issues that erode your app quality. Crashlytics saves you troubleshooting time by intelligently grouping crashes and highlighting the circumstances that lead up to them.

Find out if a particular crash is impacting a lot of users. Get alerts when an issue suddenly increases in severity. Figure out which lines of code are causing crashes.

Crashlytics helps us to collect analytics and details about crashes and errors that occur in our app. It does this through three aspects:

  • Logs: Log events in your app to be sent with the crash report for context if your app crashes.

  • Crash reports: Every crash is automatically turned into a crash report and sent when the application next opens.

  • Stack traces: Even when an error is caught and your app recovers, the Dart stack trace can still be sent.

SETUP

Setup firebase_crashlytics plugin in flutter app.

Make sure firebase_core already installed first. (see in the analytics section).

On the root of your Flutter project, run the following command to install the plugin:

flutter pub add firebase_crashlytics

Dart-only Firebase initialization for Android currently only supports reporting Dart exceptions. To report native Android exceptions, please follow the steps below.

  1. Add the following

classpath to your android/build.gradle file.

dependencies {
  // ... other dependencies such as 'com.google.gms:google-services'
  classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1'
}
  1. Apply the following to your

android/app/build.gradle file.

// ... other imports

android {
  // ... your android config
}

dependencies {
  // ... your dependencies
}

// This must appear at the bottom of the file
apply plugin: 'com.google.firebase.crashlytics'

Once complete, rebuild your Flutter application:

flutter run

Implementation Firebase Crashlytics In Flutter App.

Monitoring Module

In our flutter app development, Firebase Crashlytics will be considered as one of multiple crashlytics/monitoring platform that will be used inside flutter app. Therefore, there will be stand alone module to setup these platforms and how they are going to be used inside the app.

We create the monitoring as a module in the generic level.

The monitoring module was setup with regards to the clean architecture pattern. Therefore, inside this module there will be domain and data layer to implement monitoring in the app.

Domain Layer

  • Usecase is Responsible for setup the module.

abstract class MonitoringUsecase {
  List<MonitoringPlatform> getPlatforms();

  Future<void> setup();

  bool get isEnabled;

  void identify(String identifier) {
    for (final platform in getPlatforms()) {
      platform.identify(identifier);
    }
  }

  FutureOr reportError(dynamic exception, StackTrace? stackTrace) async {
    for (final platform in getPlatforms()) {
      try {
        await platform.reportError(exception, stackTrace);
      } catch (e) {
        continue;
      }
    }
  }

  FutureOr reportFlutterError(FlutterErrorDetails details) async {
    for (final platform in getPlatforms()) {
      try {
        await platform.reportFlutterError(details);
      } catch (e) {
        continue;
      }
    }
  }
}
  • Platform is the basic of the monitoring platforms can we use

abstract class MonitoringPlatform {
  Future<void> setup();

  bool get isEnabled;

  void identify(String identifier);

  FutureOr reportError(dynamic exception, StackTrace? stackTrace);

  FutureOr reportFlutterError(FlutterErrorDetails details);
}

Data Layer

  • We create FirebaseCrashlyticsPlatform as the one of monitoring platforms list can we use

class FirebaseCrashlyticsPlatform extends MonitoringPlatform {
  late FirebaseCrashlytics _crashlytics;

  @override
  bool get isEnabled => true;

  @override
  Future<void> setup() async {
    _crashlytics = FirebaseCrashlytics.instance;
    await _crashlytics.setCrashlyticsCollectionEnabled(true);
  }

  @override
  void identify(String identifier) {
    try {
      _crashlytics.setUserIdentifier(identifier);
    } catch (error) {
      // LOG.info('[$runtimeType] Failed to identify user');
    }
  }

  @override
  FutureOr reportError(dynamic exception, StackTrace? stackTrace) async {
    await _crashlytics.recordError(exception, stackTrace);
  }

  @override
  FutureOr reportFlutterError(FlutterErrorDetails details) async {
    await _crashlytics.recordFlutterError(details);
  }
}

Logger Module

In our generic level, the monitoring error is wrapped with logger module. Monitoring is embed in the logger error of logger module.

in the logger.dart, we create a callback function as reportError monitoring.

class LoggerUtil extends Logger {
  LogPrinter printer;
  Level level;
  bool isTestEnvironment;
  Function(dynamic error, StackTrace? stackTrace)? reportError;

  LoggerUtil({
    required this.printer,
    required this.level,
    required this.isTestEnvironment,
    this.reportError,
  }) : super(
          printer: printer,
          level: level,
        );

  @override
  void e(dynamic message, [dynamic error, StackTrace? stackTrace]) =>
      this.error(message, error, stackTrace);

  void error(dynamic message, [dynamic error, StackTrace? stackTrace]) {
    final Object exception = error ?? Exception(message);
    final StackTrace currentStackTrace = stackTrace ?? StackTrace.empty;
    super.e('[ERROR] $message', exception, currentStackTrace);
    if (!isTestEnvironment) {
        reportError?.call(exception, currentStackTrace);     
    }
  }

  @override
  void w(dynamic message, [dynamic error, StackTrace? stackTrace]) =>
      warn(message, error, stackTrace);

  void warn(dynamic message, [dynamic error, StackTrace? stackTrace]) =>
      super.w('[WARN] $message', error ?? Exception(message),
          stackTrace ?? StackTrace.current);

  @override
  void i(dynamic message, [dynamic error, StackTrace? stackTrace]) =>
      info(message, error, stackTrace!);

  void info(dynamic message, [dynamic error, StackTrace? stackTrace]) =>
      super.i('[INFO] $message', error, stackTrace);

  @override
  void d(dynamic message, [dynamic error, StackTrace? stackTrace]) =>
      debug(message, error, stackTrace!);

  void debug(dynamic message, [dynamic error, StackTrace? stackTrace]) =>
      super.d('[DEBUG] $message', error, stackTrace);

  @override
  void v(dynamic message, [dynamic error, StackTrace? stackTrace]) =>
      verbose(message, error, stackTrace!);

  void verbose(dynamic message, [dynamic error, StackTrace? stackTrace]) =>
      super.v('[VERBOSE]\n$message', error, stackTrace);

  static Level logLevel({
    String environment = 'dev',
    bool shouldEnableAllLogs = false,
    bool isTestEnvironment = false,
  }) {
    if (isTestEnvironment) {
      return Level.nothing;
    }
    switch (environment) {
      case 'prod':
      case 'preprod':
      case 'staging':
        return Level.nothing;
      case 'dev':
        return Level.verbose;
      default:
        return shouldEnableAllLogs ? Level.verbose : Level.debug;
    }
  }
}

App Level

In the app_safi module, we should implement the logger dan the monitoring module.

Monitoring Module

Inside the repository in the data layer, we write the implementation of monitoring_usecase in the monitoring_usecase_impl.dart file.

class MonitoringUsecaseImpl extends MonitoringUsecase {
  final FirebaseCrashlyticsPlatform crashlyticsPlatform;

  MonitoringUsecaseImpl(this.crashlyticsPlatform);

  @override
  List<MonitoringPlatform> getPlatforms() => [
        crashlyticsPlatform,
      ].where((platform) => platform.isEnabled).toList();

  @override
  bool get isEnabled => false;

  @override
  Future<void> setup() async {
    for (final platform in getPlatforms()) {
      await platform.setup();
    }
  }
}

Instantiate this implementation in the main.dart by invoking below method.

 Future<void> setupAnalytics({
    required AnalyticsUseCase analytics,
    required MonitoringUsecase monitoring,
  }) async {
    await Future.wait([
      analytics.setup(),
      monitoring.setup(),
    ]);
  }

Logger Module

For trigger the monitoring, we use the logger inside the app level. We create the logger.dart in the common level of app.

we embed the Monitoring Implementation in to reportError of LoggerUtil as a callback function.

MonitoringUsecase monitoring = Injector.resolve<MonitoringUsecase>();

LoggerUtil _myLogger = LoggerUtil(
  level: LoggerUtil.logLevel(
    isTestEnvironment: true,
  ),
  printer: PrettyPrinter(
    methodCount: 0,
    lineLength: 150,
    printTime: true,
    printEmojis: false,
  ),
  reportError: (exception, stackTrace) =>
      monitoring.reportError(exception, stackTrace),
  isTestEnvironment: false,
);

LoggerUtil LOG = isTestEnv ? mockLogger : _myLogger;

For example, we will implement the LOG error in the WelcomeBloc. The welcome bloc is in the login module (another module), we should create logger.dart as a LOG trigger, but the behave of this logger is handled by app module and logger module for mock.

For initiate logger for each module in the main.dart , safi_logger as logger.dart in app level.

For example, we implement LOG error before emit a event.

class WelcomeBloc extends Bloc<WelcomeEvent, WelcomeState> {
  final WelcomeUsecase _welcomeUsecase;

  WelcomeBloc(
    this._welcomeUsecase,
  ) : super(WelcomeInitial()) {
    on<WelcomeStarted>(_fetchPersistentUser);
  }

  Future<void> _fetchPersistentUser(
    WelcomeEvent event,
    Emitter<WelcomeState> emit,
  ) async {
    emit(WelcomeLoadInProgress());

    final user = await _welcomeUsecase.getPersistentUser();

    LOG.e('test error log in welcome bloc');

    if (user != null) {
      emit(WelcomeLoadUserFound(user));
    } else {
      emit(WelcomeLoadNoUser());
    }
  }
}

Debug Firebase Crashlytics.

To enable Crashlytics Debug mode on an mobile device, execute the following command:

on Android:

adb shell setprop log.tag.FirebaseCrashlytics DEBUG

on iOS(will be implement soon)

To show log in the code, execute the following command:

on Android:

adb logcat -s FirebaseCrashlytics     

on iOS(will be implement soon)

We can view LOG error monitoring in firebase console on the Crashlytics menu.