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:

Requirements

Installation

Add code_story to your dependencies in mix.exs:

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:

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:

defp deps do
[{:code_story, path: "../code_story", only: :dev}]
end

Then fetch the dependency:

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:

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):

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:

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:

CodeStory.tell(detail: :outline)
CodeStory.tell(detail: :novel, output: :file)
CodeStory.tell(show_args: false, output: :both)

How It Differs from dbg/2

dbg/2CodeStory
ScopeSingle expression or pipelineSpan of execution between tell/stop
What it showsEvery intermediate value in a pipelineOnly user-defined function calls (filters out stdlib/deps)
IdentityShows code expressionsShows function names with named arguments
PurposeDebug a specific valueHear the story — understand call flow
OutputPer-expression, inlineBuffered, 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:

Limitations (v1)

License

Copyright 2026 Angeleah Daidone

Licensed under the Apache License, Version 2.0. You may not use this project except in compliance with the License.