From fb2c080de919db564f6d2f2f39cdb842f3a65c7a Mon Sep 17 00:00:00 2001 From: Nate Berkopec Date: Wed, 6 Dec 2023 08:33:03 +0900 Subject: [PATCH] Choose your RMP action via the X-Rack-Mini-Profiler header (#578) Co-authored-by: Nate Berkopec --- README.md | 2 +- lib/mini_profiler.rb | 44 +++++++++++++++----------- lib/mini_profiler/views.rb | 2 ++ spec/integration/mini_profiler_spec.rb | 20 ++++++++++++ 4 files changed, 49 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index d0fff3c9..bf3f96c0 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ export RACK_MINI_PROFILER_PATCH_NET_HTTP="false" To generate [flamegraphs](http://samsaffron.com/archive/2013/03/19/flame-graphs-in-ruby-miniprofiler), add the [**stackprof**](https://rubygems.org/gems/stackprof) gem to your Gemfile. -Then, to view the flamegraph as a direct HTML response from your request, just visit any page in your app with `?pp=flamegraph` appended to the URL. +Then, to view the flamegraph as a direct HTML response from your request, just visit any page in your app with `?pp=flamegraph` appended to the URL, or add the header `X-Rack-Mini-Profiler` to the request with the value `flamegraph`. Conversely, if you want your regular response instead (which is specially useful for JSON and/or XHR requests), just append the `?pp=async-flamegraph` parameter to your request/fetch URL; the request will then return as normal, and the flamegraph data will be stored for later *async* viewing, both for this request and for all subsequent requests made by this page (based on the `REFERER` header). For viewing these async flamegraphs, use the 'flamegraph' link that will appear inside the MiniProfiler UI for these requests. diff --git a/lib/mini_profiler.rb b/lib/mini_profiler.rb index 994e453a..58a4ab5d 100644 --- a/lib/mini_profiler.rb +++ b/lib/mini_profiler.rb @@ -160,13 +160,12 @@ def call(env) MiniProfiler.deauthorize_request if @config.authorization_mode == :allow_authorized status = headers = body = nil - query_string = env['QUERY_STRING'] path = env['PATH_INFO'].sub('//', '/') # Someone (e.g. Rails engine) could change the SCRIPT_NAME so we save it env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME'] = ENV['PASSENGER_BASE_URI'] || env['SCRIPT_NAME'] - skip_it = /#{@config.profile_parameter}=skip/.match?(query_string) || ( + skip_it = matches_action?('skip', env) || ( @config.skip_paths && @config.skip_paths.any? do |p| if p.instance_of?(String) @@ -212,11 +211,11 @@ def call(env) has_disable_cookie = client_settings.disable_profiling? # manual session disable / enable - if query_string =~ /#{@config.profile_parameter}=disable/ || has_disable_cookie + if matches_action?('disable', env) || has_disable_cookie skip_it = true end - if query_string =~ /#{@config.profile_parameter}=enable/ + if matches_action?('enable', env) skip_it = false config.enabled = true end @@ -231,13 +230,13 @@ def call(env) client_settings.disable_profiling = false # profile gc - if query_string =~ /#{@config.profile_parameter}=profile-gc/ + if matches_action?('profile-gc', env) current.measure = false if current return serve_profile_gc(env, client_settings) end # profile memory - if query_string =~ /#{@config.profile_parameter}=profile-memory/ + if matches_action?('profile-memory', env) return serve_profile_memory(env, client_settings) end @@ -245,12 +244,12 @@ def call(env) MiniProfiler.create_current(env, @config) - if query_string =~ /#{@config.profile_parameter}=normal-backtrace/ + if matches_action?('normal-backtrace', env) client_settings.backtrace_level = ClientSettings::BACKTRACE_DEFAULT - elsif query_string =~ /#{@config.profile_parameter}=no-backtrace/ + elsif matches_action?('no-backtrace', env) current.skip_backtrace = true client_settings.backtrace_level = ClientSettings::BACKTRACE_NONE - elsif query_string =~ /#{@config.profile_parameter}=full-backtrace/ || client_settings.backtrace_full? + elsif matches_action?('full-backtrace', env) || client_settings.backtrace_full? current.full_backtrace = true client_settings.backtrace_level = ClientSettings::BACKTRACE_FULL elsif client_settings.backtrace_none? @@ -259,7 +258,7 @@ def call(env) flamegraph = nil - trace_exceptions = query_string =~ /#{@config.profile_parameter}=trace-exceptions/ && defined? TracePoint + trace_exceptions = matches_action?('trace-exceptions', env) && defined? TracePoint status, headers, body, exceptions, trace = nil if trace_exceptions @@ -283,11 +282,11 @@ def call(env) # Prevent response body from being compressed env['HTTP_ACCEPT_ENCODING'] = 'identity' if config.suppress_encoding - if query_string =~ /pp=(async-)?flamegraph/ || env['HTTP_REFERER'] =~ /pp=async-flamegraph/ + if matches_action?('flamegraph', env) || matches_action?('async-flamegraph', env) || env['HTTP_REFERER'] =~ /pp=async-flamegraph/ if defined?(StackProf) && StackProf.respond_to?(:run) # do not sully our profile with mini profiler timings current.measure = false - match_data = query_string.match(/flamegraph_sample_rate=([\d\.]+)/) + match_data = action_parameters(env)['flamegraph_sample_rate'] if match_data && !match_data[1].to_f.zero? sample_rate = match_data[1].to_f @@ -295,7 +294,7 @@ def call(env) sample_rate = config.flamegraph_sample_rate end - mode_match_data = query_string.match(/flamegraph_mode=([a-zA-Z]+)/) + mode_match_data = action_parameters(env)['flamegraph_mode'] if mode_match_data && [:cpu, :wall, :object, :custom].include?(mode_match_data[1].to_sym) mode = mode_match_data[1].to_sym @@ -342,7 +341,7 @@ def call(env) if trace_exceptions body.close if body.respond_to? :close - query_params = Rack::Utils.parse_nested_query(query_string) + query_params = action_parameters(env) trace_exceptions_filter = query_params['trace_exceptions_filter'] if trace_exceptions_filter trace_exceptions_regex = Regexp.new(trace_exceptions_filter) @@ -352,19 +351,19 @@ def call(env) return client_settings.handle_cookie(dump_exceptions exceptions) end - if query_string =~ /#{@config.profile_parameter}=env/ + if matches_action?("env", env) return tool_disabled_message(client_settings) if !advanced_debugging_enabled? body.close if body.respond_to? :close return client_settings.handle_cookie(dump_env env) end - if query_string =~ /#{@config.profile_parameter}=analyze-memory/ + if matches_action?("analyze-memory", env) return tool_disabled_message(client_settings) if !advanced_debugging_enabled? body.close if body.respond_to? :close return client_settings.handle_cookie(analyze_memory) end - if query_string =~ /#{@config.profile_parameter}=help/ + if matches_action?("help", env) body.close if body.respond_to? :close return client_settings.handle_cookie(help(client_settings, env)) end @@ -373,7 +372,7 @@ def call(env) page_struct[:user] = user(env) page_struct[:root].record_time((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000) - if flamegraph && query_string =~ /#{@config.profile_parameter}=flamegraph/ + if flamegraph && matches_action?("flamegraph", env) body.close if body.respond_to? :close return client_settings.handle_cookie(self.flamegraph(flamegraph, path, env)) elsif flamegraph # async-flamegraph @@ -403,6 +402,15 @@ def call(env) self.current = nil end + def matches_action?(action, env) + env['QUERY_STRING'] =~ /#{@config.profile_parameter}=#{action}/ || + env['HTTP_X_RACK_MINI_PROFILER'] == action + end + + def action_parameters(env) + query_params = Rack::Utils.parse_nested_query(env['QUERY_STRING']) + end + def inject_profiler(env, status, headers, body) # mini profiler is meddling with stuff, we can not cache cause we will get incorrect data # Rack::ETag has already inserted some nonesense in the chain diff --git a/lib/mini_profiler/views.rb b/lib/mini_profiler/views.rb index bfc255d1..aa6edac2 100644 --- a/lib/mini_profiler/views.rb +++ b/lib/mini_profiler/views.rb @@ -164,6 +164,8 @@ def help(client_settings, env) #{make_link "flamegraph_embed", env} : a graph representing sampled activity (requires the stackprof gem), embedded resources for use on an intranet. #{make_link "trace-exceptions", env} : will return all the spots where your application raises exceptions #{make_link "analyze-memory", env} : will perform basic memory analysis of heap + + All features can also be accessed by adding the X-Rack-Mini-Profiler header to the request, with any of the values above (e.g. 'X-Rack-Mini-Profiler: flamegraph') diff --git a/spec/integration/mini_profiler_spec.rb b/spec/integration/mini_profiler_spec.rb index 0d91ecb2..9ad9179e 100644 --- a/spec/integration/mini_profiler_spec.rb +++ b/spec/integration/mini_profiler_spec.rb @@ -376,6 +376,14 @@ def load_prof(response) expect(last_response.body).to include('QUERY_STRING') expect(last_response.body).to include('CONTENT_LENGTH') end + + it 'works via HTTP header' do + Rack::MiniProfiler.config.enable_advanced_debugging_tools = true + get '/html', nil, { 'HTTP_X_RACK_MINI_PROFILER' => 'env' } + + expect(last_response.body).to include('QUERY_STRING') + expect(last_response.body).to include('CONTENT_LENGTH') + end end end @@ -413,6 +421,11 @@ def load_prof(response) get '/html?pp=profile-gc' expect(last_response.header['Content-Type']).to include('text/plain') end + + it "should return a report when an HTTP header is used" do + get '/html', nil, { 'HTTP_X_RACK_MINI_PROFILER' => 'profile-gc' } + expect(last_response.header['Content-Type']).to include('text/plain') + end end describe 'error handling when storage_instance fails to save' do @@ -654,4 +667,11 @@ def load_prof(response) expect(last_response.body).to eq("Snapshot with id '"><qss>' not found"), "id should be escaped to prevent XSS" end end + + describe 'when triggering via HTTP header' do + it 'can trigger the help option via an HTTP header' do + get '/html', nil, { 'HTTP_X_RACK_MINI_PROFILER' => 'help' } + expect(last_response.body).to include('This is the help menu') + end + end end