MiniProfiler/rack-mini-profiler

View on GitHub
lib/mini_profiler/views.rb

Summary

Maintainability
B
4 hrs
Test Coverage
# frozen_string_literal: true
module Rack
  class MiniProfiler
    module Views
      def resources_root
        @resources_root ||= ::File.expand_path("../../html", __FILE__)
      end

      def share_template
        @share_template ||= ERB.new(::File.read(::File.expand_path("../html/share.html", ::File.dirname(__FILE__))))
      end

      def generate_html(page_struct, env, result_json = page_struct.to_json)
        # double-assigning to suppress "assigned but unused variable" warnings
        path = path = "#{env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME']}#{@config.base_url_path}"
        version = version = MiniProfiler::ASSET_VERSION
        json = json = result_json
        includes = includes = get_profile_script(env)
        name = name = page_struct[:name]
        duration = duration = page_struct.duration_ms.round(1).to_s

        share_template.result(binding)
      end

      # get_profile_script returns script to be injected inside current html page
      # By default, profile_script is appended to the end of all html requests automatically.
      # Calling get_profile_script cancels automatic append for the current page
      # Use it when:
      # * you have disabled auto append behaviour throught :auto_inject => false flag
      # * you do not want script to be automatically appended for the current page. You can also call cancel_auto_inject
      def get_profile_script(env, response_headers = {})
        path = public_base_path(env)
        version = MiniProfiler::ASSET_VERSION
        if @config.assets_url
          url = @config.assets_url.call('rack-mini-profiler.js', version, env)
          css_url = @config.assets_url.call('rack-mini-profiler.css', version, env)
        end

        url = "#{path}includes.js?v=#{version}" if !url
        css_url = "#{path}includes.css?v=#{version}" if !css_url

        configured_nonce = @config.content_security_policy_nonce
        if configured_nonce && !configured_nonce.is_a?(String)
          configured_nonce = configured_nonce.call(env, response_headers)
        end

        content_security_policy_nonce = configured_nonce ||
                                        env["action_dispatch.content_security_policy_nonce"] ||
                                        env["secure_headers_content_security_policy_nonce"]

        settings = {
         path: path,
         url: url,
         cssUrl: css_url,
         version: version,
         verticalPosition: @config.vertical_position,
         horizontalPosition: @config.horizontal_position,
         showTrivial: @config.show_trivial,
         showChildren: @config.show_children,
         maxTracesToShow: @config.max_traces_to_show,
         showControls: @config.show_controls,
         showTotalSqlCount: @config.show_total_sql_count,
         authorized: true,
         toggleShortcut: @config.toggle_shortcut,
         startHidden: @config.start_hidden,
         collapseResults: @config.collapse_results,
         htmlContainer: @config.html_container,
         hiddenCustomFields: @config.snapshot_hidden_custom_fields.join(','),
         cspNonce: content_security_policy_nonce,
         hotwireTurboDriveSupport: @config.enable_hotwire_turbo_drive_support,
        }

        if current && current.page_struct
          settings[:ids]       = ids_comma_separated(env)
          settings[:currentId] = current.page_struct[:id]
        else
          settings[:ids]       = []
          settings[:currentId] = ""
        end

        # TODO : cache this snippet
        script = ::File.read(::File.expand_path('../html/profile_handler.js', ::File.dirname(__FILE__)))
        # replace the variables
        settings.each do |k, v|
          regex = Regexp.new("\\{#{k}\\}")
          script.gsub!(regex, v.to_s)
        end

        current.inject_js = false if current
        script
      end

      BLANK_PAGE = <<~HTML
        <!DOCTYPE html>
        <html>
          <head>
            <title>Rack::MiniProfiler Requests</title>
          </head>
          <body>
          </body>
        </html>
      HTML
      def blank_page_html
        BLANK_PAGE
      end

      def make_link(postfix, env)
        link = env["PATH_INFO"] + "?" + env["QUERY_STRING"].sub("#{@config.profile_parameter}=help", "#{@config.profile_parameter}=#{postfix}")
        "#{@config.profile_parameter}=<a href='#{ERB::Util.html_escape(link)}'>#{postfix}</a>"
      end

      def flamegraph(graph, path, env)
        headers = { 'Content-Type' => 'text/html' }
        iframe_src = "#{public_base_path(env)}speedscope/index.html"
        html = <<~HTML
          <!DOCTYPE html>
          <html>
            <head>
              <title>Rack::MiniProfiler Flamegraph</title>
              <style>
                body { margin: 0; height: 100vh; }
                #speedscope-iframe { width: 100%; height: 100%; border: none; }
              </style>
            </head>
            <body>
              <script type="text/javascript">
                var graph = #{JSON.generate(graph)};
                var json = JSON.stringify(graph);
                var blob = new Blob([json], { type: 'text/plain' });
                var objUrl = encodeURIComponent(URL.createObjectURL(blob));
                var iframe = document.createElement('IFRAME');
                iframe.setAttribute('id', 'speedscope-iframe');
                document.body.appendChild(iframe);
                var iframeUrl = '#{iframe_src}#profileURL=' + objUrl + '&title=' + 'Flamegraph for #{CGI.escape(path)}';
                iframe.setAttribute('src', iframeUrl);
              </script>
            </body>
          </html>
        HTML
        [200, headers, [html]]
      end

      def help(client_settings, env)
        headers = { 'Content-Type' => 'text/html' }
        html = <<~HTML
          <!DOCTYPE html>
          <html>
            <head>
              <title>Rack::MiniProfiler Help</title>
            </head>
            <body>
              <pre style='line-height: 30px; font-size: 16px'>
                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:

                #{make_link "help", env} : display this screen
                #{make_link "env", env} : display the rack environment
                #{make_link "skip", env} : skip mini profiler for this request
                #{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)
                #{make_link "normal-backtrace", env} #{"(*) " if client_settings.backtrace_default?}: collect stack traces from all the SQL executed and filter normally
                #{make_link "full-backtrace", env} #{"(*) " if client_settings.backtrace_full?}: enable full backtraces for SQL executed (use #{@config.profile_parameter}=normal-backtrace to disable)
                #{make_link "disable", env} : disable profiling for this session
                #{make_link "enable", env} : enable profiling for this session (if previously disabled)
                #{make_link "profile-gc", env} : perform gc profiling on this request, analyzes ObjectSpace generated by request
                #{make_link "profile-memory", env} : requires the memory_profiler gem, new location based report
                #{make_link "flamegraph", env} : a graph representing sampled activity (requires the stackprof gem).
                #{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).
                #{make_link "flamegraph&flamegraph_sample_rate=1", env}: creates a flamegraph with the specified sample rate (in ms). Overrides value set in config
                #{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
                #{make_link "flamegraph&flamegraph_ignore_gc=true", env}: ignore garbage collection frames in flamegraphs. Overrides value set in config
                #{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>
        HTML

        [200, headers, [html]]
      end

      def url_for_snapshots_group(group_name)
        qs = Rack::Utils.build_query({ group_name: group_name })
        "/#{@config.base_url_path.gsub('/', '')}/snapshots?#{qs}"
      end

      def url_for_snapshot(id, group_name)
        qs = Rack::Utils.build_query({ id: id, group: group_name })
        "/#{@config.base_url_path.gsub('/', '')}/results?#{qs}"
      end

      def public_base_path(env)
        "#{env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME']}#{@config.base_url_path}"
      end
    end
  end
end