Skip to content

6. Crashes and exits

A process cannot reliably send its own crash. The report is written to disk when the crash happens and sent by the next launch. Some terminations leave no report at all, so the profile uses three sources: the crash handler, the platform’s own exit records, and inference on the next launch.

  • MUST report a crash as a log record with event name app.crash, sent on the next launch.
  • MUST carry the crashed instance’s app.build_id, service.version, os.name, os.version and session.id as record attributes (section 2.5).
  • MUST set app.crash.id, the same on every attempt to report that crash (section 4.3).
  • MUST set the record’s timeUnixNano to the time of the crash, not the time of the report.
  • SHOULD set exception.type (the exception class, or the signal and Mach exception, for example EXC_BAD_ACCESS (SIGSEGV)) and exception.message.
  • MUST put the crashing thread’s raw stack in exception.stacktrace. Symbolication happens on the backend.
  • For a native crash, the stack has one frame per line, with the binary image name and the instruction address, and the SDK MUST include the binary images the stack refers to (name, UUID or build id, load address, architecture) in the attribute apsio.crash.images, a JSON string, so the backend can symbolicate. Each image SHOULD also say whether it belongs to the app, in in_app (section 6.5).
  • For an uncaught exception in a managed runtime (the JVM or ART), the stack is the runtime’s own stack text, with its causes, as the runtime prints it. There are no binary images, so apsio.crash.images MUST be []. The backend deobfuscates the stack with the R8 mapping that app.build_id names (section 2.1).
  • MAY attach the full native report (all threads, registers) as a separate upload referenced by apsio.attachment.id.
  • SHOULD record the termination category when the platform gives one, in apsio.crash.termination_category (for example badAccess, abnormal, watchdog, taskTimeout from MetricKit on iOS 27).
  • A handled, non-fatal error is a log record with exception.type, exception.message and, when available, exception.stacktrace, at severity ERROR.
  • MAY carry apsio.error.fingerprint, a string set by the app that replaces the backend’s default grouping for that error, scoped per app. It does not apply to crashes.
  • An Android ANR is reported as the device.anr event of opentelemetry-android, or from ApplicationExitInfo on the next launch (6.4).
  • An iOS hang (the main thread blocked past the SDK’s threshold, or a MetricKit hang diagnostic) is a log record with event name apsio.hang, the main thread’s stack in exception.stacktrace and its duration in apsio.hang.duration (milliseconds).
  • An apsio.hang record MUST carry apsio.hang.source:
    • live: the SDK saw the hang while the app ran, in the session it belongs to. The record carries that session’s session.id, and its time is the moment the hang began.
    • metrickit: read from a MetricKit hang diagnostic, which arrives up to a day later and can describe a hang the SDK also saw live.
  • Android’s hangs are seen while the app runs, so they are live: an Android SDK that adds apsio.hang.source to device.anr or apsio.hang sets live.
  • A live detector on iOS SHOULD watch the main thread from another thread, only while the app is in the foreground and not under a debugger, with a threshold of 250 ms. It records one stack per hang, taken while the main thread is blocked, and the hang’s duration once the main thread runs again.
  • Unfinished hangs. The process can be killed during a hang (the system’s watchdog, a force quit), so the hang never ends. A live detector SHOULD write the hang to disk once its stack is taken, with the binary images it refers to, and update its duration while it lasts. The next launch sends it as apsio.hang with apsio.hang.source = live, apsio.hang.unfinished = true, apsio.hang.duration the time seen until the last update (a lower bound), the record’s time when the hang began, and the session and release of the process that hung as record attributes (2.5). It counts in hang rates like any live hang; a backend MUST NOT read its duration as exact.
  • A backend MUST compute hang rates and join hangs to their session’s screens from live records only, so that a hang seen both ways counts once. metrickit records feed duration distributions and stacks. A record without apsio.hang.source, from an SDK older than this rule, is metrickit when its os.name is iOS or iPadOS and live otherwise.

iOS kills apps without a crash report for memory (jetsam), watchdog timeouts, background task expiry and thermal pressure. Android has low-memory kills and ANR exits.

  • SHOULD report such a termination as a log record with event name app.abnormal_exit, sent on the next launch, with the attributes of 6.1’s second bullet and:
    • apsio.exit.reason: memory, watchdog, background_task_timeout, thermal, anr, signal or unknown, and no other value. A native crash the platform records without a crash report (Android’s REASON_CRASH_NATIVE with no tombstone) is signal; excessive resource use (Android’s REASON_EXCESSIVE_RESOURCE_USAGE) and any other platform reason not in the list are unknown;
    • apsio.exit.source: platform when read from the platform, inferred otherwise;
    • apsio.exit.foreground: whether the app was in the foreground.
  • SHOULD read the platform first:
    • on iOS 27 and later, MetricKit’s DiagnosticReport (a memoryException is a first-party out-of-memory record) and ForegroundTerminationMetric / BackgroundTerminationMetric;
    • on iOS 15 to 26, MXCrashDiagnostic, MXHangDiagnostic and MXAppExitMetric;
    • on Android 11 and later, ApplicationExitInfo (with ANR traces; native tombstones from Android 12).
  • MAY infer when the platform says nothing: the previous session ended in the foreground with no crash, no normal termination, no app or OS update and no debugger attached. The reason is then unknown unless memory pressure was observed.
  • A platform record takes precedence over an inference for the same session.
  • MetricKit diagnostics join their session through the process id the SDK recorded with the session.

Example of a crash sent by the next launch (OTLP/JSON, trimmed):

{
"eventName": "app.crash",
"timeUnixNano": "1759500000431000000",
"severityNumber": 21,
"attributes": [
{ "key": "exception.type", "value": { "stringValue": "EXC_BAD_ACCESS (SIGSEGV)" } },
{ "key": "app.crash.id", "value": { "stringValue": "4c1e8a52-0d2f-4f0b-9a8e-7f6d1c2b3a49" } },
{ "key": "app.build_id", "value": { "stringValue": "8124C1A2-6E0B-3F51-9D57-1B2E7C4A9B07" } },
{ "key": "service.version", "value": { "stringValue": "4.12.0" } },
{ "key": "session.id", "value": { "stringValue": "7f3c9b02-5d1e-4a8b-9c0f-2e6a1d4be19a" } },
{ "key": "log.record.uid", "value": { "stringValue": "01J9ZQ3W1V8M6K2T4R7N5C0B3D" } }
]
}

An image is in-app when it ships inside the app: the app’s own code, and the frameworks and libraries bundled with it, whoever wrote them. A backend groups crashes by their in-app frames (section 6.1) and asks for symbols for in-app images, so the difference matters before any symbols are uploaded.

  • Each entry of apsio.crash.images SHOULD carry in_app, a boolean, decided on the device from the path the image was loaded from. Paths are compared by whole path components (Shop.app2/Shop is not inside Shop.app).
  • On Apple platforms, in_app is true for an image inside the app’s bundle: the main executable, Xcode’s <App>.debug.dylib and __preview.dylib in debug builds, and the frameworks and libraries under the bundle. The bundle is the main bundle’s path up to and including its first component that ends in .app, so an app extension (Shop.app/PlugIns/Widget.appex, Shop.app/Extensions/Share.appex) uses its containing app, which it loads frameworks from, and macOS and Mac Catalyst bundles (Shop.app/Contents/Frameworks) work the same way. The executable’s path gives the same root (Shop.app/Contents/MacOS/Shop). Without a .app component (a test bundle run by xctest, a command-line tool), the root is the bundle’s path itself, or the executable’s directory when only the executable’s path is known.
  • The bundle is that of the process the images were loaded in (for a crash, recorded with the crash), not the reporting launch’s: an update or a reinstall moves the bundle to a new container. Paths are compared as text, not resolved (the crashed install may be gone, and resolving does I/O), after removing a leading /private followed by /var/ or /tmp/ on both sides, since the loader and the bundle APIs can report the same directory with or without it.
  • On Android the decision uses the app’s package name, not the directories of the launch that reports the crash: an update moves the install directory, and the crashed process’s paths cannot be resolved afterwards. A trailing (deleted) (a file replaced while the process ran) is removed first. in_app is true for:
    • a shared library (.so) or an APK (.apk, a library loaded straight from it when native libraries are not extracted) in the package’s install directory: under /data/app/ or /mnt/expand/<volume>/app/, optionally inside one ~~<random>== directory, a directory whose name starts with <package>- (/data/app/~~Qm9x==/com.example.shop-Zk4p==/lib/arm64/libshop.so);
    • a shared library under one of the package’s data directories: /data/data/<package>, /data/user/<user>/<package>, /data/user_de/<user>/<package>, and the same two under /mnt/expand/<volume>/ (libraries extracted at run time).
  • Every other image is false: on Android the runtime’s compiled code (.odex, .vdex, .oat, .art), anonymous and memory-file mappings ([anon:…], /memfd:…), the system’s partitions (/system, /system_ext, /vendor, /product, /apex) and other packages, such as WebView’s.
  • On Apple platforms the path is used on the device only, and the SDK MUST NOT add it to the image: the bundle’s location holds a per-install identifier.
  • A backend MUST use in_app as sent when it is present, whether or not symbols were uploaded for the image. When it is absent, from an SDK older than this rule, the backend decides its own way; this profile does not define how.
  • The vector crashes/image-in-app checks the decision (appendix B.2).