Repository: newrelic/newrelic-ruby-agent (Ruby)

Issue:
Jbuilder method tracer exception in v8.0.0
## Description
After a gem upgrade from v7.2.0 to v8.0.0, an exception is raised by the `Jbuilder#target!` method tracer.

## Expected Behavior
To continue working as it did before the upgrade.

## [Troubleshooting](https://discuss.newrelic.com/t/troubleshooting-frameworks/108787) or [NR Diag](https://docs.newrelic.com/docs/using-new-relic/cross-product-functions/troubleshooting/new-relic-diagnostics) results
Provide any other relevant log data.

```
[ActionView::Template::Error] undefined method `trace_execution_scoped' for nil:NilClass | Stack trace:
	/Users/tonci/.rvm/gems/ruby-2.6.7/gems/newrelic_rpm-8.0.0/lib/new_relic/agent/method_tracer.rb:326:in `block (2 levels) in _nr_define_traced_method'
	/Users/tonci/.rvm/gems/ruby-2.6.7/gems/jbuilder-2.11.2/lib/jbuilder/jbuilder_template.rb:81:in `target!'
	/Users/tonci/Documents/server/app/views/api/v1/authentication/login.json.jbuilder:8:in `_app_views_api_v__authentication_login_json_jbuilder___2930598681177930957_70166338224360'
	... 
```

## Steps to Reproduce
We add a method tracer to measure actual JSON serialization performance of `Jbuilder`:

```ruby
class Jbuilder
  include ::NewRelic::Agent::MethodTracer

  add_method_tracer :target!, 'View/Jbuilder/target!'
end
```

This code is executed as a Rails initializer in `config/initializers/newrelic.rb`.

Afterwards, just call the `target!` method:

```ruby
Jbuilder.new.target!
```

## Your Environment
Ruby v2.6.7, Rails v5.2.6, both macOS and Linux affected.

## Additional context
Nothing right now.



Discussion:
@toncid, thank you for updating the Ruby Agent and reporting this issue regarding our new release.  We will have someone take a look.
Hello @toncid! I'm looking at our agent code and at Jbuilder, and I think I've found the root of the problem. `Jbuilder` is actually a proxy class that does not include `Kernel` by default. The Ruby Agent 8.0.0 update changes method tracers to require `self.class` for inspection when running traced methods; because `Kernel#class` is not defined, you get the error above.

While we work on a fix, I would suggest this possible workaround in your initializer:

```ruby
class Jbuilder
  include ::Kernel
  include ::NewRelic::Agent::MethodTracer

  add_method_tracer :target!, 'View/Jbuilder/target!'
end
```
Hello @amhuntsman,

Thank you for your analysis. Unfortunately, this is probably not a good solution.

The reason for this is Jbuilder's flexibility to create JSON based on `method_missing` calls, meaning that `Kernel` methods would be called instead and therefore cause side-effects, should they be included as you proposed.

For example, if you wanted a `warn` field in JSON, it would be added like this:

```ruby
json = Jbuilder.new
json.warn true
json.target!  # yields: {"warn":true}
```

However, including the `Kernel` module would print `true` to `$stderr` and we would end up with an empty object `{}` in the end.

Hope this makes sense.
@toncid That does make sense, and thank you for the clarification. For the time being, unfortunately, Ruby Agent 8.0.0 does not support using `add_method_tracer` on proxy objects such as Jbuilder. This will be addressed and fixed in an upcoming release.

That said, one other possible workaround is to patch `target!` in the initializer to use a lower-level tracing method:
```
class Jbuilder
  def target!
    ::NewRelic::Agent::MethodTracer.trace_execution_scoped(['View/Jbuilder/target!']) do
      @attributes.to_json
    end
  end
end
```

From an agent development standpoint, we're also curious about the use case for tracing the `target!` method. My in-depth knowledge of Jbuilder is limited, but `target!` seems only to run `@attributes.to_json`. Based on a quick glance at `JbuilderTemplate` other JSON rendering solutions in the wild, could the bulk of processing overhead be in the template logic rather than in `target!`?
@amhuntsman Thank you for the reply.

> That said, one other possible workaround is to patch `target!` in the initializer to use a lower-level tracing method:

This can also work for us, at least until the patch is released. Thanks!

> From an agent development standpoint, we're also curious about the use case for tracing the `target!` method. My in-depth knowledge of Jbuilder is limited, but `target!` seems only to run `@attributes.to_json`. Based on a quick glance at `JbuilderTemplate` other JSON rendering solutions in the wild, could the bulk of processing overhead be in the template logic rather than in `target!`?

You are right - with Jbuilder, the bulk of processing is in the template logic, but in order to know how much time is actually spent in it, we have to eliminate JSON encoding that converts the final Ruby object to its JSON representation (usually handled by the `json` or `oj` gem). Hope this clarifies.



Patch:
diff --git a/lib/new_relic/agent/method_tracer.rb b/lib/new_relic/agent/method_tracer.rb
index 7f83552056..1bb40a997d 100644
--- a/lib/new_relic/agent/method_tracer.rb
+++ b/lib/new_relic/agent/method_tracer.rb
@@ -320,20 +320,25 @@ def _nr_define_traced_method(method_name, scoped_metric: nil, unscoped_metrics:
 
               instance_exec(&code_header) if code_header.kind_of?(Proc)
 
+              # NOTE: Calling ::NewRelic::Agent::MethodTracer.trace_execution_scoped and
+              # .trace_execution_unscoped below relies on the fact that MethodTracer is included
+              # in Module on agent startup. If/when this changes, these methods should be
+              # explicitly namespaced and extended in MethodTracer.
+
               # If tracing multiple metrics on this method, nest one unscoped trace inside the scoped trace.
               begin
                 if scoped_metric_eval
-                  self.class.trace_execution_scoped(scoped_metric_eval, metric: record_metrics, internal: true) do
+                  ::NewRelic::Agent::MethodTracer.trace_execution_scoped(scoped_metric_eval, metric: record_metrics, internal: true) do
                     if unscoped_metrics_eval.empty?
                       super(*args, &block)
                     else
-                      self.class.trace_execution_unscoped(unscoped_metrics_eval, internal: true) do
+                      ::NewRelic::Agent::MethodTracer.trace_execution_unscoped(unscoped_metrics_eval, internal: true) do
                         super(*args, &block)
                       end
                     end
                   end
                 elsif !unscoped_metrics_eval.empty?
-                  self.class.trace_execution_unscoped(unscoped_metrics_eval, internal: true) do
+                  ::NewRelic::Agent::MethodTracer.trace_execution_unscoped(unscoped_metrics_eval, internal: true) do
                     super(*args, &block)
                   end
                 end


Test patch:
diff --git a/test/new_relic/agent/method_tracer_test.rb b/test/new_relic/agent/method_tracer_test.rb
index 4ddb20d024..f623fa36d2 100644
--- a/test/new_relic/agent/method_tracer_test.rb
+++ b/test/new_relic/agent/method_tracer_test.rb
@@ -70,6 +70,16 @@ class << self
   end
 end
 
+class MyProxyClass < BasicObject
+  include ::NewRelic::Agent::MethodTracer
+
+  def hello
+    "hello"
+  end
+
+  add_method_tracer :hello, 'Custom/proxy/hello'
+end
+
 class NewRelic::Agent::MethodTracerTest < Minitest::Test
   attr_reader :stats_engine
 
@@ -410,6 +420,16 @@ def test_only_adds_methods_to_host_that_are_part_of_public_api
     assert_equal(public_api_methods.sort, added_methods.map(&:to_s).sort)
   end
 
+  def test_method_tracer_on_basic_object
+    proxy = MyProxyClass.new
+
+    in_transaction 'test_txn' do
+      proxy.hello
+    end
+
+    assert_metrics_recorded ['Custom/proxy/hello']
+  end
+
   def trace_no_push_scope
     in_transaction 'test_txn' do
       self.class.add_method_tracer :method_to_be_traced, 'X', :push_scope => false