Quick answer: Unity Android games compile C# through IL2CPP to native code, so Google Play crashes arrive as native tombstones with addresses, plus ANRs when the main thread blocks too long. The Play Console aggregates them but you want richer in-game reports too. Capture managed exceptions and native signals, upload NDK debug symbols for symbolication, attach device and memory context, and group duplicates across the vast Android device range.

Android is the widest hardware spectrum any indie game faces, and a Unity game compiled through IL2CPP meets all of it. On Google Play your C# becomes native code, so crashes show up as tombstones full of addresses, and a new failure class appears: the ANR, when the main thread is blocked long enough that Android decides the app is not responding. The Play Console gives you aggregate numbers, but your own in-game reports give you the detail to act. This post covers Unity Android crash structure, ANRs, NDK symbolication, and how to make sense of crashes across thousands of device models.

Crashes and ANRs are different problems

A crash on Android is the process dying, usually from a native signal like SIGSEGV that the kernel records as a tombstone, a stack of memory addresses inside your IL2CPP binary and the Unity native libraries. Google Play surfaces these as crash clusters in the Android vitals section. They are sudden and total: the game vanishes, and the player sees the OS dismiss it.

An ANR, Application Not Responding, is different. It happens when your main thread is blocked for several seconds, by a long synchronous load, a deadlock, or a stall in native code, and Android shows the player a dialog offering to close the app. ANRs are not exceptions, so an exception handler never sees them. They are a responsiveness problem, and on Google Play they count against your quality metrics just like crashes do, so you have to watch them separately and design around them deliberately.

NDK symbols and why tombstones need them

A Unity Android release built with IL2CPP produces native libraries whose debug symbols you must keep to read tombstones. Just as iOS needs dSYMs, Android needs the unstripped native symbol files for libil2cpp and the Unity libraries. The Android App Bundle workflow lets you include a debug symbols file in the bundle, and uploading those symbols to Google Play lets the Console symbolicate tombstones into function names. Keep your own copy too, so your in-game crash reports can be symbolicated independently.

The challenge is matching symbols to builds. Each release has its own symbol set, and a tombstone from an old version symbolicated against new symbols produces garbage. Archive the NDK symbol files keyed by build and version code, exactly as you would dSYMs on iOS. A symbolicated native stack that names a method in your code is worth a hundred raw tombstones, because it turns an address into a line you can open and read.

Catching what happens before the process dies

For managed C# exceptions, hook Application.logMessageReceived and report Exception entries with their stack, scene, and player state immediately, since those already symbolicate themselves. For native signals, install a startup handler that writes a minimal crash record, the signal, the faulting addresses, and breadcrumbs, to disk, then submits it on the next launch for symbolication against your archived NDK symbols.

ANRs need a separate watchdog. Run a lightweight background thread that periodically pings the main thread and notes how long the main thread has gone without responding. If that gap crosses a few seconds, record an ANR report with the main thread's recent activity and the last breadcrumbs before the stall. This catches the responsiveness failures that no exception handler will, and it often points straight at a synchronous asset load or a lock that should never have been on the main thread.

Surviving the Android device spectrum

The defining feature of Google Play is device diversity, so context is everything. Capture the device manufacturer and model, the Android API level, the SoC or chipset, total and available memory, and the ABI, since a crash on a specific older ABI or low-memory device is extremely common. Add the build's version code, the IL2CPP and Unity versions, and the exact native library build id so reports match the right symbols.

Then add game state and any custom fields. A native crash that only appears on low-memory devices on one chipset after a heavy scene loads is a memory budget problem you can solve with smaller textures or streaming. An ANR that clusters on slow storage devices points at a synchronous load you should make async. Without the device spectrum captured, these patterns are invisible, and you would treat a hardware-specific fault as a universal mystery affecting your whole player base.

Setting it up with Bugnet

Bugnet offers a Unity SDK and an in-game report button suited to Android. Wire the SDK into your logMessageReceived handler, your native crash record, and your ANR watchdog, attaching device, chipset, memory, ABI, and the native build id. Upload your NDK debug symbols to Bugnet so incoming native tombstones symbolicate against the matching build, turning raw addresses into named methods, while ANRs arrive with the main thread's recent activity already attached.

Occurrence grouping is what makes the Android spectrum tractable. Bugnet folds thousands of identical crashes and ANRs into single counted issues, so the worst device families surface instead of an undifferentiated flood. Custom fields like model, API level, chipset, and free memory become filters, so you can confirm a crash is one low-memory chipset on one heavy level and ship a targeted fix, complementing the aggregate numbers in the Play Console with the per-report detail you need to actually act.

Reading the Console and your reports together

Use Google Play Android vitals for the population-level view, your crash-free and ANR-free rates, the device breakdown, and the OS distribution, and use your own in-game reports for the rich per-event detail and the manual reports players send through the button. The two complement each other: the Console tells you whether you have a problem and how widespread, and your reports tell you precisely what happened, on which device, after what sequence of actions.

Before each release, archive the NDK symbols and confirm a forced native crash and a deliberate main-thread stall both reach your dashboard symbolicated and labeled. After release, fix the loudest grouped crashes and ANRs first, watching both your counts and the Console vitals improve. On a platform this fragmented, a disciplined two-source crash workflow is the only realistic way a small team keeps a Unity game stable across every phone Google Play can put it on.

Unity on Google Play means native tombstones plus ANRs across countless devices, so capture both, run an ANR watchdog, and archive NDK symbols by build to symbolicate.