Understand a Unity game's crashes using advanced Crashlytics features

1. Introduction

In this codelab, you'll learn how to use advanced features of Crashlytics which will give you better visibility into crashes and the circumstances that might have caused them.

You will add new functionality to a sample game, MechaHamster: Level Up with Firebase Edition. This sample game is a new version of the classic Firebase game MechaHamster that removes most of its built-in Firebase functionality, giving you the chance to implement new uses of Firebase in their place.

You'll add a debug menu to the game. This debug menu calls methods that you'll make and allows you to exercise the different functionalities of Crashlytics. These methods will show you how to annotate your automatic crash reports with custom keys, custom logs, nonfatal errors, and more.

After building out the game, you will use the debug menu, and inspect the results to understand the unique view that they provide into how your game runs in the wild.

What you'll learn

  • The types of errors that are automatically caught by Crashlytics.
  • Additional errors that can be purposefully recorded.
  • How to add more information to these errors to make them easier to understand.

What you'll need

  • Unity (Minimum Recommended Version 2019+) with one or both of the following:
    • iOS Build Support
    • Android Build Support
  • (For Android Only) The Firebase CLI (used to upload symbols for crash reports)

2. Set up your development environment

The following sections describe how to download the Level Up with Firebase code and open it in Unity.

Note that this Level Up with Firebase sample game is used by several other Firebase + Unity codelabs, so you might have already completed the tasks in this section. If so, you can go directly to the last step on this page: "Add Firebase SDKs for Unity".

Download the code

Clone this codelab's GitHub repository from the command line:

git clone https://github.com/firebase/level-up-with-firebase.git

Alternatively, if you do not have git installed, you can download the repository as a ZIP file.

Open Level Up with Firebase in the Unity editor

  1. Launch the Unity Hub and, from the Projects tab, click the dropdown arrow next to Open.
  2. Click Add project from disk.
  3. Navigate to the directory that contains the code, and then click OK.
  4. If prompted, select a Unity editor version to use and your target platform (Android or iOS).
  5. Click on the project name, level-up-with-firebase, and the project will open in the Unity editor.
  6. If your editor does not automatically open it, open MainGameScene in Assets > Hamster in the Project tab of the Unity Editor.

For more information about installing and using Unity, see Working in Unity.

3. Add Firebase to your Unity project

Create a Firebase project

  1. In the Firebase console, click Add project.
  2. To create a new project, enter the desired project name.
    This will also set the project ID (displayed below the project name) to something based on the project name. You can optionally click the edit icon on the project ID to further customize it.
  3. If prompted, review and accept the Firebase terms.
  4. Click Continue.
  5. Select the Enable Google Analytics for this project option, and then click Continue.
  6. Select an existing Google Analytics account to use or select Create a new account to create a new account.
  7. Click Create project.
  8. When the project has been created, click Continue.

Register your app with Firebase

  1. Still in the Firebase console, from the center of the project overview page, click the Unity icon to launch the setup workflow or, if you've already added an app to your Firebase project, click Add app to display the platform options.
  2. Select to register both the Apple (iOS) and Android build targets.
  3. Enter your Unity project's platform-specific ID(s). For this codelab, enter the following:
  4. (Optional) Enter your Unity project's platform-specific nickname(s).
  5. Click Register app, and then proceed to the Download config file section.

Add Firebase configuration files

After clicking Register app, you'll be prompted to download two configuration files (one config file for each build target). Your Unity project needs the Firebase metadata in these files to connect with Firebase.

  1. Download both available config files:
    • For Apple (iOS): Download GoogleService-Info.plist.
    • For Android: Download google-services.json.
  2. Open the Project window of your Unity project, then move both config files into the Assets folder.
  3. Back in the Firebase console, in the setup workflow, click Next and proceed to Add Firebase SDKs for Unity.

Add Firebase SDKs for Unity

  1. Click Download Firebase Unity SDK in the Firebase console.
  2. Unzip the SDK somewhere convenient.
  3. In your open Unity Project, navigate to Assets > Import Package > Custom Package.
  4. In the Import package dialog, navigate to the directory that contains the unzipped SDK, select FirebaseAnalytics.unitypackage, and then click Open.
  5. From the Import Unity Package dialog that appears, click Import.
  6. Repeat the previous steps to import FirebaseCrashlytics.unitypackage.
  7. Return to the Firebase console and, in the setup workflow, click Next.

For more information about adding Firebase SDKs to Unity projects, see Additional Unity installation options.

4. Set up Crashlytics in your Unity project

To use Crashlytics in Unity projects, you'll need to do a few more setup steps. Of course, you'll need to initialize the SDK. But also, you'll need to upload your symbols so that you can see symbolicated stacktraces in the Firebase console, and you'll need to force a test crash to make sure that Firebase is getting your crash events.

Initialize the Crashlytics SDK

  1. In Assets/Hamster/Scripts/MainGame.cs, add the following using statements:
    using Firebase.Crashlytics;
    using Firebase.Extensions;
    The first module allows you to use methods from the Crashlytics SDK and the second contains some extensions to the C# Tasks API. Without both using statements the following code will not work.
  2. Still in MainGame.cs, add Firebase initialization to the existing Start() method by calling InitializeFirebaseAndStartGame():
    void Start()
      Screen.SetResolution(Screen.width / 2, Screen.height / 2, true);
  3. And again, in MainGame.cs, find InitializeFirebaseAndStartGame(), declare an app variable, and then overwrite the method's implementation like so:
    public Firebase.FirebaseApp app = null;
    // Begins the firebase initialization process and afterwards, opens the main menu.
    private void InitializeFirebaseAndStartGame()
        previousTask => 
          var dependencyStatus = previousTask.Result;
          if (dependencyStatus == Firebase.DependencyStatus.Available) {
            // Create and hold a reference to your FirebaseApp,
            app = Firebase.FirebaseApp.DefaultInstance;
            // Set the recommended Crashlytics uncaught exception behavior.
            Crashlytics.ReportUncaughtExceptionsAsFatal = true;
          } else {
              $"Could not resolve all Firebase dependencies: {dependencyStatus}\n" +
              "Firebase Unity SDK is not safe to use here");

Placing the initialization logic here prevents player interaction before the Firebase dependencies are initialized.

The benefits and effects of reporting unhandled exceptions as fatal are discussed in the Crashlytics FAQ.

Build your project and upload symbols

The steps for building and uploading symbols are different for iOS and Android apps.

iOS+ (Apple platform)

  1. From the Build Settings dialog, export your project to an Xcode workspace.
  2. Build your app.
    For Apple platforms, the Firebase Unity Editor plugin automatically configures your Xcode project to generate and upload a Crashlytics-compatible symbol file to Firebase servers for each build. This symbols information is required to see symbolicated stack traces in the Crashlytics dashboard.


  1. (only during initial setup, not for each build) Set up your build:
    1. Create a new folder called Builds at the root of your project directory (i.e., as a sibling to your Assets directory), and then create a sub-folder called Android.
    2. In File > Build Settings > Player Settings > Configuration, set Scripting Backend to IL2CPP.
      • IL2CPP generally causes builds to be smaller and have better performance.
      • IL2CPP is also the ONLY available option on iOS and selecting it here allows the two platforms to be in better parity and make debugging differences between the two (if you choose to build both) simpler.
  2. Build your app. In File > Build Settings, complete the following:
    1. Make sure the Create symbols.zip is checked (or if presented with a dropdown, select Debugging).
    2. Build your APK directly from the Unity Editor into the Builds/Android sub-folder that you just made.
  3. Once your build has finished, you need to generate a Crashlytics-compatible symbol file and upload it to Firebase servers. This symbols information is required to see symbolicated stack traces for native library crashes in the Crashlytics dashboard.

    Generate and upload this symbols file by running the following Firebase CLI command:
    firebase crashlytics:symbols:upload --app=<FIREBASE_APP_ID> <PATH/TO/SYMBOLS>
    • FIREBASE_APP_ID: Your Firebase Android App ID (not your package name). Find this value in the google-services.json file that you downloaded earlier. It's the mobilesdk_app_id value.
      Example Firebase Android App ID: 1:567383003300:android:17104a2ced0c9b9b
    • PATH/TO/SYMBOLS: the path of the zipped symbol file generated in the Builds/Android directory when your build finished (for example: Builds/Android/myapp-1.0-v100.symbols.zip).

Force a test crash to finish setup

To finish setting up Crashlytics and see initial data in the Crashlytics dashboard of the Firebase console, you need to force a test crash.

  1. In the MainGameScene find the EmptyObject GameObject in the editor Hierarchy, add the following script to it and then save the scene. This script will cause a test crash a few seconds after you run your app.
    using System;
    using UnityEngine;
    public class CrashlyticsTester : MonoBehaviour {
        // Update is called once per frame
        void Update()
            // Tests your Crashlytics implementation by
            // throwing an exception every 60 frames.
            // You should see reports in the Firebase console
            // a few minutes after running your app with this method.
            if(Time.frameCount >0 && (Time.frameCount%60) == 0)
                throw new System.Exception("Test exception; please ignore");
  2. Build your app and upload symbol information after your build finishes.
    • iOS: The Firebase Unity Editor plugin automatically configures your Xcode project to upload your symbol file.
    • Android: Run the Firebase CLI crashlytics:symbols:upload command to upload your symbol file.
  3. Run your app. Once your app is running, watch the device log and wait for the exception to trigger from the CrashlyticsTester.
    • iOS: View logs in the bottom pane of Xcode.
    • Android: View logs by running the following command in the terminal: adb logcat.
  4. Visit the Crashlytics dashboard to view the exception! You'll see it in the Issues table at the bottom of the dashboard. Later in the codelab, you'll learn more about how to explore these reports.
  5. Once you've confirmed the event was uploaded to Crashlytics, select the EmptyObject GameObject you attached it to, remove only the CrashlyticsTester component, and then save the scene to restore it to its original condition.

5. Enable and understand the Debug Menu

So far, you've added Crashlytics to your Unity project, finished setup, and confirmed that the Crashlytics SDK is uploading events to Firebase. You'll now create a menu in your Unity project which will demonstrate how to use more advanced Crashlytics functionality in your game. The Level Up with Firebase Unity project already has a hidden Debug Menu that you'll make visible and write the functionality for.

Enable the Debug Menu

The button to access the Debug Menu exists in your Unity project, but it's not currently enabled. You must enable the button to access it from the MainMenu prefab:

  1. In the Unity Editor, open the prefab named MainMenu.4148538cbe9f36c5.png
  2. In the prefab hierarchy, find the disabled sub-object named DebugMenuButton, and then select it.816f8f9366280f6c.png
  3. Enable the DebugMenuButton by checking the box in the upper-left corner to the left of the text field containing DebugMenuButton.8a8089d2b4886da2.png
  4. Save the prefab.
  5. Run the game in either the editor or on your device. The menu should now be accessible.

Preview and understand the method bodies for the Debug Menu

Later in this codelab, you'll write method bodies for some preconfigured debug Crashlytics methods. In the Level Up with Firebase Unity project, though, the methods are defined in and called from DebugMenu.cs.

While some of these methods will both call Crashlytics methods and throw errors, the ability of Crashlytics to catch these errors does not depend on calling those methods first. Rather, the crash reports generated from automatically catching errors will be enhanced by the information added by these methods.

Open DebugMenu.cs, and then find the following methods:

Methods for generating and annotating Crashlytics issues:

  • CrashNow
  • LogNonfatalError
  • LogStringsAndCrashNow
  • SetAndOverwriteCustomKeyThenCrash
  • SetLogsAndKeysBeforeANR

Methods for logging Analytics events to aid in debugging:

  • LogProgressEventWithStringLiterals
  • LogIntScoreWithBuiltInEventAndParams

In later steps of this codelab, you will implement these methods and learn how they help address specific situations that can occur in game development.

6. Ensure delivery of crash reports in development

Before you start to implement these debug methods and see how they affect crash reports, make sure you understand how events are reported to Crashlytics.

For Unity projects, crash and exception events in your game are immediately written to disk. For uncaught exceptions that don't crash your game (for example, uncaught C# exceptions in game logic), you can have the Crashlytics SDK report them as fatal events by setting the Crashlytics.ReportUncaughtExceptionsAsFatal property to true where you initialize Crashlytics in your Unity project. These events are reported to Crashlytics in real-time without the need for an end-user to restart the game. Note that native crashes are always reported as fatal events and sent along when an end-user restarts the game.

In addition, be aware of the following small—but significant—differences between how the different runtime environments send Crashlytics information to Firebase:

iOS simulator:

  • Crashlytics information is reported if and only if you detach Xcode from the simulator. If Xcode is attached, it catches the errors upstream, preventing information delivery.

Mobile physical devices (Android and iOS):

  • Android-specific: ANRs are only reported on Android 11+. ANRs and non-fatal events are reported on the next run.

Unity Editor:

Test crashing your game at the touch of a button in CrashNow()

After Crashlytics is set up in your game, the Crashlytics SDK automatically records crashes and uncaught exceptions and uploads them to Firebase for analysis. And the reports are displayed in the Crashlytics dashboard in the Firebase console.

  1. To demonstrate that this is indeed automatic: open DebugMenu.cs, and then overwrite the method CrashNow() as follows:
    void CrashNow()
  2. Build your app.
  3. (Android Only) Upload your symbols by running the following Firebase CLI command:
    firebase crashlytics:symbols:upload --app=<FIREBASE_APP_ID> <PATH/TO/SYMBOLS>
  4. Tap the Crash Now button, and proceed to the next step of this codelab to find out how to view and interpret the crash report.

7. Understand issue reports in the Firebase console

When it comes to viewing your crash reports, there's a little bit more you need to know about how to get the most out of them. Each of the methods you write will show how to add different types of information to Crashlytics reports.

  1. Tap the Crash Now button, and then restart your app.
  2. Go to the Crashlytics dashboard. Scroll down to the Issues table at the bottom of the dashboard where Crashlytics groups events that all have the same root cause into "issues".
  3. Click on the new issue that's listed in the Issues table. Doing this displays the Event summary about each individual event that was sent to Firebase.

    You should see something like the following screencap. Notice how the Event summary prominently features the stack trace of the call that led to the crash.40c96abe7f90c3aa.png

Additional metadata

Another helpful tab is the Unity Metadata tab. This section informs you about the attributes of the device on which the event occurred, including physical features, the CPU model/specs, and all sorts of GPU metrics.

Here's an example where the information in this tab might be useful:
Imagine your game makes heavy use of shaders to achieve a certain look, but not all phones have GPUs that are capable of rendering this feature. The information in the Unity Metadata tab can give you a better idea of what hardware your app should test for when deciding what features to automatically make available or disable entirely.

While a bug or crash may never happen on your device, due to the massive diversity of Android devices in the wild, it helps to better understand the particular "hotspots" of your audience's devices.


8. Throw, catch, and log an exception

Oftentimes, as a developer, even if your code properly catches and handles a runtime exception, it's good to note that it occurred, and under what circumstances. Crashlytics.LogException can be used for this exact purpose—to send an exception event to Firebase so that you can further debug the issue in the Firebase console.

  1. In Assets/Hamster/Scripts/States/DebugMenu.cs, append the following to the using statements:
    // Import Firebase
    using Firebase.Crashlytics;
  2. Still in DebugMenu.cs, overwrite LogNonfatalError() as follows:
    void LogNonfatalError()
            throw new System.Exception($"Test exception thrown in {nameof(LogNonfatalError)}");
        catch(System.Exception exception)
  3. Build your app.
  4. (Android Only) Upload your symbols by running the following Firebase CLI command:
    firebase crashlytics:symbols:upload --app=<FIREBASE_APP_ID> <PATH/TO/SYMBOLS>
  5. Tap the Log Nonfatal Error button, and then restart your app.
  6. Go to the Crashlytics dashboard, and you should see something similar to what you saw in the last step of this codelab.
  7. This time, though, restrict the Event type filter to Non-fatals so that you're only viewing non-fatal errors, such as the one you just logged.

9. Log strings to Crashlytics to better understand the flow of program execution

Have you ever tried to figure out why a line of code that gets called from multiple paths, hundreds if not thousands of times per session, can suddenly generate an exception or crash? While it might be nice to step through the code in an IDE and look at the values more closely, what if this happens only among a vanishingly small percentage of your users? Even worse, what would you do if you can't replicate this crash no matter what you do?

In situations like this, having some context can make a world of difference. With Crashlytics.Log, you have the ability to write out the context you need. Think of these messages as hints to your future self about what might be going on.

While logs can be used in myriad ways, they are typically most helpful for recording situations where the order and/or absence of calls is a vitally important piece of information.

  1. In Assets/Hamster/Scripts/States/DebugMenu.cs, overwrite LogStringsAndCrashNow() as follows:
    void LogStringsAndCrashNow()
        Crashlytics.Log($"This is the first of two descriptive strings in {nameof(LogStringsAndCrashNow)}");
        const bool RUN_OPTIONAL_PATH = false;
            Crashlytics.Log(" As it stands, this log should not appear in your records because it will never be called.");
            Crashlytics.Log(" A log that will simply inform you which path of logic was taken. Akin to print debugging.");
        Crashlytics.Log($"This is the second of two descriptive strings in {nameof(LogStringsAndCrashNow)}");
  2. Build your app.
  3. (Android Only) Upload your symbols by running the following Firebase CLI command:
    firebase crashlytics:symbols:upload --app=<FIREBASE_APP_ID> <PATH/TO/SYMBOLS>
  4. Tap the Log Strings and Crash Now button, and then restart your app.
  5. Go back to the Crashlytics dashboard, and click into the newest issue listed in the Issues table. Again you should see something similar to the previous issues.
  6. However, if you click the Logs tab within an Event summary, you get a view like this:

10. Write and overwrite a custom key

Let's say you want to better understand a crash corresponding to variables set to a small number of values or configurations. It might be nice to be able to filter, based on the combination of variables and possible values you are looking at, at any given time.

On top of logging arbitrary strings, Crashlytics offers another form of debugging when it is beneficial to know the exact state of your program as it crashed: custom keys.

These are key-value pairs that you can set for a session. Unlike logs which accumulate and are purely additive, keys can be overwritten to only reflect the most recent status of a variable or condition.

In addition to being a ledger of the last recorded state of your program, these keys can then be used as powerful filters for Crashlytics issues.

  1. In Assets/Hamster/Scripts/States/DebugMenu.cs, overwrite SetAndOverwriteCustomKeyThenCrash() as follows:
    void SetAndOverwriteCustomKeyThenCrash()
        const string CURRENT_TIME_KEY = "Current Time";
        System.TimeSpan currentTime = System.DateTime.Now.TimeOfDay;
            DayDivision.GetPartOfDay(currentTime).ToString() // Values must be strings
        // Time Passes
        currentTime += DayDivision.DURATION_THAT_ENSURES_PHASE_CHANGE;
  2. Build your app.
  3. (Android Only) Upload your symbols by running the following Firebase CLI command:
    firebase crashlytics:symbols:upload --app=<FIREBASE_APP_ID> <PATH/TO/SYMBOLS>
  4. Tap the Set Custom Key and Crash button, and then restart your app.
  5. Go back to the Crashlytics dashboard, and click into the newest issue listed in the Issues table. Again you should see something similar to the previous issues.
  6. This time, though, click the Keys tab in the Event summary so that you can view the value of keys including Current Time:

Why would you want to use custom keys instead of custom logs?

  • Logs are good at storing sequential data, but custom keys are better if you only want the most recent value.
  • In the Firebase console, you can easily filter issues by the values of keys in the Issues table search box.

Similar to logs though, custom keys do have a limit. Crashlytics supports a maximum of 64 key-value pairs. After you reach this threshold, additional values are not saved. Each key-value pair can be up to 1 KB in size.

11. (Android only) Use custom keys and logs to understand and diagnose an ANR

One of the most difficult classes of issues to debug for Android developers is the Application Not Responding (ANR) error. ANRs occur when an app fails to respond to input for more than 5 seconds. If this happens, it means the app either froze, or is going very slowly. A dialog is shown to users, and they are able to choose whether to "Wait" or "Close App".

ANRs are a bad user experience and (as mentioned in the ANR link above) can affect your app's discoverability in the Google Play Store. Because of their complexity, and because they're often caused by multithreaded code with vastly different behavior on different phone models, reproducing ANRs while debugging is often very difficult, if not near impossible. As such, approaching them analytically and deductively is usually the best approach.

In this method, we will use a combination of Crashlytics.LogException, Crashlytics.Log and Crashlytics.SetCustomKey to supplement automatic issue logging and to give us more information.

  1. In Assets/Hamster/Scripts/States/DebugMenu.cs, overwrite SetLogsAndKeysBeforeANR() as follows:
    void SetLogsAndKeysBeforeANR()
        System.Action<string,long> WaitAndRecord =
        (string methodName, long targetCallLength)=>
            System.Diagnostics.Stopwatch stopWatch = new System.Diagnostics.Stopwatch();
            const string CURRENT_FUNCTION = "Current Async Function";
            // Initialize key and start timing
            Crashlytics.SetCustomKey(CURRENT_FUNCTION, methodName);
            // The actual (simulated) work being timed.
            // Stop timing
              Crashlytics.Log($"'{methodName}' is long enough to cause an ANR.");
            else if(stopWatch.ElapsedMilliseconds>=BusyWaitSimulator.SEVERE_DURATION_MILLIS)
              Crashlytics.Log($"'{methodName}' is long enough it may cause an ANR");
  2. Build your app.
  3. Upload your symbols by running the following Firebase CLI command:
    firebase crashlytics:symbols:upload --app=<FIREBASE_APP_ID> <PATH/TO/SYMBOLS>
  4. Tap the button labeled Set Logs And Keys → ANR, and then restart your app.
  5. Go back into the Crashlytics dashboard, and then click into the new issue in the Issues table to view the Event summary. If the call went through properly, you should see something like this:

    As you can see, Firebase pinpointed the busy wait on the thread as the main reason your app triggered an ANR.
  6. If you look at the logs in the Logs tab of the Event summary, you will see that the last method recorded as complete is DoSevereWork.

    By contrast, the last method listed as starting is DoExtremeWork, which indicates that the ANR occurred during this method, and the game closed before it could log DoExtremeWork.


Why do this?

  • Reproducing ANRs is incredibly hard, so being able to get rich information about code area and metrics is incredibly important for finding it out deductively.
  • With the information stored in the custom keys, you now know which async thread took the longest to run, and which ones were in danger of triggering ANRs. This sort of related logical and numeric data will show you where in your code is most necessary to optimize.

12. Interspersing Analytics events to further enrich reports

The following methods are also callable from the Debug Menu, but instead of generating issues themselves, they use Google Analytics as another source of information to better understand the workings of your game.

Unlike the other methods you've written in this codelab, you should use these methods in combination with the others. Call these methods (by pressing their corresponding button in the Debug Menu) in whatever arbitrary order you want before running one of the others. Then, when you examine the information in the specific Crashlytics issue, you will see an ordered log of Analytics events. This data can be used in a game to better understand a combination of program flow or user input, depending on how you have instrumented your app.

  1. In Assets/Hamster/Scripts/States/DebugMenu.cs, overwrite the existing implementations of the following methods:
    public void LogProgressEventWithStringLiterals()
          Firebase.Analytics.FirebaseAnalytics.LogEvent("progress", "percent", 0.4f);
    public void LogIntScoreWithBuiltInEventAndParams()
  2. Build and deploy your game, and then enter the Debug Menu.
  3. (Android Only) Upload your symbols by running the following Firebase CLI command:
    firebase crashlytics:symbols:upload --app=<FIREBASE_APP_ID> <PATH/TO/SYMBOLS>
  4. Press at least one of the following buttons one or more times to call the above functions:
    • Log String Event
    • Log Int Event
  5. Press the Crash Now button.
  6. Restart your game to have it upload the crash event to Firebase.
  7. When you log various arbitrary sequences of Analytics events and then have your game generate an event that Crashlytics creates a report from (as you just have), they get added to the Logs tab of the Crashlytics Event Summary like this:

13. Going forward

And with that, you should have a better theoretical basis on which to supplement your automatically-generated crash reports. This new information allows you to use the current state, records of past events, and existing Google Analytics events to better break down the sequence of events and logic that led to its outcome.

If your app targets Android 11 (API level 30) or higher, consider incorporating GWP-ASan, a native memory allocator feature useful for debugging crashes caused by native memory errors such as use-after-free and heap-buffer-overflow bugs. To take advantage of this debugging feature, explicitly enable GWP-ASan.

Next Steps

Proceed to the Instrument your Unity game with Remote Config codelab, where you'll learn about using Remote Config and A/B Testing in Unity.