Debugging Phoenix with IEx.pry

Previous Posts in this series

Writing a Blog Engine in Phoenix and Elixir, Part 1

Writing a Blog Engine in Phoenix and Elixir, Part 2: Authorization

Current Versions

  • Elixir: v1.1.1
  • Phoenix: v1.1.0
  • Ecto: v1.1.0
  • Comeonin: v2.0.0

If you are reading this and these are not the latest, let me know and I’ll update this tutorial accordingly.

Introduction

Setup

mix phoenix.new debug

Answer “y” to installing brunch dependencies. We’ll then follow the rest some of the instructions and do our own thing when it comes to starting up the server:

cd debug
mix ecto.create
iex -S mix phoenix.server

The last command is the most important one: we need to run mix phoenix.server inside the context of an iex shell. Without that, we will not be able to reach out to the shell to debug things.

iex -S mix phoenix.server
Erlang/OTP 18 [erts-7.1] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
[info] Running Debug.Endpoint with Cowboy on http://localhost:4000
Interactive Elixir (1.1.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 07 Oct 16:18:34 - info: compiled 5 files into 2 files, copied 3 in 1370ms

We have everything we need to be able to mess around with things, so we’ll start off simple and throw some debugging into our controller. Let’s visit http://localhost:4000/ and make sure the Phoenix starting page shows up before we continue!

Debugging a Controller

defmodule Debug.PageController do
use Debug.Web, :controller
def index(conn, _params) do
render conn, "index.html"
end
end

Let’s make it possible to debug from our index action. At the top, we’re going to add the following line before our defmodule Debug.PageController do line:

require IEx

Next, inside of our index function, we’re going to add the following line:

IEx.pry

And then we refresh our browser. We should see the following output in our terminal window:

[info] GET /
[debug] Processing by Debug.PageController.index/2
Parameters: %{}
Pipelines: [:browser]
Request to pry #PID<0.323.0> at web/controllers/page_controller.ex:7. Allow? [Yn]

We’ll answer “Y” here to get into the interactive debugger. Now, we’re in the context of that function call, so we can start to mess around with things. If params was actually being passed, we could inspect it. conn is being passed in and not thrown out, so let’s inspect that:

pry(1)> conn
%Plug.Conn{adapter: {Plug.Adapters.Cowboy.Conn, :...}, assigns: %{},
before_send: [#Function<1.27458827/1 in Plug.CSRFProtection.call/2>,
#Function<9.50638171/1 in Phoenix.Controller.fetch_flash/2>,
#Function<0.105544698/1 in Plug.Session.before_send/2>,
#Function<1.26297491/1 in Plug.Logger.call/2>,
#Function<0.65283535/1 in Phoenix.LiveReloader.before_send_inject_reloader/1>],
body_params: %{},
cookies: %{},
halted: false, host: "localhost", method: "GET", owner: #PID<0.323.0>,
params: %{}, path_info: [], peer: {{127, 0, 0, 1}, 62477}, port: 4000,
private: %{Debug.Router => {[], %{}}, :phoenix_action => :index,
:phoenix_controller => Debug.PageController,
:phoenix_endpoint => Debug.Endpoint, :phoenix_flash => %{},
:phoenix_format => "html", :phoenix_layout => {Debug.LayoutView, :app},
:phoenix_pipelines => [:browser],
:phoenix_route => #Function<1.67750312/1 in Debug.Router.match/4>,
:phoenix_router => Debug.Router, :phoenix_view => Debug.PageView,
:plug_session => %{}, :plug_session_fetch => :done}, query_params: %{},
query_string: "", remote_ip: {127, 0, 0, 1},
req_cookies: %{},
req_headers: [{"host", "localhost:4000"}, {"connection", "keep-alive"},
{"accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"},
{"upgrade-insecure-requests", "1"},
{"user-agent",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36"},
{"accept-encoding", "gzip, deflate, sdch"},
{"accept-language", "en-US,en;q=0.8"},
{"cookie",""}],
request_path: "/", resp_body: nil, resp_cookies: %{},
resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"},
{"x-request-id", "3bbpa7lnf2t5frotnn7n3svt0hu81pba"},
{"x-frame-options", "SAMEORIGIN"}, {"x-xss-protection", "1; mode=block"},
{"x-content-type-options", "nosniff"}], scheme: :http, script_name: [],
secret_key_base: "mNdPO56AiyV591ayIbXX5ykEHXQAsfJEbEs89ATuHP9yG8GJaMusKtGfglFYrmQw",
state: :unset, status: nil}

We can also access the help on any modules (or for iex in general) using h(). For example, if we just type in h (calling the h function with no arguments), it’ll print out the information for IEx and how to use it. However, you can also feed it a module name and it will give you the documentation for that module. Try it with h(Ecto.Repo) and h(Phoenix.HTML) as tests!

There’s a ton of information here that you now have access to. This is great for poking around at things and learning how things work in general. If we had an Ecto model that we were displaying to a user, for example, here we could attempt a query and look for something that was failing or bad data or anything like that.

If we want to exit out of our debugging window, we have a few options. The first is that we can just Ctrl+C twice to exit the entire shell and give up, but that’s rarely what we want to do! Instead, if you type in respawn() in the pry window, it will continue on with the rest of the request as if you had never initiated the debugger in the first place.

Debugging a View

require IExdefmodule Debug.PageView do
use Debug.Web, :view
def do_stuff() do
message = "hello"
IEx.pry
end
end

Notice we added the require IEx line and the IEx.pry line to this file. Then inside of our template web/templates/page/index.html.eex we add the following call somewhere:

<% do_stuff() %>

Which dumps us into an IEx.pry window. We can inspect the value of the message variable that we created in the function, for example. You can probably see why this is very very helpful!

Debugging a Template

<div class="jumbotron">
<h2>Welcome to Phoenix!</h2>
<p class="lead">A productive web framework that<br />does not compromise speed and maintainability.</p>
<% message = "hello" %>
<% IEx.pry %>
</div>

And then inside of our pry window, do this:

Request to pry #PID<0.393.0> at web/views/page_view.ex:5. Allow? [Yn] yInteractive Elixir (1.1.0) - press Ctrl+C to exit (type h() ENTER for help)
pry(1)> message
"hello"
pry(2)>

Debugging a Model

> mix phoenix.gen.model User users username:string
* creating priv/repo/migrations/20151007211555_create_user.exs
* creating web/models/user.ex
* creating test/models/user_test.exs
Remember to update your repository by running migrations: $ mix ecto.migrate> mix ecto.migrate
Compiled web/controllers/page_controller.ex
Compiled web/models/user.ex
Generated debug app
17:16:38.187 [info] == Running Debug.Repo.Migrations.CreateUser.change/0 forward17:16:38.187 [info] create table users17:16:38.196 [info] == Migrated in 0.0s

Restart the server first and then we’ll jump into web/models/user.ex and toss require IEx at the top. We’ll then modify our changeset function and toss IEx.pry at the top of the function. Finally, we’ll go back into our controller web/controllers/page_controller.ex and ensure that we call the changeset function to trigger this debug window. Modify the index action to read as follows:

def index(conn, _params) do
user = Debug.User.changeset(%User{}, %{username: "test"})
render conn, "index.html"
end

Refresh our browser window and you should see our familiar prompt:

[info] GET /
[debug] Processing by Debug.PageController.index/2
Parameters: %{}
Pipelines: [:browser]
Request to pry #PID<0.309.0> at web/models/user.ex:22. Allow? [Yn]

We’ll answer Y and now we should be inside of our changeset function. We’ll see what we get for our model object and what we get for our params:

pry(1)> model
%Debug.User{__meta__: #Ecto.Schema.Metadata<:built>, id: nil, inserted_at: nil,
updated_at: nil, username: nil}
pry(2)> params
%{username: "test"}

We’ll type in respawn() and carry on our merry way!

Debugging in Tests

require IExdefmodule Debug.PageControllerTest do
use Debug.ConnCase
test "GET /" do
conn = get conn(), "/"
IEx.pry
assert html_response(conn, 200) =~ "Welcome to Phoenix!"
end
end

To run our tests, the command gets a little more complicated. The command that we need to run to make this all work is:

iex -S mix test --trace

Again, we’re just using our normal command to run things but we’re doing so inside of an IEx context (thus the iex -S bit). We’re also adding the — trace option here because if we don’t, the tests will time out automatically and we don’t want that! Beyond that, this is just inside of the context of the test that we threw this command into, so we can inspect conn, for example.

pry(1)> conn
%Plug.Conn{adapter: {Plug.Adapters.Test.Conn, :...},
assigns: %{layout: {Debug.LayoutView, "app.html"}},
before_send: [#Function<1.7098650/1 in Plug.CSRFProtection.call/2>,
#Function<1.121379662/1 in Phoenix.Controller.fetch_flash/2>,
#Function<0.7621997/1 in Plug.Session.before_send/2>,
#Function<1.1051886/1 in Plug.Logger.call/2>], body_params: %{}, cookies: %{},
halted: false, host: "www.example.com", method: "GET", owner: #PID<0.2140.0>,
params: %{}, path_info: [], peer: {{127, 0, 0, 1}, 111317}, port: 80,
private: %{Debug.Router => {[], %{}}, :phoenix_action => :index,
:phoenix_controller => Debug.PageController,
:phoenix_endpoint => Debug.Endpoint, :phoenix_flash => %{},
:phoenix_format => "html", :phoenix_layout => {Debug.LayoutView, :app},
:phoenix_pipelines => [:browser], :phoenix_recycled => false,
:phoenix_route => #Function<0.121155463/1 in Debug.Router.match/4>,
:phoenix_router => Debug.Router, :phoenix_template => "index.html",
:phoenix_view => Debug.PageView, :plug_session => %{},
:plug_session_fetch => :done, :plug_skip_csrf_protection => true},
query_params: %{}, query_string: "", remote_ip: {127, 0, 0, 1},
req_cookies: %{}, req_headers: [], request_path: "/",
resp_body: "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <meta name=\"description\" content=\"\">\n <meta name=\"author\" content=\"\">\n\n <title>Hello Phoenix!</title>\n <link rel=\"stylesheet\" href=\"/css/app.css?vsn=30D2A75\">\n </head>\n\n <body>\n <div class=\"container\" role=\"main\">\n <div class=\"header\">\n <ul class=\"nav nav-pills pull-right\">\n <li><a href=\"http://www.phoenixframework.org/docs\">Get Started</a></li>\n </ul>\n <span class=\"logo\"></span>\n </div>\n\n <p class=\"alert alert-info\" role=\"alert\"></p>\n <p class=\"alert alert-danger\" role=\"alert\"></p>\n\n <div class=\"jumbotron\">\n <h2>Welcome to Phoenix!</h2>\n <p class=\"lead\">A productive web framework that<br />does not compromise speed and maintainability.</p>\n</div>\n\n<div class=\"row marketing\">\n <div class=\"col-lg-6\">\n <h4>Resources</h4>\n <ul>\n <li>\n <a href=\"http://phoenixframework.org/docs/overview\">Guides</a>\n </li>\n <li>\n <a href=\"http://hexdocs.pm/phoenix\">Docs</a>\n </li>\n <li>\n <a href=\"https://github.com/phoenixframework/phoenix\">Source</a>\n </li>\n </ul>\n </div>\n\n <div class=\"col-lg-6\">\n <h4>Help</h4>\n <ul>\n <li>\n <a href=\"http://groups.google.com/group/phoenix-talk\">Mailing list</a>\n </li>\n <li>\n <a href=\"http://webchat.freenode.net/?channels=elixir-lang\">#elixir-lang on freenode IRC</a>\n </li>\n <li>\n <a href=\"https://twitter.com/elixirphoenix\">@elixirphoenix</a>\n </li>\n </ul>\n </div>\n</div>\n\n\n </div> <!-- /container -->\n <script src=\"/js/app.js?vsn=13F765\"></script>\n </body>\n</html>\n",
resp_cookies: %{},
resp_headers: [{"content-type", "text/html; charset=utf-8"},
{"cache-control", "max-age=0, private, must-revalidate"},
{"x-request-id", "8ra1f25r0022s1eorguvmt563edgf961"},
{"x-frame-options", "SAMEORIGIN"}, {"x-xss-protection", "1; mode=block"},
{"x-content-type-options", "nosniff"}], scheme: :http, script_name: [],
secret_key_base: "mNdPO56AiyV591ayIbXX5ykEHXQAsfJEbEs89ATuHP9yG8GJaMusKtGfglFYrmQw",
state: :sent, status: 200}

There you go! You now know how to debug your way into each portion of your Phoenix stack now!

Written by

I am a software engineer, and now, published author! Check out my new book at https://www.packtpub.com/web-development/phoenix-web-development

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store