A code comprehension tool for surveying unfamiliar Elixir code.
Drop CodeStory.tell() into a function to see a nested call tree of
user-defined function calls with named arguments, values, and return values.
Usage
Wrap the call you're curious about — the trace prints and your result flows
through, no stop/0 needed:
invoice = CodeStory.tell(fn -> process_order(params) end)Or bracket a region by hand when a single entry call won't express it:
CodeStory.tell()
# ... your code ...
CodeStory.stop()For the tree as data instead of a printed trace, see narrate/2.
Options
:show_args- whentrue(default), shows argument names and values; whenfalse, shows values only:output-:terminal(default),:file, or:both.:fileand:bothwrite tocode_story_trace.login the project root. Because a trace records real argument and return values, that file can capture secrets (passwords, API keys, tokens) or personal data in plaintext — add it to your.gitignoreand delete it when you are done with it.:detail-:outlineshows only function names and arg names (no values or returns) for inspecting call flow and boundaries;:short_story(default) shows names, truncated values, and returns;:novelshows names with complete untruncated values and returns:auto_boundary- whentrue(default), Ecto repos are treated as boundary modules: a repo call (e.g.Repo.get!) is shown as a single node with its args and return, but the repo's own internal calls (Ecto plumbing, arity-delegation chains) are hidden. Set tofalseto trace repo internals.:fold_repeats- whentrue(default), consecutive sibling calls to the same function collapse into one node marked×N(or×N (varies)when the calls share a function but differ). Set tofalseto show every call.:depth- caps how many levels the rendered trace nests. A positive integer (depth: 1shows the entry call only;depth: 2adds its direct children; etc.); below the cap a node's interior is replaced by a… (N more levels)marker. Defaults to:infinity(no limit).
Summary
Functions
Runs fun while tracing, returning {result, tree} without printing.
result is whatever fun returned; tree is the raw call tree as data — a
list of %{module, function, args, return, children} node maps. This is the
programmatic counterpart to tell/0 + stop/0: nothing is written to the
terminal or a file, and no display transforms (folding, depth) are applied — the
tree is the honest, full structure. Pair it with to_encodable/2 to get
JSON-ready data.
{invoice, tree} = CodeStory.narrate(fn -> process_order(params) end)Notes:
- Traces the calling process and captures only the first top-level call, so
the clean pattern is one entry call:
narrate(fn -> entry(...) end). Afunwith no traced calls returns{result, []}. optsare trace-time only — currently:auto_boundary(defaulttrue, as intell/1). Passauto_boundary: falseto include an Ecto repo's internals in the raw tree.- Raises
ArgumentErrorif a trace is already active on this process (unliketell/1, which returns{:error, :already_tracing}— a tagged tuple would be ambiguous with a legitimate{:error, tree}result).
@spec stop() :: :ok
Stops tracing and outputs the call tree collected since tell/1.
The entire trace is written as one buffered block, using the :output and
:detail options given to tell/1 — so the tree never interleaves with other
IO from your code.
Always returns :ok. Warns and returns :ok if no trace is active on this
process, so a stray stop/0 is harmless.
@spec tell() :: :ok | {:error, term()}
Traces user-defined function calls and prints the call tree. Two forms.
Block form — tell(fun) / tell(fun, opts)
Wrap the entry call you want to understand. The trace prints, tracing is cleaned
up automatically (no stop/0), and fun's own result is returned — so it's a
transparent wrapper you can drop around any expression:
invoice = CodeStory.tell(fn -> process_order(params) end)This is the recommended form when surveying unfamiliar code: you know the entry
even when you don't know where the flow ends. It captures only the first
top-level call, so wrap a single entry call. It never breaks the wrapped
code — if a trace is already active, tracing fails to start, or the trace can't
be displayed, fun still runs and its result is still returned (with a warning).
Unlike narrate/2 (which raises on an active trace), the block form warns and
continues.
Manual form — tell() / tell(opts) + stop/0
Bracket a region by hand (e.g. a LiveView handler, or a span across several statements) when a single entry call won't express it:
CodeStory.tell()
result = process_order(params)
CodeStory.stop()The manual form returns :ok, or {:error, :already_tracing} (with a warning)
if a trace is already active.
Both forms accept the options in the module docs — :show_args,
:output, :detail, :auto_boundary, :fold_repeats, :depth:
CodeStory.tell(fn -> entry() end, detail: :outline)
CodeStory.tell(detail: :novel, output: :file)For the tree as data instead of a printed trace, see narrate/2.
@spec tell( (-> result), keyword() ) :: result when result: var
Converts a call tree (from narrate/2) into a JSON-ready plain-data structure.
Dependency-free: the result contains only strings / numbers / booleans / nil /
lists / maps, so JSON.encode!/1 (Elixir 1.18+) or Jason.encode!/1 works
directly. Faithful by default; opt into compaction with :fold_repeats,
:depth, and :detail. See CodeStory.Encoder for the schema and options.
{_result, tree} = CodeStory.narrate(fn -> entry() end)
data = CodeStory.to_encodable(tree, fold_repeats: true, depth: 4)