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.
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
@@ -0,0 +1,3 @@
1
+ test:
2
+ bundle exec rspec
3
+ WITH_ACTIVE_RECORD=true bundle exec rspec spec/active_record_spec.rb
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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
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