Scan for erl_crash.dump files the BEAM leaves behind, and turn each into
a defect capsule.
Why this exists
Every BEAM node that dies hard writes a crash dump — a text file at
$ERL_CRASH_DUMP if the env var is set, otherwise erl_crash.dump in the
runtime's cwd. Mob apps land it in the app data directory; host tooling
leaves them in the repo root. Nobody was collecting them until now: at
the time this shipped, five sibling repos on the author's machine each
had an unlooked-at dump in the root.
A crash the framework can prove happened is a defect it can report, and
the format from decisions/2026-09-04-defect-reports-are-a-shipped-feature.md
has a :beam_crash kind waiting for exactly this input.
Discipline
- Non-destructive. The dump stays on disk. A future triager or the author might want the full file; deleting after emit would lose everything the capsule cannot fit.
- Idempotent. A sha256 of the file becomes the artifact id, and the
Mob.PostMortem.Registrytracks emitted ids in a public ETS table. A re-sweep against the same file finds it seen and does nothing. - Bounded read. A dump can be gigabytes on a busy scheduler. This module reads only the header window (default 8192) — that is where slogan, system version, taints and atoms count live. Anything beyond is by construction not on the emit path.
- Fingerprint stripped of concrete data. The slogan for a
{badarg, ...}fromio:put_chars/2embeds the raw binary that was passed. Fingerprinting that verbatim opens one triage row per unique input; stripping binaries and long numeric literals groups the whole class into one row. Seenormalize_slogan/1.
Not in scope here
- The dump is not parsed for process state, ETS contents, or stack
frames. A file the caller can open in
crashdump_vieweris a better home for those. What ends up in the capsule is what a triager sees at a glance: which build, which OTP, what class of crash. - iOS MetricKit and Android
ApplicationExitInfoare separate substrates with their own modules (Mob.PostMortem.IOS/Mob.PostMortem.Android) and their own follow-up tickets.
Summary
Types
What a scanner returns for one dump file: enough to emit and to describe what was scanned, without the raw bytes.
Functions
Default paths a sweep looks at when the caller does not pass its own.
Emit a capsule for every finding not already recorded in the registry.
A version of the slogan with concrete data stripped, for the fingerprint.
Scan paths for erl_crash.dump files.
Types
@type finding() :: %{ path: String.t(), sha256: String.t(), size: non_neg_integer(), modified_at: DateTime.t() | nil, slogan: String.t() | nil, system_version: String.t() | nil, taints: [String.t()], atoms: non_neg_integer() | nil, dump_version: String.t() | nil }
What a scanner returns for one dump file: enough to emit and to describe what was scanned, without the raw bytes.
Functions
@spec default_paths() :: [Path.t()]
Default paths a sweep looks at when the caller does not pass its own.
- The value of the
ERL_CRASH_DUMPenv var, if set (that is where the BEAM would have written this VM's own dump). - The current working directory, where the BEAM writes by default.
A caller wanting a broader sweep supplies its own list.
@spec emit([finding()]) :: [Mob.Defect.Capsule.t()]
Emit a capsule for every finding not already recorded in the registry.
Returns the capsules emitted, in the same order.
A version of the slogan with concrete data stripped, for the fingerprint.
A raw slogan from a {badarg, ...} on io:put_chars/2 looks like
Runtime terminating during boot ({badarg,[{io,put_chars,[standard_error,[<<42,42,...4KB more...>>]]}]})and the binary embedded there varies per crash. Two crashes for the same root cause thus fingerprint differently, which is exactly the shattering the bus is meant to prevent (see the "fingerprint and evidence are separate" decision record).
Strip:
- Binary literals
<<...>>— replaced by<<>> - Long numeric sequences (>= 8 digits with commas or dots between them)
that look like the printable form of the same — replaced by
<<>> - Runs of whitespace collapsed
What survives: atom-shaped tokens, module and function names, arities, the structural punctuation. That is what identifies the class of crash, which is what a fingerprint is for.
Scan paths for erl_crash.dump files.
paths is a list of file or directory paths. A directory is checked
only for a top-level erl_crash.dump; this module does not recurse.
Non-existent paths are silently skipped — a sweep from a caller that
overshoots the possibilities should not have to guard each one.
Returns the list of finding/0 values.
Emission is a separate step: emit/1 takes a list of findings and
puts anything the registry has not seen onto the defect bus. That
split is deliberate — a caller can list findings without publishing
them, and tests can assert on shape without touching the bus.