CodeStory (CodeStory v0.1.0)

Copy Markdown View Source

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 - when true (default), shows argument names and values; when false, shows values only
  • :output - :terminal (default), :file, or :both. :file and :both write to code_story_trace.log in 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 .gitignore and delete it when you are done with it.
  • :detail - :outline shows 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; :novel shows names with complete untruncated values and returns
  • :auto_boundary - when true (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 to false to trace repo internals.
  • :fold_repeats - when true (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 to false to show every call.
  • :depth - caps how many levels the rendered trace nests. A positive integer (depth: 1 shows the entry call only; depth: 2 adds 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.

Stops tracing and outputs the call tree collected since tell/1.

Traces user-defined function calls and prints the call tree. Two forms.

Converts a call tree (from narrate/2) into a JSON-ready plain-data structure.

Functions

narrate(fun, opts \\ [])

@spec narrate(
  (-> result),
  keyword()
) :: {result, [map()]}
when result: var

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). A fun with no traced calls returns {result, []}.
  • opts are trace-time only — currently :auto_boundary (default true, as in tell/1). Pass auto_boundary: false to include an Ecto repo's internals in the raw tree.
  • Raises ArgumentError if a trace is already active on this process (unlike tell/1, which returns {:error, :already_tracing} — a tagged tuple would be ambiguous with a legitimate {:error, tree} result).

stop()

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

tell()

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

tell(fun)

@spec tell(keyword()) :: :ok | {:error, term()}
@spec tell((-> result)) :: result when result: var

tell(fun, opts)

@spec tell(
  (-> result),
  keyword()
) :: result
when result: var

to_encodable(tree, opts \\ [])

@spec to_encodable(
  [map()],
  keyword()
) :: [map()]

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)