Skip to content

Instantly share code, notes, and snippets.

@amkisko
Last active September 24, 2025 09:32
Show Gist options
  • Save amkisko/a52bcf8da47ef65b9ab9e3347077a828 to your computer and use it in GitHub Desktop.
Save amkisko/a52bcf8da47ef65b9ab9e3347077a828 to your computer and use it in GitHub Desktop.
Rails view probe debug info to HTML
if Rails.env.development? || ENV["VIEW_PROBE_ENABLED"] == "1"
class ViewProbeMiddleware
def initialize(app)
@app = app
end
def call(env)
return @app.call(env) unless annotate?(env)
rendered = []
subs = []
subs << ActiveSupport::Notifications.subscribe("render_template.action_view") do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
payload = event.payload
rendered << {
kind: :template,
identifier: payload[:identifier],
virtual_path: payload[:virtual_path],
layout: payload[:layout]
}
end
subs << ActiveSupport::Notifications.subscribe("render_partial.action_view") do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
payload = event.payload
rendered << {
kind: :partial,
identifier: payload[:identifier],
virtual_path: payload[:virtual_path]
}
end
status, headers, body = @app.call(env)
ensure
subs.each { |s| ActiveSupport::Notifications.unsubscribe(s) } if subs
if status && headers && body
return annotate_body(env, status, headers, body, rendered)
end
end
private
def annotate?(env)
return false unless defined?(Rails)
req = Rack::Request.new(env)
accept = env["HTTP_ACCEPT"].to_s
wants_html = accept.include?("text/html") || accept.empty?
turbo_stream = accept.include?("text/vnd.turbo-stream.html")
wants_html && !turbo_stream && req.get?
end
def annotate_body(env, status, headers, body, rendered)
ctype = headers["Content-Type"].to_s
return [status, headers, body] unless ctype.start_with?("text/html")
return [status, headers, body] if headers["Content-Disposition"].to_s =~ /attachment/i
return [status, headers, body] if headers["Cache-Control"].to_s =~ /no-transform/i
if headers["Transfer-Encoding"].to_s.downcase == "chunked"
return [status, headers, body]
end
buf = +""
begin
body.each { |chunk| buf << chunk.to_s }
ensure
body.close if body.respond_to?(:close)
end
note = build_comment(env, rendered)
if buf.include?("</body>")
buf.sub!("</body>", "#{note}\n</body>")
else
buf << note
end
if headers.key?("Content-Length")
headers["Content-Length"] = buf.bytesize.to_s
end
[status, headers, [buf]]
end
def build_comment(env, rendered)
controller = env["action_controller.instance"]
params = env["action_dispatch.request.parameters"] || {}
ctrl_name = controller&.class&.name
action = params["action"]
path_info = env["PATH_INFO"]
filtered = rendered.map { |r| r[:identifier] = r[:identifier].gsub(Rails.root.to_s, ""); r }
layouts = filtered.select { |r| r[:layout] }.map { |r| r[:layout] }.uniq
templates = filtered.select { |r| r[:kind] == :template }.map { |r| r[:identifier] }.uniq
partials = filtered.select { |r| r[:kind] == :partial }.map { |r| r[:identifier] }.uniq
payload = {
rails: Rails.version,
route: path_info,
ctrl_act: "#{ctrl_name}##{action}",
layout: layouts.join(", "),
template: templates.join(", "),
partials: partials.join(" | ")
}
"<!-- #{JSON.pretty_generate(payload)} -->"
end
end
Rails.application.config.middleware.insert_after ActionDispatch::Static, ViewProbeMiddleware
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment