Quick answer: Capture native crashes from your Android NDK game with a signal handler that records the native backtrace, keep symbols per build for symbolication, and capture the device context, since NDK code crashes with signals below the Java layer that the JVM never sees. The native crash handling plus symbols is what makes these crashes readable.
Games built with the Android NDK run native C and C++ code beneath the Android Java layer, for performance or to share a native codebase across platforms, and that native code crashes the way any native code does: with signals like segmentation faults, producing a stripped backtrace that means nothing without symbols. Crucially, these native crashes happen below the Java and JVM layer, so the usual Java exception handling never sees them. Setting up crash reporting for a native Android NDK game means installing native crash handling, managing symbols, and capturing the native crashes the JVM cannot.
NDK code crashes below the Java layer
The Android NDK lets you write game code in native C and C++, running beneath the Android Java layer through JNI, for performance or to reuse a native engine across platforms. This native code crashes like any native code, through signals, a segmentation fault from a null pointer or bad memory access, an abort, a bus error, producing a native crash with a backtrace of memory addresses.
The critical point is that these native crashes happen below the Java and JVM layer, so the Java exception handling that catches Android app crashes never sees them. A native crash in your NDK code is invisible to the Java-level crash reporting, since it occurs in native code the JVM does not manage, and without native crash handling, it crashes the app with the native crash going uncaptured except perhaps in a system tombstone. Understanding that NDK crashes are native crashes below the Java layer, invisible to Java exception handling, is the foundation for capturing them, which requires native crash handling distinct from the Java-level reporting.
Install native crash handling
Capture native NDK crashes by installing native crash handling, a signal handler for the fatal signals in your native code, that catches the crash and records the native backtrace, the signal, and the context before the process dies. This is the same native crash handling any native game needs, adapted to the Android NDK environment, and it is what catches the native crashes the JVM does not.
On Android, a common approach is to use a native crash library, such as Breakpad or a similar tool, that handles the complexities of capturing a native crash on Android, including writing a minidump or backtrace, since capturing a native crash reliably from within a signal handler is delicate. The native crash handling captures the crash that the Java layer misses, getting the native backtrace and context off the device. Installing native crash handling for your NDK code, whether your own signal handler or a native crash library, is what captures the native crashes that are otherwise invisible below the Java layer.
Manage your native symbols
A native NDK crash backtrace is a list of memory addresses, meaningless without symbols, so manage your native symbols, generating and keeping the debug symbols for your native libraries per build, keyed to the version, so you can symbolicate the backtraces into readable function names and line numbers. This is the same symbol management any native game needs, applied to your NDK libraries.
When a native crash comes in, symbolicate its backtrace against the symbols for that build native libraries to recover the real function names and lines, turning the addresses into a readable trace that points at the bug in your C or C++ code. Keep the native symbols for every release and never lose them, since without them the native crashes from that build are unreadable, which is catastrophic when a critical native crash appears. Managing your native symbols, archiving them per build for symbolication, is essential to native NDK crash reporting, since the native backtraces are useless without the symbols to make them readable.
Capture the device context
Native NDK games run across the fragmented Android device landscape, and native crashes cluster by device just as Java-level crashes do, so capture the device context, the device model, OS version, GPU, and available memory, with every native crash. A native crash that concentrates on one device family, one GPU, or low-memory devices points at a hardware-specific or memory-related cause, which the device context reveals.
Native code, being closer to the hardware, can be especially sensitive to device differences, a native graphics call that fails on a particular GPU, a memory access that crashes on a specific device, and the device context is what surfaces these patterns. Memory is especially important, since native code can hit out-of-memory conditions and the device available memory at crash time helps distinguish a memory kill from a code crash. Capturing the device context on native NDK crashes lets you cluster them by hardware and identify the device-specific and memory-related native crashes across the Android fragmentation.
Setting it up with Bugnet
Bugnet captures native Android NDK crashes through native crash handling that records the native backtrace and crash context, with symbols kept per build for symbolication, and the device context, model, OS, GPU, memory, attached. The native crashes that the Java layer never sees flow into one dashboard, alongside any Java-level crashes, with the symbolicated backtraces readable.
Add custom fields for your game state and group identical native crashes into occurrence counts. Because NDK crashes are native crashes below the Java layer, the native crash handling plus symbolication is what captures and makes them readable, and the device context is what lets you cluster them by hardware across the Android fragmentation. This gives a native Android NDK game the crash visibility that Java-level reporting alone cannot provide, surfacing the native crashes that would otherwise be invisible and making them diagnosable with readable backtraces and device clustering.
Cover both the native and Java layers
A native Android NDK game often has both native code and a Java layer, and crashes can occur in either, so cover both layers in your crash reporting. The native crash handling captures the native crashes below the Java layer, and the Java exception handling captures the Java-level crashes, and a complete picture of your game stability requires both, since a crash could be in your native code or in the Java glue.
Capturing both layers also helps you attribute crashes correctly, distinguishing a native crash in your NDK code from a Java-level crash in the Android app code, which are different to investigate and fix. Tag crashes by layer, native or Java, so they triage appropriately. Covering both the native and Java layers, with native crash handling for the NDK code and Java exception handling for the Java code, gives a native Android NDK game complete crash visibility, ensuring no crash, whether in the performance-critical native code or the Java layer above it, goes uncaptured, which is essential since the game spans both.
NDK code crashes below the Java layer, invisible to the JVM. Install native crash handling, keep symbols, and cover both layers.