Monitoring your Flutter app’s stability & users insight with Firebase Crashlytics & Analytics — FlutterByteConf

PAUL
5 min readNov 3, 2023

--

Imagine, you’re a solo developer, working hard on a project, pouring your heart and soul into it.

And finally, you launched it. A lot of users loves the “said” app features, and wants to try it. But then, one day you start receiving a flurry complaints from users about your app “crashing”.

Panic sets in, and you’re not sure where to start to fix the issues, because you can’t detect where your users stopped, after your app got crashed or you can’t tell particularly what code produced this crash. That’s when Firebase Crashlytics & Analytics comes to the rescue!

In this article we’ll cover:

  • Real-world case studies of how Firebase Analytics & Crashlytics have been used effectively.
  • Discussing the challenges faced, solutions implemented, and outcomes achieved.
  • Offering insights into how to apply similar approaches to other projects.

Firebase is a Backend-as-a-Service (Baas), which provides developers with a variety of tools and services to help them develop quality apps, grow their user base, and earn profit quickly. It is built on Google’s infrastructure.

On the other hand, we are looking very closely at two unique tools on the Firebase variety of tools. The Firebase Crashlytics & Analytics.

With Firebase Crashlytics, you can quickly identify and prioritise the most critical issues in your app. It provides real-time crash reporting and detailed insights, giving you a clear picture of what’s gone wrong. You can also see the exact line of code causing the issue and even receive email alerts when new crashes occur.

But that’s not all. Firebase Analytics lets you understand your users even better. You can track user behavior, demographics, and engagement, helping you make data-driven decisions to improve your app.

One of my most liked feature of Google Analytics is the Funnel Exploration provided by Google.

This tool lets you visualise the steps your users take to complete a task and quickly see how well they are succeeding or failing at each step.

Insights on how to apply similar approaches to your projects

Setup Process

Now, here’s the important part: Firebase Crashlytics and Analytics are incredibly beginner-friendly. Even with just a little experience, you can integrate them into your Flutter app easily.

Let me walk you through the integration process on how to setup this tool on your flutter app.

Step 1: Create a Firebase project: If you don’t already have a Firebase project, you will need to create one. Go to the Firebase Console and click “Add project” to get started.

Step 2: Add your flutter app to the Firebase project: following these simple steps.

Step 3: Add dependencies:

Run this on your project’s terminal

flutter pub add firebase_core
flutter pub add firebase_crashlytics
flutter pub add firebase_analytics

or manually adding them to your flutter app’s projectpubspec.yaml file

  firebase_core:
firebase_crashlytics:
firebase_analytics:

Step 4(Optional): Integration for native android. This is 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.7.1'
}

2. 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'

Step 5: Initialise and configure crash handlers, to capture asynchronous errors that aren’t handled by the Flutter framework, use zoned errors:

main() {
runZonedGuarded<Future<void>>(() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(const MyApp());
}, FirebaseCrashlytics.instance.recordError);
}

From these setup, you should be able to record auto crash reports and, simple analytics data. To go deeply on writing custom solution, see insights to apply my process to your projects.

Create a singleton class service to handle your crashlytics and analytics on your flutter app.

For crashlytics:

In our flutter app, we can log this types of report to Firebase Crashlytics:

  • 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.
class CrashlyticsService {
final FirebaseCrashlytics _firebaseCrashlytics = FirebaseCrashlytics.instance;

// this code logs flutter errors & exceptions when called on try.. catches block.
Future<void> logError(
dynamic e, {
String? identifier, // user Id, to identify exact user a crashed happened on
StackTrace? trace,
String? errorDescription, // add custom message to uniqually know which line
bool crash = false,
bool fatal = false, // prioritise your crashes
}) async {

final exception = "$e";

try {
_firebaseCrashlytics.log(exception);

if (crash && identifier != null) {
final details = FlutterErrorDetails(
exception: e,
stack: trace,
context: ErrorDescription(errorDescription ?? 'An error type ${trace.runtimeType}'),
);

_firebaseCrashlytics.setUserIdentifier(identifier);
_firebaseCrashlytics.recordFlutterError(details, fatal: fatal);
}
} catch (e) {
debugPrint(e.toString());
}
}
}

You can call logError() to manually report crashes on your app.

For Analytics:

The SDK logs two primary types of information:

  • Events: What is happening in your app, such as user actions, system events, or errors.
  • User properties: Attributes you define to describe segments of your user base, such as language preference or geographic location.

We need to define how we group and label these events that is logged to us, so we can track, and funnel them sequentially when the time comes.

On the code below, you’d see that, we defined two different enums to group how we log data. This is to help us know events that are top level, and event that are steps to achieve a certain task.

/// top level event names
enum UserEventCategories {
authentication,
account_setup,
}


/// event steps to be grouped under top level events
enum UserEvents {
// authentication
authentication_started,
authentication_failed,
registration_complated,
login_completed,

// account_setup
get_started,
select_account_type,
enter_names,
enter_phone_number,
}

Now lets log the data, we’d create a singleton class, same as crashlytics to handle everything analytics in it. We’d also, write our logEvent() methods here and, as well as our screen observers.

class AnalyticsService {
final FirebaseAnalytics _firebaseAnalytics = FirebaseAnalytics.instance;


Future<bool> logUserEvents(
UserEventCategories category, { // top level custom event types
required UserEvents event, // event steps
Map<String, dynamic>? analyticsPayload, // additional data when logging events
}) async {
try {
final bool userExists = firebaseUser != null;
Map<String, dynamic> analyticData = {};

if (userExists) { // attaching an authenticated user to logged event
analyticData["uid"] = firebaseUser!.uid;
analyticData["email"] = firebaseUser!.email;
}

analyticData['steps'] = event.name; // logging steps

if (analyticsPayload != null) {
analyticData.addAll(analyticsPayload);
}

if (UserEventCategories.authentication == category) { // logging authentition events base on type of steps
if (event == UserEvents.login_completed) {
await _firebaseAnalytics.logLogin(loginMethod: authRepo.authProvider.name);
} else if (event == UserEvents.registration_complated) {
await _firebaseAnalytics.logSignUp(signUpMethod: authRepo.authProvider.name);
}
} else if (UserEventCategories.user_profile == category) {
await _firebaseAnalytics.setUserId(id: firebaseUser!.uid);
}

await _firebaseAnalytics.logEvent(name: category.name, parameters: analyticData);

debugPrint("========= LOGGING EVENT (${event.name}) in ${category.name.toUpperCase()} ========");
} catch (e, trace) {
logError(ref, e, trace: trace);
debugPrint(e.toString());
}

return false;
}
}

Logging an event example.

analytics.logUserEvents(
UserEventCategories.authentication, // custom event name
event: UserEvents.authentication_started, // event steps taken
);

--

--

PAUL
PAUL

Written by PAUL

Mobile Engineer | Flutter | TechTalks | Guitar

No responses yet