CodeTips#4: Debugging Ruby applications

When our code is behaving unexpectedly, our first attempt is usually trying to understand why it happened, but just thinking about it might not be enough. It’s easier to understand it when you have more info about the problem. We can try to check the default logs or add others to understand it, but even that might not suffice to give you enough information to find out what is going on. In this case, we would need to go deeper and inspect the code at runtime.

Some gems help with this process. Some powerful ones are binding.pry, byebug, and debugger, but in this article, we will cover a bit about the byebug and pry-remote gems, with some tips for debugging queries.

Using byebug gem

Byebug is a simple to use and feature-rich debugger for Ruby. Therefore, Byebug doesn’t depend on internal core sources. Byebug is also fast and reliable. It is developed as a C extension and it is supported by a full test suite.

This debugger permits the ability to understand what is going on inside a Ruby program while it executes and offers many of the traditional debugging features such as:

Stepping: Running your program one line at a time.
Breaking: Pausing the program at some event or specified instruction, to examine the current state.
Evaluating: Basic REPL functionality, although pry does a better job at that.
Tracking: Keeping track of the different values of your variables or the different lines executed by your program.

So let’s see how to use it.

First, we need to set up the gem. We can run
$ gem install byebug
and then you can set breakpoints in the code to invoke the debugger. For instance:

class UserController < ApplicationController
  def create
    byebug
    @user = User.new(create_params)
    @user.save  
  end
end

Now we can start the application and hit the controller:

bundle exec rails s

So we can see the breakpoint invoke the debugger:

* Listening on tcp://localhost:3000
Use Ctrl-C to stop
Started GET "/users" for ::1 at 2022-07-17 20:07:42 -0300
   (0.6ms)  SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
Processing by UsersController#index as HTML
  Rendering users/index.html.erb within layouts/application
  User Load (0.1ms)  SELECT "users".* FROM "users"
  Rendered users/index.html.erb within layouts/application (7.5ms)
Completed 200 OK in 134ms (Views: 128.7ms | ActiveRecord: 0.5ms)

Started GET "/users" for ::1 at 2022-07-17 20:18:54 -0300
Processing by UsersController#index as HTML

[1, 10] in /home/paty/Projetos/byebug_application/blog/app/controllers/users_controller.rb
    1: # frozen_string_literal: true
    2: 
    3: class UsersController  6:     @users = User.all
    7:   end
    8: 
    9:   def create
   10:    byebug
(byebug) info

and, with that, we can have more information like the parameters sent using:

(byebug) params
"users", "action"=>"index"} permitted: false>

we can also check other instance information to identify more of the context, for example:

(byebug) @user
#

ByeBug Command List:

  • catch: Handles exception points.
  • condition: Sets conditions on breakpoints.
  • continue: Runs to the end of the program, hits a breakpoint, or hits a line.
  • debug : Generates a subdebugger.
  • delete: Deletes breakpoints.
  • disable: Disables breakpoints or displays.
  • display: Evaluates expressions every time the debugger stops.
  • down: Moves to a lower frame in the stack trace.
  • edit: Edit source files.
  • enable: Enables breakpoints or views.
  • finish: Runs the program until the frame returns.
  • help: Helps you to use byebug.
  • history: Shows the history of byebug commands.
  • info: Shows various information about the program being debugged.
  • interrupt: Interrupts the program.
  • irb: Starts an IRB session.
  • kill: Sends a signal to the current process.
  • list: Lists lines of source code.
  • method —Shows methods of an object, class, or module.
  • next: Executes the next code line.
  • pry: Starts a pry session.
  • quit: Quits byebug.
  • restart: Restarts the debugged program.
  • save: Saves the current byebug session to a file.
  • set: Modifies the byebug settings.
  • show: Shows the byebug settings.
  • source: Restores a previously saved byebug session.
  • step: Steps in blocks or methods one or more times.
  • thread: Commands to manipulate threads.
  • tracevar: Allows tracing of a global variable.
  • undisplay: Stops displaying all or some expressions when the program stops.
  • untracevar: For tracking a global variable.
  • up: Moves to a higher frame in the stack trace.
  • var: Displays variables and their values.
  • where: Shows the backtrace. Another alias is bt.

See more informations here.

Pry-remote for Applications with Foreman

In the case of applications using Foreman, we will need a specific gem called pry-remote. It is a way to start Pry remotely and connect to it using DRb. This allows accessing the state of the running program from anywhere.
We can add it to the project just by doing:
gem install pry-remote
and to set the breakpoint we will need to add the instruction binding.remote_pry, then it would be something like:

class UserController < ApplicationController
  def create
    binding.remote_pry
    @user = User.new(create_params)
    @user.save  
  end
end

In the shell, we will need two tabs, one for running the foreman app, and another for remote_pry. When the breakpoint is hit, the pry-remote will block the app. Then, we can run pry-remote in a shell and it will connect to the session and allow us to interact with pry.

$ pry-remote
From: example.rb @ line 7 in UsersController#index:
     2:
     3: class UsersController   5:     binding.remote_pry
     6:     @users = User.all
     8:   end
     9: end

To quit, you can write exit in the shell to unblock the app.

What to do when we don't know exactly where the error is?

In some cases, we have to try to figure out where the error could be happening and we have no idea how to start. In cases like this, we need to debug from the input and sometimes even the output.

Routes

In a Rails application, the first thing we can look at is the routes. The routes.rb file is where the routes are defined and which controller action should be triggered if that route is hit.

Rails.application.routes.draw do
  resources :users
  resources :orders do
     post :update_shipping_info, on: :member
     get :details, on: :member
  end
end

Controllers

The second step would be to look at the controller and the triggered action. There we need to ensure that the data entered is arriving, if it is being allowed, if we have validation or authorization problems;

UseCases, Services, Models

The next step will be to debug the classes that have business logic. Here we can look at validations, data manipulation, and debugging if the written logic is correct.

Responsers, views, serializers

Finally, the last step would be to see if the data presented is correct, like if there is to be any treatment for the presentation of data that may be null.

Logging queries and identifying N+1

Understanding the logs in the terminal is not a simple task, especially when we see several database queries. It is difficult to know which method is causing all the queries in the database.
One way to see which line of code is causing problematic queries is to use ActiveRecord::Base.verbose_query_logs = true in the Rails session, so we allow verbose query logs.

irb(main):003:0> User.with_comments
  User Load (0.2ms)  SELECT "users".* FROM "users"
  ↳ app/models/user.rb:5
  Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."user_id" = ?  [["user_id", 1]]
  ↳ app/models/user.rb:6
  Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."user_id" = ?  [["user_id", 2]]
  ↳ app/models/user.rb:6
  Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."user_id" = ?  [["user_id", 3]]
  ↳ app/models/user.rb:6
=> #

With this, we will be able to see under each database instruction, arrows pointing exactly to the file and the line that caused that query.
This helps us identify and resolve performance issues caused by N+1 queries. Verbose query logs are enabled by default in the development environment logs after Rails 5.2.

IRB and Rails Console

Another way to get more context and see if a given approach is correct is using the IRB(Read-Eval-Print Loop) or the Rails Console. Usually, they are both IRB, the difference is just that the rails console is set up such that the rails environment is all set and ready to work with, while IRB has almost nothing loaded by default.

➜ rails c
Running via Spring preloader in process 19228
Loading development environment (Rails 5.1.7)
2.6.5 :001 > User.all
  User Load (0.9ms)  SELECT  "users".* FROM "users" LIMIT ?  [["LIMIT", 11]]
 => # 
2.6.5 :002 > 

The rails console allows us to use Rails, Active Record, and other methods to manipulate the data, helping to avoid problems like N+1 queries because you can see the result in the shell before adding the code to the application.

Conclusion

Well, that’s it. These are some tips for you to know how to make debugging easier and discover possible errors, as well as check the performance of queries and avoid N+1 queries problems. I hope it helps and if you have any other tips, share them in the comments 🙂

Useful links:

We want to work with you. Check out our "What We Do" section!