Have you ever looked at a controller action like this
def index
@posts = Post.all
end
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
end
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
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|
puts(payload.method_name_and_location)
end
end
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}")
end
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(payload.method_name_and_location)
puts(" Arguments: #{payload.arguments}")
end
end
end
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!
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)
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!
Ben