Skip to content

[BUG] Native crash stacktraces are never delivered — context key mismatch between Android SDK and Dart #219

@robert-northmind

Description

@robert-northmind

Description

On Android, native crash events (type=crash) always arrive in Grafana with
context_stacktrace="No stacktrace", even when ApplicationExitInfo has
captured a trace. The root cause is a JSON key mismatch between the Android
plugin and the Dart side of the SDK, plus a missing forward of the field.

Reproduced on faro: 0.14.0 (latest, published 2026-04-14) against a
Flutter demo app running on an Android 11 emulator (API 30). The app
triggered a Kotlin NullPointerException (via value!!.length) on the
main thread, which is a REASON_CRASH from ApplicationExitInfo.

Example event in Loki (trimmed):

timestamp=2026-04-17T19:53:04.255Z kind=exception level=error type=crash
  value="CRASH , status: 0"
  context_description=crash
  context_importance=100
  context_processName=com.example.flutter_mobile_o11y_demo
  context_stacktrace="No stacktrace"
  context_timestamp_readable_utc=2026-04-17T19:52:59.794Z
  sdk_name=faro-mobile-flutter sdk_version=0.14.0

Note the literal string "No stacktrace" — it is a hard-coded Dart
fallback, not a missing field.

Root cause

There are two compounding issues in the SDK. Either alone would be
enough to produce the symptom.

1. Key mismatch between Android and Dart

The Android plugin writes the trace under the key "trace":

android/src/main/java/com/grafana/faro/ExitInfoHelper.java (around line 159):

int reasonCode = exitInfo.getReason();
if ((reasonCode == ApplicationExitInfo.REASON_CRASH ||
     reasonCode == ApplicationExitInfo.REASON_CRASH_NATIVE ||
     reasonCode == ApplicationExitInfo.REASON_ANR) &&
    exitInfo.getTraceInputStream() != null) {

    String trace = readTraceInputStream(exitInfo);
    if (trace != null && !trace.isEmpty()) {
        jsonObject.put("trace", trace);   // <-- key is "trace"
    }
}

The Dart side reads from "stacktrace" — a key that never exists:

lib/src/faro.dart (around line 763):

final stacktrace =
    stringifiedContext['stacktrace'] ?? 'No stacktrace';   // <-- wrong key

Result: the fallback literal string 'No stacktrace' is always used.

2. The "trace" field is not forwarded in the error context

Even if the Dart code read the right key, the context passed to
pushError is a hard-coded allow-list of six fields and does not
include any trace:

lib/src/faro.dart (around line 773):

_instance.pushError(
  type: 'crash',
  value: '$reason , status: $status',
  context: {
    'description': description,
    'stacktrace': stacktrace,      // <-- must also be populated here
    'timestamp': timestamp,
    'timestamp_readable_utc': humanReadableTimestamp,
    'importance': importance,
    'processName': processName,
  },
);

So even after fixing the key, the trace would still be discarded unless
this map is updated.

Expected behavior

When ApplicationExitInfo.getTraceInputStream() returns a non-null,
non-empty trace for REASON_CRASH, REASON_CRASH_NATIVE, or REASON_ANR,
that trace should appear on the resulting Faro event in a usable form
(either as a parsed set of StackFrames — per #212 — or at minimum as
a context_stacktrace text field).

Scope

  • Affected: All Android native crashes reported through
    enableCrashReporter() (JVM crashes, native NDK crashes via
    REASON_CRASH_NATIVE). This is the entire native-crash path on Android.
  • Not affected: iOS crashes (separate codepath via
    CrashReportingIntegration.swift / PLCrashReporter) and Dart unhandled
    exceptions (via FlutterError.onError / zone handler).
  • Partially related: ANR traces ([ENHANCEMENT] ANR stack traces are not parsed into structured Faro frames #212) — those do arrive, but as raw
    text rather than structured frames. That's a separate codepath
    (native_integration.dart_getAnrStatus()), not the
    getCrashReport() path discussed here.

Suggested fix

Minimal change in lib/src/faro.dart:

final stacktrace =
    stringifiedContext['trace'] ?? 'No stacktrace';   // read "trace"

and make sure it ends up in the forwarded context (the map already has a
'stacktrace' key, so the fix is purely the read key).

A follow-up, aligned with #212, would be to parse the Java-format trace
into structured StackFrames so it renders like a Dart exception in
Grafana Frontend Observability.

Caveat about Android itself

Independent of these SDK bugs: ApplicationExitInfo.getTraceInputStream()
is only documented to reliably return a trace for REASON_ANR and
REASON_CRASH_NATIVE. For REASON_CRASH (JVM exceptions) it is often
null on many OEMs / Android versions. So even with the SDK fixed,
JVM-crash traces may still be missing on some devices. This is an
Android limitation, not an SDK bug — but it's worth documenting.

Environment

  • faro: 0.14.0
  • Flutter: Dart 3.11.0 (android_arm64)
  • Device: Android 11 emulator (SDK 30, sdk_gphone_arm64)
  • Repro app: grafana/mobile-o11y-demo Flutter app, "Crash (simulated NPE)" button in Debug tab

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions