tapping_device 0.5.3 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|

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

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