Skip to content
New issue

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

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

Already on GitHub? # to your account

Choose your RMP action via the X-Rack-Mini-Profiler header #578

Merged
merged 1 commit into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading