Packages

A code comprehension tool that traces user-defined function calls as a nested call tree with named arguments and return values.

Current section

Files

Jump to
code_story README.md
Raw

README.md

# CodeStory
Every codebase has a story. CodeStory lets you read it. Drop `CodeStory.tell()` into a function and see the narrative unfold: which functions are called, with what arguments (by name and value), and what they return — rendered as a nested call tree.
**Use cases:**
- Joining a new codebase and understanding how it actually works
- Tracing call flow before refactoring
- Spotting redundant or unexpected function calls
- Exposing your code's vocabulary and catching ubiquitous language mismatches
- Debugging by seeing exactly where data goes wrong
## Requirements
- **Elixir 1.15+**
- **OTP 27+** — CodeStory uses the session-based `:trace` module introduced in
OTP 27. On older OTP releases, starting a trace will fail.
## Installation
Add `code_story` to your dependencies in `mix.exs`:
```elixir
defp deps do
[{:code_story, "~> 0.1.0", only: :dev}]
end
```
Install it as `only: :dev` so that any `CodeStory.tell()` calls you forget to
remove fail to compile in production rather than shipping.
To track the development version instead, point at the repository:
```elixir
defp deps do
[{:code_story, github: "angeleah/code_story", only: :dev}]
end
```
Or if you've cloned it locally, point to the path on disk:
```elixir
defp deps do
[{:code_story, path: "../code_story", only: :dev}]
end
```
Then fetch the dependency:
```bash
mix deps.get
```
No `require`, no `use`, no macros. Just `CodeStory.tell()` and `CodeStory.stop()`.
## Usage
The quickest way is to **wrap the call you want to understand**. The trace prints,
tracing cleans up on its own (no `stop()`), and your result flows through unchanged:
```elixir
invoice = CodeStory.tell(fn -> process_order(params) end)
```
This is ideal for unfamiliar code: you know the entry point even when you don't
know where the flow ends. Wrapping is always safe — if tracing can't run for any
reason, your function still runs and returns normally.
Prefer to bracket a region by hand — a LiveView handler, or a span across several
statements? Use the manual pair: `CodeStory.tell()` before, `CodeStory.stop()`
after (`stop()` is what prints):
```elixir
def handle_request(params) do
CodeStory.tell()
result = process_order(params)
CodeStory.stop()
result
end
```
Need the tree as data instead of a printed trace? See `CodeStory.narrate/2`.
This outputs a nested call tree to the terminal:
```
--- CodeStory Trace ---
process_order
params: %{items: [...], customer_id: 7}
validate_item ×3 (varies)
item: %{sku: "A1", qty: 2}
=> :ok
calculate_total
items: [...]
=> 29.97
Repo.insert!
changeset: #Ecto.Changeset<...>
=> %Invoice{id: 42}
=> process_order returned %Invoice{id: 42}
--- End Trace ---
```
Each function name appears on its own line, with arguments and return values indented below it. Functions with children are visually separated by blank lines.
A few things happen by default to keep the story readable:
- **Repeated calls fold.** The three `validate_item` calls collapse into one node marked `×3` — or `×3 (varies)` when the calls share a function but differ in their arguments (a single representative call is shown). Turn this off with `fold_repeats: false`.
- **Infrastructure stays at the boundary.** A call into your Ecto repo (`Repo.insert!`) appears as a single node with its arguments and return; the repo's internal Ecto plumbing is hidden. Turn this off with `auto_boundary: false`.
Only your project's own functions appear in the trace. Standard library calls, dependency code, framework-generated functions (like `__struct__/0`, `__changeset__/0`), and CodeStory itself are filtered out automatically.
## Options
All options are passed to `CodeStory.tell/1`:
```elixir
CodeStory.tell(detail: :outline)
CodeStory.tell(detail: :novel, output: :file)
CodeStory.tell(show_args: false, output: :both)
```
- **`detail`** — how much of the story to tell. Default: `:short_story`.
- `:outline` — function names and argument names only. No values, no returns. Great for seeing the shape of a call flow, spotting boundary crossings, and finding redundant calls.
- `:short_story` — names, truncated values, and returns. The default — enough detail to follow the plot without getting lost in the data.
- `:novel` — names with complete, untruncated values and returns. Every detail, nothing elided. Use when you need to see the full picture.
- **`show_args`** — show argument names alongside values. Default: `true`.
Set to `false` to show values only.
- **`output`** — where to write the trace. Default: `:terminal`.
`:file` writes to `code_story_trace.log` in your project root (ANSI codes stripped).
`:both` writes to terminal and file.
> **Add `code_story_trace.log` to your `.gitignore` before using `:file`.**
> A trace records real argument and return values, so tracing code that
> handles passwords, API keys, tokens, or personal data writes those values
> to the log in plaintext. Treat the file as sensitive and delete it when
> you're done reading it.
- **`auto_boundary`** — treat Ecto repos as _boundary modules_: a repo call
(e.g. `Repo.get!`) shows as a single node with its arguments and return, but the
repo's internal Ecto plumbing is hidden. Default: `true`. Set to `false` to
trace repo internals.
- **`fold_repeats`** — collapse consecutive sibling calls to the same function
into one node marked `×N` (or `×N (varies)` when the calls share a function but
differ). Default: `true`. Set to `false` to show every call.
- **`depth`** — cap how many levels the trace nests. A positive integer
(`depth: 1` shows the entry call only; `depth: 2` adds its direct children, and
so on); below the cap a node's interior is replaced by a `… (N more levels)`
marker. Default: `:infinity` (no limit).
## How It Differs from `dbg/2`
| | `dbg/2` | CodeStory |
| ----------------- | -------------------------------------- | ---------------------------------------------------------- |
| **Scope** | Single expression or pipeline | Span of execution between tell/stop |
| **What it shows** | Every intermediate value in a pipeline | Only user-defined function calls (filters out stdlib/deps) |
| **Identity** | Shows code expressions | Shows function names with named arguments |
| **Purpose** | Debug a specific value | Hear the story — understand call flow |
| **Output** | Per-expression, inline | Buffered, dumped as one cohesive block |
## How It Works
1. **Module detection** — reads your `mix.exs` app name and finds all your project's modules
2. **Argument name extraction** — reads Elixir debug info from BEAM files to recover original parameter names, scanning across all function clauses to find the best names
3. **Erlang tracing** — sets up trace sessions on the calling process for your modules
4. **Tree building** — a collector process receives trace events and builds a nested call tree
5. **Formatted output** — the tree is rendered with indentation and ANSI colors, then dumped as one block
## Color Scheme
Terminal output uses ANSI colors for readability:
- **Header/footer** (`--- CodeStory Trace ---`): cyan
- **Function names**: blue
- **Argument names**: yellow
- **Argument values**: default terminal color
- **Return values**: green
## Limitations (v1)
- **Single process only** — traces the calling process. Calls in spawned Tasks, GenServers, etc. are not captured.
- **Modules detected at tell time** — hot-reloaded modules mid-trace won't be traced.
- **Dev only** — installed with `only: :dev`, so leftover `CodeStory.tell()` calls fail to compile in prod.
- **One trace per process** — calling `tell()` while a trace is already active warns and returns an error.
## License
Copyright 2026 Angeleah Daidone
Licensed under the [Apache License, Version 2.0](LICENSE). You may not use this
project except in compliance with the License.