Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Rails to provide timing data in browser dev-tools via Server-Timing header #1771

Merged
merged 20 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ nav_order: 5

## main

* Allow instrumentation to be automatically included in Server-Timing headers generated by Rails. To enable this set the config `config.use_deprecated_instrumentation_name = false`. The old key `!render.view_component` is deprecated: update ActiveSupport::Notification subscriptions to `render.view_component`.

*Travis Gaff*

## 3.3.0

* Include InlineTemplate by default in Base. **Note:** It's no longer necessary to include `ViewComponent::InlineTemplate` to use inline templates.
Expand Down
11 changes: 8 additions & 3 deletions docs/guide/instrumentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,19 @@ To enable ActiveSupport notifications, use the `instrumentation_enabled` option:
# config/application.rb
# Enable ActiveSupport notifications for all ViewComponents
config.view_component.instrumentation_enabled = true
config.view_component.use_deprecated_instrumentation_name = false
```

Then subscribe to the `!render.view_component` event:
Setting `use_deprecated_instrumentation_name` configures the event name. If `false` the name is `"render.view_component"`. If `true` (default) the deprecated `"!render.view_component"` will be used.

Subscribe to the event:

```ruby
ActiveSupport::Notifications.subscribe("!render.view_component") do |*args|
ActiveSupport::Notifications.subscribe("render.view_component") do |*args| # or !render.view_component
event = ActiveSupport::Notifications::Event.new(*args)
event.name # => "!render.view_component"
event.name # => "render.view_component"
event.payload # => { name: "MyComponent", identifier: "/Users/mona/project/app/components/my_component.rb" }
end
```

When using `render.view_component` with `config.server_timing = true` (default in development) in Rails 7, the Chrome developer tools display the sum total timing information in Network > Timing under the key `render.view_component`.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not important but: specifying Chrome here is perhaps a little "chrome-centric". The feature works nearly the same in FF, Safari, Edge and Opera. The screenshot in the PR is from FF 113 IIRC.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there anyway you can add Firefox and safari instructions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are nearly identical aside from coloration, so I don't think it's necessary.

  1. Network tab (refresh if its empty)
  2. Click the request you're interested in.
  3. on the right-side OR the bottom-side (depends on size of the dev-tools area) you'll see several tabs for that request; choose "Timing" or "Timings" (in FF)
  4. Look for server timings at the bottom beneath the client-side data.

Biggest caveats with Server-Timings in general:

  • It's not provided for every request. Normal MVC rails controllers in development mode will provide it (it's a middleware) - but assets won't. In production you won't see it without enabling it in production.rb.
  • It does not give you the level of detail (or cross-request metrics) you can get from a product like Skylight or New Relic. I'd suggest using it for quick-checks mainly, e.g.: "huh active record is crazy on this one, maybe I should take a look at my queries."
  • Does not exist prior to Rails 7.

timings
From top-left going clockwise: Chrome, Firefox, Safari. You can see its pretty much the same.

Copy link
Member

@joelhawksley joelhawksley Jul 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we just say "browser" here? Perhaps we should add this screenshot to the docs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just added a smaller variant of the screenshot. When running the docs locally that path seems to work and display properly, but there may be some risk of a dead img src when deployed.

joelhawksley marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ ViewComponent is built by over a hundred members of the community, including:
<img src="https://avatars.githubusercontent.com/tbroad-ramsey?s=64" alt="tbroad-ramsey" width="32" />
<img src="https://avatars.githubusercontent.com/tclem?s=64" alt="tclem" width="32" />
<img src="https://avatars.githubusercontent.com/tenderlove?s=64" alt="tenderlove" width="32" />
<img src="https://avatars.githubusercontent.com/tgaff?s=64" alt="tgaff" width="32" />
<img src="https://avatars.githubusercontent.com/thutterer?s=64" alt="thutterer" width="32" />
<img src="https://avatars.githubusercontent.com/tonkpils?s=64" alt="tonkpils" width="32" />
<img src="https://avatars.githubusercontent.com/traels?s=64" alt="traels" width="32" />
Expand Down
8 changes: 8 additions & 0 deletions lib/view_component/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def defaults
preview_route: "/rails/view_components",
show_previews_source: false,
instrumentation_enabled: false,
use_deprecated_instrumentation_name: true,
tgaff marked this conversation as resolved.
Show resolved Hide resolved
render_monkey_patch_enabled: true,
view_component_path: "app/components",
component_parent_class: nil,
Expand Down Expand Up @@ -99,6 +100,13 @@ def defaults
# Whether ActiveSupport notifications are enabled.
# Defaults to `false`.

# @!attribute use_deprecated_instrumentation_name
# @return [Boolean]
# Whether ActiveSupport Notifications use the private name "!render.view_component"
# or are made more publicly available via "render.view_component".
# Will default to false in next major version.
# Defaults to `true`.

# @!attribute render_monkey_patch_enabled
# @return [Boolean] Whether the #render method should be monkey patched.
# If this is disabled, use `#render_component` or
Expand Down
7 changes: 7 additions & 0 deletions lib/view_component/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require "rails"
require "view_component/config"
require "view_component/deprecation"

module ViewComponent
class Engine < Rails::Engine # :nodoc:
Expand Down Expand Up @@ -42,6 +43,12 @@ class Engine < Rails::Engine # :nodoc:
# :nocov:
ViewComponent::Base.prepend(ViewComponent::Instrumentation)
# :nocov:
if app.config.view_component.instrumentation_use_deprecated_name
ViewComponent::Deprecation.deprecation_warning(
"!render.view_component",
"Use the new instrumentation key `render.view_component` instead. See https://viewcomponent.org/guide/instrumentation.html"
)
end
end
end
end
Expand Down
10 changes: 9 additions & 1 deletion lib/view_component/instrumentation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def self.included(mod)

def render_in(view_context, &block)
ActiveSupport::Notifications.instrument(
"!render.view_component",
notification_name,
{
name: self.class.name,
identifier: self.class.identifier
Expand All @@ -19,5 +19,13 @@ def render_in(view_context, &block)
super(view_context, &block)
end
end

private

def notification_name
return "!render.view_component" if ViewComponent::Base.config.use_deprecated_instrumentation_name

"render.view_component"
end
end
end
1 change: 1 addition & 0 deletions test/sandbox/test/config_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def test_defaults_are_correct
assert_equal @config.preview_route, "/rails/view_components"
assert_equal @config.show_previews_source, false
assert_equal @config.instrumentation_enabled, false
assert_equal @config.use_deprecated_instrumentation_name, true
assert_equal @config.render_monkey_patch_enabled, true
assert_equal @config.show_previews, true
assert_equal @config.preview_paths, ["#{Rails.root}/test/components/previews"]
Expand Down
21 changes: 17 additions & 4 deletions test/sandbox/test/instrumentation_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,29 @@

class InstrumentationTest < ViewComponent::TestCase
def test_instrumentation
with_config_option(:use_deprecated_instrumentation_name, false) do
events = []
ActiveSupport::Notifications.subscribe("render.view_component") do |*args|
events << ActiveSupport::Notifications::Event.new(*args)
end
render_inline(InstrumentationComponent.new)

assert_selector("div", text: "hello,world!")
assert_equal(events.size, 1)
assert_equal("render.view_component", events[0].name)
assert_equal(events[0].payload[:name], "InstrumentationComponent")
assert_match("app/components/instrumentation_component.rb", events[0].payload[:identifier])
end
end

def test_instrumentation_with_deprecated_name
events = []
ActiveSupport::Notifications.subscribe("!render.view_component") do |*args|
events << ActiveSupport::Notifications::Event.new(*args)
end
render_inline(InstrumentationComponent.new)

assert_selector("div", text: "hello,world!")
assert_equal(events.size, 1)
assert_equal(events[0].name, "!render.view_component")
assert_equal(events[0].payload[:name], "InstrumentationComponent")
assert_match("app/components/instrumentation_component.rb", events[0].payload[:identifier])
assert_equal("!render.view_component", events[0].name)
end
end