Skip to content

Commit

Permalink
Update Action View instrumentation (#487)
Browse files Browse the repository at this point in the history
  • Loading branch information
jrothrock committed Feb 5, 2024
1 parent 3b802ec commit 12150d8
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 3 deletions.
2 changes: 2 additions & 0 deletions gems/instruments.gemfile
Expand Up @@ -4,3 +4,5 @@ gem 'httpclient'
gem 'http'
gem 'redis'
gem 'moped'
gem 'actionpack'
gem 'actionview'
65 changes: 62 additions & 3 deletions lib/scout_apm/instruments/action_view.rb
Expand Up @@ -72,9 +72,67 @@ def install_using_prepend

logger.info "Instrumenting ActionView::TemplateRenderer"
::ActionView::TemplateRenderer.prepend(ActionViewTemplateRendererInstruments)

if defined?(::ActionView::CollectionRenderer)
logger.info "Instrumenting ActionView::CollectionRenderer"
::ActionView::CollectionRenderer.prepend(ActionViewCollectionRendererInstruments)
end
end

# In Rails 6.1 collection was moved to CollectionRenderer.
module ActionViewCollectionRendererInstruments
def render_collection(*args, **kwargs)
req = ScoutApm::RequestManager.lookup

maybe_template = args[3]

template_name ||= maybe_template.virtual_path rescue nil
template_name ||= "Unknown Collection"
layer_name = template_name + "/Rendering"

layer = ScoutApm::Layer.new("View", layer_name)
layer.subscopable!

begin
req.start_layer(layer)
if ScoutApm::Agent.instance.context.environment.supports_kwarg_delegation?
super(*args, **kwargs)
else
super(*args)
end
ensure
req.stop_layer
end
end
end

module ActionViewPartialRendererInstruments
# In Rails 6.1, render_partial was renamed to render_partial_template
def render_partial_template(*args, **kwargs)
req = ScoutApm::RequestManager.lookup

# Template was moved to the third argument in Rails 6.1.
maybe_template = args[2]

template_name ||= maybe_template.virtual_path rescue nil
template_name ||= "Unknown Partial"

layer_name = template_name + "/Rendering"
layer = ScoutApm::Layer.new("View", layer_name)
layer.subscopable!

begin
req.start_layer(layer)
if ScoutApm::Agent.instance.context.environment.supports_kwarg_delegation?
super(*args, **kwargs)
else
super(*args)
end
ensure
req.stop_layer
end
end

# In Rails 6, the signature changed to pass the view & template args directly, as opposed to through the instance var
# New signature is: def render_partial(view, template)
def render_partial(*args, **kwargs)
Expand All @@ -83,7 +141,7 @@ def render_partial(*args, **kwargs)
maybe_template = args[1]

template_name = @template.virtual_path rescue nil # Works on Rails 3.2 -> end of Rails 5 series
template_name ||= maybe_template.virtual_path rescue nil # Works on Rails 6 -> 6.0.3.5
template_name ||= maybe_template.virtual_path rescue nil # Works on Rails 6 -> 6.0.6
template_name ||= "Unknown Partial"

layer_name = template_name + "/Rendering"
Expand All @@ -102,13 +160,14 @@ def render_partial(*args, **kwargs)
end
end

# This method was moved in Rails 6.1 to CollectionRender.
def collection_with_template(*args, **kwargs)
req = ScoutApm::RequestManager.lookup

maybe_template = args[1]

template_name = @template.virtual_path rescue nil # Works on Rails 3.2 -> end of Rails 5 series
template_name ||= maybe_template.virtual_path rescue nil # Works on Rails 6 -> 6.0.3.5
template_name ||= maybe_template.virtual_path rescue nil # Works on Rails 6 -> 6.0.6
template_name ||= "Unknown Collection"
layer_name = template_name + "/Rendering"

Expand Down Expand Up @@ -137,7 +196,7 @@ def render_template(*args)
maybe_template = args[1]

template_name = args[0].virtual_path rescue nil # Works on Rails 3.2 -> end of Rails 5 series
template_name ||= maybe_template.virtual_path rescue nil # Works on Rails 6 -> 6.1.3
template_name ||= maybe_template.virtual_path rescue nil # Works on Rails 6 -> 7.1.3
template_name ||= "Unknown"
layer_name = template_name + "/Rendering"

Expand Down
6 changes: 6 additions & 0 deletions test/test_helper.rb
Expand Up @@ -65,7 +65,13 @@ def method_missing(sym)
end
end

def remove_rails_namespace
Object.send(:remove_const, "Rails") if defined?(Rails)
end

def fake_rails(version)
remove_rails_namespace if (ENV["SCOUT_TEST_FEATURES"] || "").include?("instruments")

Kernel.const_set("Rails", Module.new)
Kernel.const_set("ActionController", Module.new)
r = Kernel.const_get("Rails")
Expand Down
102 changes: 102 additions & 0 deletions test/unit/instruments/action_view_test.rb
@@ -0,0 +1,102 @@
# Most of this was taken from Rails:
# https://github.com/rails/rails/blob/v7.1.3/actionview/test/actionpack/controller/render_test.rb
# https://github.com/rails/rails/blob/v7.1.3/actionview/test/abstract_unit.rb

if (ENV["SCOUT_TEST_FEATURES"] || "").include?("instruments")
require 'test_helper'
require 'action_view'
require 'action_pack'
require 'action_controller'

FIXTURE_LOAD_PATH = File.expand_path("fixtures", __dir__)

include ActionView::Context
include ActionView::Helpers::TagHelper
include ActionView::Helpers::TextHelper

module ActionController

class Base
self.view_paths = FIXTURE_LOAD_PATH

def self.test_routes(&block)
routes = ActionDispatch::Routing::RouteSet.new
routes.draw(&block)
include routes.url_helpers
routes
end
end

class TestCase
include ActionDispatch::TestProcess

def self.with_routes(&block)
routes = ActionDispatch::Routing::RouteSet.new
routes.draw(&block)
include Module.new {
define_method(:setup) do
super()
@routes = routes
@controller.singleton_class.include @routes.url_helpers if @controller
end
}
routes
end
end
end

class TestController < ActionController::Base

def render_test_view
render template: "test_view"
end
end

class RenderTest < ActionController::TestCase

tests TestController

with_routes do
get :render_test_view, to: "test#render_test_view"
end

def setup
super
@controller.logger = ActiveSupport::Logger.new(nil)
ActionView::Base.logger = ActiveSupport::Logger.new(nil)

@request.host = "www.scoutapm.com"

@old_view_paths = ActionController::Base.view_paths
ActionController::Base.view_paths = FIXTURE_LOAD_PATH
end

def teardown
ActionView::Base.logger = nil

ActionController::Base.view_paths = @old_view_paths
end

def test_partial_instrumentation
recorder = FakeRecorder.new
agent_context.recorder = recorder

instrument = ScoutApm::Instruments::ActionView.new(agent_context)
instrument.install(prepend: true)

get :render_test_view
assert_response :success

root_layer = recorder.requests.first.root_layer
children = root_layer.children.to_a
assert_equal 2, children.size

partial_layer = children[0]
collection_layer = children[1]

assert_equal "test_view/Rendering", root_layer.name
assert_equal "test/_test_partial/Rendering", partial_layer.name
assert_equal "test/_test_partial_collection/Rendering", collection_layer.name
end
end
end
3 changes: 3 additions & 0 deletions test/unit/instruments/fixtures/test/_test_partial.html.erb
@@ -0,0 +1,3 @@
<div>
<p><%= message %></p>
</div>
@@ -0,0 +1,3 @@
<div>
<p><%= index %></p>
</div>
10 changes: 10 additions & 0 deletions test/unit/instruments/fixtures/test_view.html.erb
@@ -0,0 +1,10 @@
<html>
<head>
<title>View</title>
</head>
<body>
<h1>View</h1>
<%= render partial: "test_partial", locals: { message: 'Partial' } %>
<%= render partial: "test_partial_collection", collection: [1, 2, 3], as: :index %>
</body>
</html>

0 comments on commit 12150d8

Please sign in to comment.