FIREBASE ANALYTICS
Overview.
Setup.
Implementation
Firebase Analytics
In Flutter App.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 bytrackEvent
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 fromAnalyticsEventEntity
and it is consist of methodfromJson
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
Overview.
Setup.
Implementation
Firebase Crashlytics
In Flutter App.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.
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' }
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.
Attachments:
analytics folder structure 01.png (image/png)
analytics folder structure 01.png (image/png)
analytics folder structure 02.png (image/png)
action widget folder structure.png (image/png)
Screen Shot 2022-05-03 at 10.41.15.png (image/png)
Screen Shot 2022-05-03 at 10.57.59.png (image/png)
Screen Shot 2022-05-03 at 11.00.00.png (image/png)
Screen Shot 2022-05-03 at 11.01.06.png (image/png)
Screen Shot 2022-05-03 at 11.01.28.png (image/png)
image-20220508-122417.png (image/png)
image-20220509-004507.png (image/png)
image-20220509-005841.png (image/png)
image-20220509-011635.png (image/png)
image-20220509-011711.png (image/png)
image-20220509-011752.png (image/png)
image-20220509-011917.png (image/png)
image-20220509-012402.png (image/png)
image-20220509-012547.png (image/png)
image-20220509-012943.png (image/png)
image-20220509-015626.png (image/png)
image-20220509-020503.png (image/png)
image-20220512-050409.png (image/png)
image-20220512-050649.png (image/png)
image-20220512-050655.png (image/png)
image-20220512-050939.png (image/png)