trace
Use when debugging how a value or request reaches Elixir code, finding who calls a function, or planning a signature change. Builds the call tree with mix xref callers instead of reading files one by one.
Install
npx skills add https://github.com/oliver-kriska/claude-elixir-phoenix/tree/main/plugins/elixir-phoenix/skills/trace
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install oliver-kriska-claude-elixir-phoenix@llmmart
git clone https://github.com/oliver-kriska/claude-elixir-phoenix.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole oliver-kriska/claude-elixir-phoenix collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Call Tracing
Build call trees showing how functions are reached from entry points.
Iron Laws - Never Violate These
- Always use
mix xref callersfirst - It's authoritative; grep is fallback only - Stop at entry points - Controllers, LiveView callbacks, Oban workers, GenServer callbacks
- Track visited MFAs - Prevent infinite loops from circular calls
- Extract argument patterns - Just knowing "who calls" isn't enough; HOW they call matters
- Max depth 10 - Deeper trees indicate architectural issues, not useful traces
When to Build Call Tree (Use Proactively)
| Condition | Why Call Tree Helps |
|---|---|
| Unexpected nil/value at runtime | Trace where the value originates |
| Bug can't reproduce locally | See all entry points that reach the code |
| Changing function signature | Find all callers and their argument patterns |
| Incomplete stack trace | Get full path context |
| "Where does X come from?" | Visual answer to data flow question |
Quick Trace
Run the caller query first, then inspect another function in the chain as needed:
mix xref callers MyApp.Accounts.update_user/2
mix xref callers MyApp.Accounts.get_user/1
Read the reported locations to see argument patterns.
Entry Points (Stop Here)
| Pattern | Type |
|---|---|
def mount/3, def handle_event/3 |
LiveView |
def index/2, def show/2, def create/2 |
Controller |
def perform(%Oban.Job{}) |
Oban Worker |
def handle_call/3, def handle_cast/2 |
GenServer |
Delegate to call-tracer Agent
For full recursive tree with argument extraction and parallel category tracing:
Determine the effective maximum nesting depth. Use an explicit positive-integer
CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH value first; when it is unset, inspect
claude --version (the default is 1 in 2.1.217–2.1.218 and 3 in 2.1.219+).
If the version is unavailable, conservatively use 1. At depth 2+, delegate to
the orchestrator below. At depth 1, keep orchestration in this main session:
spawn the applicable controller,
LiveView, worker, and internal tracing prompts directly, then merge their
results. Never spawn an orchestrator that cannot delegate.
Agent(subagent_type: "phx:call-tracer", prompt: "Build call tree for MyApp.Accounts.update_user/2")
The call-tracer agent uses parallel subagents for each entry point category:
- Controllers subagent (HTTP paths)
- LiveView subagent (WebSocket paths)
- Workers subagent (Background jobs)
- Internal subagent (Cross-context calls)
Each gets a fresh context for deep exploration.
Output Location
.claude/plans/{slug}/research/call-tree-{function}.md
References
For detailed patterns:
${CLAUDE_SKILL_DIR}/references/mix-xref-usage.md- Full mix xref commands and options${CLAUDE_SKILL_DIR}/references/entry-points.md- All Phoenix/OTP entry point patterns${CLAUDE_SKILL_DIR}/references/argument-extraction.md- AST parsing for argument patterns
Files (claude-elixir-phoenix)
-
references
-
argument-extraction.md 5.8 KB
# Argument Extraction Techniques for extracting argument patterns from call sites. ## Why Arguments Matter Knowing "who calls" isn't enough. **HOW** they call reveals: - Data flow through the system - Where nil values originate - Pattern mismatches (string vs atom keys) - Missing validations ## Basic Extraction ### From Call Site Line ```elixir # Call site: lib/web/controllers/user_controller.ex:45 Accounts.update_user(user, attrs) # Extract: # Arg 1: `user` - variable # Arg 2: `attrs` - variable # Need to trace where these variables come from in the same function ``` ### Trace Variable Origins ```elixir def update(conn, %{"id" => id, "user" => user_params}) do user = Accounts.get_user!(id) # <- user comes from DB query attrs = sanitize_params(user_params) # <- attrs comes from params + transform case Accounts.update_user(user, attrs) do # <- call site {:ok, user} -> redirect(conn, to: ~p"/users/#{user}") {:error, changeset} -> render(conn, :edit, changeset: changeset) end end # Full trace: # user = Accounts.get_user!(id) where id = params["id"] (string!) # attrs = sanitize_params(user_params) where user_params = params["user"] ``` ## Common Argument Patterns ### Direct from Params (Controller) ```elixir def create(conn, %{"user" => user_params}) do Accounts.create_user(user_params) # ^^^^^^^^^^^ # Source: conn.params["user"] (STRING KEYS!) end ``` ### From Socket Assigns (LiveView) ```elixir def handle_event("save", %{"user" => params}, socket) do Accounts.update_user(socket.assigns.current_user, params) # ^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^ # Source 1: socket.assigns (set in mount) # Source 2: event params (STRING KEYS from form!) end ``` ### From Job Args (Oban) ```elixir def perform(%Oban.Job{args: %{"user_id" => user_id}}) do user = Accounts.get_user!(user_id) Accounts.sync_user(user) # ^^^^ # Source: DB query using job.args["user_id"] end ``` ### Piped/Transformed ```elixir users |> Enum.map(&Accounts.update_user(&1, %{status: :active})) # ^^ # Source: element from `users` list (need to trace where users comes from) ``` ## AST-Based Extraction (Advanced) Using Sourceror for precise extraction: ```elixir defmodule ArgumentExtractor do def extract_call_args(file_path, line, {target_mod, target_fun, _arity}) do file_path |> File.read!() |> Sourceror.parse_string!() |> find_call_at_line(line, target_mod, target_fun) |> extract_args() end defp find_call_at_line(ast, target_line, target_mod, target_fun) do {_ast, result} = Macro.prewalk(ast, nil, fn # Remote call: Module.function(args) {{:., meta, [{:__aliases__, _, mod_parts}, fun_name]}, _, args} = node, acc -> if meta[:line] == target_line and Module.concat(mod_parts) == target_mod and fun_name == target_fun do {node, args} else {node, acc} end node, acc -> {node, acc} end) result end defp extract_args(nil), do: [] defp extract_args(args), do: Enum.map(args, &arg_to_string/1) defp arg_to_string({var, _, nil}) when is_atom(var), do: "#{var}" defp arg_to_string({{:., _, [Access, :get]}, _, [base, key]}), do: "#{arg_to_string(base)}[#{inspect(key)}]" defp arg_to_string({:@, _, [{name, _, _}]}), do: "@#{name}" defp arg_to_string(literal) when is_binary(literal), do: inspect(literal) defp arg_to_string(literal) when is_atom(literal), do: inspect(literal) defp arg_to_string(literal) when is_number(literal), do: inspect(literal) defp arg_to_string(_), do: "<complex expression>" end ``` ## Grep-Based Extraction (Simpler) When AST parsing is overkill: ```bash # Get the line with context sed -n '43,47p' lib/web/controllers/user_controller.ex # Output: # user = Accounts.get_user!(id) # # case Accounts.update_user(user, params) do # {:ok, user} -> redirect(conn, to: ~p"/users/#{user}") ``` Then parse visually or with simple regex. ## Documenting Arguments in Call Tree Format for clarity: ```markdown ## Call Site: lib/web/controllers/user_controller.ex:45 **Call:** `Accounts.update_user(user, attrs)` **Arguments:** 1. `user` - Variable - Defined at line 42: `user = Accounts.get_user!(id)` - Origin: Database query using `params["id"]` 2. `attrs` - Variable - Defined at line 43: `attrs = params["user"]` - Origin: Request params (string keys!) **Data Flow:** ``` HTTP Request → params["id"] → DB Query → user → params["user"] → attrs → update_user(user, attrs) ``` ``` ## Key Patterns to Flag ### String vs Atom Key Mismatch ```elixir # Controller receives string keys def update(conn, %{"user" => params}) do # But internal function might expect atom keys Accounts.update_user(user, params) # ⚠️ params has string keys! end ``` ### Nil Propagation Risk ```elixir # get_user returns nil on not found user = Accounts.get_user(id) # might be nil! Accounts.update_user(user, attrs) # ⚠️ passing nil? # vs safe version user = Accounts.get_user!(id) # raises on nil ``` ### Unvalidated External Data ```elixir def perform(%Oban.Job{args: args}) do # args comes from untrusted source (whoever enqueued the job) Accounts.delete_user!(args["user_id"]) # ⚠️ no validation! end ``` ## Integration with Call Tracer When building call tree, for each call site: 1. Read 10 lines before call site (variable definitions) 2. Extract argument expressions from call 3. Trace each argument to its origin 4. Note any transformations 5. Flag potential issues (nil, string keys, unvalidated) ```markdown ├─► MyAppWeb.UserController.update/2 │ └── lib/my_app_web/controllers/user_controller.ex:45 │ **Arguments:** │ - `user`: from `Accounts.get_user!(params["id"])` ✓ │ - `attrs`: from `params["user"]` ⚠️ string keys ``` -
entry-points.md 5.5 KB
# Entry Points Reference Patterns for identifying entry points in Elixir/Phoenix applications. These are where request/event handling begins - stop tracing here. ## Phoenix Controllers ```elixir # Standard REST actions def index(conn, _params) def show(conn, %{"id" => id}) def new(conn, _params) def create(conn, %{"user" => user_params}) def edit(conn, %{"id" => id}) def update(conn, %{"id" => id, "user" => user_params}) def delete(conn, %{"id" => id}) # Custom actions def custom_action(conn, params) ``` **Detection pattern:** ```regex def (index|show|new|create|edit|update|delete|\w+)\(conn[,\s] ``` **Entry point info:** - Route: Check `router.ex` for matching path - HTTP method: GET/POST/PUT/PATCH/DELETE - Params come from: URL params, query string, request body ## Phoenix LiveView ```elixir # Lifecycle def mount(params, session, socket) def handle_params(params, uri, socket) def terminate(reason, socket) # Events def handle_event("event_name", params, socket) def handle_event("event_name", %{"key" => value}, socket) # Messages def handle_info(message, socket) def handle_info({:ref, data}, socket) def handle_info(%Phoenix.Socket.Broadcast{}, socket) # Async operations def handle_async(name, async_fun_result, socket) ``` **Detection patterns:** ```regex def mount\(_?\w*, _?\w*, socket\) def handle_event\("[\w-]+", .*, socket\) def handle_info\(.*, socket\) def handle_params\(.*, .*, socket\) ``` **Entry point info:** - mount: Initial page load, params from URL - handle_event: User interaction, params from JS/form - handle_info: PubSub messages, process messages - handle_params: URL changes (live_patch) ## LiveComponent ```elixir # Lifecycle def mount(socket) def update(assigns, socket) def handle_event("event", params, socket) ``` **Note:** LiveComponents receive assigns from parent, but handle_event is an entry point for component-specific events. ## Oban Workers ```elixir # Standard perform def perform(%Oban.Job{args: args} = job) def perform(%Oban.Job{args: %{"user_id" => user_id}}) # With meta def perform(%Oban.Job{args: args, meta: meta}) ``` **Detection pattern:** ```regex def perform\(%Oban\.Job\{ ``` **Entry point info:** - Triggered by: Oban queue processing - Args source: `Oban.insert(%{args: %{...}})` - No user context (unless passed in args) ## GenServer ```elixir # Synchronous calls def handle_call(request, from, state) def handle_call({:get, key}, _from, state) def handle_call(:status, _from, state) # Asynchronous casts def handle_cast(request, state) def handle_cast({:update, value}, state) # Info messages def handle_info(message, state) def handle_info(:tick, state) def handle_info({:DOWN, ref, :process, pid, reason}, state) # Init def init(args) ``` **Detection patterns:** ```regex def handle_call\(.*, _?from, state\) def handle_cast\(.*, state\) def handle_info\(.*, state\) def init\( ``` **Entry point info:** - handle_call: From `GenServer.call(pid, request)` - handle_cast: From `GenServer.cast(pid, request)` - handle_info: From `send(pid, message)` or system messages ## Plugs ```elixir # Module plug def call(conn, opts) def init(opts) # Function plug (in controller) plug :authenticate def authenticate(conn, _opts) ``` **Detection pattern:** ```regex def call\(conn, opts?\) ``` **Note:** Plugs are middleware, often not final entry points but part of the chain. ## Mix Tasks ```elixir def run(args) def run([]) def run(["--flag", value | rest]) ``` **Detection pattern:** ```regex def run\(\[ def run\(args\) ``` **Entry point info:** - Triggered by: `mix task_name args` - Args: Command line arguments as list ## Phoenix Channels ```elixir # Join def join(topic, payload, socket) def join("room:" <> room_id, _payload, socket) # Messages def handle_in(event, payload, socket) def handle_in("new_msg", %{"body" => body}, socket) # Info def handle_info(message, socket) ``` **Detection patterns:** ```regex def join\("[\w:]+.*, .*, socket\) def handle_in\("[\w_]+", .*, socket\) ``` ## Broadway (Message Processing) ```elixir def handle_message(processor, message, context) def handle_batch(batcher, messages, batch_info, context) def handle_failed(messages, context) ``` **Entry point info:** - Messages from: Kafka, RabbitMQ, SQS, etc. - Batch processing context ## Absinthe (GraphQL) ```elixir # Resolver def resolve(parent, args, resolution) def resolve(_parent, %{id: id}, _resolution) # Middleware def call(resolution, config) ``` **Entry point info:** - Triggered by: GraphQL query/mutation - Args from: GraphQL variables ## Entry Point Detection Code ```elixir @entry_patterns [ # Phoenix Controllers ~r/def\s+(index|show|new|create|edit|update|delete)\s*\(\s*conn/, ~r/def\s+\w+\s*\(\s*conn\s*,/, # LiveView ~r/def\s+mount\s*\([^)]*socket\s*\)/, ~r/def\s+handle_event\s*\("/, ~r/def\s+handle_info\s*\([^)]*socket\s*\)/, ~r/def\s+handle_params\s*\(/, # Oban ~r/def\s+perform\s*\(\s*%Oban\.Job/, # GenServer ~r/def\s+handle_call\s*\(/, ~r/def\s+handle_cast\s*\(/, ~r/def\s+handle_info\s*\([^)]*state\s*\)/, ~r/def\s+init\s*\(/, # Plug ~r/def\s+call\s*\(\s*conn\s*,\s*opts?\s*\)/, # Mix Task ~r/def\s+run\s*\(\s*[\[\w]/ ] def entry_point?(line) do Enum.any?(@entry_patterns, &Regex.match?(&1, line)) end ``` ## Contextualizing Entry Points When you find an entry point, gather this context: | Entry Point Type | Find This | |------------------|-----------| | Controller | Route in `router.ex`, auth plugs | | LiveView | Route, on_mount hooks | | Oban Worker | Queue config, scheduling | | GenServer | How it's started, supervision tree | | Channel | Socket config, join conditions | -
mix-xref-usage.md 4.2 KB
# Mix Xref Usage Complete reference for using `mix xref` to trace function calls. ## Basic Commands ### Find All Callers ```bash # Find who calls a specific function mix xref callers MyApp.Accounts.update_user/2 # Output format: # lib/my_app_web/controllers/user_controller.ex:45: MyApp.Accounts.update_user/2 # lib/my_app_web/live/settings_live.ex:67: MyApp.Accounts.update_user/2 ``` ### Trace a File ```bash # Show all external calls FROM a file mix xref trace lib/my_app/accounts.ex # Output: # lib/my_app/accounts.ex:5: call Ecto.Changeset.cast/4 (runtime) # lib/my_app/accounts.ex:12: call MyApp.Repo.insert/1 (runtime) # lib/my_app/accounts.ex:20: struct MyApp.Accounts.User (export) ``` ### Dependency Graph ```bash # Text format (default) mix xref graph # DOT format for visualization mix xref graph --format dot > deps.dot dot -Tpng deps.dot -o deps.png # JSON format (Elixir 1.19+) mix xref graph --format json --output deps.json # Stats only mix xref graph --format stats ``` ## Dependency Types `mix xref` tracks three types of dependencies: | Type | Description | Example | |------|-------------|---------| | `compile` | Compile-time dependency (macros, module body) | `use MyMacro` | | `export` | Struct or public definition usage | `%User{}` | | `runtime` | Function calls inside functions | `Repo.get(User, id)` | ### Filter by Type ```bash # Only runtime dependencies (function calls) mix xref graph --only-runtime # Only compile dependencies (macros) mix xref graph --only-compile # Exclude specific type mix xref graph --exclude runtime ``` ## Filtering Results ### By Source/Sink ```bash # Calls FROM a specific file mix xref graph --source lib/my_app/accounts.ex # Calls TO a specific module mix xref graph --sink MyApp.Repo # Combine mix xref graph --source lib/my_app/accounts.ex --sink MyApp.Repo ``` ### By Label (Module Pattern) ```bash # Only show calls to specific modules mix xref graph --label MyApp.Accounts # Multiple labels mix xref graph --label MyApp.Accounts --label MyApp.Users ``` ## Practical Examples ### Find All Database Calls ```bash # Where is Repo used? mix xref callers MyApp.Repo # Which files call Repo.insert? mix xref callers MyApp.Repo.insert/1 mix xref callers MyApp.Repo.insert/2 ``` ### Find All Uses of a Context ```bash # Who uses the Accounts context? mix xref graph --sink MyApp.Accounts --format stats ``` ### Check Circular Dependencies ```bash # Find compile-time cycles (runtime cycles like verified_routes() are benign) mix xref graph --format cycles --label compile # Output: No cycles found (good!) # Or: lib/a.ex -> lib/b.ex -> lib/a.ex (bad!) ``` ### Analyze a Single Module ```bash # What does this module depend on? mix xref graph --source lib/my_app/accounts.ex # What depends on this module? mix xref graph --sink lib/my_app/accounts.ex ``` ## Integration with Call Tracer For recursive call tree building: ```bash # Step 1: Find direct callers callers=$(mix xref callers MyApp.Target.function/2) # Step 2: For each caller, find the containing function # Parse: lib/path/file.ex:42: MyApp.Target.function/2 # Extract file and line, then read to find enclosing function # Step 3: Recurse # For each calling function, run mix xref callers again ``` ## Fallback: When mix xref Unavailable If not in a Mix project or xref fails: ```bash # Grep for function calls (less accurate) grep -rn "Accounts\.update_user\|update_user(" lib/ --include="*.ex" | grep -v "def update_user" # Find function definitions grep -rn "def update_user" lib/ --include="*.ex" # Find module usage grep -rn "alias.*Accounts\|MyApp\.Accounts\." lib/ --include="*.ex" ``` ## Common Issues ### "Could not find callers" ```bash # Ensure project is compiled mix compile # Check if function exists mix run -e "IO.inspect MyApp.Accounts.__info__(:functions)" ``` ### Too Many Results ```bash # Filter by directory mix xref callers MyApp.Repo.get/2 | grep "controllers" # Focus on runtime only (skip compile-time) mix xref graph --only-runtime --sink MyApp.Module ``` ### Private Functions `mix xref callers` only finds calls to public functions. For private functions: ```bash # Grep within the module file grep -n "function_name" lib/my_app/module.ex ```
-
-
SKILL.md 3.2 KB
--- name: trace description: "Use when debugging how a value or request reaches Elixir code, finding who calls a function, or planning a signature change. Builds the call tree with mix xref callers instead of reading files one by one." effort: medium --- # Call Tracing Build call trees showing how functions are reached from entry points. ## Iron Laws - Never Violate These 1. **Always use `mix xref callers` first** - It's authoritative; grep is fallback only 2. **Stop at entry points** - Controllers, LiveView callbacks, Oban workers, GenServer callbacks 3. **Track visited MFAs** - Prevent infinite loops from circular calls 4. **Extract argument patterns** - Just knowing "who calls" isn't enough; HOW they call matters 5. **Max depth 10** - Deeper trees indicate architectural issues, not useful traces ## When to Build Call Tree (Use Proactively) | Condition | Why Call Tree Helps | |-----------|---------------------| | Unexpected nil/value at runtime | Trace where the value originates | | Bug can't reproduce locally | See all entry points that reach the code | | Changing function signature | Find all callers and their argument patterns | | Incomplete stack trace | Get full path context | | "Where does X come from?" | Visual answer to data flow question | ## Quick Trace Run the caller query first, then inspect another function in the chain as needed: ```bash mix xref callers MyApp.Accounts.update_user/2 mix xref callers MyApp.Accounts.get_user/1 ``` Read the reported locations to see argument patterns. ## Entry Points (Stop Here) | Pattern | Type | |---------|------| | `def mount/3`, `def handle_event/3` | LiveView | | `def index/2`, `def show/2`, `def create/2` | Controller | | `def perform(%Oban.Job{})` | Oban Worker | | `def handle_call/3`, `def handle_cast/2` | GenServer | ## Delegate to call-tracer Agent For full recursive tree with argument extraction and **parallel category tracing**: Determine the effective maximum nesting depth. Use an explicit positive-integer `CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH` value first; when it is unset, inspect `claude --version` (the default is 1 in 2.1.217–2.1.218 and 3 in 2.1.219+). If the version is unavailable, conservatively use 1. At depth 2+, delegate to the orchestrator below. At depth 1, keep orchestration in this main session: spawn the applicable controller, LiveView, worker, and internal tracing prompts directly, then merge their results. Never spawn an orchestrator that cannot delegate. ``` Agent(subagent_type: "phx:call-tracer", prompt: "Build call tree for MyApp.Accounts.update_user/2") ``` The call-tracer agent uses **parallel subagents** for each entry point category: - Controllers subagent (HTTP paths) - LiveView subagent (WebSocket paths) - Workers subagent (Background jobs) - Internal subagent (Cross-context calls) Each gets a fresh context for deep exploration. ## Output Location `.claude/plans/{slug}/research/call-tree-{function}.md` ## References For detailed patterns: - `${CLAUDE_SKILL_DIR}/references/mix-xref-usage.md` - Full mix xref commands and options - `${CLAUDE_SKILL_DIR}/references/entry-points.md` - All Phoenix/OTP entry point patterns - `${CLAUDE_SKILL_DIR}/references/argument-extraction.md` - AST parsing for argument patterns
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.