Advanced Android Firebase Crashlytics Integration
After you’ve added Crashlytics to your project, this article will guide you through creating a logging abstraction using a tool such as Timber to append your log entries to the crash reports (and remove Logcat output from production builds). Explains how to integrate a dedicated abstraction for Non-Fatal errors. Lastly, leverage Non-Fatal crashes to send logs up into Firebase during support calls.
Quite ambitious really... I hope it’s Sunday.
Report crashes
If you have followed the Getting Started Documentation for Crashylitics, then you have already completed this step — and honestly, it’s 80% of the job done. The rest of this article is squeezing the most out of you new Crashylitics integration.
Once you have crash reports coming in AND you are paying attention to them, significant flaws in your app will be sent to you as alerts, you have a great artefact (a crash report) to help you fix them, as well as a dashboard to give you a clear overview of their statuses.
A crashing app is an unhealthy one. If you notice that your app has a lot of crashes, stop reading here and go fix each one as best you can. Once your crash rate is low, then you can start optimising.
Add breadcrumbs
Breadcrumbs can be attached to crash reports to give you an idea of what the user was up to before the crash occurred.
Firebase.crashlytics.log("message")
Crashlytics calls them “Logs” and they can be found in the Firebase Console here along with any “Events” that it happens to capture:
Crashlytics uses what is called Log Rotation to store a FIFO list of log entries, this file is attached to every crash report and can give you a good idea of what was happening before the crash happened.
But you don’t want to scatter Firebase.crashlytics.log(“message”)
code all through your app, as well as Log.e(“message”)
. You don’t want to report crashes when you are developing on your local machine either… your Firebase console will get very noisy.
Enter a logging tool such as Timber. Timber allows you to control when and how you log and is built around the Android Log implementation, so it will feel familiar. Timber allows us to control whether we log to Logcat, Crashlytics, a file, or all three! You can create your own ‘Tree’ that will do whatever you like with your logs, and then use dependency injection to decide which log implementations to use and when. So you would log to Crashlytics in production, but not in local dev builds, and to Logcat in local debug builds but not in production.
class MyApplication:Application(){
override fun onCreate() {
super.onCreate()
initLogging()
testLogging()
}
}
fun testLogging(){
Timber.d("Testing 123")
}
fun initLogging(){
val tree = if(BuildConfig.DEBUG) Timber.DebugTree()
else CrashlyticsTree(FirebaseCrashlytics.getInstance())
Timber.plant(tree)
}
class CrashlyticsTree(
private val crashlytics: FirebaseCrashlytics
): Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
crashlytics.log(message)
}
}
Timber is a great place to start, but once you understand how it works, or if you find yourself limited by it (many projects are third party library averse — esp since the log4j scare), it’s not difficult to recreate Timber, and even build something that suits your project more closely.
Report Non-Fatals
Firebase.crashlytics.recordException(e)
Non-Fatals are a source of confusion in many projects because their purpose is often misunderstood and therefore misused.
Non-Fatal != Analytics
In many instances, logging the frequency of a recoverable error case can and should be done with analytics. Ask yourself: do I need breadcrumbs and a stack trace in order to take action (yes -> Use Non-Fatal)? Or is it enough just to know the frequency of the error case (then use Analytics)?
Non-Fatal != Log.e
These two statements are not the same:
Log.e(“Something serious happened”)
and
Firebase.crashlytics.recordException(e)
An Error-level log does not imply that something is broken and has to be fixed. A Non-Fatal crash does. It’s best to reserve Non-Fatal crash reports for situations in your code base that require action to be taken.
Non-Fatals deserve their own abstraction — in other words, they deserve their own Timber. I’m going to call it “NearMiss” since you narrowly avoided a fatal crash. NearMiss only has one function: report(Throwable)
NearMiss.report(e)
This will allow you to leverage all the alerting and stacktraces and breadcrumbs that you would normally see in a crash, except the app didn’t crash. Bonus. But don’t get carried away and use it all over the place. Remember, each report requires action to be taken — and if you are recording a Non-Fatal, you should already know what that action will be.
Non-Fatals are often used when you don’t know why other crashes are happening — to collect more information and devise a better picture of what is happeneing.
Another place to record Non-Fatals is to catch exceptions that you don’t expect to happen — like never ever. Some hardliners (including myself) would argue that if you are bold enough to claim that a line of code will never be hit, then put your money where your mouth is and just let the App crash instead of recording a Non-Fatal. If you aren’t willing to risk an app crash, then you don’t have confidence in your own code and you should refactor and avoid the exception or catch and recover from the crash. In other words, do it again, and this time do better.
If you notice a Non-Fatal crash report that is languishing in the Firebase console from inaction, remove it from the code base and downgrade it to an Error-level log.
A good rule of thumb to distinguish whether to report a Non-Fatal or to Log.e is: whether an exception has taken place or not. This is evident in the method signatures:
Log.e(tag:String, msg:String, ex:Throwable?)
vs
Firebase.crashlytics.recordException(e)
Log.e
requires a message, but not an exception. But look at the recordException
method signature — it requires an Exception.
class CrashlyticsTree(
private val crashlytics: FirebaseCrashlytics
): Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
crashlytics.log(message)
// Don't do this!
if(priority == Log.ERROR && t != null)
crashlytics.recordException(t)
}
}
Many Timber.Tree
implementations I’ve seen took the tempting route of stuffing a square peg in a round hole and simply invented an Exception with a message from the Log.e so that it can be reported as a Non-Fatal… big mistake.
class CrashlyticsTree(
private val crashlytics: FirebaseCrashlytics
): Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
crashlytics.log(message)
// Don't do this!
if(priority == Log.ERROR)
if(t != null)
crashlytics.recordException(t)
else
// REALLY don't do this!
crashlytics.recordException(Exception(message))
}
}
Doing so will group all your ‘invented’ Non-Fatal reports in the Firebase Console because, while they may have different message fields, they share the exact same stack trace. Crashlytics’ algorithm groups them — thinking they originate in the same place, which, technically, they do.
This creates a lot of noise and makes it difficult to distinguish real problems (that require urgent attention) from error logs (that require no attention).
Non-Fatal crash reports deserve their own interface — so that developers can explicitly report exceptional error cases to a dedicated space in the console, where each Non-Fatal exception requires/deserves the same level of attention as an actual crash.
Send logs
Finally, a cool little trick is to have an area in your app that either the support desk can lead the user to, or which your app can display if it detects errors. Allow the users to “Send Logs” back into Firebase with an identifier to link their Support Case with Crash logs.
class NearMiss {
companion object {
fun record(e: Throwable) {
FirebaseCrashlytics.getInstance().recordException(e)
}
fun sendFeedback(): String {
val randomWords = generateWords() // eg "RandomWordPlay"
with(FirebaseCrashlytics.getInstance()) {
setCustomKey("SupportCode", randomWords)
record(Exception("Dummy"))
sendUnsentReports()
}
return randomWords
}
private fun generateWords(): String = TODO()
}
}
Without adding any user identifiable information to the crash report, you can add custom keys — a readable generated code such as “HappyGreenFlowers” to the crash report before you send it and then ask the user to note down the generated code. This avoids attaching things like User Id’s to the crash reports and violating GDPR restrictions. The user can press a button in the app, give the support desk a code, and the support desk can go to the Firebase Console and find the user’s Breadcrumbs and monitor them at the user’s discretion.
Summary
To summarise, start by just adding Crashlytics to your project — catching crashes is industry standard and has been for a long time. Then leverage logging abstraction using a tool such as Timber to append your application's logs to the crash reports (and remove Logcat logs from production builds). Next, create a separate, dedicated abstraction for Non-Fatal errors. Lastly, manually create Non-Fatal crashes via a “Send us your logs” screen as a way of sending Breadcrumbs during support calls or identifying individual users upon request.