Is a gem you’re using not behaving the way you expect or giving a cryptic error you don’t understand? If you’re comfortable diving into the gems your app depends on, you’ll be able to track down and fix a whole new class of bugs that you might otherwise have to work around.
There’s no wrong way to debug an issue so long as you eventually figure out the problem. I’m going to outline how I go about tackling these kinds of issues, but if you have or find a way that works better for you, go for it. My goal is to show you how to get started debugging issues that fall outside of your application’s codebase so that you can expand the kinds of issues you can solve, as well as give you a path to start contributing back to the open source community. Where you take it from there is up to you.
I do almost all my debugging with Pry. Pry is a great debugger for Ruby that gives you a REPL with super powers.
Once you’ve added Pry to your project, put binding.pry
somewhere in your code. When your app executes that line, it will open up the Pry REPL, which is similar to IRB (what the command rails console
uses by default.)
For example, if you have a controller in Rails and you need to look at what’s happening inside its index
action, you’ll add a call to pry like this:
class ThingsController < ApplicationController
def index
@things = Thing.all
binding.pry
end
end
With that in place, when you hit that action either with your browser or with an automated test (using something like RSpec or minitest), the execution of the program will stop and drop out to a Pry REPL. For example, when I visited localhost:3000/things
with my browser and switched back to my Rails server, I saw this:
From: /Users/jardo/Codes/thing_app/app/controllers/things_controller.rb @ line 4 ThingsController#index:
2: def index
3: @things = Thing.all
=> 4: binding.pry
5: end
[1] pry(#<ThingsController>)>
You can type any Ruby you want to evaluate into this REPL, but unlike rails console
, it all evaluates as if you were running it right where the call to binding.pry
is. If you want to see the value of that instance variable, you can do this:
[1] pry(#<ThingsController>)> @things
=> []
If that wasn’t the value of @things
that you expected, you might want to examine the SQL that was getting run when you ran Things.all
:
[2] pry(#<ThingsController>)> Thing.all.to_sql
=> "SELECT \"things\".* FROM \"things\""
So far I’ve only shown you the features of Pry that IRB has too. Pry has four features that I find extremely handy and wouldn’t want to live without.
The first is ls
. Typing ls
into Pry will list all the constants, methods, variables, and instances variables in the current context. If I run it inside the binding.pry
console inside my ThingsController
I get a huge dump of information.
If you scan through it, you’ll see it lists all the methods, constants, and variables available in this context. They are organized by their source or type, and the methods are all shown in the order they appear in the inheritance chain of the current object.
Rails controllers contain many mixins and anonymous modules, so this can be somewhat overwhelming, but near the bottom you’ll see some more useful things, the method we defined (index
) and the instance variable we set (@things
). If we’d defined some more methods, variables, or instance variables, we’d see them all listed here:
ThingsController#methods: index
instance variables: @_action_has_layout @_action_name @_config @_lookup_context @_request @_response @_response_body @_routes @marked_for_same_origin_verification @things
locals: _ __ _dir_ _ex_ _file_ _in_ _out_ _pry_
Check out the tips at the end of the article for more useful ways to use the ls
command.
Another useful tool Pry includes is the cd
command. It blew my mind the first time I used it. This allows you to change contexts completely, moving away from the context where you called binding.pry
into any other object you want. Here’s an example:
[1] pry(main)> class Cat
[1] pry(main)* def initialize(name)
[1] pry(main)* @name = name
[1] pry(main)* end
[1] pry(main)* def meow
[1] pry(main)* puts "meow!"
[1] pry(main)* end
[1] pry(main)* def greet
[1] pry(main)* puts "Hi, I'm #{@name}."
[1] pry(main)* end
[1] pry(main)* end
=> :greet
[2] pry(main)> wally = Cat.new("Wally")
=> #<Cat:0x00007faf3c118738 @name="Wally">
[3] pry(main)> cd wally
[4] pry(#<Cat>):1> ls
Cat#methods: greet meow
self.methods: __pry__
instance variables: @name
locals: _ __ _dir_ _ex_ _file_ _in_ _out_ _pry_
[5] pry(#<Cat>):1> @name
=> "Wally"
In this example we define a Cat
class, create an instance of it (named “Wally”), cd
into that instance and finally examine the instance variables set inside that object. This can be useful for examining the internal states of objects and calling their private methods to better understand how they work.
The final two tools I use all the time are the ?
and $
operators. They will show you the documentation and source code of a method/class.
For example, let’s look at the documentation for a class method that the Mail gem provides:
[1] pry(main)> require 'mail'
=> true
[2] pry(main)> ? Mail.register_interceptor
From: /Users/jardo/.gem/ruby/2.6.3/gems/mail-2.7.1/lib/mail/mail.rb @ line 208:
Owner: #<Class:Mail>
Visibility: public
Signature: register_interceptor(interceptor)
Number of lines: 7
You can register an object to be given every mail object that will be sent,
before it is sent. So if you want to add special headers or modify any
email that gets sent through the Mail library, you can do so.
Your object needs to respond to a single method #delivering_email(mail)
which receives the email that is about to be sent. Make your modifications
directly to this object.
That might be helpful, but what if the documentation is out of date, or the even having read the documentation we find that the method isn’t doing what we expect? We can use $
to look at the actual source of the method:
[3] pry(main)> $ Mail.register_interceptor
From: /Users/jardo/.gem/ruby/2.6.3/gems/mail-2.7.1/lib/mail/mail.rb @ line 215:
Owner: #<Class:Mail>
Visibility: public
Number of lines: 5
def self.register_interceptor(interceptor)
unless @@delivery_interceptors.include?(interceptor)
@@delivery_interceptors << interceptor
end
end
I don’t have much experience with the mail
gem, so I still don’t really know what this method is for, but I bet that if I was using the gem and this method wasn’t doing what I expected, this might help me start digging in. Using the tools I’ve shown here, I could cd
into the Mail
class, examine the value of the @@delivery_interceptors
class variable, and start to track down how it gets used.
There are lots of ways to manage gems. Some people use gemsets using tools like rvm and rbenv. I’m a minimalist (to a fault, sometimes) and prefer a “simpler” method. I install the gems for each project inside the project folder itself. The bundler documentation shows that you can do this by running bundle install --path=vendor/bundle
. Instead of installing the gems in their default location (this will depend on your setup), they’ll get installed into the vendor/bundle
directory inside your project.
The reason I like to do this is two-fold:
Firstly, it allows me to modify those gems as much as I like, potentially breaking them completely, and easily delete and reinstall them by running rm -r vendor/bundle && bundle install
.
Secondly, it encourages me to think of the code in my project’s dependencies as part of my project. While it may not be my code as far as licensing is concerned, it’s no less a part of my running application as the code that I write myself. I think this is important to remember.
If you choose to install your gems inside your project too, just make sure you add /vendor/bundle
to your .gitignore
so you don’t accidentally commit all the gems to your git repo.
Regardless of where your gems are installed, you can ask bundler where a specific gem is installed with bundle show GEMNAME
. Once you’ve got the location of a gem, you can open it up and see its source code!
With access to the source code, you’re free to browse, modify, and debug it. One good place to start is by adding a binding.pry
into a method that your app is calling that’s behaving unexpectedly. Just make sure you restart your application for the changes to take effect. Rails will reload code found in app/
in your project, but generally not in third party gems.
A good REPL debugger and access to the source code of a gem is often all that you’ll need to track down the source of the problem you’re experiencing. Let’s assume that you’ve found a bug in your app, but as you try to track it down, you discover that some methods or classes from another gem aren’t behaving how you expected them to. Here’s the process I like to go through.
Reproduction is the first step. You’ll want to either write a test that demonstrates the problem, or at least find a way to trigger the bug manually.
Once you’ve reproduced the unexpected behaviour, start inserting calls to binding.pry
in places you expect it to be called. There’s no wrong places to put them. If you put them in and they don’t get hit when you reproduce the bug, then you’ve just learned that the part of the code you put the binding.pry
in isn’t getting executed. If you find your binding.pry
is getting hit, but when you look at all the local and instance variables at that point in the code and they all contain what you expect, then you’ve learned that the bug probably lies elsewhere.
Use Pry to view the source of the methods that are getting called, cd
into those objects and examine what they’re doing. Use bundle show
to open the gems and insert more calls to binding.pry
to try to uncover the cause of the issue.
As you continue learn more and more about what your program (and its dependencies are doing), eventually you’re liable to find something that violates some assumption you’ve made about how the system behaves. This isn’t usually the “root cause”, but a symptom of something you don’t yet understand about how the system behaves.
From here, you can work backwards to find the root cause. This is kind of like applying the 5 Whys technique, continuing to ask “why is the system behaving this way?” and moving upwards through the behaviour of the program to understand what’s happening at higher and higher levels until you have a root cause. Unfortunately, there’s no guarantee that a mere five “whys” will get you to the solution. Keep going until you’ve found the real root cause.
With the root cause in hand, there are a few different situations you might find yourself in:
You might have found that you’ve been using a gem incorrectly. The solution here is usually to rework your application to fix the problem. It’s generally best to use gems the way they are intended to be used because it makes it less likely that future changes will be incompatible with your application.
Alternatively, you might have found that there’s a bug in a gem you’re using. If so, you should have a look the gem’s issues to see if the bug has been reported. If it hasn’t, you should probably report the issue.
If it has been reported and hasn’t been fixed yet, consider trying to fix the bug yourself! After all, you’ve been digging through the gem’s code, you’re qualified to take a stab at it.
Finally, it’s possible you never really found the root cause. Hopefully this isn’t the case, but we’ve all been there. Unusual or complex code, metaprogramming, excessive indirection can all get in the way of debugging a gem.
Even if you end up here, you’ve probably got a ton more context on the bug than when you started. If there are people around who can help you, you’ll be in a much better position to communicate the problem to them, putting them in a better position to help you. All is not lost!
Being comfortable jumping into code outside of your project will expand your ability to fix bugs, and open up new opportunities to contribute back to the open source community. While different employers have different policies when it comes to open source work, in my experience, most companies don’t care whether the commit that fixes a bug in their product goes into their codebase or some other gem, so long as the bug gets fixed.
Fixing bugs in dependencies might also be one of the few chances you get to read real production code written by people outside your project. I feel strongly that exposing yourself to lots of code written by different people is a great way to level up your programming skills. Tracking down bugs forces you to really understand the code that you’re reading, making the process even more valuable.
Here’s a few more Pry tricks that I find useful.
The command whereami
shows you where your Pry session is executing. If you’ve just hit a binding.pry
it’ll show you the source code, and if you cd
into another object it’ll tell you what object you’re in.
You can use the wtf
command to print some information about the last exception that was raised. Adding questions marks to the command (like wtf??????
) will show more and more of the stack trace of the exception.
ls
When dealing with controllers and models this can all be a little overwhelming because of the sheer amount of superclasses and concerns that Rails provides. If you have some idea of what you’re looking for, you can use -G
to “grep” for the methods or variables you want. Only lines matching the string you provide will be printed.
[4] pry(#<ThingsController>)> ls -G thing
#<Module:0x00007fc27ce15ed8>#methods: edit_thing_url new_thing_url thing_url things_url
#<Module:0x00007fc27ce15eb0>#methods: edit_thing_path new_thing_path thing_path things_path
instance variables: @_action_has_layout @_action_name @_config @_lookup_context @_request @_response @_response_body @_routes @marked_for_same_origin_verification @things
pry-stack_explorer
is a gem that allows you to move up and down the call stack. It’s a little more advanced, but I find it extremely useful when working backwards to track down the root cause of an issue.
These tips aren’t exhaustive, so check out the Pry GitHub page to learn about more useful things you can do like running shell commands with .
, or sharing parts of your Pry session with the gist
integration.