tapping_device 0.5.3 → 0.5.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -2
- data/Gemfile.lock +2 -2
- data/README.md +100 -133
- data/lib/tapping_device.rb +14 -5
- data/lib/tapping_device/configurable.rb +2 -0
- data/lib/tapping_device/output/payload.rb +5 -1
- data/lib/tapping_device/payload.rb +1 -1
- data/lib/tapping_device/trackable.rb +48 -18
- data/lib/tapping_device/trackers/initialization_tracker.rb +8 -0
- data/lib/tapping_device/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 231efa59a44f630df303db52d0d3bb460698c02a116374838e4bad4d71d7f69e
|
4
|
+
data.tar.gz: ca80df8a0ea00350c1e074628425848b5cba4d7f53a7aa4176947d55a3164929
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 77131aab9e85a2661694ee57470b9067ee44d479d574cbe395eefabd7f88350eaf31071c4ed0d117b1608856e9e6beff9d7ccae592a665c8969231425dafdc43
|
7
|
+
data.tar.gz: c06dedd14da7798cf93d80ae0d4866928c9e2522184f453588691fdf1a3220b21cfdf953f04575f1015cc1cb58c5bcb2cb5673fd9384cf5515855d048d0bbdb3
|
data/CHANGELOG.md
CHANGED
@@ -1,16 +1,18 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
## [
|
3
|
+
## [v0.5.3](https://github.com/st0012/tapping_device/tree/v0.5.3) (2020-06-21)
|
4
4
|
|
5
|
-
[Full Changelog](https://github.com/st0012/tapping_device/compare/v0.5.2...
|
5
|
+
[Full Changelog](https://github.com/st0012/tapping_device/compare/v0.5.2...v0.5.3)
|
6
6
|
|
7
7
|
**Closed issues:**
|
8
8
|
|
9
|
+
- Global Configuration [\#46](https://github.com/st0012/tapping_device/issues/46)
|
9
10
|
- Support write\_\* helpers [\#44](https://github.com/st0012/tapping_device/issues/44)
|
10
11
|
- Use Method\#source to replace Payload\#method\_head’s implementation [\#19](https://github.com/st0012/tapping_device/issues/19)
|
11
12
|
|
12
13
|
**Merged pull requests:**
|
13
14
|
|
15
|
+
- Support Global Configuration [\#48](https://github.com/st0012/tapping_device/pull/48) ([st0012](https://github.com/st0012))
|
14
16
|
- Support write\_\* helpers [\#47](https://github.com/st0012/tapping_device/pull/47) ([st0012](https://github.com/st0012))
|
15
17
|
- Hijack attr methods with `hijack\_attr\_methods` option [\#45](https://github.com/st0012/tapping_device/pull/45) ([st0012](https://github.com/st0012))
|
16
18
|
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
tapping_device (0.5.
|
4
|
+
tapping_device (0.5.4)
|
5
5
|
activerecord (>= 5.2)
|
6
6
|
activesupport
|
7
7
|
pry
|
@@ -56,7 +56,7 @@ GEM
|
|
56
56
|
thread_safe (0.3.6)
|
57
57
|
tzinfo (1.2.7)
|
58
58
|
thread_safe (~> 0.1)
|
59
|
-
zeitwerk (2.3.
|
59
|
+
zeitwerk (2.3.1)
|
60
60
|
|
61
61
|
PLATFORMS
|
62
62
|
ruby
|
data/README.md
CHANGED
@@ -8,187 +8,119 @@
|
|
8
8
|
|
9
9
|
|
10
10
|
## Introduction
|
11
|
-
|
11
|
+
As the name states, `TappingDevice` allows you to secretly listen to different events of an object:
|
12
12
|
|
13
|
-
|
13
|
+
- `Method Calls` - what does the object do
|
14
|
+
- `Traces` - how is the object used by the application
|
15
|
+
- `State Mutations` - what happens inside the object
|
14
16
|
|
15
|
-
|
17
|
+
After collecting the events, `TappingDevice` will output them in a nice, readable format to either stdout or a file.
|
16
18
|
|
17
|
-
|
18
|
-
- `print_traces(object)` to see how the object interacts with other objects (like used as an argument)
|
19
|
-
- `print_mutations(object)` to see what actions changed the object's state (instance variables)
|
19
|
+
**Ultimately, its goal is to let you know all the information you need for debugging with just 1 line of code.**
|
20
20
|
|
21
|
-
|
21
|
+
## Usages
|
22
22
|
|
23
|
-
###
|
23
|
+
### Track Method Calls
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
```ruby
|
28
|
-
def create
|
29
|
-
@manager_params = create_params
|
30
|
-
@manager_params[:first_post_checks] = !is_api?
|
31
|
-
|
32
|
-
manager = NewPostManager.new(current_user, @manager_params)
|
33
|
-
|
34
|
-
if is_api?
|
35
|
-
memoized_payload = DistributedMemoizer.memoize(signature_for(@manager_params), 120) do
|
36
|
-
result = manager.perform
|
37
|
-
MultiJson.dump(serialize_data(result, NewPostResultSerializer, root: false))
|
38
|
-
end
|
39
|
-
|
40
|
-
parsed_payload = JSON.parse(memoized_payload)
|
41
|
-
backwards_compatible_json(parsed_payload, parsed_payload['success'])
|
42
|
-
else
|
43
|
-
result = manager.perform
|
44
|
-
json = serialize_data(result, NewPostResultSerializer, root: false)
|
45
|
-
backwards_compatible_json(json, result.success?)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
```
|
49
|
-
|
50
|
-
As you can see, it doesn't even exist in the controller action, which makes tracking it by reading code very hard to do.
|
51
|
-
|
52
|
-
But with `TappingDevice`. You can use `print_calls` to show what method calls the object performs
|
53
|
-
|
54
|
-
```ruby
|
55
|
-
def create
|
56
|
-
# you can retrieve the current guardian object by calling guardian in the controller
|
57
|
-
print_calls(guardian)
|
58
|
-
@manager_params = create_params
|
59
|
-
|
60
|
-
# .....
|
61
|
-
```
|
62
|
-
|
63
|
-
Now, if you execute the code, like via tests:
|
64
|
-
|
65
|
-
```shell
|
66
|
-
$ rspec spec/requests/posts_controller_spec.rb:603
|
67
|
-
```
|
68
|
-
|
69
|
-
You can get all the method calls it performs with basically everything you need to know
|
25
|
+
By tracking an object's method calls, you'll be able to observe the object's behavior very easily
|
70
26
|
|
71
27
|
<img src="https://github.com/st0012/tapping_device/blob/master/images/print_calls.png" alt="image of print_calls output" width="50%">
|
72
28
|
|
73
|
-
|
74
|
-
- method name
|
75
|
-
-
|
29
|
+
Each entry consists of 5 pieces of information:
|
30
|
+
- method name
|
31
|
+
- source of the method
|
76
32
|
- call site
|
77
33
|
- arguments
|
78
34
|
- return value
|
79
35
|
|
80
36
|
![explanation of individual entry](https://github.com/st0012/tapping_device/blob/master/images/print_calls%20-%20single%20entry.png)
|
81
37
|
|
82
|
-
|
38
|
+
#### Helpers
|
83
39
|
|
40
|
+
- `print_calls(object)` - prints the result to stdout
|
41
|
+
- `write_calls(object, log_file: "file_name")` - writes the result to a file
|
42
|
+
- the default file is `/tmp/tapping_device.log`, but you can change it with `log_file: "new_path"` option
|
84
43
|
|
85
|
-
|
44
|
+
#### Use Cases
|
45
|
+
- Understand a service object/form object's behavior
|
46
|
+
- Debug a messy controller
|
86
47
|
|
87
|
-
|
48
|
+
### Track Traces
|
88
49
|
|
89
|
-
|
90
|
-
def create
|
91
|
-
@manager_params = create_params
|
92
|
-
@manager_params[:first_post_checks] = !is_api?
|
93
|
-
|
94
|
-
manager = NewPostManager.new(current_user, @manager_params)
|
95
|
-
|
96
|
-
print_traces(manager)
|
97
|
-
# .....
|
98
|
-
```
|
50
|
+
By tracking an object's traces, you'll be able to observe the object's journey in your application
|
99
51
|
|
100
|
-
|
52
|
+
![image of print_traces output](https://github.com/st0012/tapping_device/blob/master/images/print_traces.png)
|
101
53
|
|
102
|
-
|
103
|
-
$ rspec spec/requests/posts_controller_spec.rb:603
|
104
|
-
```
|
54
|
+
#### Helpers
|
105
55
|
|
106
|
-
|
56
|
+
- `print_traces(object)` - prints the result to stdout
|
57
|
+
- `write_traces(object, log_file: "file_name")` - writes the result to a file
|
58
|
+
- the default file is `/tmp/tapping_device.log`, but you can change it with `log_file: "new_path"` option
|
107
59
|
|
108
|
-
|
60
|
+
#### Use Cases
|
61
|
+
- Debug argument related issues
|
62
|
+
- Understand how a library uses your objects
|
109
63
|
|
110
|
-
###
|
64
|
+
### Track State Mutations
|
111
65
|
|
112
|
-
|
66
|
+
By tracking an object's traces, you'll be able to observe the state changes happen inside the object between each method call
|
113
67
|
|
114
|
-
|
68
|
+
<img src="https://github.com/st0012/tapping_device/blob/master/images/print_mutations.png" alt="image of print_mutations output" width="50%">
|
115
69
|
|
116
|
-
|
117
|
-
# app/controllers/posts_controller.rb
|
118
|
-
class PostsController
|
119
|
-
def update
|
120
|
-
# ......
|
121
|
-
revisor = PostRevisor.new(post, topic)
|
122
|
-
revisor.revise!(current_user, changes, opts)
|
123
|
-
# ......
|
124
|
-
end
|
125
|
-
end
|
126
|
-
```
|
70
|
+
#### Helpers
|
127
71
|
|
128
|
-
|
72
|
+
- `print_mutations(object)` - prints the result to stdout
|
73
|
+
- `write_mutations(object, log_file: "file_name")` - writes the result to a file
|
74
|
+
- the default file is `/tmp/tapping_device.log`, but you can change it with `log_file: "new_path"` option
|
129
75
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
@editor = editor
|
134
|
-
@fields = fields.with_indifferent_access
|
135
|
-
@opts = opts
|
76
|
+
#### Use Cases
|
77
|
+
- Debug state related issues
|
78
|
+
- Debug memoization issues
|
136
79
|
|
137
|
-
|
138
|
-
|
139
|
-
# ......
|
80
|
+
### Track All Instances Of A Class
|
140
81
|
|
141
|
-
|
142
|
-
@last_version_at = @post.last_version_at || Time.now
|
82
|
+
It's not always easy to directly access the objects we want to track, especially when they're managed by a library (e.g. `ActiveRecord::Relation`). In such cases, you can use these helpers to track the class's instances:
|
143
83
|
|
144
|
-
|
145
|
-
|
84
|
+
- `print_instance_calls(ObjectKlass)`
|
85
|
+
- `print_instance_traces(ObjectKlass)`
|
86
|
+
- `print_instance_mutations(ObjectKlass)`
|
87
|
+
- `write_instance_calls(ObjectKlass)`
|
88
|
+
- `write_instance_traces(ObjectKlass)`
|
89
|
+
- `write_instance_mutations(ObjectKlass)`
|
146
90
|
|
147
|
-
@validate_post = true
|
148
|
-
# ......
|
149
|
-
end
|
150
|
-
```
|
151
91
|
|
152
|
-
|
92
|
+
### Use `with_HELPER_NAME` for chained method calls
|
153
93
|
|
154
|
-
|
94
|
+
In Ruby programs, we often chain multiple methods together like this:
|
155
95
|
|
156
96
|
```ruby
|
157
|
-
|
158
|
-
class PostsController
|
159
|
-
def update
|
160
|
-
# ......
|
161
|
-
revisor = PostRevisor.new(post, topic)
|
162
|
-
print_mutations(revisor)
|
163
|
-
revisor.revise!(current_user, changes, opts)
|
164
|
-
# ......
|
165
|
-
end
|
166
|
-
end
|
97
|
+
SomeService.new(params).perform
|
167
98
|
```
|
168
99
|
|
169
|
-
And
|
100
|
+
And to debug it, we'll need to break the method chain into
|
170
101
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
102
|
+
```ruby
|
103
|
+
service = SomeService.new(params)
|
104
|
+
print_calls(service, options)
|
105
|
+
service.perform
|
106
|
+
```
|
176
107
|
|
177
|
-
|
108
|
+
This kind of code changes are usually annoying, and that's one of the problems I want to solve with `TappingDevice`.
|
178
109
|
|
179
|
-
|
110
|
+
So here's another option, just insert a `with_HELPER_NAME` call in between:
|
180
111
|
|
181
|
-
|
182
|
-
|
183
|
-
|
112
|
+
```ruby
|
113
|
+
SomeService.new(params).with_print_calls(options).perform
|
114
|
+
```
|
184
115
|
|
185
|
-
|
116
|
+
And it'll behave exactly like
|
186
117
|
|
187
118
|
```ruby
|
188
|
-
|
119
|
+
service = SomeService.new(params)
|
120
|
+
print_calls(service, options)
|
121
|
+
service.perform
|
189
122
|
```
|
190
123
|
|
191
|
-
|
192
124
|
## Installation
|
193
125
|
Add this line to your application's Gemfile:
|
194
126
|
|
@@ -296,6 +228,40 @@ The default is `false` because
|
|
296
228
|
2. It's still unclear if this hack safe enough for most applications.
|
297
229
|
|
298
230
|
|
231
|
+
#### `ignore_private`
|
232
|
+
|
233
|
+
Sometimes we use many private methods to perform trivial operations, like
|
234
|
+
|
235
|
+
```ruby
|
236
|
+
class Operation
|
237
|
+
def extras
|
238
|
+
dig_attribute("extras")
|
239
|
+
end
|
240
|
+
|
241
|
+
private
|
242
|
+
|
243
|
+
def data
|
244
|
+
@data
|
245
|
+
end
|
246
|
+
|
247
|
+
def dig_attribute(attr)
|
248
|
+
data.dig("attributes", attr)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
```
|
252
|
+
|
253
|
+
And we may not be interested in those method calls. If that's the case, you can use the `ignore_private` option
|
254
|
+
|
255
|
+
```ruby
|
256
|
+
operation = Operation.new(params)
|
257
|
+
print_calls(operation, ignore_private: true) #=> only prints the `extras` call
|
258
|
+
```
|
259
|
+
|
260
|
+
#### `only_private`
|
261
|
+
|
262
|
+
This option does the opposite of the `ignore_private` option does.
|
263
|
+
|
264
|
+
|
299
265
|
### Global Configuration
|
300
266
|
|
301
267
|
If you don't want to pass options every time you use a helper, you can use global configuration to change the default values:
|
@@ -341,3 +307,4 @@ The gem is available as open-source under the terms of the [MIT License](https:/
|
|
341
307
|
## Code of Conduct
|
342
308
|
|
343
309
|
Everyone interacting in the TappingDevice project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/tapping_device/blob/master/CODE_OF_CONDUCT.md).
|
310
|
+
|
data/lib/tapping_device.rb
CHANGED
@@ -34,7 +34,7 @@ class TappingDevice
|
|
34
34
|
def initialize(options = {}, &block)
|
35
35
|
@block = block
|
36
36
|
@output_block = nil
|
37
|
-
@options = process_options(options)
|
37
|
+
@options = process_options(options.dup)
|
38
38
|
@calls = []
|
39
39
|
@disabled = false
|
40
40
|
@with_condition = nil
|
@@ -94,15 +94,19 @@ class TappingDevice
|
|
94
94
|
private
|
95
95
|
|
96
96
|
def build_minimum_trace_point(event_type:)
|
97
|
-
TracePoint.new(event_type) do |tp|
|
97
|
+
TracePoint.new(*event_type) do |tp|
|
98
98
|
next unless filter_condition_satisfied?(tp)
|
99
|
-
next if is_tapping_device_call?(tp)
|
100
99
|
|
101
100
|
filepath, line_number = get_call_location(tp)
|
102
101
|
payload = build_payload(tp: tp, filepath: filepath, line_number: line_number)
|
103
102
|
|
104
|
-
|
105
|
-
|
103
|
+
unless @options[:force_recording]
|
104
|
+
next if is_tapping_device_call?(tp)
|
105
|
+
next if should_be_skipped_by_paths?(filepath)
|
106
|
+
next unless with_condition_satisfied?(payload)
|
107
|
+
next if payload.is_private_call? && @options[:ignore_private]
|
108
|
+
next if !payload.is_private_call? && @options[:only_private]
|
109
|
+
end
|
106
110
|
|
107
111
|
yield(payload)
|
108
112
|
end
|
@@ -148,6 +152,7 @@ class TappingDevice
|
|
148
152
|
line_number: line_number,
|
149
153
|
defined_class: tp.defined_class,
|
150
154
|
trace: get_traces(tp),
|
155
|
+
is_private_call?: tp.defined_class.private_method_defined?(tp.callee_id),
|
151
156
|
tp: tp
|
152
157
|
})
|
153
158
|
end
|
@@ -204,6 +209,10 @@ class TappingDevice
|
|
204
209
|
options[:event_type] ||= config[:event_type]
|
205
210
|
options[:hijack_attr_methods] ||= config[:hijack_attr_methods]
|
206
211
|
options[:track_as_records] ||= config[:track_as_records]
|
212
|
+
options[:ignore_private] ||= config[:ignore_private]
|
213
|
+
options[:only_private] ||= config[:only_private]
|
214
|
+
# for debugging the gem more easily
|
215
|
+
options[:force_recording] ||= false
|
207
216
|
|
208
217
|
options[:descendants] ||= []
|
209
218
|
options[:root_device] ||= self
|
@@ -2,7 +2,7 @@ class TappingDevice
|
|
2
2
|
class Payload < Hash
|
3
3
|
ATTRS = [
|
4
4
|
:target, :receiver, :method_name, :method_object, :arguments, :return_value, :filepath, :line_number,
|
5
|
-
:defined_class, :trace, :tp, :ivar_changes
|
5
|
+
:defined_class, :trace, :tp, :ivar_changes, :is_private_call?
|
6
6
|
]
|
7
7
|
|
8
8
|
ATTRS.each do |attr|
|
@@ -20,28 +20,29 @@ class TappingDevice
|
|
20
20
|
TappingDevice::Trackers::MutationTracker.new(options, &block).track(object)
|
21
21
|
end
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
[:calls, :traces, :mutations].each do |subject|
|
24
|
+
[:print, :write].each do |output_action|
|
25
|
+
helper_method_name = "#{output_action}_#{subject}"
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
|
27
|
+
define_method helper_method_name do |target, options = {}|
|
28
|
+
send("output_#{subject}", target, options, output_action: "and_#{output_action}")
|
29
|
+
end
|
30
30
|
|
31
|
-
|
32
|
-
|
33
|
-
|
31
|
+
define_method "with_#{helper_method_name}" do |options = {}|
|
32
|
+
send(helper_method_name, self, options)
|
33
|
+
self
|
34
|
+
end
|
34
35
|
|
35
|
-
|
36
|
-
|
37
|
-
end
|
36
|
+
define_method "#{output_action}_instance_#{subject}" do |target_klass, options = {}|
|
37
|
+
collection_proxy = AsyncCollectionProxy.new
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
|
39
|
+
tap_init!(target_klass, options) do |payload|
|
40
|
+
collection_proxy << send(helper_method_name, payload.return_value, options)
|
41
|
+
end
|
42
42
|
|
43
|
-
|
44
|
-
|
43
|
+
collection_proxy
|
44
|
+
end
|
45
|
+
end
|
45
46
|
end
|
46
47
|
|
47
48
|
private
|
@@ -84,12 +85,15 @@ class TappingDevice
|
|
84
85
|
[options, output_options]
|
85
86
|
end
|
86
87
|
|
88
|
+
# CollectionProxy delegates chained actions to multiple devices
|
87
89
|
class CollectionProxy
|
90
|
+
CHAINABLE_ACTIONS = [:stop!, :stop_when, :with]
|
91
|
+
|
88
92
|
def initialize(devices)
|
89
93
|
@devices = devices
|
90
94
|
end
|
91
95
|
|
92
|
-
|
96
|
+
CHAINABLE_ACTIONS.each do |method|
|
93
97
|
define_method method do |&block|
|
94
98
|
@devices.each do |device|
|
95
99
|
device.send(method, &block)
|
@@ -97,6 +101,32 @@ class TappingDevice
|
|
97
101
|
end
|
98
102
|
end
|
99
103
|
end
|
104
|
+
|
105
|
+
# AsyncCollectionProxy delegates chained actions to multiple device "asyncronously"
|
106
|
+
# when we use tapping methods like `tap_init!` to create sub-devices
|
107
|
+
# we need to find a way to pass the chained actions to every sub-device that's created
|
108
|
+
# and this can only happen asyncronously as we won't know when'll that happen
|
109
|
+
class AsyncCollectionProxy < CollectionProxy
|
110
|
+
def initialize(devices = [])
|
111
|
+
super
|
112
|
+
@blocks = {}
|
113
|
+
end
|
114
|
+
|
115
|
+
CHAINABLE_ACTIONS.each do |method|
|
116
|
+
define_method method do |&block|
|
117
|
+
super(&block)
|
118
|
+
@blocks[method] = block
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def <<(device)
|
123
|
+
@devices << device
|
124
|
+
|
125
|
+
@blocks.each do |method, block|
|
126
|
+
device.send(method, &block)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
100
130
|
end
|
101
131
|
end
|
102
132
|
|
@@ -1,6 +1,14 @@
|
|
1
1
|
class TappingDevice
|
2
2
|
module Trackers
|
3
3
|
class InitializationTracker < TappingDevice
|
4
|
+
def initialize(options = {}, &block)
|
5
|
+
super
|
6
|
+
event_type = @options[:event_type]
|
7
|
+
# if a class doesn't override the 'initialize' method
|
8
|
+
# Class.new will only trigger c_return or c_call
|
9
|
+
@options[:event_type] = [event_type, "c_#{event_type}"]
|
10
|
+
end
|
11
|
+
|
4
12
|
def build_payload(tp:, filepath:, line_number:)
|
5
13
|
payload = super
|
6
14
|
payload[:return_value] = payload[:receiver]
|
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.5.
|
4
|
+
version: 0.5.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- st0012
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-07-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|