Want to know more about your Rails app? Tap on your objects!
Tags: rails, ruby, contributorswanted
Published: 2019-12-09 10:09:37

Have you ever looked at a controller action like this

```ruby
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`](https://github.com/st0012/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`](https://github.com/st0012/tapping_device) into your Gemfile

```ruby
gem "tapping_device", group: [:development, :test]
```

And include `TappingDevice::Trackable` in your controller:

```ruby
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.

```ruby
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:

```ruby
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
```

```erb
# 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:

```ruby
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

```ruby
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](https://github.com/st0012/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](https://github.com/st0012/tapping_device) 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 😄