Essay

CodeStory: The Short Story

It was so exciting to get to share CodeStory at ElixirConf. I told the full story there. This is the short one.

Raise your hand if you’ve ever littered your files with IO.inspect.

IO.inspect(order, label: "HERE")
IO.inspect(user, label: "HERE 2")
IO.inspect(total, label: "WHY")
IO.inspect(x, label: "WHAT IS THIS")
IO.inspect(id, label: "😭")

You can read code all day and the source will show you the shape. What it won’t show you is what happens when it runs… how it flows, what it wastes, how many times, where it breaks. Inspects only show you what you can think to look at. Full traces are like drinking from the firehose.

About 6 years ago I decided I wanted a way to strip the noise and focus on an app’s own code. So I started on the project, then I stopped, then I started again. After years of this, I was debugging one day and decided it was time to finish bringing it to life.

Meet CodeStory

CodeStory is a small library that lets your code tell its story. You add one line to your dev deps…

{:code_story, "~> 0.2.0", only: :dev}

Then you wrap the call you’re curious about…

CodeStory.tell(fn -> Confab.Quote.estimate(ticket, 2) end)

No macros, no use, no require. You just call a function.

Names and values for every argument, indentation for depth, and a fat arrow for what came back. It only shows your own code. Erlang, the standard library, and your dependencies stay out of it, and a Repo call shows up as one clean node with the Ecto plumbing tucked away. If something runs 300 times, you get one line with a ×300 on it instead of 300 lines.

Your result comes back like normal and you get this…

--- CodeStory Trace ---
Confab.Quote.estimate(ticket: %Ticket{name: "GA", price: 50, tier: :ga, ...}, qty: 2)
  Confab.Quote.subtotal(price: 50, qty: 2) => 100
  Confab.Quote.apply_discount(amount: 100, rate: 0.2) => 80
=> Confab.Quote.estimate returned 80
--- End Trace ---

(Confab is a fictional conference app. Nobody was overcharged in the making of this post.)

How avid of a reader are you?

You can tell any story with more or less detail, and so can CodeStory. Set detail: :outline and you get just the shape, function names and argument names with no values. :short_story is what you see above. :novel is every field with nothing truncated, for when you want to read everything before you touch anything. You don’t have to pick just one. You can read each layer until you understand it.

Where you might reach for it

Maybe it’s your first day and you want to know what actually happens when someone clicks Register. Maybe the order total is $15 short and nothing is blowing up. Maybe you’ve been asked to refactor a 500+ line function. Maybe the code works and it’s just slow. Maybe you suspect one person has five different names as they move through your app (I have opinions on that one).

You and your team aren’t the only ones reading it anymore either. Pass output: :file and the trace lands in a plain text log that any file-reading agent can run and read straight back. So instead of grepping its way through your modules, your agent can look at what actually ran. I packaged that loop into a skill if you want to give it a try. Point it at a call, and it runs the trace, reads it, and tells you what it found.

There is still work to be done. For now CodeStory only traces a single process, so anything in a spawned Task or GenServer won’t show up, and it needs OTP 27 or later. I’m still poking at it, so this stuff can change.

It’s open source and on Hex, the code is on GitHub, and the docs have the rest of the options.

That’s the short story. The novel is already sitting in your codebase. Let your code tell its story.