MetricKit-backed post-mortem ingest for iOS.
The framework attaches an MXMetricManagerSubscriber in the NIF on
the first sweep/0 call, receives MetricKit payloads on a background
queue thereafter, and drains the bounded in-memory queue into
Mob.Defect.Capsules on Mob.Defect.Bus each time sweep/0 is
called.
Scope of a payload
MetricKit hands the OS-delivered payload to the delegate once per incident class per day (approximately — the delivery cadence is under Apple's control). Each delivery may contain one or more diagnostics of different kinds:
| MetricKit diagnostic | kind |
|---|---|
MXCrashDiagnostic | :native_crash |
MXHangDiagnostic | :anr (iOS's word is "hang"; the defect taxonomy uses ANR) |
MXCPUExceptionDiagnostic | :perf_regression |
MXDiskWriteExceptionDiagnostic | :perf_regression |
The Payload suffix belongs on the container MXDiagnosticPayload
the OS hands to didReceiveDiagnosticPayloads:, which then exposes
the individual diagnostics above.
What the delivery contract looks like from Elixir
sweep/0 is safe to call at any point; MetricKit's own delivery is
asynchronous and the queue accumulates until a caller drains it. A
typical shape:
# In your app's on_start
def on_start do
# ...
Mob.PostMortem.sweep()
endThe top-level Mob.PostMortem.sweep/0 calls into this module. Nothing
auto-runs — that is the framework-wide discipline: mob owns the format
and the bus but never becomes the collector.
Until observed, not once
The native queue is cleared as it is drained, and MetricKit does not
deliver a payload twice, so the capsule on this boot's bus is the
only copy; a boot that dies before anyone looks would take the
diagnostic with it. So each drained payload is written to
Mob.PostMortem.Journal before it is emitted, and every sweep, in
this boot or a later one, emits it again until its capsule was
handed to a subscriber when emitted or returned by
Mob.Defect.Bus.recent/1. Within one boot a re-sweep emits nothing
it already emitted.
Redaction
MetricKit's MXCallStackTree carries binary UUIDs, image names and
mangled symbol offsets: safe identifiers, no user data. The
capsule's fingerprint key uses just the top frame's binary name and
offset — enough to group the same crash across launches without
embedding anything that varies per crash. The full payload JSON goes
onto evidence, bounded by the capsule's existing string-truncation
rule.
Platform gating
The NIF is registered on both iOS and Android (Android returns an
empty list unconditionally — see android/jni/mob_nif.zig). This
module additionally gates on :mob_nif.platform() == :ios so a
caller on Android does no work at all, and a caller in a host test
environment where the NIF is not loaded fails gracefully.
Summary
Functions
@spec sweep() :: [Mob.Defect.Capsule.t()]
Sweep the MetricKit queue and emit a capsule for each new payload, and for every earlier one not yet observed.
Returns the list of capsules emitted: unobserved journaled payloads first, then new ones in the order the OS delivered them. Empty on Android, on iOS < 14 (no MetricKit delivery API), and when nothing is new or waiting to be observed.