# `Mob.PostMortem.IOS`
[🔗](https://github.com/genericjam/mob/blob/master/lib/mob/post_mortem/ios.ex#L1)

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.Capsule`s 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()
    end

The 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.

# `sweep`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
