tapping_device 0.4.11 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8328ddf34918bc8be607eb8bad74e3588f435a13dcefdf0690dd7f03ec4774f
4
- data.tar.gz: d34dd35817a235e92e12f7c086a3911d11c8564851a29e3ae31676b9b699f67f
3
+ metadata.gz: 9806974025c4e1ba9042bf2df5a4b4211a7e4a9630b291e05892d472e54263cc
4
+ data.tar.gz: fc609ed5be23cac4943e90c34d7126aaffd53816910320f7300d2a33a9d40585
5
5
  SHA512:
6
- metadata.gz: c12632e9ecdfed8970b36f191f2d2ea736cf1dc61a12096559cf25dfd791ee22c240e80d0230ffad4dc334d8dd9e6071efb1d6372256e339cd4387f79316a995
7
- data.tar.gz: 92a81f59e1f130ea25a41e58fb06c4ca153fa0eaa021094c59782737d16af34160ccb257a42a1bdd0a589d3e739a17ebf126c2cf72e233a0b61eab133825311e
6
+ metadata.gz: 15ee39e171f665492e47c378bcffedd3d7bc1c8204269607e54c1ac174f44fe5ee4cc33a0ffcd82d4b732abc63def63eefb8b1fda386d4e40503a4bd959e71e2
7
+ data.tar.gz: 3b4bb1b05949fd4735fde568995e8c92c64634ad39f196f76649e2f401834d8fdc605080546655f90920e8c2cfc1cebb4bf686b7da34494217c677e058f6c73b
Binary file
@@ -1,23 +1,23 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- tapping_device (0.4.11)
4
+ tapping_device (0.5.0)
5
5
  activerecord (>= 5.2)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- activemodel (6.0.2.2)
11
- activesupport (= 6.0.2.2)
12
- activerecord (6.0.2.2)
13
- activemodel (= 6.0.2.2)
14
- activesupport (= 6.0.2.2)
15
- activesupport (6.0.2.2)
10
+ activemodel (6.0.3.1)
11
+ activesupport (= 6.0.3.1)
12
+ activerecord (6.0.3.1)
13
+ activemodel (= 6.0.3.1)
14
+ activesupport (= 6.0.3.1)
15
+ activesupport (6.0.3.1)
16
16
  concurrent-ruby (~> 1.0, >= 1.0.2)
17
17
  i18n (>= 0.7, < 2)
18
18
  minitest (~> 5.1)
19
19
  tzinfo (~> 1.1)
20
- zeitwerk (~> 2.2)
20
+ zeitwerk (~> 2.2, >= 2.2.2)
21
21
  coderay (1.1.2)
22
22
  concurrent-ruby (1.1.6)
23
23
  database_cleaner (1.7.0)
@@ -27,7 +27,7 @@ GEM
27
27
  concurrent-ruby (~> 1.0)
28
28
  json (2.3.0)
29
29
  method_source (0.9.2)
30
- minitest (5.14.0)
30
+ minitest (5.14.1)
31
31
  pry (0.12.2)
32
32
  coderay (~> 1.1.0)
33
33
  method_source (~> 0.9.0)
data/README.md CHANGED
@@ -6,417 +6,175 @@
6
6
  [![Test Coverage](https://api.codeclimate.com/v1/badges/3e3732a6983785bccdbd/test_coverage)](https://codeclimate.com/github/st0012/tapping_device/test_coverage)
7
7
  [![Open Source Helpers](https://www.codetriage.com/st0012/tapping_device/badges/users.svg)](https://www.codetriage.com/st0012/tapping_device)
8
8
 
9
- ## Related Posts
10
- - [Optimize Your Debugging Process With Object-Oriented Tracing and tapping_device](http://bit.ly/object-oriented-tracing)
11
- - [Debug Rails issues effectively with tapping_device](https://dev.to/st0012/debug-rails-issues-effectively-with-tappingdevice-c7c)
12
- - [Want to know more about your Rails app? Tap on your objects!](https://dev.to/st0012/want-to-know-more-about-your-rails-app-tap-on-your-objects-bd3)
13
-
14
-
15
- ## Table of Content
16
- - [Introduction](#introduction)
17
- - [Print Object’s Traces](print-objects-traces)
18
- - [Track Calls that Generates SQL Queries](#track-calls-that-generates-sql-queries)
19
- - [Installation](#installation)
20
- - [Usages](#usages)
21
- - Tracing Helpers
22
- - [print_traces](#print_traces)
23
- - [print_calls_in_detail](#print_calls_in_detail)
24
- - Tapping Methods
25
- - [tap_init!](#tap_init)
26
- - [tap_on!](#tap_on)
27
- - [tap_passed!](#tap_passed)
28
- - [tap_assoc!](#tap_assoc)
29
- - [tap_sql!](#tap_sql)
30
- - [Options](#options)
31
- - [Payload](#payload-of-the-call)
32
- - [Advance Usages](#advance-usages)
33
9
 
34
10
  ## Introduction
35
- `tapping_device` is a debugging tool built based on a concept called `object-oriented tracing` and on top of Ruby's `TracePoint` class. It allows you to inspect an object’s behavior, and thus build the program’s execution path for later debugging. Here’s a post to explain how to use `object-oriented tracing` and this gem to improve your debugging workflow: [Optimize Your Debugging Process With Object-Oriented Tracing and tapping_device](http://bit.ly/object-oriented-tracing).
36
-
37
- Sample usage:
38
-
39
- ### Print Object’s Traces
11
+ `TappingDevice` makes the objects tell you what they do, so you don't need to track them yourself.
40
12
 
41
- Let your objects report to you, so you don’t need to guess how they work!
13
+ #### Contract Tracing For Objects
42
14
 
43
- ```ruby
44
- class OrdersController < ApplicationController
45
- def create
46
- @cart = Cart.find(order_params[:cart_id])
47
- print_traces(@cart, exclude_by_paths: [/gems/])
48
- @order = OrderCreationService.new.perform(@cart)
49
- end
50
- ```
51
-
52
- ```
53
- Passed as 'cart' in 'OrderCreationService#perform' at /Users/st0012/projects/tapping_device-demo/app/controllers/orders_controller.rb:10
54
- Passed as 'cart' in 'OrderCreationService#validate_cart' at /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:8
55
- Called :reserved_until FROM /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:18
56
- Called :errors FROM /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:9
57
- Passed as 'cart' in 'OrderCreationService#apply_discount' at /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:10
58
- Called :apply_discount FROM /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:24
59
- ……
60
- ```
15
+ The concept is very simple. It's basically like [contact tracing](https://en.wikipedia.org/wiki/Contact_tracing) for your Ruby objects. You can use
61
16
 
62
- (Also see [print_calls_in_detail](#print_calls_in_detail))
17
+ - `print_calls(object)` to see what the object does
18
+ - `print_traces(object)` to see how the object interacts with other objects (like used as an argument)
63
19
 
20
+ Still sounds vague? Let's see some examples:
64
21
 
65
- However, depending on the size of your application, tapping any object could **harm the performance significantly**. **Don't use this on production**
22
+ ### `print_calls` To Track Method Calls
66
23
 
67
- ## Installation
68
- Add this line to your application's Gemfile:
24
+ In [Discourse](https://github.com/discourse/discourse), it uses the `Guardian` class for authorization (like policy objects). It's barely visible in controller actions, but it does many checks under the hood. Now, let's say we want to know what the `Guadian` would do when a user creates a post; here's the controller action:
69
25
 
70
26
  ```ruby
71
- gem 'tapping_device', group: :development
72
- ```
73
-
74
- And then execute:
75
-
76
- ```
77
- $ bundle
78
- ```
79
-
80
- Or install it yourself as:
81
-
82
- ```
83
- $ gem install tapping_device
84
- ```
85
-
86
- ## Usages
87
-
88
- ### print_traces
89
-
90
- It prints the object's trace. It's like mounting a GPS tracker + a spy camera on your object, so you can inspect your program through the object's eyes.
91
-
92
- ```ruby
93
- class OrdersController < ApplicationController
94
27
  def create
95
- @cart = Cart.find(order_params[:cart_id])
96
- print_traces(@cart, exclude_by_paths: [/gems/])
97
- @order = OrderCreationService.new.perform(@cart)
28
+ @manager_params = create_params
29
+ @manager_params[:first_post_checks] = !is_api?
30
+
31
+ manager = NewPostManager.new(current_user, @manager_params)
32
+
33
+ if is_api?
34
+ memoized_payload = DistributedMemoizer.memoize(signature_for(@manager_params), 120) do
35
+ result = manager.perform
36
+ MultiJson.dump(serialize_data(result, NewPostResultSerializer, root: false))
37
+ end
38
+
39
+ parsed_payload = JSON.parse(memoized_payload)
40
+ backwards_compatible_json(parsed_payload, parsed_payload['success'])
41
+ else
42
+ result = manager.perform
43
+ json = serialize_data(result, NewPostResultSerializer, root: false)
44
+ backwards_compatible_json(json, result.success?)
45
+ end
98
46
  end
99
47
  ```
100
48
 
101
- ```
102
- Passed as 'cart' in 'OrderCreationService#perform' at /Users/st0012/projects/tapping_device-demo/app/controllers/orders_controller.rb:10
103
- Passed as 'cart' in 'OrderCreationService#validate_cart' at /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:8
104
- Called :reserved_until FROM /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:18
105
- Called :errors FROM /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:9
106
- Passed as 'cart' in 'OrderCreationService#apply_discount' at /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:10
107
- Called :apply_discount FROM /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:24
108
- ……
109
- ```
110
-
111
- ### print_calls_in_detail
112
-
113
- It prints the object's calls in detail (including call location, arguments, and return value). It's useful for observing an object's behavior when debugging.
49
+ As you can see, it doesn't even exist in the controller action, which makes tracking it by reading code very hard to do.
114
50
 
115
- #### Options
116
- - `inspect:` - will print objects with `#inspect` instead of `#to_s` if set to `true` (very noisy when having large objects). Default is `false`.
51
+ But with `TappingDevice`. You can use `print_calls` to show what method calls the object performs
117
52
 
118
53
  ```ruby
119
- class OrdersController < ApplicationController
120
54
  def create
121
- @cart = Cart.find(order_params[:cart_id])
122
- service = OrderCreationService.new
123
- print_calls_in_detail(service)
124
- @order = service.perform(@cart)
125
- end
126
- ```
127
-
55
+ # you can retrieve the current guardian object by calling guardian in the controller
56
+ print_calls(guardian)
57
+ @manager_params = create_params
58
+
59
+ # .....
128
60
  ```
129
- :validate_cart # OrderCreationService
130
- <= {:cart=>#<Cart id: 1, total: 10, customer_id: 1, promotion_id: nil, reserved_until: nil, created_at: "2020-01-05 09:48:28", updated_at: "2020-01-05 09:48:28">}
131
- => nil
132
- FROM /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:8
133
- :apply_discount # OrderCreationService
134
- <= {:cart=>#<Cart id: 1, total: 5, customer_id: 1, promotion_id: 1, reserved_until: nil, created_at: "2020-01-05 09:48:28", updated_at: "2020-01-05 09:48:28">, :promotion=>#<Promotion id: 1, amount: 0.5e1, customer_id: nil, created_at: "2020-01-05 09:48:28", updated_at: "2020-01-05 09:48:28">}
135
- => true
136
- FROM /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:10
137
- :create_order # OrderCreationService
138
- <= {:cart=>#<Cart id: 1, total: 5, customer_id: 1, promotion_id: 1, reserved_until: nil, created_at: "2020-01-05 09:48:28", updated_at: "2020-01-05 09:48:28">}
139
- => #<Order:0x00007f9ebcb17f08>
140
- FROM /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:11
141
- :perform # OrderCreationService
142
- <= {:cart=>#<Cart id: 1, total: 5, customer_id: 1, promotion_id: 1, reserved_until: nil, created_at: "2020-01-05 09:48:28", updated_at: "2020-01-05 09:48:28">, :promotion=>#<Promotion id: 1, amount: 0.5e1, customer_id: nil, created_at: "2020-01-05 09:48:28", updated_at: "2020-01-05 09:48:28">}
143
- => #<Order:0x00007f9ebcb17f08>
144
- FROM /Users/st0012/projects/tapping_device-demo/app/controllers/orders_controller.rb:11
145
- ```
146
-
147
- The output's order might look strange. This is because `tapping_device` needs to wait for the call to return in order to have its return value.
148
-
149
- ### tap_init!
150
61
 
151
- `tap_init!(class)` - tracks a class’ instance initialization
62
+ Now, if you execute the code, like via tests:
152
63
 
153
- ```ruby
154
- calls = []
155
- tap_init!(Student) do |payload|
156
- calls << [payload[:method_name], payload[:arguments]]
157
- end
158
-
159
- Student.new("Stan", 18)
160
- Student.new("Jane", 23)
161
-
162
- puts(calls.to_s) #=> [[:initialize, {:name=>"Stan", :age=>18}], [:initialize, {:name=>"Jane", :age=>23}]]
64
+ ```shell
65
+ $ rspec spec/requests/posts_controller_spec.rb:603
163
66
  ```
164
67
 
165
- ### tap_on!
166
-
167
- `tap_on!(object)` - tracks any calls received by the object.
68
+ You can get all the method calls it performs with basically everything you need to know
168
69
 
169
- ```ruby
170
- class PostsController < ApplicationController
171
- before_action :set_post, only: [:show, :edit, :update, :destroy]
70
+ <img src="https://github.com/st0012/tapping_device/blob/master/images/print_calls.png" alt="image of print_calls output" width="50%">
172
71
 
173
- def show
174
- tap_on!(@post).and_print(:method_name_and_location)
175
- end
176
- end
177
- ```
72
+ Let's take a closer look at each entry. Everyone of them contains the method call's
73
+ - method name
74
+ - method source class/module
75
+ - call site
76
+ - arguments
77
+ - return value
178
78
 
179
- And you can see these in log:
79
+ ![explanation of individual entry](https://github.com/st0012/tapping_device/blob/master/images/print_calls%20-%20single%20entry.png)
180
80
 
181
- ```
182
- name FROM /PROJECT_PATH/sample/app/views/posts/show.html.erb:5
183
- user_id FROM /PROJECT_PATH/sample/app/views/posts/show.html.erb:10
184
- to_param FROM /RUBY_PATH/gems/2.6.0/gems/actionpack-5.2.0/lib/action_dispatch/routing/route_set.rb:236
185
- ```
81
+ These are the information you'd have to look up one by one manually (probably with many debug code writing). Now you can get all of them in just one line of code.
186
82
 
187
- Also check the `track_as_records` option if you want to track `ActiveRecord` records.
188
83
 
189
- ### tap_passed!
84
+ ### `print_traces` To See The Object's Traces
190
85
 
191
- `tap_passed!(target)` tracks method calls that **takes the target as its argument**. This is particularly useful when debugging libraries. It saves your time from jumping between files and check which path the object will go.
86
+ If you're not interested in what an object does, but what it interacts with other parts of the program, e.g., used as arguments. You can use the `print_traces` helper. Let's see how `Discourse` uses the `manager` object when creating a post
192
87
 
193
88
  ```ruby
194
- class PostsController < ApplicationController
195
- # GET /posts/new
196
- def new
197
- @post = Post.new
89
+ def create
90
+ @manager_params = create_params
91
+ @manager_params[:first_post_checks] = !is_api?
92
+
93
+ manager = NewPostManager.new(current_user, @manager_params)
198
94
 
199
- tap_passed!(@post) do |payload|
200
- puts(payload.passed_at(with_method_head: true))
201
- end
202
- end
203
- end
95
+ print_traces(manager)
96
+ # .....
204
97
  ```
205
98
 
206
- ```
207
- Passed as 'record' in method ':polymorphic_mapping'
208
- > def polymorphic_mapping(record)
209
- at /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/actionpack-6.0.0/lib/action_dispatch/routing/polymorphic_routes.rb:131
210
- Passed as 'klass' in method ':get_method_for_class'
211
- > def get_method_for_class(klass)
212
- at /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/actionpack-6.0.0/lib/action_dispatch/routing/polymorphic_routes.rb:269
213
- Passed as 'record' in method ':handle_model'
214
- > def handle_model(record)
215
- at /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/actionpack-6.0.0/lib/action_dispatch/routing/polymorphic_routes.rb:227
216
- Passed as 'record_or_hash_or_array' in method ':polymorphic_method'
217
- > def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options)
218
- at /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/actionpack-6.0.0/lib/action_dispatch/routing/polymorphic_routes.rb:139
219
- ```
99
+ And after running the test case
220
100
 
221
- ### tap_assoc!
101
+ ```shell
102
+ $ rspec spec/requests/posts_controller_spec.rb:603
103
+ ```
222
104
 
223
- `tap_assoc!(activerecord_object)` tracks association calls on a record, like `post.comments`
105
+ You will see that it performs 2 calls: `perform` and `perform_create_post`. And it's also used as `manager` argument in various of calls of the `NewPostManager` class.
224
106
 
225
- ```ruby
226
- tap_assoc!(order).and_print(:method_name_and_location)
227
- ```
107
+ ![image of print_traces output](https://github.com/st0012/tapping_device/blob/master/images/print_traces.png)
228
108
 
229
- ```
230
- payments FROM /RUBY_PATH/gems/2.6.0/gems/jsonapi-resources-0.9.10/lib/jsonapi/resource.rb:124
231
- line_items FROM /MY_PROJECT/app/models/line_item_container_helpers.rb:44
232
- effective_line_items FROM /MY_PROJECT/app/models/line_item_container_helpers.rb:110
233
- amending_orders FROM /MY_PROJECT/app/models/order.rb:385
234
- amends_order FROM /MY_PROJECT/app/models/order.rb:432
235
- ```
109
+ **You can try these examples on [my fork of discourse](https://github.com/st0012/discourse/tree/demo-for-tapping-device)**
236
110
 
237
- ### tap_sql!
238
111
 
239
- `tap_sql!(anything_that_generates_sql_queries)` tracks sql queries generated from the target
112
+ ## Installation
113
+ Add this line to your application's Gemfile:
240
114
 
241
115
  ```ruby
242
- class PostsController < ApplicationController
243
- def index
244
- # simulate current_user
245
- @current_user = User.last
246
- # reusable ActiveRecord::Relation
247
- @posts = Post.all
248
-
249
- tap_sql!(@posts) do |payload|
250
- puts("Method: #{payload[:method_name]} generated sql: #{payload[:sql]} from #{payload[:filepath]}:#{payload[:line_number]}")
251
- end
252
- end
253
- end
116
+ gem 'tapping_device', group: :development
254
117
  ```
255
118
 
256
- ```erb
257
- <h1>Posts (<%= @posts.count %>)</h1>
258
- ......
259
- <% @posts.each do |post| %>
260
- ......
261
- <% end %>
262
- ......
263
- <p>Posts created by you: <%= @posts.where(user: @current_user).count %></p>
264
- ```
119
+ And then execute:
265
120
 
266
121
  ```
267
- Method: count generated sql: SELECT COUNT(*) FROM "posts" from /PROJECT_PATH/rails-6-sample/app/views/posts/index.html.erb:3
268
- Method: each generated sql: SELECT "posts".* FROM "posts" from /PROJECT_PATH/rails-6-sample/app/views/posts/index.html.erb:16
269
- Method: count generated sql: SELECT COUNT(*) FROM "posts" WHERE "posts"."user_id" = ? from /PROJECT_PATH/rails-6-sample/app/views/posts/index.html.erb:31
270
- ```
271
-
272
-
273
- ### Options
274
- #### with_trace_to
275
- It takes an integer as the number of traces we want to put into `trace`. Default is `nil`, so `trace` would be empty.
276
-
277
- ```ruby
278
- stan = Student.new("Stan", 18)
279
- tap_on!(stan, with_trace_to: 5)
280
-
281
- stan.name
282
-
283
- puts(device.calls.first.trace) #=>
284
- /Users/st0012/projects/tapping_device/spec/tapping_device_spec.rb:287:in `block (4 levels) in <top (required)>'
285
- /Users/st0012/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rspec-core-3.8.2/lib/rspec/core/example.rb:257:in `instance_exec'
286
- /Users/st0012/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rspec-core-3.8.2/lib/rspec/core/example.rb:257:in `block in run'
287
- /Users/st0012/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rspec-core-3.8.2/lib/rspec/core/example.rb:503:in `block in with_around_and_singleton_context_hooks'
288
- /Users/st0012/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rspec-core-3.8.2/lib/rspec/core/example.rb:460:in `block in with_around_example_hooks'
289
- /Users/st0012/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rspec-core-3.8.2/lib/rspec/core/hooks.rb:464:in `block in run'
122
+ $ bundle
290
123
  ```
291
124
 
292
- #### track_as_records
293
- It makes the device to track objects as they are ActiveRecord instances. For example:
125
+ Or install it directly:
294
126
 
295
- ```ruby
296
- tap_on!(@post, track_as_records: true)
297
- post = Post.find(@post.id) # same record but a different object
298
- post.title #=> this call will be recorded as well
299
127
  ```
300
-
301
- #### exclude_by_paths
302
- It takes an array of call path patterns that we want to skip. This could be very helpful when working on a large project like Rails applications.
303
-
304
- ```ruby
305
- tap_on!(@post, exclude_by_paths: [/active_record/]).and_print(:method_name_and_location)
128
+ $ gem install tapping_device
306
129
  ```
307
130
 
308
- ```
309
- _read_attribute FROM /RUBY_PATH/gems/2.6.0/gems/activerecord-5.2.0/lib/active_record/attribute_methods/read.rb:40
310
- name FROM /PROJECT_PATH/sample/app/views/posts/show.html.erb:5
311
- _read_attribute FROM /RUBY_PATH/gems/2.6.0/gems/activerecord-5.2.0/lib/active_record/attribute_methods/read.rb:40
312
- user_id FROM /PROJECT_PATH/sample/app/views/posts/show.html.erb:10
313
- .......
131
+ **Depending on the size of your application, `TappingDevice` could harm the performance significantly. So make sure you don't put it inside the production group**
314
132
 
315
- # versus
316
133
 
317
- name FROM /PROJECT_PATH/sample/app/views/posts/show.html.erb:5
318
- user_id FROM /PROJECT_PATH/sample/app/views/posts/show.html.erb:10
319
- to_param FROM /RUBY_PATH/gems/2.6.0/gems/actionpack-5.2.0/lib/action_dispatch/routing/route_set.rb:236
320
- ```
134
+ ### Advance Usages & Options
321
135
 
322
- #### filter_by_paths
323
-
324
- Like `exclude_by_paths`, but work in the opposite way.
325
-
326
-
327
- ### Payload of The Call
328
- All tapping methods (start with `tap_`) takes a block and yield a `Payload` object as a block argument. It responds to
329
-
330
- - `target` - the target for `tap_x` call
331
- - `receiver` - the receiver object
332
- - `method_name` - method’s name (symbol)
333
- - e.g. `:name`
334
- - `method_object` - the method object that's being called. It might be `nil` in some edge cases.
335
- - `arguments` - arguments of the method call
336
- - e.g. `{name: “Stan”, age: 25}`
337
- - `return_value` - return value of the method call
338
- - `filepath` - path to the file that performs the method call
339
- - `line_number`
340
- - `defined_class` - in which class that defines the method being called
341
- - `trace` - stack trace of the call. Default is an empty array unless `with_trace_to` option is set
342
- - `sql` - sql that generated from the call (only present in `tap_sql!` payloads)
343
- - `tp` - trace point object of this call
344
-
345
-
346
- #### Symbols for Payload Helpers
347
- - `FROM` for method call’s location
348
- - `<=` for arguments
349
- - `=>` for return value
350
- - `@` for defined class
351
-
352
- #### Payload Helpers
353
- - `method_name_and_location` - `initialize FROM /PROJECT_PATH/tapping_device/spec/payload_spec.rb:7`
354
- - `method_name_and_arguments` - `initialize <= {:name=>\"Stan\", :age=>25}`
355
- - `method_name_and_return_value` - `ten => 10`
356
- - `method_name_and_defined_class` - `initialize @ Student`
357
- - `passed_at` -
358
- ```
359
- Passed as 'object' in method ':initialize'
360
- at /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/actionview-6.0.0/lib/action_view/helpers/tags/label.rb:60
361
- ```
136
+ #### Add Conditions With `.with`
362
137
 
363
- You can also set `passed_at(with_method_head: true)` to see the method's head.
138
+ Sometimes we don't need to know all the calls or traces of an object; we just want some of them. In those cases, we can chain the helpers with `.with` to filter the calls/traces.
364
139
 
365
- ```
366
- Passed as 'object' in method ':initialize'
367
- > def initialize(template_object, object_name, method_name, object, tag_value)
368
- at /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/actionview-6.0.0/lib/action_view/helpers/tags/label.rb:60
140
+ ```ruby
141
+ # only prints calls with name matches /foo/
142
+ print_calls(object).with do |payload|
143
+ payload.method_name.to_s.match?(/foo/)
144
+ end
369
145
  ```
370
146
 
371
- - `detail_call_info`
372
-
373
- ```
374
- initialize @ Student
375
- <= {:name=>"Stan", :age=>25}
376
- => 25
377
- FROM /Users/st0012/projects/tapping_device/spec/payload_spec.rb:7
378
- ```
147
+ #### `colorize: false`
379
148
 
380
- ### Advance Usages
149
+ By default `print_calls` and `print_traces` colorize their output. If you don't want the colors, you can use `colorize: false` to disable it.
381
150
 
382
- Tapping methods introduced above like `tap_on!` are designed for simple use cases. They're actually short for
383
151
 
384
152
  ```ruby
385
- device = TappingDevice.new { # tapping action }
386
- device.tap_on!(object)
153
+ print_calls(object, colorize: false)
387
154
  ```
388
155
 
389
- And if you want to do some more configurations like stopping them manually or setting stop condition, you must have a `TappingDevie` instance. You can either get them like the above code or save the return value of `tap_*!` method calls.
390
156
 
391
- #### Stop tapping
157
+ #### `inspect: true`
392
158
 
393
- Once you have a `TappingDevice` instance in hand, you will be able to stop the tapping by
394
- 1. Manually calling `device.stop!`
395
- 2. Setting stop condition with `device.stop_when`, like
159
+ As you might have noticed, all the objects are converted into strings with `#to_s` instead of `#inspect`. This is because when used on some Rails objects, `#inspect` can generate a significantly larger string than `#to_s`. For example:
396
160
 
397
- ```ruby
398
- device.stop_when do |payload|
399
- device.calls.count >= 10 # stop after gathering 10 calls’ data
400
- end
161
+ ``` ruby
162
+ post.to_s #=> #<Post:0x00007f89a55201d0>
163
+ post.inspect #=> #<Post id: 649, user_id: 3, topic_id: 600, post_number: 1, raw: "Hello world", cooked: "<p>Hello world</p>", created_at: "2020-05-24 08:07:29", updated_at: "2020-05-24 08:07:29", reply_to_post_number: nil, reply_count: 0, quote_count: 0, deleted_at: nil, off_topic_count: 0, like_count: 0, incoming_link_count: 0, bookmark_count: 0, score: nil, reads: 0, post_type: 1, sort_order: 1, last_editor_id: 3, hidden: false, hidden_reason_id: nil, notify_moderators_count: 0, spam_count: 0, illegal_count: 0, inappropriate_count: 0, last_version_at: "2020-05-24 08:07:29", user_deleted: false, reply_to_user_id: nil, percent_rank: 1.0, notify_user_count: 0, like_score: 0, deleted_by_id: nil, edit_reason: nil, word_count: 2, version: 1, cook_method: 1, wiki: false, baked_at: "2020-05-24 08:07:29", baked_version: 2, hidden_at: nil, self_edits: 0, reply_quoted: false, via_email: false, raw_email: nil, public_version: 1, action_code: nil, image_url: nil, locked_by_id: nil, image_upload_id: nil>
401
164
  ```
402
165
 
403
- #### Device states & Managing Devices
404
166
 
405
- Each `TappingDevice` instance can have 3 states:
167
+ ### Lower-Level Helpers
168
+ `print_calls` and `print_traces` aren't the only helpers you can get from `TappingDevice`. They are actually built on top of other helpers, which you can use as well. To know more about them, please check [this page](https://github.com/st0012/tapping_device/wiki/Advance-Usages)
406
169
 
407
- - `Initial` - means the instance is initialized but hasn't tapped on anything.
408
- - `Enabled` - means the instance is tapping on something (has called `tap_*` methods).
409
- - `Disabled` - means the instance has been disabled. It will no longer receive any call info.
410
170
 
411
- When debugging, we may create many device instances and tap objects in several places. Then it'll be quite annoying to manage their states. So `TappingDevice` has several class methods that allows you to manage all `TappingDevice` instances:
171
+ ### Related Blog Posts
172
+ - [Optimize Your Debugging Process With Object-Oriented Tracing and tapping_device](http://bit.ly/object-oriented-tracing)
173
+ - [Debug Rails issues effectively with tapping_device](https://dev.to/st0012/debug-rails-issues-effectively-with-tappingdevice-c7c)
174
+ - [Want to know more about your Rails app? Tap on your objects!](https://dev.to/st0012/want-to-know-more-about-your-rails-app-tap-on-your-objects-bd3)
412
175
 
413
- - `TappingDevice.devices` - Lists all registered devices with `initial` or `enabled` state. Note that any instance that's been stopped will be removed from the list.
414
- - `TappingDevice.stop_all!` - Stops all registered devices and remove them from the `devices` list.
415
- - `TappingDevice.suspend_new!` - Suspends any device instance from changing their state from `initial` to `enabled`. Which means any `tap_*` calls after it will no longer work.
416
- - `TappingDevice.reset!` - Cancels `suspend_new` (if called) and stops/removes all created devices. Useful to reset the environment between test cases.
417
176
 
418
177
  ## Development
419
-
420
178
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
421
179
 
422
180
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
Binary file
Binary file
@@ -2,6 +2,7 @@ require "active_record"
2
2
  require "tapping_device/version"
3
3
  require "tapping_device/manageable"
4
4
  require "tapping_device/payload"
5
+ require "tapping_device/output_payload"
5
6
  require "tapping_device/trackable"
6
7
  require "tapping_device/exceptions"
7
8
  require "tapping_device/sql_tapping_methods"
@@ -25,6 +26,7 @@ class TappingDevice
25
26
  @options = process_options(options)
26
27
  @calls = []
27
28
  @disabled = false
29
+ @with_condition = nil
28
30
  self.class.devices << self
29
31
  end
30
32
 
@@ -49,8 +51,21 @@ class TappingDevice
49
51
  track(record, condition: :tap_associations?)
50
52
  end
51
53
 
52
- def and_print(payload_method)
53
- @output_block = -> (payload) { puts(payload.send(payload_method)) }
54
+ def and_print(payload_method = nil, &block)
55
+ @output_block =
56
+ if block
57
+ -> (output_payload) { puts(block.call(output_payload)) }
58
+ elsif payload_method
59
+ -> (output_payload) { puts(output_payload.send(payload_method)) }
60
+ else
61
+ raise "need to provide either a payload method name or a block"
62
+ end
63
+
64
+ self
65
+ end
66
+
67
+ def with(&block)
68
+ @with_condition = block
54
69
  end
55
70
 
56
71
  def set_block(&block)
@@ -94,6 +109,8 @@ class TappingDevice
94
109
 
95
110
  payload = build_payload(tp: tp, filepath: filepath, line_number: line_number, &payload_block)
96
111
 
112
+ next unless with_condition_satisfied?(payload)
113
+
97
114
  record_call!(payload)
98
115
 
99
116
  stop_if_condition_fulfilled(payload)
@@ -247,7 +264,7 @@ class TappingDevice
247
264
  def record_call!(payload)
248
265
  return if @disabled
249
266
 
250
- @output_block.call(payload) if @output_block
267
+ @output_block.call(OutputPayload.init(payload)) if @output_block
251
268
 
252
269
  if @block
253
270
  root_device.calls << @block.call(payload)
@@ -255,4 +272,8 @@ class TappingDevice
255
272
  root_device.calls << payload
256
273
  end
257
274
  end
275
+
276
+ def with_condition_satisfied?(payload)
277
+ @with_condition.blank? || @with_condition.call(payload)
278
+ end
258
279
  end
@@ -0,0 +1,145 @@
1
+ class TappingDevice
2
+ class OutputPayload < Payload
3
+ alias :raw_arguments :arguments
4
+ alias :raw_return_value :return_value
5
+
6
+ def method_name(options = {})
7
+ ":#{super(options)}"
8
+ end
9
+
10
+ def arguments(options = {})
11
+ generate_string_result(raw_arguments, options[:inspect])
12
+ end
13
+
14
+ def return_value(options = {})
15
+ generate_string_result(raw_return_value, options[:inspect])
16
+ end
17
+
18
+ def self.full_color_code(code)
19
+ end
20
+
21
+ COLOR_CODES = {
22
+ green: 10,
23
+ yellow: 11,
24
+ blue: 12,
25
+ megenta: 13,
26
+ cyan: 14,
27
+ orange: 214
28
+ }
29
+
30
+ COLORS = COLOR_CODES.each_with_object({}) do |(name, code), hash|
31
+ hash[name] = "\u001b[38;5;#{code}m"
32
+ end.merge(
33
+ reset: "\u001b[0m",
34
+ nocolor: ""
35
+ )
36
+
37
+ PAYLOAD_ATTRIBUTES = {
38
+ method_name: {symbol: "", color: COLORS[:blue]},
39
+ location: {symbol: "from:", color: COLORS[:green]},
40
+ sql: {symbol: "QUERIES", color: COLORS[:nocolor]},
41
+ return_value: {symbol: "=>", color: COLORS[:megenta]},
42
+ arguments: {symbol: "<=", color: COLORS[:orange]},
43
+ defined_class: {symbol: "#", color: COLORS[:yellow]}
44
+ }
45
+
46
+ PAYLOAD_ATTRIBUTES.each do |attribute, attribute_options|
47
+ color = attribute_options[:color]
48
+
49
+ alias_method "original_#{attribute}".to_sym, attribute
50
+
51
+ # regenerate attributes with `colorize: true` support
52
+ define_method attribute do |options = {}|
53
+ call_result = send("original_#{attribute}", options)
54
+
55
+ if options[:colorize]
56
+ "#{color}#{call_result}#{COLORS[:reset]}"
57
+ else
58
+ call_result
59
+ end
60
+ end
61
+
62
+ define_method "#{attribute}_with_color" do |options = {}|
63
+ send(attribute, options.merge(colorize: true))
64
+ end
65
+
66
+ PAYLOAD_ATTRIBUTES.each do |and_attribute, and_attribute_options|
67
+ next if and_attribute == attribute
68
+
69
+ define_method "#{attribute}_and_#{and_attribute}" do |options = {}|
70
+ "#{send(attribute, options)} #{and_attribute_options[:symbol]} #{send(and_attribute, options)}"
71
+ end
72
+
73
+ define_method "#{attribute}_and_#{and_attribute}_with_color" do |options = {}|
74
+ send("#{attribute}_and_#{and_attribute}", options.merge(colorize: true))
75
+ end
76
+ end
77
+ end
78
+
79
+ def passed_at(options = {})
80
+ with_method_head = options.fetch(:with_method_head, false)
81
+ arg_name = raw_arguments.keys.detect { |k| raw_arguments[k] == target }
82
+
83
+ return unless arg_name
84
+
85
+ arg_name = ":#{arg_name}"
86
+ arg_name = value_with_color(arg_name, :orange) if options[:colorize]
87
+ msg = "Passed as #{arg_name} in '#{defined_class(options)}##{method_name(options)}' at #{location(options)}"
88
+ msg += "\n > #{method_head.strip}" if with_method_head
89
+ msg
90
+ end
91
+
92
+ def detail_call_info(options = {})
93
+ <<~MSG
94
+ #{method_name_and_defined_class(options)}
95
+ from: #{location(options)}
96
+ <= #{arguments(options)}
97
+ => #{return_value(options)}
98
+
99
+ MSG
100
+ end
101
+
102
+ private
103
+
104
+ def value_with_color(value, color)
105
+ "#{COLORS[color]}#{value}#{COLORS[:reset]}"
106
+ end
107
+
108
+ def generate_string_result(obj, inspect)
109
+ case obj
110
+ when Array
111
+ array_to_string(obj, inspect)
112
+ when Hash
113
+ hash_to_string(obj, inspect)
114
+ when String
115
+ "\"#{obj}\""
116
+ else
117
+ inspect ? obj.inspect : obj.to_s
118
+ end
119
+ end
120
+
121
+ def array_to_string(array, inspect)
122
+ elements_string = array.map do |elem|
123
+ generate_string_result(elem, inspect)
124
+ end.join(", ")
125
+ "[#{elements_string}]"
126
+ end
127
+
128
+ def hash_to_string(hash, inspect)
129
+ elements_string = hash.map do |key, value|
130
+ "#{key.to_s}: #{generate_string_result(value, inspect)}"
131
+ end.join(", ")
132
+ "{#{elements_string}}"
133
+ end
134
+
135
+ def obj_to_string(element, inspect)
136
+ to_string_method = inspect ? :inspect : :to_s
137
+
138
+ if !inspect && element.is_a?(String)
139
+ "\"#{element}\""
140
+ else
141
+ element.send(to_string_method)
142
+ end
143
+ end
144
+ end
145
+ end
@@ -6,7 +6,7 @@ class TappingDevice
6
6
  ]
7
7
 
8
8
  ATTRS.each do |attr|
9
- define_method attr do
9
+ define_method attr do |options = {}|
10
10
  self[attr]
11
11
  end
12
12
  end
@@ -19,86 +19,13 @@ class TappingDevice
19
19
  h
20
20
  end
21
21
 
22
- def passed_at(with_method_head: false)
23
- arg_name = arguments.keys.detect { |k| arguments[k] == target }
24
- return unless arg_name
25
- msg = "Passed as '#{arg_name}' in method ':#{method_name}'"
26
- msg += "\n > #{method_head.strip}" if with_method_head
27
- msg += "\n at #{location}"
28
- msg
29
- end
30
-
31
22
  def method_head
32
23
  source_file, source_line = method_object.source_location
33
24
  IO.readlines(source_file)[source_line-1]
34
25
  end
35
26
 
36
- def location
27
+ def location(options = {})
37
28
  "#{filepath}:#{line_number}"
38
29
  end
39
-
40
- SYMBOLS = {
41
- location: "from:",
42
- sql: "QUERIES",
43
- return_value: "=>",
44
- arguments: "<=",
45
- defined_class: "#"
46
- }
47
-
48
- SYMBOLS.each do |name, symbol|
49
- define_method "method_name_and_#{name}" do
50
- ":#{method_name} #{symbol} #{send(name)}"
51
- end
52
- end
53
-
54
- def detail_call_info(inspect: false)
55
- arguments_output = generate_string_result(arguments, inspect)
56
- return_value_output = generate_string_result(return_value, inspect)
57
-
58
- <<~MSG
59
- #{method_name_and_defined_class}
60
- from: #{location}
61
- <= #{arguments_output}
62
- => #{return_value_output}
63
-
64
- MSG
65
- end
66
-
67
- def generate_string_result(obj, inspect)
68
- case obj
69
- when Array
70
- array_to_string(obj, inspect)
71
- when Hash
72
- hash_to_string(obj, inspect)
73
- when String
74
- "\"#{obj}\""
75
- else
76
- inspect ? obj.inspect : obj.to_s
77
- end
78
- end
79
-
80
- def array_to_string(array, inspect)
81
- elements_string = array.map do |elem|
82
- generate_string_result(elem, inspect)
83
- end.join(", ")
84
- "[#{elements_string}]"
85
- end
86
-
87
- def hash_to_string(hash, inspect)
88
- elements_string = hash.map do |key, value|
89
- "#{key.to_s}: #{generate_string_result(value, inspect)}"
90
- end.join(", ")
91
- "{#{elements_string}}"
92
- end
93
-
94
- def obj_to_string(element, inspect)
95
- to_string_method = inspect ? :inspect : :to_s
96
-
97
- if !inspect && element.is_a?(String)
98
- "\"#{element}\""
99
- else
100
- element.send(to_string_method)
101
- end
102
- end
103
30
  end
104
31
  end
@@ -8,23 +8,24 @@ class TappingDevice
8
8
 
9
9
  def print_traces(target, options = {})
10
10
  options[:event_type] = :call
11
+ inspect = options.delete(:inspect)
12
+ colorize = options.fetch(:colorize, true)
11
13
 
12
- device_1 = tap_on!(target, options) do |payload|
13
- puts("Called #{payload.method_name_and_location}")
14
+ device_1 = tap_on!(target, options).and_print do |output_payload|
15
+ "Called #{output_payload.method_name_and_location(inspect: inspect, colorize: colorize)}"
14
16
  end
15
- device_2 = tap_passed!(target, options) do |payload|
16
- arg_name = payload.arguments.keys.detect { |k| payload.arguments[k] == target }
17
- next unless arg_name
18
- puts("Passed as '#{arg_name}' in '#{payload.defined_class}##{payload.method_name}' at #{payload.location}")
17
+ device_2 = tap_passed!(target, options).and_print do |output_payload|
18
+ output_payload.passed_at(inspect: inspect, colorize: colorize)
19
19
  end
20
20
  [device_1, device_2]
21
21
  end
22
22
 
23
- def print_calls_in_detail(target, options = {})
23
+ def print_calls(target, options = {})
24
24
  inspect = options.delete(:inspect)
25
+ colorize = options.fetch(:colorize, true)
25
26
 
26
- tap_on!(target, options) do |payload|
27
- puts(payload.detail_call_info(inspect: inspect))
27
+ tap_on!(target, options).and_print do |output_payload|
28
+ output_payload.detail_call_info(inspect: inspect, colorize: colorize)
28
29
  end
29
30
  end
30
31
 
@@ -1,3 +1,3 @@
1
1
  class TappingDevice
2
- VERSION = "0.4.11"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["st0012"]
9
9
  spec.email = ["stan001212@gmail.com"]
10
10
 
11
- spec.summary = %q{tapping_device provides useful helpers to intercept method calls}
12
- spec.description = %q{tapping_device provides useful helpers to intercept method calls}
11
+ spec.summary = %q{tapping_device lets you understand what your Ruby objects do without digging into the code}
12
+ spec.description = %q{tapping_device lets you understand what your Ruby objects do without digging into the code}
13
13
  spec.homepage = "https://github.com/st0012/tapping_device"
14
14
  spec.license = "MIT"
15
15
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tapping_device
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.11
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - st0012
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-04-19 00:00:00.000000000 Z
11
+ date: 2020-05-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -122,13 +122,15 @@ dependencies:
122
122
  - - '='
123
123
  - !ruby/object:Gem::Version
124
124
  version: 0.17.1
125
- description: tapping_device provides useful helpers to intercept method calls
125
+ description: tapping_device lets you understand what your Ruby objects do without
126
+ digging into the code
126
127
  email:
127
128
  - stan001212@gmail.com
128
129
  executables: []
129
130
  extensions: []
130
131
  extra_rdoc_files: []
131
132
  files:
133
+ - ".DS_Store"
132
134
  - ".github/workflows/gempush.yml"
133
135
  - ".github/workflows/ruby.yml"
134
136
  - ".gitignore"
@@ -143,9 +145,13 @@ files:
143
145
  - Rakefile
144
146
  - bin/console
145
147
  - bin/setup
148
+ - images/print_calls - single entry.png
149
+ - images/print_calls.png
150
+ - images/print_traces.png
146
151
  - lib/tapping_device.rb
147
152
  - lib/tapping_device/exceptions.rb
148
153
  - lib/tapping_device/manageable.rb
154
+ - lib/tapping_device/output_payload.rb
149
155
  - lib/tapping_device/payload.rb
150
156
  - lib/tapping_device/sql_tapping_methods.rb
151
157
  - lib/tapping_device/trackable.rb
@@ -176,5 +182,6 @@ requirements: []
176
182
  rubygems_version: 3.0.3
177
183
  signing_key:
178
184
  specification_version: 4
179
- summary: tapping_device provides useful helpers to intercept method calls
185
+ summary: tapping_device lets you understand what your Ruby objects do without digging
186
+ into the code
180
187
  test_files: []