Skip to content

Commit

Permalink
Choose your RMP action via the X-Rack-Mini-Profiler header (#578)
Browse files Browse the repository at this point in the history
Co-authored-by: Nate Berkopec <nate.berkopec@gmail.coml>
  • Loading branch information
nateberkopec and Nate Berkopec committed Dec 5, 2023
1 parent c5fc9b1 commit fb2c080
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 19 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -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.

Expand Down
44 changes: 26 additions & 18 deletions lib/mini_profiler.rb
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -231,26 +230,26 @@ 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

# any other requests past this point are going to the app to be profiled

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?
Expand All @@ -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
Expand All @@ -283,19 +282,19 @@ 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
else
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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/mini_profiler/views.rb
Expand Up @@ -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')
</pre>
</body>
</html>
Expand Down
20 changes: 20 additions & 0 deletions spec/integration/mini_profiler_spec.rb
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -654,4 +667,11 @@ def load_prof(response)
expect(last_response.body).to eq("Snapshot with id '&quot;&gt;&lt;qss&gt;' 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

0 comments on commit fb2c080

Please sign in to comment.