st0012/object_tracer

View on GitHub
README.md

Summary

Maintainability
Test Coverage
> [!WARNING]
> This gem has been deprecated in favor of [ruby/tracer](https://github.com/ruby/tracer)

# ObjectTracer (previously called TappingDevice)

![GitHub Action](https://github.com/st0012/object_tracer/workflows/Ruby/badge.svg)
[![Gem Version](https://badge.fury.io/rb/object_tracer.svg)](https://badge.fury.io/rb/object_tracer)
[![Maintainability](https://api.codeclimate.com/v1/badges/3e3732a6983785bccdbd/maintainability)](https://codeclimate.com/github/st0012/object_tracer/maintainability)
[![Test Coverage](https://api.codeclimate.com/v1/badges/3e3732a6983785bccdbd/test_coverage)](https://codeclimate.com/github/st0012/object_tracer/test_coverage)
[![Open Source Helpers](https://www.codetriage.com/st0012/object_tracer/badges/users.svg)](https://www.codetriage.com/st0012/object_tracer)


## Introduction
As the name states, `ObjectTracer` allows you to secretly listen to different events of an object:

- `Method Calls` - what does the object do
- `Traces` - how is the object used by the application
- `State Mutations` - what happens inside the object

After collecting the events, `ObjectTracer` will output them in a nice, readable format to either stdout or a file. 

**Ultimately, its goal is to let you know all the information you need for debugging with just 1 line of code.**

## Usages

### Track Method Calls

By tracking an object's method calls, you'll be able to observe the object's behavior very easily

<img src="https://github.com/st0012/object_tracer/blob/master/images/print_calls.png" alt="image of print_calls output" width="50%">

Each entry consists of 5 pieces of information:
- method name
- source of the method
- call site
- arguments
- return value

![explanation of individual entry](https://github.com/st0012/object_tracer/blob/master/images/print_calls%20-%20single%20entry.png)

#### Helpers

- `print_calls(object)` - prints the result to stdout
- `write_calls(object, log_file: "file_name")` - writes the result to a file
    - the default file is `/tmp/object_tracer.log`, but you can change it with `log_file: "new_path"` option

#### Use Cases
- Understand a service object/form object's behavior
- Debug a messy controller

### Track Traces

By tracking an object's traces, you'll be able to observe the object's journey in your application

![image of print_traces output](https://github.com/st0012/object_tracer/blob/master/images/print_traces.png)

#### Helpers

- `print_traces(object)` - prints the result to stdout
- `write_traces(object, log_file: "file_name")` - writes the result to a file
    - the default file is `/tmp/object_tracer.log`, but you can change it with `log_file: "new_path"` option

#### Use Cases
- Debug argument related issues
- Understand how a library uses your objects

### Track State Mutations

By tracking an object's traces, you'll be able to observe the state changes happen inside the object between each method call

<img src="https://github.com/st0012/object_tracer/blob/master/images/print_mutations.png" alt="image of print_mutations output" width="50%">

#### Helpers

- `print_mutations(object)` - prints the result to stdout
- `write_mutations(object, log_file: "file_name")` - writes the result to a file
    - the default file is `/tmp/object_tracer.log`, but you can change it with `log_file: "new_path"` option

#### Use Cases
- Debug state related issues
- Debug memoization issues

### Track All Instances Of A Class

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:

- `print_instance_calls(ObjectKlass)`
- `print_instance_traces(ObjectKlass)`
- `print_instance_mutations(ObjectKlass)`
- `write_instance_calls(ObjectKlass)`
- `write_instance_traces(ObjectKlass)`
- `write_instance_mutations(ObjectKlass)`


### Use `with_HELPER_NAME` for chained method calls

In Ruby programs, we often chain multiple methods together like this:

```ruby
SomeService.new(params).perform
```

And to debug it, we'll need to break the method chain into

```ruby
service = SomeService.new(params)
print_calls(service, options)
service.perform
```

This kind of code changes are usually annoying, and that's one of the problems I want to solve with `ObjectTracer`.

So here's another option, just insert a `with_HELPER_NAME` call in between:

```ruby
SomeService.new(params).with_print_calls(options).perform
```

And it'll behave exactly like

```ruby
service = SomeService.new(params)
print_calls(service, options)
service.perform
```

## Installation
Add this line to your application's Gemfile:

```ruby
gem 'object_tracer', group: :development
```

And then execute:

```
$ bundle
```

Or install it directly:

```
$ gem install object_tracer
```

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


## Advance Usages & Options 

### Add Conditions With `.with`

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.

```ruby
# only prints calls with name matches /foo/
print_calls(object).with do |payload|
  payload.method_name.to_s.match?(/foo/)
end
```

### Options

There are many options you can pass when using a helper method. You can list all available options and their default value with

```ruby
ObjectTracer::Configurable::DEFAULTS #=> {
  :filter_by_paths=>[], 
  :exclude_by_paths=>[], 
  :with_trace_to=>50, 
  :event_type=>:return, 
  :hijack_attr_methods=>false, 
  :track_as_records=>false, 
  :inspect=>false, 
  :colorize=>true, 
  :log_file=>"/tmp/object_tracer.log"
}
```

Here are some commonly used options:

#### `colorize: false` 

- default: `true`

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.


```ruby
print_calls(object, colorize: false)
```


#### `inspect: true` 

- default: `false`

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:

``` ruby
post.to_s #=> #<Post:0x00007f89a55201d0>
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>
```

#### `hijack_attr_methods: true` 

- default: `false`
    - except for `tap_mutation!` and `print_mutations`
    
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. 

For example, it generates

```ruby
def name=(val)
  @name = val
end
```

for

```ruby
attr_writer :name
```

This hack will only be applied to the target instance with `instance_eval`. So other instances of the class remain untouched.

The default is `false` because

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. 
2. It's still unclear if this hack safe enough for most applications.


#### `ignore_private`

Sometimes we use many private methods to perform trivial operations, like

```ruby
class Operation
  def extras
    dig_attribute("extras")
  end

  private

  def data
    @data
  end

  def dig_attribute(attr)
    data.dig("attributes", attr) 
  end
end
```

And we may not be interested in those method calls. If that's the case, you can use the `ignore_private` option

```ruby
operation = Operation.new(params)
print_calls(operation, ignore_private: true) #=> only prints the `extras` call
```

#### `only_private`

This option does the opposite of the `ignore_private` option does.


### Global Configuration

If you don't want to pass options every time you use a helper, you can use global configuration to change the default values:

```ruby
ObjectTracer.config[:colorize] = false
ObjectTracer.config[:hijack_attr_methods] = true
```

And if you're using Rails, you can put the configs under `config/initializers/object_tracer.rb` like this:

```ruby
if defined?(ObjectTracer)
  ObjectTracer.config[:colorize] = false
  ObjectTracer.config[:hijack_attr_methods] = true
end
```


### Lower-Level Helpers
`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)


### Related Blog Posts
- [Optimize Your Debugging Process With Object-Oriented Tracing and object_tracer](http://bit.ly/object-oriented-tracing) 
- [Debug Rails issues effectively with object_tracer](https://dev.to/st0012/debug-rails-issues-effectively-with-tappingdevice-c7c)
- [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)


## Development
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.

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

## Contributing

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.

## License

The gem is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).

## Code of Conduct

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