Categories: Data Engineering

This Week in Glean: Instrumenting Android Crashes with Glean

(“This Week in Glean” is a series of blog posts that the Glean Team at Mozilla is using to try to communicate better about our work. They could be release notes, documentation, hopes, dreams, or whatever: so long as it is inspired by Glean. You can find an index of all TWiG posts online.)

For the last year I’ve been working on this shiny new library by Mozilla known as Glean SDK. The Glean SDK is a cross-platform telemetry library that makes collecting data easier for developers to understand and instrument in their applications. Just take a look to the tagline, “Telemetry for humans” to understand the objective of Glean. Glean is being used right now in things like Firefox Preview and Firefox for Fire TV and I’m sure we will see it in even more Mozilla products very soon.

One of the things that we might want to collect some data on in the applications we create is crashes. Every application will have them from time to time. Sometimes they are the application’s fault, sometimes it’s the fault of the OS, but they happen and we usually want to know more about them so we can fix them (if possible). So let’s take a look at how we can use Glean to instrument an Android application and record some data when it crashes.

Before We Start

There are a few things that we need to have installed in order to get ready. The biggest one of those is Android Studio, which (if you include the Android SDK) can take a little while to download and get installed. Second, I’m going to recommend having the Glean documentation open and available as we will be referring to it throughout this walk-through as we instrument our app. This walkthrough also assumes that you already have some knowledge of Android application development. We won’t be doing anything too involved, but knowing where to go to create a new project and how to add dependencies to a Gradle file will be helpful to know beforehand.

Create A Project

With our tools and documentation installed and open, let’s get started by creating a new Android Studio project. We could choose any type of project or template to start with, but let go with the “Basic Activity” template, simply because it comes with a button already on the screen that we can rig to crash the app in order to collect our metrics. We need to give the project a name: something along the lines of “GleanCrashExample” sounds reasonable enough. Then we make sure to choose Kotlin for the language and API level 21 for the minimum SDK level. Tada! A new project ready for integration with Glean!

Setup Build Configuration

Add Glean As A Dependency

Now that we have a project started, we can add Glean to it as a dependency in the Gradle configuration. Find the build.gradle file for your app module and add the following line to the dependencies section:

implementation 'org.mozilla.components:service-glean:20.0.0'

As of this writing, version 20.0.0 is the latest version of Android Components. Android Components is a collection of tools for building browsers on Android and it is the easiest way to consume Glean on Android at the moment. You should probably take a look at the Android-Components releases and check that you are using the latest version of Android-Components for your project. So just replace the version above with the latest.

Add Python Environment

We need to add a Python environment for the glean_parser to run in by adding the following lines to the very top of the build.gradle file:

plugins {
    id "com.jetbrains.python.envs" version "0.0.26"
}

Import SDK Generator Script

The last part of the changes to the app’s build.gradle file is to include the sdk_generator.gradle script. Right before the end of the build.gradle file, add the following (making sure the version here matches the version of the implementation line above):

apply from: 'https://github.com/mozilla-mobile/android-components/raw/v20.0.0/components/service/glean/scripts/sdk_generator.gradle'

Add Mozilla Maven Repository

Finally, in order to be able to download Glean, we need to add Mozilla’s Maven repository URL to the Project build.gradle file (as opposed to the app module’s build.gradle file). For our project named “GleanCrashExample”, there will be a build.gradle that has (Project: GleanCrashExample) next to it in the project explorer of Android Studio. In this Gradle file, you will find a section called allProjects which has in it a section called repositories. You may already find google() and jcenter() there, but we need to add the following in order to download Glean:

maven {
    url "https://maven.mozilla.org/maven2"
}

That should be it for additions to the build configuration files. You should try to do a Gradle sync now to make sure everything works. If you have any problems, try going back through the configuration steps to make sure you got everything.

Instrument The Project With Glean

Initial Integration

Now that we have added Glean to our app, we need to do a little initialization in order to get it ready to record and send data. It is important to override the Application class so that we can initialize the Glean SDK in the Application.onCreate() method. Add a new file with a class that extends Application called GleanCrashExampleApplication and update the application manifest to add android:name=".GleanCrashExampleApplication" as a property in the application section so that the app knows to use the custom Application class. Your GleanCrashExampleApplication.kt file should look like this:

package org.mozilla.gleancrashexample

import android.app.Application
import mozilla.components.service.glean.Glean
import org.mozilla.gleancrashexample.GleanMetrics.Pings

class GleanCrashExampleApplication: Application() {
    override fun onCreate() {
        super.onCreate()

        // Register the sample application's custom pings.
        Glean.registerPings(Pings)

        // Set upload enabled
        Glean.setUploadEnabled(true)

        // Initialize the Glean library. Ideally, this is the first thing that
        // must be done right after enabling logging.
        Glean.initialize(applicationContext)
    }
}

This adds the necessary import statements, and then registers pings, sets the upload enabled state, and initializes the Glean SDK within the Application.onCreate() method.

That’s it! The app now has Glean installed and enabled and will already be able to send the baseline ping without any additional work!

Add Custom Metric

Since we are instrumenting crashes with some custom metrics, we still have some work to do. We will need to add a metrics.yaml file to define the metrics we will use to record our crash information and a pings.yaml file to define a custom ping which will give us some control over the scheduling of the uploading of the crash telemetry.

In order to do this we need to make a decision: What metric type will we use to represent our crash data? This could probably be argued several ways, but I have chosen to instrument it as an event, because events capture information in a nice concise way and have a built-in way of passing additional information using the extras field. So if we want to pass along the cause of the crash, or a few lines to describe the crash, or even potentially a few lines of stack trace, events let us do that easily (with some limitations).

Now that we have decided what metric type we will use, we can create our metrics.yaml. Inside of the app folder in the Android Studio project we are going to create a new file and call it metrics.yaml. Then we can add the schema definition and the metric definition to the file, so that it looks like this:

# Required to indicate this is a `metrics.yaml` file
$schema: moz://mozilla.org/schemas/glean/metrics/1-0-0

crash:
  exception:
    type: event
    description: |
      Event to record crashes caused by unhandled exceptions
    notification_emails:
      - crashes@example.com
    bugs:
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1582479
    data_reviews:
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1582479
    expires:
      2099-01-01
    send_in_pings:
      - crash
    extra_keys:
      cause:
        description: The cause of the crash
      message:
        description: The exception message

By way of explanation, this creates a metric called exception within a metric category called crash. There is a brief description and the required notification, bug, data review, and expiry fields. Then we have the send_in_pings field with a value of - crash. This means that we will send the crash data via a custom ping named crash (which we haven’t created, yet). Finally we have the extra_keys field with two keys defined, cause and message. This will allow us to send a couple of pieces of additional information along with the event.

Add Custom Ping

Now we need to define the custom ping by creating a pings.yaml file in the same directory as we just created the metrics.yaml file. We already know what the name of the ping is, crash, so the pings.yaml file should look like this:

# Required to indicate this is a `pings.yaml` file
$schema: moz://mozilla.org/schemas/glean/pings/1-0-0

crash:
  description: >
    A ping to transport crash data
  include_client_id: true
  notification_emails:
    - crash@example.com
  bugs:
    - https://bugzilla.mozilla.org/show_bug.cgi?id=1582479
  data_reviews:
    - https://bugzilla.mozilla.org/show_bug.cgi?id=1582479

Before we can use our newly defined metric or ping, we need to build the application. This will cause the glean_parser to work its magic and generate the API files that represent the metrics we defined

NOTE: Just a little advice from my personal experience here, but I would recommend running the clean task on your project before building any time you have modified one of the Glean YAML files. Sometimes it doesn’t regenerate the files without cleaning, so something to keep in mind if you aren’t seeing your changes to the YAML files show up in the project.

We now have a couple of tasks to perform back in the MainActivity.kt file in order to make use of the metric and ping we defined. First, we need to add an import line:

import org.mozilla.gleancrashexample.GleanMetrics.Pings

Then we need to register our custom ping by calling Glean.registerPings(Pings). In the MainActivity.onCreate() function, add the following line:

Glean.registerPings(Pings)

This registers the custom ping with Glean so that it knows about it and can manage the storage and other important details of it like sending it when we tell it to.

Instrument The App To Record The Event

Next, we need to make the MainActivity handle uncaught exceptions, so we extend the class definition by adding Thread.UncaughtExceptionHandler as an inherited class like this:

class MainActivity : AppCompatActivity(), Thread.UncaughtExceptionHandler {
    ...
}

In order to be a responsible Thread.UncaughtExceptionHandler, we need to implement an override for the uncaughtException() function. Somewhere in your MainActivity class, add the following override:

override fun uncaughtException(p0: Thread, p1: Throwable) {
    Crash.exception.record(
        mapOf(
            Crash.exceptionKeys.cause to p1.cause!!.toString(),
            Crash.exceptionKeys.message to p1.message!!)
    )
    Pings.crash.send()
}

To explain what’s happening here, first we are recording to the Crash.exception metric we created. If you recall, the category of the metric was crash and the name was exception so we access it by calling record() on the Crash.exception object that was created by the magic of Glean. You can also see where we are passing in the extra information for the cause and the message which will get packaged up and sent along with our ping when the second action of Pings.crash.send() is called. Basically this forces our ping to be sent immediately after the recording of the event.

Finally, we need to register our MainActivity as the default uncaught exception handler by adding the following line to the MainActivity.onCreate() function:

Thread.setDefaultUncaughtExceptionHandler(this)

Make A Way To Crash The App

That’s pretty much it! Now we just need to make a way to crash our app with an uncaught exception. The “Basic Activity” template has a floating action button called fab that can be used, and it’s set up in the onCreate() of the MainActivity. We can change the click listener to make it look like this in order to make it throw an exception:

fab.setOnClickListener {
    // Go boom!
    throw NullPointerException()
}

Now run the application and click the floating action button and you should have just crashed the app while recording and sending crash telemetry! I should mention, this information didn’t really get recorded by anything, as there is additional work that is required on our ingestion pipeline in order to accept telemetry from new applications, and that step is for another blog post, but there are docs available if you are interested.

Conclusion

If you are interested in seeing the fully operational project used to help write this tutorial, you can find it on GitHub here.

Important Note: This is a very simple example of a strategy to instrument crashes by using Glean. There probably will be challenges to using this approach in a production application that should be considered. For instance, when an app crashes it can be in an unknown state and may not be able to do things like upload data to a server. There is a less trivial implementation of instrumenting crashes in Android Components called lib-crash that takes this a step further by taking into consideration such things as multiprocessing and persistence.

(( This is a syndicated copy of the original post. ))