object_tracer 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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