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

|
4
|
+
[](https://badge.fury.io/rb/object_tracer)
|
5
|
+
[](https://codeclimate.com/github/st0012/object_tracer/maintainability)
|
6
|
+
[](https://codeclimate.com/github/st0012/object_tracer/test_coverage)
|
7
|
+
[](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
|
+

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

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