Skip to content
PLAY VIDEO PLAY VIDEO PLAY VIDEO
By Jonathan Miles

Dissecting Code With Ruby's caller Method

Dissecting Code With Ruby’s caller Method

       location = caller.first
       location = location[0...location.rindex(":")]
       line_number = location[(location.rindex(":")+1)..-1]
       file = location[0...location.rindex(":")]
       puts location
       puts File.open(file).readlines[line_number.to_i - 1]
       puts "#{values}"
       puts "--"*20

This short snippet has become one of my favourite debugging tools recently. It’s not efficient, definitely not elegant, but man is it useful.

But let’s back up a moment, how did I get to this stage?

Background

As part of our consultancy service at reinteractive, we often inherit applications developed by other Rails shops of varying quality. Previous developers of the code we receive have often used, well, let’s just say “creative” approaches.

In most rails apps a line like this is pretty clear:

widget.cost_of_production

Simple. Widget is an ActiveRecord model, cost_of_production is either a DB field or a method defined in Widget. Easy right? Except when widget is not an ActiveRecord class.

Off the rails

In this particular case, the previous developer(s) used immutable structs (via the Hamsterdam gem) internally. Not only so, but each ActiveRecord model would be transformed into three of these structs – raw, calculated, and combined. With combined being a wrapper that zipped data and calculated into one struct.

Basically, imagine re-implementing ActiveRecord but without any of the “magic” that makes it nice to use.

Crawling through the code was difficult to say the least. “Jump to definition” fails because the “method” you’re looking for is not a method, it is a member of a struct. Even a simple grep is misleading because the structs are combined using .merge on the hashes, so the individual “fields” may not be named in the file. Not to mention that the structs and the DB may be using different names for the same field…

Debugging Ruby

Aaron Patterson (of core Ruby and core Rails fame) has an awesome blog post titled I am a puts debuggerer which should be required reading for any Rubyist, where he outlines some of the things we can do in Ruby when debugging. Perhaps the most memorable sentence is this one:

This is debugging code, so you can do whatever you want.

Debugging code is inherently throw-away code. It can be messy, inefficient, and downright ugly, as long as it’s useful.

A simple example

I’ll use OpenStruct here as a simple example of using the snippet at the top of this post, but you can drop it into any class really.

Let’s start with a basic script:

require 'ostruct'

struct = OpenStruct.new(
  value: 'complicated calc result'
)

puts "#{struct}"

Easy stuff. Run the code and we get:

#<OpenStruct value="complicated calc result">

Now imagine you don’t know where that value came from, and jumping through the code isn’t making things any clearer.

Luckily, Ruby allows us to re-open a class and inject our own code (aka “monkey patching”). This makes for a great debugging tool in the tool belt.

Using our own OpenStruct constructor and snippet, we get this script:

require 'ostruct'

class OpenStruct
  def initialize(hash=nil)
    @table = {}
    if hash
      hash.each_pair do |k, v|
        k = k.to_sym
        @table[k] = v
      end
      location = caller.first
      location = location[0...location.rindex(":")]
      line_number = location[(location.rindex(":")+1)..-1]
      file = location[0...location.rindex(":")]
      puts location
      puts File.open(file).readlines[line_number.to_i - 1]
      puts "#{hash}"
      puts "--"*20
    end
  end
end

struct = OpenStruct.new(
  value: 'complicated calc result'
)

puts "#{struct}"

The basic initialise method code here is taken straight from the OpenStruct source code, with our snippet added to the bottom of the if hash block.

So what’s happening here?

caller gives us a stack trace up to that line. For my purposes, I only care about the first entry (i.e. the point where OpenStruct.new was called).

Next, we do some quick-and-dirty parsing of that line to grab the filename and line number (the trace will be in a format like: ./<filename>:<linenum>: in ‘<method>’)

Lastly, we print out: the file location, the line in that file that called the method I’m in, any values we want to see, and then a lot of hyphens (hyphens purely as a divider in the log as there might be a lot of these printed out).

Running the above gives:

./sharp_knives.rb:25
struct = OpenStruct.new(
{:value=>"complicated calc result"}
----------------------------------------
#<OpenStruct value="complicated calc result">

Location of call, actual line that does the call, and the values passed in.

Now tracing back the value allocations can be done by looking at the logs. I wouldn’t say this made the process “easy” by any means, but it was certainly an improvement over trying to trace through the convoluted logic with my brain.

Cool story bro

You might be saying to yourself “Yeah well that was cool and all for that one weird scenario, but in a normal rails app we don’t need that kind of stuff right?”

Maybe.

Since “crafting” that snippet (and yes, I’m being extremely generous with the term craft here) I’ve already used it on another project.

This time the scenario was a Rails upgrade. The code was much more conventional and, most importantly, used the money) gem.

Around version 6.7 money included a breaking change so that == would raise when comparing against other non-Money objects. i.e. Money.new(1) == 1 would now raise an exception, unless you are comparing against zero – Money.new(1) == 0 will not raise an exception.

This was pretty tricky to clear up, in part because you can’t just search your codebase for Money objects (unless you know every single Money object’s name I suppose). And in some cases the call may not be for == but rather eq (as in a spec). In one particularly troublesome case, I was actually hitting this exception inside ActiveRecord’s validations when creating a new test object (using FactoryBot), so the call stack was full of ActiveSupport and FactoryBot entries, making it difficult to pinpoint where the error was occurring in the app code. To help out, I just monkey-patched the above snippet into the Money::Arithmetic#== to have a look at what was causing the exception.

Poking around I eventually discovered that ActiveRecord’s numericality validation basically calls old_value != new_value (not an exact copy, just to give the idea) and this was causing my problem. Some more investigation and I found there was a custom validation inside the model that updates one of the fields. The problem? It was updating the raw value_cents field (i.e. not the Money-ified value version) with a Money object. This Numeric <-> Money mismatch was the root cause of my problem, and the solution was as simple as changing value_cents = <Money obj> to value = <Money obj>.

Summary

Ruby has some very powerful tools baked in (source_location is another notable mention worth knowing about), and combined with the ability to monkey-patch these into existing classes, it gives us some very useful debugging techniques that few languages can compete with.

Just remember, anything goes with debugging code. It can have terrible performance, it can be riddled with dodgy monkey-patching, it can be full of duplication. After all, there’s no point polishing it – once the bug is fixed you’ll be throwing it away.

Latest Articles by Our Team

Our expert team of designers and developers love what the do and enjoy sharing their knowledge with the world.

We Hire Only the Best

reinteractive is Australia’s largest dedicated Ruby on Rails development company. We don’t cut corners and we know what we are doing.

We are an organisation made up of amazing individuals and we take pride in our team. We are 100% remote work enabling us to choose the best talent no matter which part of the country they live in. reinteractive is dedicated to making it a great place for any developer to work.

Free Community Workshops

We created the Ruby on Rails InstallFest and Ruby on Rails Development Hub to help introduce new people to software development and to help existing developers hone their skills. These workshops provide invaluable mentorship to train developers, addressing key skills shortages in the industry. Software development is a great career choice for all ages and these events help you get started and skilled up.

  • Webinars

    Webinars

    Webinars are our online portal for tips, tricks and lessons learned in everything we do. Make the most of this free resource to help you become a better developer.

    Learn more about webinars

  • Installfest

    Installfest

    The Ruby on Rails Installfest includes a full setup of your development environment and step-by-step instructions on how to build your first app hosted on Heroku. Over 1,800 attendees to date and counting.

    Learn more about Installfest

  • Development Hub

    Development Hub

    The Ruby on Rails Development Hub is a monthly event where you will get the chance to spend time with our team and others in the community to improve and hone your Ruby on Rails skills.

    Learn more about Development Hub

Get the “reinteractive Review” Monthly Email