class ObjectTracer

Constants

CALLER_START_POINT
C_CALLER_START_POINT
VERSION

Attributes

calls[R]
options[R]
target[R]
trace_point[R]

Public Class Methods

config() click to toggle source
# File lib/object_tracer/configuration.rb, line 31
def self.config
  @config ||= Configuration.new
end
new(options = {}, &block) click to toggle source
# File lib/object_tracer.rb, line 31
def initialize(options = {}, &block)
  @block = block
  @output_block = nil
  @options = process_options(options.dup)
  @calls = []
  @disabled = false
  @with_condition = nil
  ObjectTracer.devices << self
end

Public Instance Methods

create_child_device() click to toggle source
# File lib/object_tracer.rb, line 58
def create_child_device
  new_device = self.class.new(@options.merge(root_device: root_device), &@block)
  new_device.stop_when(&@stop_when)
  new_device.instance_variable_set(:@target, @target)
  self.descendants << new_device
  new_device
end
descendants() click to toggle source
# File lib/object_tracer.rb, line 70
def descendants
  options[:descendants]
end
root_device() click to toggle source
# File lib/object_tracer.rb, line 66
def root_device
  options[:root_device]
end
set_block(&block) click to toggle source
# File lib/object_tracer.rb, line 45
def set_block(&block)
  @block = block
end
stop!() click to toggle source
# File lib/object_tracer.rb, line 49
def stop!
  @disabled = true
  ObjectTracer.delete_device(self)
end
stop_when(&block) click to toggle source
# File lib/object_tracer.rb, line 54
def stop_when(&block)
  @stop_when = block
end
track(object) click to toggle source
# File lib/object_tracer.rb, line 74
def track(object)
  @target = object
  validate_target!

  MethodHijacker.new(@target).hijack_methods! if options[:hijack_attr_methods]

  @trace_point = build_minimum_trace_point(event_type: options[:event_type]) do |payload|
    record_call!(payload)

    stop_if_condition_fulfilled!(payload)
  end

  @trace_point.enable unless ObjectTracer.suspend_new

  self
end
with(&block) click to toggle source
# File lib/object_tracer.rb, line 41
def with(&block)
  @with_condition = block
end

Private Instance Methods

build_minimum_trace_point(event_type:) { |payload| ... } click to toggle source
# File lib/object_tracer.rb, line 93
def build_minimum_trace_point(event_type:)
  TracePoint.new(*event_type) do |tp|
    next unless filter_condition_satisfied?(tp)

    filepath, line_number = get_call_location(tp)
    payload = build_payload(tp: tp, filepath: filepath, line_number: line_number)

    unless @options[:force_recording]
      next if is_object_tracer_call?(tp)
      next if should_be_skipped_by_paths?(filepath)
      next unless with_condition_satisfied?(payload)
      next if payload.is_private_call? && @options[:ignore_private]
      next if !payload.is_private_call? && @options[:only_private]
    end

    yield(payload)
  end
end
build_payload(tp:, filepath:, line_number:) click to toggle source
# File lib/object_tracer.rb, line 142
def build_payload(tp:, filepath:, line_number:)
  Payload.new(
    target: @target,
    receiver: tp.self,
    method_name: tp.callee_id,
    method_object: get_method_object_from(tp.self, tp.callee_id),
    arguments: collect_arguments(tp),
    return_value: (tp.return_value rescue nil),
    filepath: filepath,
    line_number: line_number,
    defined_class: tp.defined_class,
    trace: get_traces(tp),
    is_private_call: tp.defined_class.private_method_defined?(tp.callee_id),
    tag: options[:tag],
    tp: tp
  )
end
collect_arguments(tp) click to toggle source
# File lib/object_tracer.rb, line 189
def collect_arguments(tp)
  parameters =
    if RUBY_VERSION.to_f >= 2.6
      tp.parameters
    else
      get_method_object_from(tp.self, tp.callee_id)&.parameters || []
    end.map { |parameter| parameter[1] }

  tp.binding.local_variables.each_with_object({}) do |name, args|
    args[name] = tp.binding.local_variable_get(name) if parameters.include?(name)
  end
end
config() click to toggle source
# File lib/object_tracer.rb, line 255
def config
  ObjectTracer.config
end
filter_condition_satisfied?(tp) click to toggle source
# File lib/object_tracer.rb, line 114
def filter_condition_satisfied?(tp)
  false
end
get_call_location(tp, padding: 0) click to toggle source
# File lib/object_tracer.rb, line 168
def get_call_location(tp, padding: 0)
  caller(get_trace_index(tp) + padding).first.split(":")[0..1]
end
get_method_object_from(target, method_name) click to toggle source
# File lib/object_tracer.rb, line 160
def get_method_object_from(target, method_name)
  Object.instance_method(:method).bind(target).call(method_name)
rescue NameError
  # if any part of the program uses Refinement to extend its methods
  # we might still get NoMethodError when trying to get that method outside the scope
  nil
end
get_trace_index(tp) click to toggle source
# File lib/object_tracer.rb, line 172
def get_trace_index(tp)
  if tp.event == :c_call
    C_CALLER_START_POINT
  else
    CALLER_START_POINT
  end
end
get_traces(tp) click to toggle source
# File lib/object_tracer.rb, line 180
def get_traces(tp)
  if with_trace_to = options[:with_trace_to]
    trace_index = get_trace_index(tp)
    caller[trace_index..(trace_index + with_trace_to)]
  else
    []
  end
end
is_from_target?(tp) click to toggle source
# File lib/object_tracer.rb, line 219
def is_from_target?(tp)
  comparsion = tp.self
  is_the_same_record?(comparsion) || target.__id__ == comparsion.__id__
end
is_object_tracer_call?(tp) click to toggle source
# File lib/object_tracer.rb, line 126
def is_object_tracer_call?(tp)
  if tp.defined_class == ObjectTracer::Trackable || tp.defined_class == ObjectTracer
    return true
  end

  if Module.respond_to?(:module_parents)
    tp.defined_class.module_parents.include?(ObjectTracer)
  elsif Module.respond_to?(:parents)
    tp.defined_class.parents.include?(ObjectTracer)
  end
end
is_the_same_record?(comparsion) click to toggle source
# File lib/object_tracer.rb, line 224
def is_the_same_record?(comparsion)
  return false unless options[:track_as_records]
  if target.is_a?(ActiveRecord::Base) && comparsion.is_a?(target.class)
    primary_key = target.class.primary_key
    target.send(primary_key) && target.send(primary_key) == comparsion.send(primary_key)
  end
end
process_options(options) click to toggle source
# File lib/object_tracer.rb, line 202
def process_options(options)
  options[:filter_by_paths] ||= config[:filter_by_paths]
  options[:exclude_by_paths] ||= config[:exclude_by_paths]
  options[:with_trace_to] ||= config[:with_trace_to]
  options[:event_type] ||= config[:event_type]
  options[:hijack_attr_methods] ||= config[:hijack_attr_methods]
  options[:track_as_records] ||= config[:track_as_records]
  options[:ignore_private] ||= config[:ignore_private]
  options[:only_private] ||= config[:only_private]
  # for debugging the gem more easily
  options[:force_recording] ||= false

  options[:descendants] ||= []
  options[:root_device] ||= self
  options
end
record_call!(payload) click to toggle source
# File lib/object_tracer.rb, line 232
def record_call!(payload)
  return if @disabled

  write_output!(payload) if @output_writer

  if @block
    root_device.calls << @block.call(payload)
  else
    root_device.calls << payload
  end
end
should_be_skipped_by_paths?(filepath) click to toggle source

this needs to be placed upfront so we can exclude noise before doing more work

# File lib/object_tracer.rb, line 119
def should_be_skipped_by_paths?(filepath)
  exclude_by_paths = options[:exclude_by_paths]
  filter_by_paths = options[:filter_by_paths]
  exclude_by_paths.any? { |pattern| pattern.match?(filepath) } ||
    (filter_by_paths && !filter_by_paths.empty? && !filter_by_paths.any? { |pattern| pattern.match?(filepath) })
end
stop_if_condition_fulfilled!(payload) click to toggle source
# File lib/object_tracer.rb, line 248
def stop_if_condition_fulfilled!(payload)
  if @stop_when&.call(payload)
    stop!
    root_device.stop!
  end
end
validate_target!() click to toggle source
# File lib/object_tracer.rb, line 112
def validate_target!; end
with_condition_satisfied?(payload) click to toggle source
# File lib/object_tracer.rb, line 138
def with_condition_satisfied?(payload)
  @with_condition.nil? || @with_condition.call(payload)
end
write_output!(payload) click to toggle source
# File lib/object_tracer.rb, line 244
def write_output!(payload)
  @output_writer.write!(payload)
end