Getting Started with Crashlytics and Unity, Part 1

What has crashed shall never crash

Patrick Martin
Firebase Developers
6 min readJun 24, 2019

--

We test games as much as we can before we ship them, but it’s inevitable that something slips through no matter how much work we put into ensuring that our games are bug free. When a crash does occur, we can either wait for a player to turn it into an animated gif that trends on social media or we can hook up a crash reporting tool to aggregate issues as players experience them.

I want to walk you through Firebase’s solution for crash aggregation and reporting. To do this, I will synthesize a number of common crash archetypes in a Unity codebase and show you how Crashlytics reports on them.

The Setup

I will assume that you’ve already installed and setup Firebase for your app. If not, check out our existing documentation on the subject. In addition to just setting up Firebase, you should ensure that you add the Crashlytics Unity SDK from the Firebase SDK you setup your project with.

In addition, make sure that Firebase is enabled in your Firebase Console. To do this, go over to the Crashlytics entry in the sidebar:

And make sure to explicitly enable integration:

To test Crashlytics, I wrote a few example MonoBehaviour scripts designed to cause C# exceptions. I then attach these to a series of buttons, so I can run through on a device and generate some nice crash data. For clarity, I’ve inlined each exception below, but feel free to go through and copy them all into your test project right now.

Crash reports will be automatically batched up and sent to the Crashlytics server, typically on the next run. Therefore you should relaunch your game to see any of the exceptions generated in the following code snippets (the most notable exception to this is that if your game has a fatal crash on Android, all pending crash logs are pushed up in the background). The Crashlytics dashboard is realtime, which means we can usually see the results of tests within seconds.

Now that we’ve made it through the setup, let’s write some really bad and unstable code!

Basic Exceptions

I’ll start with a standard C# exception, this occurs basically whenever you end up doing something that you shouldn’t:

When you expand the TestBasicException crash you can see that we get a super handy stack trace and information about the device it happened on. The device information is particularly useful if crashes only happen to some users. You might be able to work out common OS features (or lack thereof) between all your issues, especially if you compare against Firebase Analytics.

You can see here that the crash occurs in the method TriggerException on TestBasicException. This is the sort of exception that you should see most of the time.

Empty Prefab — NullReferenceException

I don’t have hard numbers to look at, but most of the crashes I see in the wild for my own games seem to be a variant of some broken prefab reference. Often times this occurs because of a merge or refactor, but sometimes I just forget to drag a thing into another thing. Therefore, if you see this script:

And a stack trace in PrintTarget of type NullReferenceException:

Your very first reaction should probably be along the lines of checking for usages of this script and making sure the prefab references are all hooked up.

Coroutine Exceptions

The calling card of an experienced Unity developer tends to be the use of coroutines. Especially early in the engine’s history, this was the most reliable way to execute some logic asynchronously without causing a threading error. So, let’s build a simple coroutine that will throw:

Leading to this strange looking bit of callstack:

The most important bits to note here are that InvokeMoveNext indicates that this is likely a coroutine, the class it happened in is TestBasicExceptionInCoroutine, and the +<TriggerCoroutine> basically means you’re in the function named TriggerCoroutine on this class.

If you’re as unsatisfied with that answer as I am though, we need to dig a bit deeper into how Unity coroutines work. Normally, to iterate over collections, you have the collection return an IEnumerator. You can then iterate over the collection by writing:

or by using the shorthand:

Behind the scenes, when you have a function that returns IEnumerator or IEnumerator<T> and have any yield return instructions, it automatically generates a class that conforms to the IEnumerator interface. When you call StartCoroutine(), the IEnumerator passed to it is cached in a bank somewhere. Then MoveNext() is invoked on all the coroutines started this way at a fixed interval (usually between Update and LateUpdate).

In this case, we’re seeing that the implicitly generated MoveNext() has been invoked in some Unity function UnityEngine.SetupCoroutine.InvokeMoveNext. We’re getting the exception in this generated code. The exact generated internal class may depend on the compiler you use, in this case it’s the class <TriggerCoroutine>d_1 corresponding to the function we wrote IEnumerator TriggerCoroutine(). This isn’t the prettiest call stack, but it still brings us to exactly where our issue occurred.

Threaded Exceptions

The final situation I want to highlight is an exception thrown in a background thread. Many classes in the Unity engine aren’t thread safe, and functions such as FirebaseApp.CheckDependenciesAsync() may execute your continuation logic in the background. If you don’t know what to look for, this may take awhile to debug. Let’s look at an exception thrown in the background:

When you examine the exception in the Crashlytics dashboard:

it now contains a number of references to System.Threading rather than the stack trace for thread.Start(). This indicates that we’re potentially running in the background.

To complicate things further, I chose to use a lambda function (a pretty common pattern) to represent my background work. Therefore, in addition to the confusing callstack, I see the fingerprint of generated code in +<>c.<TriggerException>b__0_0. Although I don’t know where this thread started, I can at least see that it occurred in the class TestThreadException and the method surrounding my lambda expression is TriggerException.

The good news is that if this code is crashing explicitly due to threading, and you’re using .ContinueWith(), it’s really easy to append TaskScheduler.FromCurrentSynchronizationContext() after your lambda expression to force your continuation onto the main thread (if you’re calling .ContinueWith() on the main thread). In addition, newer versions of the Firebase SDK also include an extension .ContinueWithOnMainThread() that you can use in place of .ContinueWith() to force your continuation onto the main thread.

What Next?

At this point, you hopefully have a good idea about what Crashlytics can bring to your game by default. You just have to add the plugin to your iOS or Android game, and you’ll immediately start getting crash data from your real user base out in the wild. In my next post, I’ll talk about enhancing the information you get from Crashlytics using custom keys and cross referencing other datasets you may have on hand.

--

--

Patrick Martin
Firebase Developers

I’ve been a software engineer on everything from games to connected toys. I’m now a developer advocate for Google’s Firebase.