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.
6.1 Crashes
Section titled “6.1 Crashes”- 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.versionandsession.idas 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
timeUnixNanoto 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 exampleEXC_BAD_ACCESS (SIGSEGV)) andexception.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, inin_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.imagesMUST be[]. The backend deobfuscates the stack with the R8 mapping thatapp.build_idnames (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 examplebadAccess,abnormal,watchdog,taskTimeoutfrom MetricKit on iOS 27).
6.2 Handled errors
Section titled “6.2 Handled errors”- A handled, non-fatal error is a log record with
exception.type,exception.messageand, when available,exception.stacktrace, at severityERROR. - 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.
6.3 Hangs and ANRs
Section titled “6.3 Hangs and ANRs”- An Android ANR is reported as the
device.anrevent of opentelemetry-android, or fromApplicationExitInfoon 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 inexception.stacktraceand its duration inapsio.hang.duration(milliseconds). - An
apsio.hangrecord MUST carryapsio.hang.source:live: the SDK saw the hang while the app ran, in the session it belongs to. The record carries that session’ssession.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 addsapsio.hang.sourcetodevice.anrorapsio.hangsetslive. - 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.hangwithapsio.hang.source = live,apsio.hang.unfinished = true,apsio.hang.durationthe 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 anylivehang; a backend MUST NOT read its duration as exact. - A backend MUST compute hang rates and join hangs to their session’s screens from
liverecords only, so that a hang seen both ways counts once.metrickitrecords feed duration distributions and stacks. A record withoutapsio.hang.source, from an SDK older than this rule, ismetrickitwhen itsos.nameis iOS or iPadOS andliveotherwise.
6.4 Terminations without a crash report
Section titled “6.4 Terminations without a crash report”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,signalorunknown, and no other value. A native crash the platform records without a crash report (Android’sREASON_CRASH_NATIVEwith no tombstone) issignal; excessive resource use (Android’sREASON_EXCESSIVE_RESOURCE_USAGE) and any other platform reason not in the list areunknown;apsio.exit.source:platformwhen read from the platform,inferredotherwise;apsio.exit.foreground: whether the app was in the foreground.
- SHOULD read the platform first:
- on iOS 27 and later, MetricKit’s
DiagnosticReport(amemoryExceptionis a first-party out-of-memory record) andForegroundTerminationMetric/BackgroundTerminationMetric; - on iOS 15 to 26,
MXCrashDiagnostic,MXHangDiagnosticandMXAppExitMetric; - on Android 11 and later,
ApplicationExitInfo(with ANR traces; native tombstones from Android 12).
- on iOS 27 and later, MetricKit’s
- 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
unknownunless 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" } } ]}6.5 In-app images
Section titled “6.5 In-app images”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.imagesSHOULD carryin_app, a boolean, decided on the device from the path the image was loaded from. Paths are compared by whole path components (Shop.app2/Shopis not insideShop.app). - On Apple platforms,
in_appistruefor an image inside the app’s bundle: the main executable, Xcode’s<App>.debug.dyliband__preview.dylibin 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.appcomponent (a test bundle run byxctest, 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
/privatefollowed 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_appistruefor:- 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).
- a shared library (
- 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_appas 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-appchecks the decision (appendix B.2).