Debug Rails issues effectively with tapping_device

Stan Lo - Dec 22 '19 - - Dev Community

Debug Rails issues effectively with tapping_device

Hello everyone, šŸ‘‹ Iā€™m the author of the tapping_device gem, a debugging tool I created a while ago. I'm also a Rails contributor that helps people solve their issues from time to time. Today I want to use a real-world example to demonstrate how did the tapping_device gem help me find the cause of a Rails issue in an hour ;-)

Basic Intro of tapping_device

tapping_device is built on top of Rubyā€™s TracePoint. It has several helpers that provide a high-level point of view of how your program runs. For example:

  1. tap_on!(target) - you can use it to track all method calls performed on the target
  2. tap_sql!(target) - this is a Rails-specific helper. You can use it to track on method calls that generate SQL queries
  3. tap_passed!(target) - you can use it to track what method takes target as its argument

For more info, hereā€™s a post contains some useful examples that you can use in your Rails apps.

The Issue (#38041)

The issue is about a behavioral change on DateTime calculation:

# Rails 5.2.4.1: OK
ā€œ2019-12-19 10:00ā€.to_datetime - BigDecimal(1).hour
=> Thu, 19 Dec 2019 09:00:00 +0000
# OK

# Rails 6.0.2.1: WRONG
ā€œ2019-12-19 10:00ā€.to_datetime - BigDecimal(1).hour
=> Thu, 19 Dec 2019 08:59:59 +0000
Enter fullscreen mode Exit fullscreen mode

Itā€™s clear that something has been changed between version 5.2.4 and 6.0.2. But we donā€™t know where's the change nor when itā€™s been made.

How Iā€™d do, in the past

  1. Check if ā€œ2019-12-19 10:00ā€.to_datetime returns the same result in both versions
  2. Check if BigDecimal(1).hour returns the same result in both versions
  3. Digging into what happens when calling DateTime#- in both versions
  4. (Jumping between files and adding breakpoints or puts)

What I did instead, with tapping_device

  1. I wrote a script that prints out how the objects are used by tracking their method calls. The first thing Iā€™d check is that whatā€™s returned in each method call so I can locate whatā€™s been changed. And itā€™s also important to see where the calls were performed.
require "active_support/all"
require "tapping_device"

puts("Rails: #{ActiveSupport.version}")

time = "2019-12-19 10:00".to_datetime
one_h = BigDecimal(1).hour

device_for_time = TappingDevice.new
device_for_one_h = TappingDevice.new

device_for_time.tap_on!(time)
device_for_one_h.tap_on!(one_h)

time - one_h

puts("Calls on time\n")
device_for_time.calls.each do |payload|
  puts(payload.method_name_and_location + " => #{payload.return_value}")
end

puts("==============================")

puts("Calls on one_h\n")
device_for_one_h.calls.each do |payload|
  puts(payload.method_name_and_location + " => #{payload.return_value}")
end
Enter fullscreen mode Exit fullscreen mode
  1. Then I ran the same script on both versions. Here are the outputs:

Output - Rails 5.2

Rails: 5.2.4
Calls on time
Method: :acts_like?, line: /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activesupport-5.2.4/lib/active_support/duration.rb:404 => true
Method: :+, line: /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activesupport-5.2.4/lib/active_support/core_ext/date_time/calculations.rb:113 => 2019-12-19T09:00:00+00:00
Method: :since, line: /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activesupport-5.2.4/lib/active_support/duration.rb:410 => 2019-12-19T09:00:00+00:00
Method: :plus_with_duration, line: /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activesupport-5.2.4/lib/active_support/core_ext/date/calculations.rb:102 => 2019-12-19T09:00:00+00:00
Method: :-, line: time_test.rb:15 => 2019-12-19T09:00:00+00:00
==============================
Calls on one_h
Method: :is_a?, line: /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activesupport-5.2.4/lib/active_support/duration.rb:145 => true
Method: :-@, line: /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activesupport-5.2.4/lib/active_support/core_ext/date/calculations.rb:102 => -3600.0
Enter fullscreen mode Exit fullscreen mode

Output - Rails 6

Rails: 6.0.2
Calls on time
Method: :acts_like?, line: /Users/st0012/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.0.2/lib/active_support/duration.rb:405 => true
Method: :+, line: /Users/st0012/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.0.2/lib/active_support/core_ext/date_time/calculations.rb:113 => 2019-12-19T08:59:59+00:00
Method: :since, line: /Users/st0012/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.0.2/lib/active_support/duration.rb:411 => 2019-12-19T08:59:59+00:00
Method: :plus_with_duration, line: /Users/st0012/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.0.2/lib/active_support/core_ext/date/calculations.rb:102 => 2019-12-19T08:59:59+00:00
Method: :-, line: time_test.rb:15 => 2019-12-19T08:59:59+00:00
==============================
Calls on one_h
Method: :is_a?, line: /Users/st0012/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.0.2/lib/active_support/duration.rb:144 => true
Method: :-@, line: /Users/st0012/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.0.2/lib/active_support/core_ext/date/calculations.rb:102 => -3600.0
Enter fullscreen mode Exit fullscreen mode

We can see that the + method callā€™s results were different in the 2 versions. Version 5.2.4 returned 2019-12-19T09:00:00+00:00 while 6.0.2 returned 2019-12-19T08:59:59+00:00. Letā€™s check whatā€™s defined in active_support/core_ext/date_time/calculations.rb:113.

  def since(seconds)
    self + Rational(seconds, 86400)
  end
Enter fullscreen mode Exit fullscreen mode

Nothing weird. Maybe it has been changed recently?

$ git blame lib/active_support/core_ext/date_time/calculations.rb | grep 113
c85e3f65f34 (Edouard CHIN        2018-10-09 13:36:56 -0400 113)     self + Rational(seconds, 86400)
Enter fullscreen mode Exit fullscreen mode
commit c85e3f65f3409fc329732912908c3601d8e5fac9
Author: Edouard CHIN <edouard.chin@shopify.com>
Date:   Tue Oct 9 13:36:56 2018 -0400

    Fix issue where duration where always rounded up to a second:

    - Adding a Float as a duration to a datetime would result in the Float
      being rounded. Doing something like would have no effect because the
      0.45 seconds would be rounded to 0 second.

      ```

ruby
        time = DateTime.parse("2018-1-1")
        time += 0.45.seconds


      ```

      This behavior was intentionally added a very long time ago, the
      reason was because Ruby 1.8 was using `Integer#gcd` in the
      constructor of Rational which didn't accept a float value.

      That's no longer the case and doing `Rational(0.45, 86400)` would
      now perfectly work fine.

    - Fixes #34008
Enter fullscreen mode Exit fullscreen mode

Bingo! There was a round call in 5.2 but it has been removed 6.0 via this commit.

Isnā€™t this a super-easy way to debug an issue šŸ˜‰

What does this mean to me (and you)?

As you can see from the above example, what tapping_device provides is a new way to spot behavioral changes in your program.

Whether youā€™re a pry/bye bug debugger or a put debugger, you always need to go through files so you can build a rough execution path of the program in your mind before actually adding breakpoints or puts to the right location. The downsides of this are:

  1. If youā€™re not familiar with the codebase or itā€™s not written in an easily-understandable way, there could be a lot of trial-and-error before you actually found the right place to inspect your program.
  2. And itā€™ll be very easy to lose the big picture when you dive into individual method calls.

But if you use tapping_device, you can inspect the program from an objectā€™s point of view. This means you wonā€™t need to know which way itā€™ll go to intercept it. You just need to know where itā€™ll start its journey! It can save you hours when debugging a large application/library.

I hope you can give tapping_device a try after reading this post. And if you do try it, please don't be shy to give me any feedback! Iā€™d love to fix any issue or add any feature that can help people debug more happily!

. . . . . . . . . . . .
Terabox Video Player