object_tracer 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.github/workflows/gempush.yml +28 -0
- data/.github/workflows/ruby.yml +59 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +296 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +20 -0
- data/LICENSE.txt +21 -0
- data/Makefile +3 -0
- data/README.md +310 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/images/print_calls - single entry.png +0 -0
- data/images/print_calls.png +0 -0
- data/images/print_mutations.png +0 -0
- data/images/print_traces.png +0 -0
- data/lib/object_tracer.rb +258 -0
- data/lib/object_tracer/configuration.rb +34 -0
- data/lib/object_tracer/exceptions.rb +16 -0
- data/lib/object_tracer/manageable.rb +37 -0
- data/lib/object_tracer/method_hijacker.rb +55 -0
- data/lib/object_tracer/output.rb +41 -0
- data/lib/object_tracer/output/payload_wrapper.rb +186 -0
- data/lib/object_tracer/output/writer.rb +22 -0
- data/lib/object_tracer/payload.rb +40 -0
- data/lib/object_tracer/trackable.rb +133 -0
- data/lib/object_tracer/trackers/association_call_tracker.rb +17 -0
- data/lib/object_tracer/trackers/initialization_tracker.rb +53 -0
- data/lib/object_tracer/trackers/method_call_tracker.rb +9 -0
- data/lib/object_tracer/trackers/mutation_tracker.rb +111 -0
- data/lib/object_tracer/trackers/passed_tracker.rb +16 -0
- data/lib/object_tracer/version.rb +3 -0
- data/object_tracer.gemspec +29 -0
- metadata +113 -0
data/Gemfile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
rails_version = ENV["RAILS_VERSION"]
|
6
|
+
rails_version = "6.1.0" if rails_version.nil?
|
7
|
+
|
8
|
+
if rails_version.to_f < 6
|
9
|
+
gem "sqlite3", "~> 1.3.0"
|
10
|
+
else
|
11
|
+
gem "sqlite3"
|
12
|
+
end
|
13
|
+
|
14
|
+
gem "activerecord", "~> #{rails_version}"
|
15
|
+
|
16
|
+
gem "rake", "~> 13.0"
|
17
|
+
gem "rspec", "~> 3.0"
|
18
|
+
gem "simplecov", "~> 0.17.1"
|
19
|
+
gem "database_cleaner", "~> 2.0.0"
|
20
|
+
gem "pry"
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2019 st0012
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/Makefile
ADDED
data/README.md
ADDED
@@ -0,0 +1,310 @@
|
|
1
|
+
# ObjectTracer (previously called TappingDevice)
|
2
|
+
|
3
|
+
![GitHub Action](https://github.com/st0012/object_tracer/workflows/Ruby/badge.svg)
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/object_tracer.svg)](https://badge.fury.io/rb/object_tracer)
|
5
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/3e3732a6983785bccdbd/maintainability)](https://codeclimate.com/github/st0012/object_tracer/maintainability)
|
6
|
+
[![Test Coverage](https://api.codeclimate.com/v1/badges/3e3732a6983785bccdbd/test_coverage)](https://codeclimate.com/github/st0012/object_tracer/test_coverage)
|
7
|
+
[![Open Source Helpers](https://www.codetriage.com/st0012/object_tracer/badges/users.svg)](https://www.codetriage.com/st0012/object_tracer)
|
8
|
+
|
9
|
+
|
10
|
+
## Introduction
|
11
|
+
As the name states, `ObjectTracer` allows you to secretly listen to different events of an object:
|
12
|
+
|
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
|
16
|
+
|
17
|
+
After collecting the events, `ObjectTracer` will output them in a nice, readable format to either stdout or a file.
|
18
|
+
|
19
|
+
**Ultimately, its goal is to let you know all the information you need for debugging with just 1 line of code.**
|
20
|
+
|
21
|
+
## Usages
|
22
|
+
|
23
|
+
### Track Method Calls
|
24
|
+
|
25
|
+
By tracking an object's method calls, you'll be able to observe the object's behavior very easily
|
26
|
+
|
27
|
+
<img src="https://github.com/st0012/object_tracer/blob/master/images/print_calls.png" alt="image of print_calls output" width="50%">
|
28
|
+
|
29
|
+
Each entry consists of 5 pieces of information:
|
30
|
+
- method name
|
31
|
+
- source of the method
|
32
|
+
- call site
|
33
|
+
- arguments
|
34
|
+
- return value
|
35
|
+
|
36
|
+
![explanation of individual entry](https://github.com/st0012/object_tracer/blob/master/images/print_calls%20-%20single%20entry.png)
|
37
|
+
|
38
|
+
#### Helpers
|
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/object_tracer.log`, but you can change it with `log_file: "new_path"` option
|
43
|
+
|
44
|
+
#### Use Cases
|
45
|
+
- Understand a service object/form object's behavior
|
46
|
+
- Debug a messy controller
|
47
|
+
|
48
|
+
### Track Traces
|
49
|
+
|
50
|
+
By tracking an object's traces, you'll be able to observe the object's journey in your application
|
51
|
+
|
52
|
+
![image of print_traces output](https://github.com/st0012/object_tracer/blob/master/images/print_traces.png)
|
53
|
+
|
54
|
+
#### Helpers
|
55
|
+
|
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/object_tracer.log`, but you can change it with `log_file: "new_path"` option
|
59
|
+
|
60
|
+
#### Use Cases
|
61
|
+
- Debug argument related issues
|
62
|
+
- Understand how a library uses your objects
|
63
|
+
|
64
|
+
### Track State Mutations
|
65
|
+
|
66
|
+
By tracking an object's traces, you'll be able to observe the state changes happen inside the object between each method call
|
67
|
+
|
68
|
+
<img src="https://github.com/st0012/object_tracer/blob/master/images/print_mutations.png" alt="image of print_mutations output" width="50%">
|
69
|
+
|
70
|
+
#### Helpers
|
71
|
+
|
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/object_tracer.log`, but you can change it with `log_file: "new_path"` option
|
75
|
+
|
76
|
+
#### Use Cases
|
77
|
+
- Debug state related issues
|
78
|
+
- Debug memoization issues
|
79
|
+
|
80
|
+
### Track All Instances Of A Class
|
81
|
+
|
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:
|
83
|
+
|
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)`
|
90
|
+
|
91
|
+
|
92
|
+
### Use `with_HELPER_NAME` for chained method calls
|
93
|
+
|
94
|
+
In Ruby programs, we often chain multiple methods together like this:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
SomeService.new(params).perform
|
98
|
+
```
|
99
|
+
|
100
|
+
And to debug it, we'll need to break the method chain into
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
service = SomeService.new(params)
|
104
|
+
print_calls(service, options)
|
105
|
+
service.perform
|
106
|
+
```
|
107
|
+
|
108
|
+
This kind of code changes are usually annoying, and that's one of the problems I want to solve with `ObjectTracer`.
|
109
|
+
|
110
|
+
So here's another option, just insert a `with_HELPER_NAME` call in between:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
SomeService.new(params).with_print_calls(options).perform
|
114
|
+
```
|
115
|
+
|
116
|
+
And it'll behave exactly like
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
service = SomeService.new(params)
|
120
|
+
print_calls(service, options)
|
121
|
+
service.perform
|
122
|
+
```
|
123
|
+
|
124
|
+
## Installation
|
125
|
+
Add this line to your application's Gemfile:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
gem 'object_tracer', group: :development
|
129
|
+
```
|
130
|
+
|
131
|
+
And then execute:
|
132
|
+
|
133
|
+
```
|
134
|
+
$ bundle
|
135
|
+
```
|
136
|
+
|
137
|
+
Or install it directly:
|
138
|
+
|
139
|
+
```
|
140
|
+
$ gem install object_tracer
|
141
|
+
```
|
142
|
+
|
143
|
+
**Depending on the size of your application, `ObjectTracer` could harm the performance significantly. So make sure you don't put it inside the production group**
|
144
|
+
|
145
|
+
|
146
|
+
## Advance Usages & Options
|
147
|
+
|
148
|
+
### Add Conditions With `.with`
|
149
|
+
|
150
|
+
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.
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
# only prints calls with name matches /foo/
|
154
|
+
print_calls(object).with do |payload|
|
155
|
+
payload.method_name.to_s.match?(/foo/)
|
156
|
+
end
|
157
|
+
```
|
158
|
+
|
159
|
+
### Options
|
160
|
+
|
161
|
+
There are many options you can pass when using a helper method. You can list all available options and their default value with
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
ObjectTracer::Configurable::DEFAULTS #=> {
|
165
|
+
:filter_by_paths=>[],
|
166
|
+
:exclude_by_paths=>[],
|
167
|
+
:with_trace_to=>50,
|
168
|
+
:event_type=>:return,
|
169
|
+
:hijack_attr_methods=>false,
|
170
|
+
:track_as_records=>false,
|
171
|
+
:inspect=>false,
|
172
|
+
:colorize=>true,
|
173
|
+
:log_file=>"/tmp/object_tracer.log"
|
174
|
+
}
|
175
|
+
```
|
176
|
+
|
177
|
+
Here are some commonly used options:
|
178
|
+
|
179
|
+
#### `colorize: false`
|
180
|
+
|
181
|
+
- default: `true`
|
182
|
+
|
183
|
+
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.
|
184
|
+
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
print_calls(object, colorize: false)
|
188
|
+
```
|
189
|
+
|
190
|
+
|
191
|
+
#### `inspect: true`
|
192
|
+
|
193
|
+
- default: `false`
|
194
|
+
|
195
|
+
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:
|
196
|
+
|
197
|
+
``` ruby
|
198
|
+
post.to_s #=> #<Post:0x00007f89a55201d0>
|
199
|
+
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>
|
200
|
+
```
|
201
|
+
|
202
|
+
#### `hijack_attr_methods: true`
|
203
|
+
|
204
|
+
- default: `false`
|
205
|
+
- except for `tap_mutation!` and `print_mutations`
|
206
|
+
|
207
|
+
Because `TracePoint` doesn't track methods generated by `attr_*` helpers (see [this issue](https://bugs.ruby-lang.org/issues/16383) for more info), we need to redefine those methods with the normal method definition.
|
208
|
+
|
209
|
+
For example, it generates
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
def name=(val)
|
213
|
+
@name = val
|
214
|
+
end
|
215
|
+
```
|
216
|
+
|
217
|
+
for
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
attr_writer :name
|
221
|
+
```
|
222
|
+
|
223
|
+
This hack will only be applied to the target instance with `instance_eval`. So other instances of the class remain untouched.
|
224
|
+
|
225
|
+
The default is `false` because
|
226
|
+
|
227
|
+
1. Checking what methods are generated by `attr_*` helpers isn't free. It's an `O(n)` operation, where `n` is the number of methods the target object has.
|
228
|
+
2. It's still unclear if this hack safe enough for most applications.
|
229
|
+
|
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
|
+
|
265
|
+
### Global Configuration
|
266
|
+
|
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:
|
268
|
+
|
269
|
+
```ruby
|
270
|
+
ObjectTracer.config[:colorize] = false
|
271
|
+
ObjectTracer.config[:hijack_attr_methods] = true
|
272
|
+
```
|
273
|
+
|
274
|
+
And if you're using Rails, you can put the configs under `config/initializers/object_tracer.rb` like this:
|
275
|
+
|
276
|
+
```ruby
|
277
|
+
if defined?(ObjectTracer)
|
278
|
+
ObjectTracer.config[:colorize] = false
|
279
|
+
ObjectTracer.config[:hijack_attr_methods] = true
|
280
|
+
end
|
281
|
+
```
|
282
|
+
|
283
|
+
|
284
|
+
### Lower-Level Helpers
|
285
|
+
`print_calls` and `print_traces` aren't the only helpers you can get from `ObjectTracer`. 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/object_tracer/wiki/Advance-Usages)
|
286
|
+
|
287
|
+
|
288
|
+
### Related Blog Posts
|
289
|
+
- [Optimize Your Debugging Process With Object-Oriented Tracing and object_tracer](http://bit.ly/object-oriented-tracing)
|
290
|
+
- [Debug Rails issues effectively with object_tracer](https://dev.to/st0012/debug-rails-issues-effectively-with-tappingdevice-c7c)
|
291
|
+
- [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)
|
292
|
+
|
293
|
+
|
294
|
+
## Development
|
295
|
+
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.
|
296
|
+
|
297
|
+
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).
|
298
|
+
|
299
|
+
## Contributing
|
300
|
+
|
301
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/st0012/object_tracer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
302
|
+
|
303
|
+
## License
|
304
|
+
|
305
|
+
The gem is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
306
|
+
|
307
|
+
## Code of Conduct
|
308
|
+
|
309
|
+
Everyone interacting in the ObjectTracer project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [code of conduct](https://github.com/st0012/object_tracer/blob/master/CODE_OF_CONDUCT.md).
|
310
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "object_tracer"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,258 @@
|
|
1
|
+
require "method_source" # for using Method#source
|
2
|
+
|
3
|
+
require "object_tracer/version"
|
4
|
+
require "object_tracer/manageable"
|
5
|
+
require "object_tracer/payload"
|
6
|
+
require "object_tracer/output"
|
7
|
+
require "object_tracer/trackable"
|
8
|
+
require "object_tracer/configuration"
|
9
|
+
require "object_tracer/exceptions"
|
10
|
+
require "object_tracer/method_hijacker"
|
11
|
+
require "object_tracer/trackers/initialization_tracker"
|
12
|
+
require "object_tracer/trackers/passed_tracker"
|
13
|
+
require "object_tracer/trackers/association_call_tracker"
|
14
|
+
require "object_tracer/trackers/method_call_tracker"
|
15
|
+
require "object_tracer/trackers/mutation_tracker"
|
16
|
+
|
17
|
+
class ObjectTracer
|
18
|
+
|
19
|
+
CALLER_START_POINT = 3
|
20
|
+
C_CALLER_START_POINT = 2
|
21
|
+
|
22
|
+
attr_reader :options, :calls, :trace_point, :target
|
23
|
+
|
24
|
+
@devices = []
|
25
|
+
@suspend_new = false
|
26
|
+
|
27
|
+
extend Manageable
|
28
|
+
|
29
|
+
include Output::Helpers
|
30
|
+
|
31
|
+
def initialize(options = {}, &block)
|
32
|
+
@block = block
|
33
|
+
@output_block = nil
|
34
|
+
@options = process_options(options.dup)
|
35
|
+
@calls = []
|
36
|
+
@disabled = false
|
37
|
+
@with_condition = nil
|
38
|
+
ObjectTracer.devices << self
|
39
|
+
end
|
40
|
+
|
41
|
+
def with(&block)
|
42
|
+
@with_condition = block
|
43
|
+
end
|
44
|
+
|
45
|
+
def set_block(&block)
|
46
|
+
@block = block
|
47
|
+
end
|
48
|
+
|
49
|
+
def stop!
|
50
|
+
@disabled = true
|
51
|
+
ObjectTracer.delete_device(self)
|
52
|
+
end
|
53
|
+
|
54
|
+
def stop_when(&block)
|
55
|
+
@stop_when = block
|
56
|
+
end
|
57
|
+
|
58
|
+
def create_child_device
|
59
|
+
new_device = self.class.new(@options.merge(root_device: root_device), &@block)
|
60
|
+
new_device.stop_when(&@stop_when)
|
61
|
+
new_device.instance_variable_set(:@target, @target)
|
62
|
+
self.descendants << new_device
|
63
|
+
new_device
|
64
|
+
end
|
65
|
+
|
66
|
+
def root_device
|
67
|
+
options[:root_device]
|
68
|
+
end
|
69
|
+
|
70
|
+
def descendants
|
71
|
+
options[:descendants]
|
72
|
+
end
|
73
|
+
|
74
|
+
def track(object)
|
75
|
+
@target = object
|
76
|
+
validate_target!
|
77
|
+
|
78
|
+
MethodHijacker.new(@target).hijack_methods! if options[:hijack_attr_methods]
|
79
|
+
|
80
|
+
@trace_point = build_minimum_trace_point(event_type: options[:event_type]) do |payload|
|
81
|
+
record_call!(payload)
|
82
|
+
|
83
|
+
stop_if_condition_fulfilled!(payload)
|
84
|
+
end
|
85
|
+
|
86
|
+
@trace_point.enable unless ObjectTracer.suspend_new
|
87
|
+
|
88
|
+
self
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def build_minimum_trace_point(event_type:)
|
94
|
+
TracePoint.new(*event_type) do |tp|
|
95
|
+
next unless filter_condition_satisfied?(tp)
|
96
|
+
|
97
|
+
filepath, line_number = get_call_location(tp)
|
98
|
+
payload = build_payload(tp: tp, filepath: filepath, line_number: line_number)
|
99
|
+
|
100
|
+
unless @options[:force_recording]
|
101
|
+
next if is_object_tracer_call?(tp)
|
102
|
+
next if should_be_skipped_by_paths?(filepath)
|
103
|
+
next unless with_condition_satisfied?(payload)
|
104
|
+
next if payload.is_private_call? && @options[:ignore_private]
|
105
|
+
next if !payload.is_private_call? && @options[:only_private]
|
106
|
+
end
|
107
|
+
|
108
|
+
yield(payload)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def validate_target!; end
|
113
|
+
|
114
|
+
def filter_condition_satisfied?(tp)
|
115
|
+
false
|
116
|
+
end
|
117
|
+
|
118
|
+
# this needs to be placed upfront so we can exclude noise before doing more work
|
119
|
+
def should_be_skipped_by_paths?(filepath)
|
120
|
+
exclude_by_paths = options[:exclude_by_paths]
|
121
|
+
filter_by_paths = options[:filter_by_paths]
|
122
|
+
exclude_by_paths.any? { |pattern| pattern.match?(filepath) } ||
|
123
|
+
(filter_by_paths && !filter_by_paths.empty? && !filter_by_paths.any? { |pattern| pattern.match?(filepath) })
|
124
|
+
end
|
125
|
+
|
126
|
+
def is_object_tracer_call?(tp)
|
127
|
+
if tp.defined_class == ObjectTracer::Trackable || tp.defined_class == ObjectTracer
|
128
|
+
return true
|
129
|
+
end
|
130
|
+
|
131
|
+
if Module.respond_to?(:module_parents)
|
132
|
+
tp.defined_class.module_parents.include?(ObjectTracer)
|
133
|
+
elsif Module.respond_to?(:parents)
|
134
|
+
tp.defined_class.parents.include?(ObjectTracer)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def with_condition_satisfied?(payload)
|
139
|
+
@with_condition.nil? || @with_condition.call(payload)
|
140
|
+
end
|
141
|
+
|
142
|
+
def build_payload(tp:, filepath:, line_number:)
|
143
|
+
Payload.new(
|
144
|
+
target: @target,
|
145
|
+
receiver: tp.self,
|
146
|
+
method_name: tp.callee_id,
|
147
|
+
method_object: get_method_object_from(tp.self, tp.callee_id),
|
148
|
+
arguments: collect_arguments(tp),
|
149
|
+
return_value: (tp.return_value rescue nil),
|
150
|
+
filepath: filepath,
|
151
|
+
line_number: line_number,
|
152
|
+
defined_class: tp.defined_class,
|
153
|
+
trace: get_traces(tp),
|
154
|
+
is_private_call: tp.defined_class.private_method_defined?(tp.callee_id),
|
155
|
+
tag: options[:tag],
|
156
|
+
tp: tp
|
157
|
+
)
|
158
|
+
end
|
159
|
+
|
160
|
+
def get_method_object_from(target, method_name)
|
161
|
+
Object.instance_method(:method).bind(target).call(method_name)
|
162
|
+
rescue NameError
|
163
|
+
# if any part of the program uses Refinement to extend its methods
|
164
|
+
# we might still get NoMethodError when trying to get that method outside the scope
|
165
|
+
nil
|
166
|
+
end
|
167
|
+
|
168
|
+
def get_call_location(tp, padding: 0)
|
169
|
+
caller(get_trace_index(tp) + padding).first.split(":")[0..1]
|
170
|
+
end
|
171
|
+
|
172
|
+
def get_trace_index(tp)
|
173
|
+
if tp.event == :c_call
|
174
|
+
C_CALLER_START_POINT
|
175
|
+
else
|
176
|
+
CALLER_START_POINT
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def get_traces(tp)
|
181
|
+
if with_trace_to = options[:with_trace_to]
|
182
|
+
trace_index = get_trace_index(tp)
|
183
|
+
caller[trace_index..(trace_index + with_trace_to)]
|
184
|
+
else
|
185
|
+
[]
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def collect_arguments(tp)
|
190
|
+
parameters =
|
191
|
+
if RUBY_VERSION.to_f >= 2.6
|
192
|
+
tp.parameters
|
193
|
+
else
|
194
|
+
get_method_object_from(tp.self, tp.callee_id)&.parameters || []
|
195
|
+
end.map { |parameter| parameter[1] }
|
196
|
+
|
197
|
+
tp.binding.local_variables.each_with_object({}) do |name, args|
|
198
|
+
args[name] = tp.binding.local_variable_get(name) if parameters.include?(name)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def process_options(options)
|
203
|
+
options[:filter_by_paths] ||= config[:filter_by_paths]
|
204
|
+
options[:exclude_by_paths] ||= config[:exclude_by_paths]
|
205
|
+
options[:with_trace_to] ||= config[:with_trace_to]
|
206
|
+
options[:event_type] ||= config[:event_type]
|
207
|
+
options[:hijack_attr_methods] ||= config[:hijack_attr_methods]
|
208
|
+
options[:track_as_records] ||= config[:track_as_records]
|
209
|
+
options[:ignore_private] ||= config[:ignore_private]
|
210
|
+
options[:only_private] ||= config[:only_private]
|
211
|
+
# for debugging the gem more easily
|
212
|
+
options[:force_recording] ||= false
|
213
|
+
|
214
|
+
options[:descendants] ||= []
|
215
|
+
options[:root_device] ||= self
|
216
|
+
options
|
217
|
+
end
|
218
|
+
|
219
|
+
def is_from_target?(tp)
|
220
|
+
comparsion = tp.self
|
221
|
+
is_the_same_record?(comparsion) || target.__id__ == comparsion.__id__
|
222
|
+
end
|
223
|
+
|
224
|
+
def is_the_same_record?(comparsion)
|
225
|
+
return false unless options[:track_as_records]
|
226
|
+
if target.is_a?(ActiveRecord::Base) && comparsion.is_a?(target.class)
|
227
|
+
primary_key = target.class.primary_key
|
228
|
+
target.send(primary_key) && target.send(primary_key) == comparsion.send(primary_key)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def record_call!(payload)
|
233
|
+
return if @disabled
|
234
|
+
|
235
|
+
write_output!(payload) if @output_writer
|
236
|
+
|
237
|
+
if @block
|
238
|
+
root_device.calls << @block.call(payload)
|
239
|
+
else
|
240
|
+
root_device.calls << payload
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def write_output!(payload)
|
245
|
+
@output_writer.write!(payload)
|
246
|
+
end
|
247
|
+
|
248
|
+
def stop_if_condition_fulfilled!(payload)
|
249
|
+
if @stop_when&.call(payload)
|
250
|
+
stop!
|
251
|
+
root_device.stop!
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def config
|
256
|
+
ObjectTracer.config
|
257
|
+
end
|
258
|
+
end
|