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

st0012 profile image Stan Lo Updated on ・4 min read

Have you ever looked at a controller action like this

def index
  @posts = Post.all

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]

And include TappingDevice::Trackable in your controller:

class PostsController < ApplicationController
  include TappingDevice::Trackable

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|

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

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|

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
# 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>

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}")

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

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("  Arguments: #{payload.arguments}")
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=>[]}

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


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 😄

Posted on by:

st0012 profile

Stan Lo


Senior Software Engineer @ticketsolve. Love Cats, Ruby on Rails and Boxing. Created Goby. Working on tapping_device during weekends.


Editor guide

Looks great Stan can't wait to use this.


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


Cheers. I hope i can work through the post!