Error handling on Android part 6: adding useful metadata to crash reports

Jamie Lynch

Error handling on Android

Accent divider.

How many times have you been in the middle of using a new shiny app, only to have it crash on you?

Android app displaying a crash dialog

This is the sixth in a series of posts that will investigate how the exception handling mechanism works in Java and Android, and how crash reporting SDKs can capture diagnostic information, so that you're not flying blind in production.

Capturing useful metadata in Android crash reports

We previously learnt how to handle deobfuscating stacktraces from a minified Android app. Our goal now is to add some useful metadata to our crash reports, so that we can prioritise the most important issues and debug faster.

Stacktraces are not always enough

Stacktraces are not always enough — sometimes capturing additional metadata can be essential for tracking down the root cause of an issue. Many of us will have previously encountered a bug that was impossible to reproduce on a certain device, but was definitely happening in production, and was due to some environment or device-specific issue. A good crash reporter should tell you the basic information: what the device model was, who manufactured it, and what the Android API level was, so that you can spend your time fixing bugs.

Along with basic defaults that we'll always want to capture, sometimes it can make sense to attach additional information onto a specific report. For example, if an exception is thrown within a networking module, it may make sense to append request information to any crash report. The more useful data is added to crash reports, the more you will be able to search and segment the useful errors, and spend your time fixing the important issues.

Adding metadata directly to a report

To start off, we'll add a #Map to our #Report object, which will contain all the metadata that is collected as part of a crash report. This will be serialised just like JSON has been in previous posts, and will look something like this:

class Report(exc: Throwable) {
   private val metaData: MutableMap<String, Any> = mutableMapOf()
   
   fun addMetaData(key: String, value: Any) {
       metaData[key] = value
   }
}

fun createReport(exc: Throwable) {
   val report = Report(exc)
   report.addMetaData("manufacturer", Build.MANUFACTURER)
   report.addMetaData("availableMemory", Runtime.getRuntime().freeMemory())
}

An important distinction here is that some metadata is mutable and will change during a session, such as the amount of free memory in bytes. Other information is immutable, and will never change over the lifetime of a session, or even the device, as above in the case of the device's manufacturer. This opens up the possibility of caching some values on the first error report, and avoiding unnecessary work - which is important as crash reporters and libraries in general should have as minimal as possible impact on application performance.

Allowing users to add metadata onto a report

We can now allow end-users of our crash reporting SDK to modify the report with their own data, by using Kotlin's higher order functions. If we pass in a function as a parameter and invoke it after the report is generated but before it is delivered to the error reporting API, it's possible to append data to a crash report:

fun performTask() {
   notify(RuntimeException()) { report ->
       report.addMetaData("payingCustomer", true)
       report.addMetaData("landingScreen", "B")
   }
}

fun notify(exc: Throwable, callback: (report: Report) -> Unit) {
   val report = createReport(exc)
   callback(report) // mutate the report here
   delivery.deliver(report)
}

These crash reports can then be searched and segmented, allowing us to do interesting things such as compare the crash rate for A/B experiments, or to prioritise paid customers, if we take the time to append the necessary metadata to all our crash reports.

Collecting sensitive data in error reports and complying with GDPR/HIPAA

A good rule when it comes to sensitive data is that you shouldn't collect something if you wouldn’t be happy with a stranger viewing the equivalent data from your device. Some pieces of data should definitely be avoided such as IMEI and IP addresses, as they can uniquely identify a user and typically have little benefit in diagnosing what caused a crash. Additionally, there's no point collecting metadata if you don't need it, as this is likely to make your job harder by adding more noise and less signal to your crash reports.

We live in a world where GDPR and HIPAA dictate how sensitive data should be accessed, so it's critical to have a policy in place for how you store and delete crash data, as information collected in crash reports can be sensitive. It's also considered good form to ask for consent before initialising crash reporting, which is typically achieved by showing a dialog on the first startup of your application.

Now that we've covered collecting metadata, read on to discover how to capture errors in the NDK, and some of the particular challenges for generating a crash report there.

Capturing breadcrumbs

Another useful piece of information we can capture is breadcrumbs. Breadcrumbs are a global queue of events which occurred in your application before a crash, which allow you to diagnose errors caused by seemingly unrelated events.

Breadcrumbs in dashboard

For example, a breadcrumb could be logged when the user rotates the screen, and any subsequent crash caused by incorrect storage of UI state could be easily diagnosed. bugsnag-android automatically logs breadcrumbs for several system broadcasts and activity lifecycle events, and also allows manual logging of custom breadcrumbs, such as events within your own application code.

Would you like to know more?

Hopefully this has helped you learn a bit more about Error Handling on Android. If you have any questions or feedback, please feel free to get in touch.

———

Try Bugsnag's Android crash reporting.

Share