rack-mini-profiler 1.0.2 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +145 -21
  3. data/README.md +201 -94
  4. data/lib/enable_rails_patches.rb +5 -0
  5. data/lib/generators/rack_mini_profiler/USAGE +9 -0
  6. data/lib/generators/rack_mini_profiler/install_generator.rb +13 -0
  7. data/lib/generators/{rack_profiler/templates/rack_profiler.rb → rack_mini_profiler/templates/rack_mini_profiler.rb} +1 -1
  8. data/lib/generators/rack_profiler/install_generator.rb +6 -3
  9. data/lib/html/dot.1.1.2.min.js +2 -0
  10. data/lib/html/includes.css +144 -45
  11. data/lib/html/includes.js +1420 -1009
  12. data/lib/html/includes.scss +538 -441
  13. data/lib/html/includes.tmpl +231 -148
  14. data/lib/html/pretty-print.js +810 -0
  15. data/lib/html/profile_handler.js +1 -1
  16. data/lib/html/rack-mini-profiler.css +3 -0
  17. data/lib/html/rack-mini-profiler.js +2 -0
  18. data/lib/html/share.html +0 -1
  19. data/lib/html/speedscope/LICENSE +21 -0
  20. data/lib/html/speedscope/README.md +3 -0
  21. data/lib/html/speedscope/demangle-cpp.1768f4cc.js +4 -0
  22. data/lib/html/speedscope/favicon-16x16.f74b3187.png +0 -0
  23. data/lib/html/speedscope/favicon-32x32.bc503437.png +0 -0
  24. data/lib/html/speedscope/file-format-schema.json +324 -0
  25. data/lib/html/speedscope/fonts/source-code-pro-regular.css +8 -0
  26. data/lib/html/speedscope/fonts/source-code-pro-v13-regular.woff +0 -0
  27. data/lib/html/speedscope/fonts/source-code-pro-v13-regular.woff2 +0 -0
  28. data/lib/html/speedscope/import.cf0fa83f.js +115 -0
  29. data/lib/html/speedscope/index.html +2 -0
  30. data/lib/html/speedscope/release.txt +3 -0
  31. data/lib/html/speedscope/reset.8c46b7a1.css +2 -0
  32. data/lib/html/speedscope/source-map.438fa06b.js +24 -0
  33. data/lib/html/speedscope/speedscope.44364064.js +200 -0
  34. data/lib/html/vendor.js +848 -0
  35. data/lib/mini_profiler/asset_version.rb +3 -2
  36. data/lib/mini_profiler/client_settings.rb +15 -7
  37. data/lib/mini_profiler/config.rb +51 -5
  38. data/lib/mini_profiler/gc_profiler.rb +1 -1
  39. data/lib/mini_profiler/profiling_methods.rb +13 -8
  40. data/lib/mini_profiler/snapshots_transporter.rb +109 -0
  41. data/lib/mini_profiler/storage/abstract_store.rb +52 -1
  42. data/lib/mini_profiler/storage/file_store.rb +7 -3
  43. data/lib/mini_profiler/storage/memcache_store.rb +13 -7
  44. data/lib/mini_profiler/storage/memory_store.rb +100 -7
  45. data/lib/mini_profiler/storage/redis_store.rb +226 -3
  46. data/lib/mini_profiler/storage.rb +7 -0
  47. data/lib/mini_profiler/timer_struct/base.rb +2 -0
  48. data/lib/mini_profiler/timer_struct/custom.rb +1 -0
  49. data/lib/mini_profiler/timer_struct/page.rb +60 -4
  50. data/lib/mini_profiler/timer_struct/request.rb +53 -11
  51. data/lib/mini_profiler/timer_struct/sql.rb +6 -2
  52. data/lib/mini_profiler/timer_struct.rb +8 -0
  53. data/lib/mini_profiler/version.rb +2 -1
  54. data/lib/{mini_profiler/profiler.rb → mini_profiler.rb} +400 -83
  55. data/lib/mini_profiler_rails/railtie.rb +89 -7
  56. data/lib/mini_profiler_rails/railtie_methods.rb +61 -0
  57. data/lib/patches/db/activerecord.rb +1 -12
  58. data/lib/patches/db/mongo.rb +1 -1
  59. data/lib/patches/db/moped.rb +1 -1
  60. data/lib/patches/db/mysql2/alias_method.rb +30 -0
  61. data/lib/patches/db/mysql2/prepend.rb +34 -0
  62. data/lib/patches/db/mysql2.rb +4 -27
  63. data/lib/patches/db/plucky.rb +4 -4
  64. data/lib/patches/db/riak.rb +1 -1
  65. data/lib/patches/net_patches.rb +21 -10
  66. data/lib/patches/sql_patches.rb +13 -5
  67. data/lib/prepend_mysql2_patch.rb +5 -0
  68. data/lib/prepend_net_http_patch.rb +5 -0
  69. data/lib/rack-mini-profiler.rb +1 -24
  70. data/rack-mini-profiler.gemspec +17 -8
  71. metadata +156 -32
  72. data/lib/html/jquery.1.7.1.js +0 -4
  73. data/lib/html/jquery.tmpl.js +0 -486
  74. data/lib/html/list.css +0 -9
  75. data/lib/html/list.js +0 -38
  76. data/lib/html/list.tmpl +0 -34
@@ -1,10 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'cgi'
4
+ require 'json'
5
+ require 'erb'
6
+
7
+ require 'mini_profiler/timer_struct'
8
+ require 'mini_profiler/storage'
9
+ require 'mini_profiler/config'
10
+ require 'mini_profiler/profiling_methods'
11
+ require 'mini_profiler/context'
12
+ require 'mini_profiler/client_settings'
13
+ require 'mini_profiler/gc_profiler'
14
+ require 'mini_profiler/snapshots_transporter'
15
+
3
16
  module Rack
4
17
  class MiniProfiler
5
18
  class << self
6
19
 
7
20
  include Rack::MiniProfiler::ProfilingMethods
21
+ attr_accessor :subscribe_sql_active_record
22
+
23
+ def patch_rails?
24
+ !!defined?(Rack::MINI_PROFILER_ENABLE_RAILS_PATCHES)
25
+ end
8
26
 
9
27
  def generate_id
10
28
  rand(36**20).to_s(36)
@@ -20,11 +38,11 @@ module Rack
20
38
  end
21
39
 
22
40
  def resources_root
23
- @resources_root ||= ::File.expand_path("../../html", __FILE__)
41
+ @resources_root ||= ::File.expand_path("../html", __FILE__)
24
42
  end
25
43
 
26
44
  def share_template
27
- @share_template ||= ERB.new(::File.read(::File.expand_path("../html/share.html", ::File.dirname(__FILE__))))
45
+ @share_template ||= ERB.new(::File.read(::File.expand_path("html/share.html", ::File.dirname(__FILE__))))
28
46
  end
29
47
 
30
48
  def current
@@ -33,9 +51,21 @@ module Rack
33
51
 
34
52
  def current=(c)
35
53
  # we use TLS cause we need access to this from sql blocks and code blocks that have no access to env
54
+ Thread.current[:mini_profiler_snapshot_custom_fields] = nil
55
+ Thread.current[:mp_ongoing_snapshot] = nil
36
56
  Thread.current[:mini_profiler_private] = c
37
57
  end
38
58
 
59
+ def add_snapshot_custom_field(key, value)
60
+ thread_var_key = :mini_profiler_snapshot_custom_fields
61
+ Thread.current[thread_var_key] ||= {}
62
+ Thread.current[thread_var_key][key] = value
63
+ end
64
+
65
+ def get_snapshot_custom_fields
66
+ Thread.current[:mini_profiler_snapshot_custom_fields]
67
+ end
68
+
39
69
  # discard existing results, don't track this request
40
70
  def discard_results
41
71
  self.current.discard = true if current
@@ -62,6 +92,32 @@ module Rack
62
92
  Thread.current[:mp_authorized]
63
93
  end
64
94
 
95
+ def advanced_tools_message
96
+ <<~TEXT
97
+ This feature is disabled by default, to enable set the enable_advanced_debugging_tools option to true in Mini Profiler config.
98
+ TEXT
99
+ end
100
+
101
+ def binds_to_params(binds)
102
+ return if binds.nil? || config.max_sql_param_length == 0
103
+ # map ActiveRecord::Relation::QueryAttribute to [name, value]
104
+ params = binds.map { |c| c.kind_of?(Array) ? [c.first, c.last] : [c.name, c.value] }
105
+ if (skip = config.skip_sql_param_names)
106
+ params.map { |(n, v)| n =~ skip ? [n, nil] : [n, v] }
107
+ else
108
+ params
109
+ end
110
+ end
111
+
112
+ def snapshots_transporter?
113
+ !!config.snapshots_transport_destination_url &&
114
+ !!config.snapshots_transport_auth_key
115
+ end
116
+
117
+ def redact_sql_queries?
118
+ Thread.current[:mp_ongoing_snapshot] == true &&
119
+ Rack::MiniProfiler.config.snapshots_redact_sql_queries
120
+ end
65
121
  end
66
122
 
67
123
  #
@@ -71,7 +127,7 @@ module Rack
71
127
  MiniProfiler.config.merge!(config)
72
128
  @config = MiniProfiler.config
73
129
  @app = app
74
- @config.base_url_path << "/" unless @config.base_url_path.end_with? "/"
130
+ @config.base_url_path += "/" unless @config.base_url_path.end_with? "/"
75
131
  unless @config.storage_instance
76
132
  @config.storage_instance = @config.storage.new(@config.storage_options)
77
133
  end
@@ -84,15 +140,24 @@ module Rack
84
140
 
85
141
  def serve_results(env)
86
142
  request = Rack::Request.new(env)
87
- id = request[:id]
88
- page_struct = @storage.load(id)
89
- unless page_struct
143
+ id = request.params['id']
144
+ group_name = request.params['group']
145
+ is_snapshot = group_name && group_name.size > 0
146
+ if is_snapshot
147
+ page_struct = @storage.load_snapshot(id, group_name)
148
+ else
149
+ page_struct = @storage.load(id)
150
+ end
151
+ if !page_struct && is_snapshot
152
+ id = ERB::Util.html_escape(id)
153
+ return [404, {}, ["Snapshot with id '#{id}' not found"]]
154
+ elsif !page_struct
90
155
  @storage.set_viewed(user(env), id)
91
- id = ERB::Util.html_escape(request['id'])
156
+ id = ERB::Util.html_escape(id)
92
157
  user_info = ERB::Util.html_escape(user(env))
93
158
  return [404, {}, ["Request not found: #{id} - user #{user_info}"]]
94
159
  end
95
- unless page_struct[:has_user_viewed]
160
+ if !page_struct[:has_user_viewed] && !is_snapshot
96
161
  page_struct[:client_timings] = TimerStruct::Client.init_from_form_data(env, page_struct)
97
162
  page_struct[:has_user_viewed] = true
98
163
  @storage.save(page_struct)
@@ -127,11 +192,13 @@ module Rack
127
192
  file_name = path.sub(@config.base_url_path, '')
128
193
 
129
194
  return serve_results(env) if file_name.eql?('results')
195
+ return handle_snapshots_request(env) if file_name.eql?('snapshots')
196
+ return serve_flamegraph(env) if file_name.eql?('flamegraph')
130
197
 
131
198
  resources_env = env.dup
132
199
  resources_env['PATH_INFO'] = file_name
133
200
 
134
- rack_file = Rack::File.new(MiniProfiler.resources_root, 'Cache-Control' => 'max-age:86400')
201
+ rack_file = Rack::File.new(MiniProfiler.resources_root, 'Cache-Control' => "max-age=#{cache_control_value}")
135
202
  rack_file.call(resources_env)
136
203
  end
137
204
 
@@ -147,11 +214,18 @@ module Rack
147
214
  @config
148
215
  end
149
216
 
150
- def call(env)
217
+ def advanced_debugging_enabled?
218
+ config.enable_advanced_debugging_tools
219
+ end
220
+
221
+ def tool_disabled_message(client_settings)
222
+ client_settings.handle_cookie(text_result(Rack::MiniProfiler.advanced_tools_message))
223
+ end
151
224
 
225
+ def call(env)
152
226
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
153
227
  client_settings = ClientSettings.new(env, @storage, start)
154
- MiniProfiler.deauthorize_request if @config.authorization_mode == :whitelist
228
+ MiniProfiler.deauthorize_request if @config.authorization_mode == :allow_authorized
155
229
 
156
230
  status = headers = body = nil
157
231
  query_string = env['QUERY_STRING']
@@ -160,15 +234,31 @@ module Rack
160
234
  # Someone (e.g. Rails engine) could change the SCRIPT_NAME so we save it
161
235
  env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME'] = ENV['PASSENGER_BASE_URI'] || env['SCRIPT_NAME']
162
236
 
163
- skip_it = (@config.pre_authorize_cb && !@config.pre_authorize_cb.call(env)) ||
164
- (@config.skip_paths && @config.skip_paths.any? { |p| path.start_with?(p) }) ||
165
- query_string =~ /pp=skip/
237
+ skip_it = /#{@config.profile_parameter}=skip/.match?(query_string) || (
238
+ @config.skip_paths &&
239
+ @config.skip_paths.any? do |p|
240
+ if p.instance_of?(String)
241
+ path.start_with?(p)
242
+ elsif p.instance_of?(Regexp)
243
+ p.match?(path)
244
+ end
245
+ end
246
+ )
247
+ if skip_it
248
+ return client_settings.handle_cookie(@app.call(env))
249
+ end
250
+
251
+ skip_it = (@config.pre_authorize_cb && !@config.pre_authorize_cb.call(env))
166
252
 
167
253
  if skip_it || (
168
- @config.authorization_mode == :whitelist &&
254
+ @config.authorization_mode == :allow_authorized &&
169
255
  !client_settings.has_valid_cookie?
170
256
  )
171
- return client_settings.handle_cookie(@app.call(env))
257
+ if take_snapshot?(path)
258
+ return client_settings.handle_cookie(take_snapshot(env, start))
259
+ else
260
+ return client_settings.handle_cookie(@app.call(env))
261
+ end
172
262
  end
173
263
 
174
264
  # handle all /mini-profiler requests here
@@ -176,11 +266,11 @@ module Rack
176
266
 
177
267
  has_disable_cookie = client_settings.disable_profiling?
178
268
  # manual session disable / enable
179
- if query_string =~ /pp=disable/ || has_disable_cookie
269
+ if query_string =~ /#{@config.profile_parameter}=disable/ || has_disable_cookie
180
270
  skip_it = true
181
271
  end
182
272
 
183
- if query_string =~ /pp=enable/
273
+ if query_string =~ /#{@config.profile_parameter}=enable/
184
274
  skip_it = false
185
275
  config.enabled = true
186
276
  end
@@ -194,13 +284,26 @@ module Rack
194
284
  end
195
285
 
196
286
  # profile gc
197
- if query_string =~ /pp=profile-gc/
287
+ if query_string =~ /#{@config.profile_parameter}=profile-gc/
288
+ return tool_disabled_message(client_settings) if !advanced_debugging_enabled?
198
289
  current.measure = false if current
199
290
  return client_settings.handle_cookie(Rack::MiniProfiler::GCProfiler.new.profile_gc(@app, env))
200
291
  end
201
292
 
202
293
  # profile memory
203
- if query_string =~ /pp=profile-memory/
294
+ if query_string =~ /#{@config.profile_parameter}=profile-memory/
295
+ return tool_disabled_message(client_settings) if !advanced_debugging_enabled?
296
+
297
+ unless defined?(MemoryProfiler) && MemoryProfiler.respond_to?(:report)
298
+ message = "Please install the memory_profiler gem and require it: add gem 'memory_profiler' to your Gemfile"
299
+ status, headers, body = @app.call(env)
300
+ body.close if body.respond_to? :close
301
+
302
+ return client_settings.handle_cookie(
303
+ text_result(message, status: 500, headers: headers)
304
+ )
305
+ end
306
+
204
307
  query_params = Rack::Utils.parse_nested_query(query_string)
205
308
  options = {
206
309
  ignore_files: query_params['memory_profiler_ignore_files'],
@@ -218,12 +321,12 @@ module Rack
218
321
 
219
322
  MiniProfiler.create_current(env, @config)
220
323
 
221
- if query_string =~ /pp=normal-backtrace/
324
+ if query_string =~ /#{@config.profile_parameter}=normal-backtrace/
222
325
  client_settings.backtrace_level = ClientSettings::BACKTRACE_DEFAULT
223
- elsif query_string =~ /pp=no-backtrace/
326
+ elsif query_string =~ /#{@config.profile_parameter}=no-backtrace/
224
327
  current.skip_backtrace = true
225
328
  client_settings.backtrace_level = ClientSettings::BACKTRACE_NONE
226
- elsif query_string =~ /pp=full-backtrace/ || client_settings.backtrace_full?
329
+ elsif query_string =~ /#{@config.profile_parameter}=full-backtrace/ || client_settings.backtrace_full?
227
330
  current.full_backtrace = true
228
331
  client_settings.backtrace_level = ClientSettings::BACKTRACE_FULL
229
332
  elsif client_settings.backtrace_none?
@@ -232,7 +335,7 @@ module Rack
232
335
 
233
336
  flamegraph = nil
234
337
 
235
- trace_exceptions = query_string =~ /pp=trace-exceptions/ && defined? TracePoint
338
+ trace_exceptions = query_string =~ /#{@config.profile_parameter}=trace-exceptions/ && defined? TracePoint
236
339
  status, headers, body, exceptions, trace = nil
237
340
 
238
341
  if trace_exceptions
@@ -256,27 +359,56 @@ module Rack
256
359
  # Prevent response body from being compressed
257
360
  env['HTTP_ACCEPT_ENCODING'] = 'identity' if config.suppress_encoding
258
361
 
259
- if query_string =~ /pp=flamegraph/
260
- unless defined?(Flamegraph) && Flamegraph.respond_to?(:generate)
261
-
262
- flamegraph = "Please install the flamegraph gem and require it: add gem 'flamegraph' to your Gemfile"
263
- status, headers, body = @app.call(env)
264
- else
362
+ if query_string =~ /pp=(async-)?flamegraph/ || env['HTTP_REFERER'] =~ /pp=async-flamegraph/
363
+ if defined?(StackProf) && StackProf.respond_to?(:run)
265
364
  # do not sully our profile with mini profiler timings
266
365
  current.measure = false
267
366
  match_data = query_string.match(/flamegraph_sample_rate=([\d\.]+)/)
268
367
 
269
- mode = query_string =~ /mode=c/ ? :c : :ruby
270
-
271
368
  if match_data && !match_data[1].to_f.zero?
272
369
  sample_rate = match_data[1].to_f
273
370
  else
274
371
  sample_rate = config.flamegraph_sample_rate
275
372
  end
276
- flamegraph = Flamegraph.generate(nil, fidelity: sample_rate, embed_resources: query_string =~ /embed/, mode: mode) do
373
+
374
+ mode_match_data = query_string.match(/flamegraph_mode=([a-zA-Z]+)/)
375
+
376
+ if mode_match_data && [:cpu, :wall, :object, :custom].include?(mode_match_data[1].to_sym)
377
+ mode = mode_match_data[1].to_sym
378
+ else
379
+ mode = config.flamegraph_mode
380
+ end
381
+
382
+ flamegraph = StackProf.run(
383
+ mode: mode,
384
+ raw: true,
385
+ aggregate: false,
386
+ interval: (sample_rate * 1000).to_i
387
+ ) do
277
388
  status, headers, body = @app.call(env)
278
389
  end
390
+ else
391
+ message = "Please install the stackprof gem and require it: add gem 'stackprof' to your Gemfile"
392
+ status, headers, body = @app.call(env)
393
+ body.close if body.respond_to? :close
394
+
395
+ return client_settings.handle_cookie(
396
+ text_result(message, status: status, headers: headers)
397
+ )
279
398
  end
399
+ elsif path == '/rack-mini-profiler/requests'
400
+ blank_page_html = <<~HTML
401
+ <!DOCTYPE html>
402
+ <html>
403
+ <head>
404
+ <title>Rack::MiniProfiler Requests</title>
405
+ </head>
406
+ <body>
407
+ </body>
408
+ </html>
409
+ HTML
410
+
411
+ status, headers, body = [200, { 'Content-Type' => 'text/html' }, [blank_page_html.dup]]
280
412
  else
281
413
  status, headers, body = @app.call(env)
282
414
  end
@@ -287,7 +419,7 @@ module Rack
287
419
 
288
420
  skip_it = current.discard
289
421
 
290
- if (config.authorization_mode == :whitelist && !MiniProfiler.request_authorized?)
422
+ if (config.authorization_mode == :allow_authorized && !MiniProfiler.request_authorized?)
291
423
  skip_it = true
292
424
  end
293
425
 
@@ -307,17 +439,19 @@ module Rack
307
439
  return client_settings.handle_cookie(dump_exceptions exceptions)
308
440
  end
309
441
 
310
- if query_string =~ /pp=env/ && !config.disable_env_dump
442
+ if query_string =~ /#{@config.profile_parameter}=env/
443
+ return tool_disabled_message(client_settings) if !advanced_debugging_enabled?
311
444
  body.close if body.respond_to? :close
312
445
  return client_settings.handle_cookie(dump_env env)
313
446
  end
314
447
 
315
- if query_string =~ /pp=analyze-memory/
448
+ if query_string =~ /#{@config.profile_parameter}=analyze-memory/
449
+ return tool_disabled_message(client_settings) if !advanced_debugging_enabled?
316
450
  body.close if body.respond_to? :close
317
451
  return client_settings.handle_cookie(analyze_memory)
318
452
  end
319
453
 
320
- if query_string =~ /pp=help/
454
+ if query_string =~ /#{@config.profile_parameter}=help/
321
455
  body.close if body.respond_to? :close
322
456
  return client_settings.handle_cookie(help(client_settings, env))
323
457
  end
@@ -326,9 +460,12 @@ module Rack
326
460
  page_struct[:user] = user(env)
327
461
  page_struct[:root].record_time((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000)
328
462
 
329
- if flamegraph
463
+ if flamegraph && query_string =~ /#{@config.profile_parameter}=flamegraph/
330
464
  body.close if body.respond_to? :close
331
- return client_settings.handle_cookie(self.flamegraph(flamegraph))
465
+ return client_settings.handle_cookie(self.flamegraph(flamegraph, path, env))
466
+ elsif flamegraph # async-flamegraph
467
+ page_struct[:has_flamegraph] = true
468
+ page_struct[:flamegraph] = flamegraph
332
469
  end
333
470
 
334
471
  begin
@@ -369,7 +506,7 @@ module Rack
369
506
 
370
507
  # inject header
371
508
  if headers.is_a? Hash
372
- headers['X-MiniProfiler-Ids'] = ids_json(env)
509
+ headers['X-MiniProfiler-Ids'] = ids_comma_separated(env)
373
510
  end
374
511
 
375
512
  if current.inject_js && content_type =~ /text\/html/
@@ -421,7 +558,7 @@ module Rack
421
558
 
422
559
  body << "\nBacktraces\n"
423
560
  exceptions.each_with_index do |e, i|
424
- body << "##{i + 1}: #{e.class} - \"#{e.message}\"\n #{e.backtrace.join("\n ")}\n\n"
561
+ body << "##{i + 1}: #{e.class} - \"#{e.message.lines.first.chomp}\"\n #{e.backtrace.join("\n ")}\n\n"
425
562
  end
426
563
  end
427
564
  text_result(body)
@@ -521,48 +658,82 @@ module Rack
521
658
  text_result(body)
522
659
  end
523
660
 
524
- def text_result(body)
525
- headers = { 'Content-Type' => 'text/plain' }
526
- [200, headers, [body]]
661
+ def text_result(body, status: 200, headers: nil)
662
+ headers = (headers || {}).merge('Content-Type' => 'text/plain; charset=utf-8')
663
+ [status, headers, [body]]
527
664
  end
528
665
 
529
666
  def make_link(postfix, env)
530
- link = env["PATH_INFO"] + "?" + env["QUERY_STRING"].sub("pp=help", "pp=#{postfix}")
531
- "pp=<a href='#{link}'>#{postfix}</a>"
667
+ link = env["PATH_INFO"] + "?" + env["QUERY_STRING"].sub("#{@config.profile_parameter}=help", "#{@config.profile_parameter}=#{postfix}")
668
+ "#{@config.profile_parameter}=<a href='#{ERB::Util.html_escape(link)}'>#{postfix}</a>"
532
669
  end
533
670
 
534
671
  def help(client_settings, env)
535
672
  headers = { 'Content-Type' => 'text/html' }
536
- body = "<html><body>
537
- <pre style='line-height: 30px; font-size: 16px;'>
538
- Append the following to your query string:
539
-
540
- #{make_link "help", env} : display this screen
541
- #{make_link "env", env} : display the rack environment
542
- #{make_link "skip", env} : skip mini profiler for this request
543
- #{make_link "no-backtrace", env} #{"(*) " if client_settings.backtrace_none?}: don't collect stack traces from all the SQL executed (sticky, use pp=normal-backtrace to enable)
544
- #{make_link "normal-backtrace", env} #{"(*) " if client_settings.backtrace_default?}: collect stack traces from all the SQL executed and filter normally
545
- #{make_link "full-backtrace", env} #{"(*) " if client_settings.backtrace_full?}: enable full backtraces for SQL executed (use pp=normal-backtrace to disable)
546
- #{make_link "disable", env} : disable profiling for this session
547
- #{make_link "enable", env} : enable profiling for this session (if previously disabled)
548
- #{make_link "profile-gc", env} : perform gc profiling on this request, analyzes ObjectSpace generated by request (ruby 1.9.3 only)
549
- #{make_link "profile-memory", env} : requires the memory_profiler gem, new location based report
550
- #{make_link "flamegraph", env} : works best on Ruby 2.0, a graph representing sampled activity (requires the flamegraph gem).
551
- #{make_link "flamegraph&flamegraph_sample_rate=1", env}: creates a flamegraph with the specified sample rate (in ms). Overrides value set in config
552
- #{make_link "flamegraph_embed", env} : works best on Ruby 2.0, a graph representing sampled activity (requires the flamegraph gem), embedded resources for use on an intranet.
553
- #{make_link "trace-exceptions", env} : requires Ruby 2.0, will return all the spots where your application raises exceptions
554
- #{make_link "analyze-memory", env} : requires Ruby 2.0, will perform basic memory analysis of heap
555
- </pre>
556
- </body>
557
- </html>
558
- "
559
-
560
- [200, headers, [body]]
561
- end
562
-
563
- def flamegraph(graph)
673
+ html = <<~HTML
674
+ <!DOCTYPE html>
675
+ <html>
676
+ <head>
677
+ <title>Rack::MiniProfiler Help</title>
678
+ </head>
679
+ <body>
680
+ <pre style='line-height: 30px; font-size: 16px'>
681
+ This is the help menu of the <a href='#{Rack::MiniProfiler::SOURCE_CODE_URI}'>rack-mini-profiler</a> gem, append the following to your query string for more options:
682
+
683
+ #{make_link "help", env} : display this screen
684
+ #{make_link "env", env} : display the rack environment
685
+ #{make_link "skip", env} : skip mini profiler for this request
686
+ #{make_link "no-backtrace", env} #{"(*) " if client_settings.backtrace_none?}: don't collect stack traces from all the SQL executed (sticky, use #{@config.profile_parameter}=normal-backtrace to enable)
687
+ #{make_link "normal-backtrace", env} #{"(*) " if client_settings.backtrace_default?}: collect stack traces from all the SQL executed and filter normally
688
+ #{make_link "full-backtrace", env} #{"(*) " if client_settings.backtrace_full?}: enable full backtraces for SQL executed (use #{@config.profile_parameter}=normal-backtrace to disable)
689
+ #{make_link "disable", env} : disable profiling for this session
690
+ #{make_link "enable", env} : enable profiling for this session (if previously disabled)
691
+ #{make_link "profile-gc", env} : perform gc profiling on this request, analyzes ObjectSpace generated by request
692
+ #{make_link "profile-memory", env} : requires the memory_profiler gem, new location based report
693
+ #{make_link "flamegraph", env} : a graph representing sampled activity (requires the stackprof gem).
694
+ #{make_link "async-flamegraph", env} : store flamegraph data for this page and all its AJAX requests. Flamegraph links will be available in the mini-profiler UI (requires the stackprof gem).
695
+ #{make_link "flamegraph&flamegraph_sample_rate=1", env}: creates a flamegraph with the specified sample rate (in ms). Overrides value set in config
696
+ #{make_link "flamegraph&flamegraph_mode=cpu", env}: creates a flamegraph with the specified mode (one of cpu, wall, object, or custom). Overrides value set in config
697
+ #{make_link "flamegraph_embed", env} : a graph representing sampled activity (requires the stackprof gem), embedded resources for use on an intranet.
698
+ #{make_link "trace-exceptions", env} : will return all the spots where your application raises exceptions
699
+ #{make_link "analyze-memory", env} : will perform basic memory analysis of heap
700
+ </pre>
701
+ </body>
702
+ </html>
703
+ HTML
704
+
705
+ [200, headers, [html]]
706
+ end
707
+
708
+ def flamegraph(graph, path, env)
564
709
  headers = { 'Content-Type' => 'text/html' }
565
- [200, headers, [graph]]
710
+ iframe_src = "#{public_base_path(env)}speedscope/index.html"
711
+ html = <<~HTML
712
+ <!DOCTYPE html>
713
+ <html>
714
+ <head>
715
+ <title>Rack::MiniProfiler Flamegraph</title>
716
+ <style>
717
+ body { margin: 0; height: 100vh; }
718
+ #speedscope-iframe { width: 100%; height: 100%; border: none; }
719
+ </style>
720
+ </head>
721
+ <body>
722
+ <script type="text/javascript">
723
+ var graph = #{JSON.generate(graph)};
724
+ var json = JSON.stringify(graph);
725
+ var blob = new Blob([json], { type: 'text/plain' });
726
+ var objUrl = encodeURIComponent(URL.createObjectURL(blob));
727
+ var iframe = document.createElement('IFRAME');
728
+ iframe.setAttribute('id', 'speedscope-iframe');
729
+ document.body.appendChild(iframe);
730
+ var iframeUrl = '#{iframe_src}#profileURL=' + objUrl + '&title=' + 'Flamegraph for #{CGI.escape(path)}';
731
+ iframe.setAttribute('src', iframeUrl);
732
+ </script>
733
+ </body>
734
+ </html>
735
+ HTML
736
+ [200, headers, [html]]
566
737
  end
567
738
 
568
739
  def ids(env)
@@ -574,10 +745,6 @@ Append the following to your query string:
574
745
  all
575
746
  end
576
747
 
577
- def ids_json(env)
578
- ::JSON.generate(ids(env))
579
- end
580
-
581
748
  def ids_comma_separated(env)
582
749
  ids(env).join(",")
583
750
  end
@@ -589,11 +756,25 @@ Append the following to your query string:
589
756
  # * you have disabled auto append behaviour throught :auto_inject => false flag
590
757
  # * you do not want script to be automatically appended for the current page. You can also call cancel_auto_inject
591
758
  def get_profile_script(env)
592
- path = "#{env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME']}#{@config.base_url_path}"
759
+ path = public_base_path(env)
760
+ version = MiniProfiler::ASSET_VERSION
761
+ if @config.assets_url
762
+ url = @config.assets_url.call('rack-mini-profiler.js', version, env)
763
+ css_url = @config.assets_url.call('rack-mini-profiler.css', version, env)
764
+ end
765
+
766
+ url = "#{path}includes.js?v=#{version}" if !url
767
+ css_url = "#{path}includes.css?v=#{version}" if !css_url
768
+
769
+ content_security_policy_nonce = @config.content_security_policy_nonce ||
770
+ env["action_dispatch.content_security_policy_nonce"] ||
771
+ env["secure_headers_content_security_policy_nonce"]
593
772
 
594
773
  settings = {
595
774
  path: path,
596
- version: MiniProfiler::ASSET_VERSION,
775
+ url: url,
776
+ cssUrl: css_url,
777
+ version: version,
597
778
  verticalPosition: @config.vertical_position,
598
779
  horizontalPosition: @config.horizontal_position,
599
780
  showTrivial: @config.show_trivial,
@@ -605,7 +786,10 @@ Append the following to your query string:
605
786
  toggleShortcut: @config.toggle_shortcut,
606
787
  startHidden: @config.start_hidden,
607
788
  collapseResults: @config.collapse_results,
608
- htmlContainer: @config.html_container
789
+ htmlContainer: @config.html_container,
790
+ hiddenCustomFields: @config.snapshot_hidden_custom_fields.join(','),
791
+ cspNonce: content_security_policy_nonce,
792
+ hotwireTurboDriveSupport: @config.enable_hotwire_turbo_drive_support,
609
793
  }
610
794
 
611
795
  if current && current.page_struct
@@ -617,7 +801,7 @@ Append the following to your query string:
617
801
  end
618
802
 
619
803
  # TODO : cache this snippet
620
- script = IO.read(::File.expand_path('../html/profile_handler.js', ::File.dirname(__FILE__)))
804
+ script = ::File.read(::File.expand_path('html/profile_handler.js', ::File.dirname(__FILE__)))
621
805
  # replace the variables
622
806
  settings.each do |k, v|
623
807
  regex = Regexp.new("\\{#{k.to_s}\\}")
@@ -633,5 +817,138 @@ Append the following to your query string:
633
817
  current.inject_js = false
634
818
  end
635
819
 
820
+ def cache_control_value
821
+ 86400
822
+ end
823
+
824
+ private
825
+
826
+ def handle_snapshots_request(env)
827
+ self.current = nil
828
+ MiniProfiler.authorize_request
829
+ status = 200
830
+ headers = { 'Content-Type' => 'text/html' }
831
+ qp = Rack::Utils.parse_nested_query(env['QUERY_STRING'])
832
+ if group_name = qp["group_name"]
833
+ list = @storage.snapshots_group(group_name)
834
+ list.each do |snapshot|
835
+ snapshot[:url] = url_for_snapshot(snapshot[:id], group_name)
836
+ end
837
+ data = {
838
+ group_name: group_name,
839
+ list: list
840
+ }
841
+ else
842
+ list = @storage.snapshots_overview
843
+ list.each do |group|
844
+ group[:url] = url_for_snapshots_group(group[:name])
845
+ end
846
+ data = {
847
+ page: "overview",
848
+ list: list
849
+ }
850
+ end
851
+ data_html = <<~HTML
852
+ <div style="display: none;" id="snapshots-data">
853
+ #{data.to_json}
854
+ </div>
855
+ HTML
856
+ response = Rack::Response.new([], status, headers)
857
+
858
+ response.write <<~HTML
859
+ <!DOCTYPE html>
860
+ <html>
861
+ <head>
862
+ <title>Rack::MiniProfiler Snapshots</title>
863
+ </head>
864
+ <body class="mp-snapshots">
865
+ HTML
866
+ response.write(data_html)
867
+ script = self.get_profile_script(env)
868
+ response.write(script)
869
+ response.write <<~HTML
870
+ </body>
871
+ </html>
872
+ HTML
873
+ response.finish
874
+ end
875
+
876
+ def serve_flamegraph(env)
877
+ request = Rack::Request.new(env)
878
+ id = request.params['id']
879
+ page_struct = @storage.load(id)
880
+
881
+ if !page_struct
882
+ id = ERB::Util.html_escape(id)
883
+ user_info = ERB::Util.html_escape(user(env))
884
+ return [404, {}, ["Request not found: #{id} - user #{user_info}"]]
885
+ end
886
+
887
+ if !page_struct[:flamegraph]
888
+ return [404, {}, ["No flamegraph available for #{ERB::Util.html_escape(id)}"]]
889
+ end
890
+
891
+ self.flamegraph(page_struct[:flamegraph], page_struct[:request_path], env)
892
+ end
893
+
894
+ def rails_route_from_path(path, method)
895
+ if defined?(Rails) && defined?(ActionController::RoutingError)
896
+ hash = Rails.application.routes.recognize_path(path, method: method)
897
+ if hash && hash[:controller] && hash[:action]
898
+ "#{hash[:controller]}##{hash[:action]}"
899
+ end
900
+ end
901
+ rescue ActionController::RoutingError
902
+ nil
903
+ end
904
+
905
+ def url_for_snapshots_group(group_name)
906
+ qs = Rack::Utils.build_query({ group_name: group_name })
907
+ "/#{@config.base_url_path.gsub('/', '')}/snapshots?#{qs}"
908
+ end
909
+
910
+ def url_for_snapshot(id, group_name)
911
+ qs = Rack::Utils.build_query({ id: id, group: group_name })
912
+ "/#{@config.base_url_path.gsub('/', '')}/results?#{qs}"
913
+ end
914
+
915
+ def take_snapshot?(path)
916
+ @config.snapshot_every_n_requests > 0 &&
917
+ !path.start_with?(@config.base_url_path) &&
918
+ @storage.should_take_snapshot?(@config.snapshot_every_n_requests)
919
+ end
920
+
921
+ def take_snapshot(env, start)
922
+ MiniProfiler.create_current(env, @config)
923
+ Thread.current[:mp_ongoing_snapshot] = true
924
+ results = @app.call(env)
925
+ status = results[0].to_i
926
+ if status >= 200 && status < 300
927
+ page_struct = current.page_struct
928
+ page_struct[:root].record_time(
929
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
930
+ )
931
+ custom_fields = MiniProfiler.get_snapshot_custom_fields
932
+ page_struct[:custom_fields] = custom_fields if custom_fields
933
+ if Rack::MiniProfiler.snapshots_transporter?
934
+ Rack::MiniProfiler::SnapshotsTransporter.transport(page_struct)
935
+ else
936
+ group_name = rails_route_from_path(page_struct[:request_path], page_struct[:request_method])
937
+ group_name ||= page_struct[:request_path]
938
+ group_name = "#{page_struct[:request_method]} #{group_name}"
939
+ @storage.push_snapshot(
940
+ page_struct,
941
+ group_name,
942
+ @config
943
+ )
944
+ end
945
+ end
946
+ self.current = nil
947
+ results
948
+ end
949
+
950
+ def public_base_path(env)
951
+ "#{env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME']}#{@config.base_url_path}"
952
+ end
636
953
  end
637
954
  end