DEV Community

Stan Lo
Stan Lo

Posted on • Edited on

Want to know more about your Rails app? Tap on your objects!

Have you ever looked at a controller action like this

def index
  @posts = Post.all
end
Enter fullscreen mode Exit fullscreen mode

And wonder who’ll eventually use the @posts variable? Is there an easy way to do that without going through multiple files and write puts everywhere?

Well, with the help of the tapping_device gem, it’s not just possible, it’s also super easy (with only 3 lines of code!)

Install tapping_device and include TappingDevice::Trackable

First, add tapping_device into your Gemfile

gem "tapping_device", group: [:development, :test]
Enter fullscreen mode Exit fullscreen mode

And include TappingDevice::Trackable in your controller:

class PostsController < ApplicationController
  include TappingDevice::Trackable
end
Enter fullscreen mode Exit fullscreen mode

The tap_on! helper

Now let’s get the most basic information: who’s calling methods on @posts? To do this, you need to use the tap_on! method and define a callback. The block will be executed every time a @posts ’s method is being called.

def index
  @posts = Post.all
  tap_on!(@posts) do |payload|
    puts(payload.method_name_and_location)
  end
end
Enter fullscreen mode Exit fullscreen mode

Then send a request to /posts, you’ll see everything that’s been called on the @posts object:

Method: :eager_load_values, line: /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-6.0.0/lib/active_record/relation.rb:668
Method: :includes_values, line: /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-6.0.0/lib/active_record/relation.rb:669
Method: :eager_loading?, line: /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-6.0.0/lib/active_record/relation/calculations.rb:226
Method: :includes_values, line: /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-6.0.0/lib/active_record/relation/calculations.rb:226
Method: :has_include?, line: /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-6.0.0/lib/active_record/relation/calculations.rb:128
Method: :distinct_value, line: /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-6.0.0/lib/active_record/relation/calculations.rb:234
......
Enter fullscreen mode Exit fullscreen mode

Apparently, most of the methods are called by ActiveRecord and it’s not helping us here. Let’s filter them out with the exclude_by_paths option:

def index
  @posts = Post.all
  tap_on!(@posts, exclude_by_paths: [/activerecord/]) do |payload|
    puts(payload.method_name_and_location)
  end
end
Enter fullscreen mode Exit fullscreen mode

Now we can see who’s using the @posts object inside our application:

Method: :count, line: /PROJECT_PATH/rails-6-sample/app/views/posts/index.html.erb:3
Method: :each, line: /PROJECT_PATH/rails-6-sample/app/views/posts/index.html.erb:16
Method: :where, line: /PROJECT_PATH/rails-6-sample/app/views/posts/index.html.erb:31
Enter fullscreen mode Exit fullscreen mode
# index.html.erb

<h1>Posts (<%= @posts.count %>)</h1>
......
  <% @posts.each do |post| %>
    ......
  <% end %>
......
<p>Posts created by you: <%= @posts.where(user: @current_user).count %></p>
Enter fullscreen mode Exit fullscreen mode

Isn’t this super easy?

However, in most cases, we don’t really care about who calls methods on @posts. What we care about is “who calls @posts and generates sql queries?”.

tap_sql! to the rescue!

To see what queries @posts generates, we need to use the tap_sql! method instead. It runs the callback every time a sql query is generated from the object.

Let’s change our setup a little bit to:

tap_sql!(@posts, exclude_by_paths: [/activerecord/]) do |payload|
  puts("Method `#{payload.method_name}` generates sql: #{payload.sql}")
  puts("  From #{payload.filepath}:#{payload.line_number}")
end
Enter fullscreen mode Exit fullscreen mode

Now it prints out the method’s name, the sql it generated and the location.

Method `count` generates sql: SELECT COUNT(*) FROM "posts"
  From /PROJECT_PATH/rails-6-sample/app/views/posts/index.html.erb:3
Method `each` generates sql: SELECT "posts".* FROM "posts"
  From /PROJECT_PATH/rails-6-sample/app/views/posts/index.html.erb:16
Method `count` generates sql: SELECT COUNT(*) FROM "posts" WHERE "posts"."user_id" = ?
  From /PROJECT_PATH/rails-6-sample/app/views/posts/index.html.erb:31
Enter fullscreen mode Exit fullscreen mode

If you look at the output very carefully, you may notice something isn’t quite right: The last query SELECT COUNT(*) FROM “posts” WHERE “posts”. “user_id” = ? wasn’t actually generated from @posts.

Well, this is a hidden feature of the tap_sql! method. It also tracks the scopes created from @posts, so if you call scope chaining methods like order or where on @posts, the queries generated from the new scopes will also be recorded!

Bonus: what’s passed into the method?

Sometimes, only knowing which method is called isn’t enough. What’s passed into the method is also very important. For example, assume we want to know which user was passed into the where call, we can do

def index
  @posts = Post.all
  tap_on!(@posts, exclude_by_paths: [/activerecord/]) do |payload|
    if payload.method_name == :where
      puts(payload.method_name_and_location)
      puts("  Arguments: #{payload.arguments}")
    end
  end
end
Enter fullscreen mode Exit fullscreen mode
Method: :where, line: /PROJECT_PATH/rails-6-sample/app/views/posts/index.html.erb:31
  Arguments: {:opts=>{:user=>#<User id: 20, name: "Stan", age: 25, created_at: "2019-12-09 09:06:29", updated_at: "2019-12-09 09:06:29">}, :rest=>[]}
Enter fullscreen mode Exit fullscreen mode

Now we know it’s the “Stan” user, and we don’t even need to leave the controller!

Conclusion

tapping_device is a new project, but I already use it very often on my day job. So I believe it’ll be helpful for others too! If you want to know more about it, please visit the repo and read the readme for more features. And if you have any feature requests, please feel free to open an issue, and we can discuss it! Any other feedback is welcomed as well 😄

Top comments (3)

Collapse
 
bkspurgeon profile image
Ben Koshy

Looks great Stan can't wait to use this.

Collapse
 
st0012 profile image
Stan Lo

Thank you!
And here's another post about how it can help you improve the debugging process in general, and more examples! bit.ly/object-oriented-tracing

Collapse
 
bkspurgeon profile image
Ben Koshy

Cheers. I hope i can work through the post!

Ben