rails-profiler 0.35.2 → 0.36.0

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.
@@ -33,6 +33,15 @@
33
33
  return window.__PROFILER_PARENT_TOKEN__;
34
34
  }
35
35
 
36
+ // A Turbo Drive visit to a page the profiler left alone brings no inline script to set a new
37
+ // token, and the window keeps the one of the previous page: forget it, or every request of
38
+ // this page would be linked to a page the user has left.
39
+ document.addEventListener('turbo:load', function() {
40
+ if (!document.getElementById('profiler-toolbar')) {
41
+ delete window.__PROFILER_PARENT_TOKEN__;
42
+ }
43
+ });
44
+
36
45
  function isRequest(input) {
37
46
  return typeof Request !== 'undefined' && input instanceof Request;
38
47
  }
@@ -10,16 +10,21 @@ module Profiler
10
10
 
11
11
  BUILD_MISSING = "Profiler assets are not built (%s is missing). Run `yarn build:full` in the profiler gem."
12
12
 
13
+ # A year: the URL a page uses carries the digest of the content (?v=), so a new version of
14
+ # the gem is a new URL. Without the digest, or with an old one, the file is only kept an
15
+ # hour, as it may be replaced under the same URL.
16
+ VERSIONED_CACHE_CONTROL = "public, max-age=#{365 * 24 * 3600}, immutable".freeze
17
+
13
18
  def toolbar_js
14
- serve("profiler-toolbar.js", "application/javascript")
19
+ serve "profiler-toolbar.js", "application/javascript"
15
20
  end
16
21
 
17
22
  def main_js
18
- serve("profiler.js", "application/javascript")
23
+ serve "profiler.js", "application/javascript"
19
24
  end
20
25
 
21
26
  def main_css
22
- serve("profiler.css", "text/css")
27
+ serve "profiler.css", "text/css"
23
28
  end
24
29
 
25
30
  def icon
@@ -30,8 +35,12 @@ module Profiler
30
35
  private
31
36
 
32
37
  def serve(name, content_type)
33
- body = File.read(Profiler::Engine.root.join("app", "assets", "builds", name))
34
- expires_in 1.hour, public: true
38
+ body = Profiler::Assets.read(name)
39
+ if Profiler::Assets.current?(name, params[:v])
40
+ response.headers["Cache-Control"] = VERSIONED_CACHE_CONTROL
41
+ else
42
+ expires_in 1.hour, public: true
43
+ end
35
44
  render plain: body, content_type: content_type
36
45
  rescue Errno::ENOENT
37
46
  render plain: format(BUILD_MISSING, name), status: :not_found
@@ -10,8 +10,8 @@
10
10
  <meta name="profiler-name" content="<%= Profiler.configuration.resolved_name %>">
11
11
  <meta name="profiler-is-master" content="<%= Profiler.configuration.master? %>">
12
12
 
13
- <link rel="stylesheet" href="/_profiler/assets/profiler.css">
14
- <script src="/_profiler/assets/profiler.js" defer></script>
13
+ <link rel="stylesheet" href="<%= Profiler::Assets.url("profiler.css") %>">
14
+ <script src="<%= Profiler::Assets.url("profiler.js") %>" defer></script>
15
15
  </head>
16
16
 
17
17
  <body>
@@ -1,3 +1,5 @@
1
+ <% nonce = content_security_policy_nonce %>
2
+ <% nonce_attr = nonce ? %( nonce="#{ERB::Util.html_escape(nonce)}").html_safe : "" %>
1
3
  <!DOCTYPE html>
2
4
  <html data-theme="<%= params[:theme].presence&.then { |t| %w[light dark].include?(t) ? t : 'dark' } || 'dark' %>">
3
5
  <head>
@@ -8,16 +10,16 @@
8
10
  <%= csp_meta_tag %>
9
11
  <meta name="profiler-version" content="<%= Profiler.version %>">
10
12
 
11
- <link rel="stylesheet" href="/_profiler/assets/profiler.css">
12
- <script src="/_profiler/assets/profiler.js" defer></script>
13
+ <link rel="stylesheet" href="<%= Profiler::Assets.url("profiler.css") %>">
14
+ <script src="<%= Profiler::Assets.url("profiler.js") %>" defer></script>
13
15
 
14
- <style>
16
+ <style<%= nonce_attr %>>
15
17
  /* Embedded overrides, tighter layout, no page chrome */
16
18
  body { overflow-y: auto; }
17
19
  .container { padding: 16px 20px; }
18
20
  </style>
19
21
 
20
- <script>
22
+ <script<%= nonce_attr %>>
21
23
  // Receive theme changes from the Chrome extension panel via postMessage.
22
24
  // The extension sends: { type: 'profiler:set-theme', theme: 'light' | 'dark' }
23
25
  window.addEventListener('message', function(e) {
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Profiler
6
+ # The gem's built front end files. Each is read once and kept until the file changes on disk
7
+ # (a rebuild in development), and named by a digest of its content so that a browser that
8
+ # cached one never keeps it past a new version of the gem.
9
+ module Assets
10
+ NAMES = %w[profiler.js profiler.css profiler-toolbar.js].freeze
11
+ URL_PREFIX = "/_profiler/assets"
12
+
13
+ Entry = Struct.new(:mtime, :content, :digest)
14
+
15
+ @entries = {}
16
+ @mutex = Mutex.new
17
+
18
+ class << self
19
+ # Where the build writes the files; the gem's own, never the application's.
20
+ def builds_dir
21
+ File.expand_path("../../app/assets/builds", __dir__)
22
+ end
23
+
24
+ # The file's content, or raises Errno::ENOENT when the assets are not built.
25
+ def read(name)
26
+ entry(name).content
27
+ end
28
+
29
+ # Short digest of the content; the gem version when the file is not built.
30
+ def digest(name)
31
+ entry(name).digest
32
+ rescue Errno::ENOENT
33
+ Digest::SHA256.hexdigest(Profiler::VERSION)[0, 12]
34
+ end
35
+
36
+ # The URL a page uses for the asset, carrying its digest.
37
+ def url(name)
38
+ "#{URL_PREFIX}/#{name}?v=#{digest(name)}"
39
+ end
40
+
41
+ def current?(name, version)
42
+ version.is_a?(String) && !version.empty? && version == digest(name)
43
+ end
44
+
45
+ private
46
+
47
+ def entry(name)
48
+ raise ArgumentError, "unknown asset: #{name}" unless NAMES.include?(name)
49
+
50
+ path = File.join(builds_dir, name)
51
+ mtime = File.mtime(path)
52
+ cached = @mutex.synchronize { @entries[name] }
53
+ return cached if cached && cached.mtime == mtime
54
+
55
+ content = File.read(path)
56
+ fresh = Entry.new(mtime, content, Digest::SHA256.hexdigest(content)[0, 12])
57
+ @mutex.synchronize { @entries[name] = fresh }
58
+ end
59
+ end
60
+ end
61
+ end
@@ -38,6 +38,24 @@ module Profiler
38
38
  )*+
39
39
  }xi
40
40
 
41
+ INTERCEPTOR_PATH = File.expand_path("../../../app/assets/javascript/profiler-ajax-interceptor.js", __dir__)
42
+
43
+ class << self
44
+ # The script is the same for every request of a version of the gem: read once. A failed
45
+ # read is not kept, so that the next request tries again.
46
+ def ajax_interceptor_code
47
+ @ajax_interceptor_code ||= File.read(INTERCEPTOR_PATH).freeze
48
+ rescue => e
49
+ Profiler.log_error("ToolbarInjector: could not load the AJAX interceptor script", e)
50
+ ""
51
+ end
52
+
53
+ # The toolbar CSS is a constant of the class too, built once.
54
+ def toolbar_styles
55
+ @toolbar_styles ||= build_toolbar_styles
56
+ end
57
+ end
58
+
41
59
  def initialize(body, token, nonce = nil)
42
60
  @body = body
43
61
  @token = token
@@ -139,13 +157,7 @@ module Profiler
139
157
  end
140
158
 
141
159
  def ajax_interceptor_code
142
- @ajax_interceptor_code ||= begin
143
- path = File.join(__dir__, "..", "..", "..", "app", "assets", "javascript", "profiler-ajax-interceptor.js")
144
- File.read(path)
145
- rescue => e
146
- Profiler.log_error("ToolbarInjector: could not load the AJAX interceptor script", e)
147
- ""
148
- end
160
+ self.class.ajax_interceptor_code
149
161
  end
150
162
 
151
163
  # The middleware hands over the page it has already read: an Array of parts, or a String.
@@ -157,11 +169,11 @@ module Profiler
157
169
  def toolbar_html
158
170
  <<~HTML
159
171
  #{ajax_interceptor_script}
160
- <style>#{toolbar_styles}</style>
172
+ <style#{nonce_attr}>#{self.class.toolbar_styles}</style>
161
173
  <div id="profiler-toolbar" class="profiler-root" data-token="#{ERB::Util.html_escape(@token)}"></div>
162
174
  <button id="profiler-toolbar-toggle" class="profiler-root" title="Toggle profiler (Alt+P)"><span class="profiler-toggle-icon">&#9654;</span></button>
163
- <script#{nonce_attr}>(function(){var c=localStorage.getItem('profiler-toolbar-collapsed')==='true',t=localStorage.getItem('profiler-theme'),theme=t==='light'?'light':t==='dark'?'dark':(window.matchMedia('(prefers-color-scheme:light)').matches?'light':'dark'),el=document.getElementById('profiler-toolbar'),tog=document.getElementById('profiler-toolbar-toggle');if(c){el.style.cssText='animation:none!important;transform:translateX(calc(100% + 44px))';tog.dataset.collapsed='true';}el.setAttribute('data-theme',theme);tog.setAttribute('data-theme',theme);})();</script>
164
- <script src="/_profiler/assets/profiler-toolbar.js" defer#{nonce_attr}></script>
175
+ <script#{nonce_attr}>(function(){function g(k){try{return localStorage.getItem(k)}catch(e){return null}}var c=g('profiler-toolbar-collapsed')==='true',t=g('profiler-theme'),theme=t==='light'?'light':t==='dark'?'dark':(window.matchMedia('(prefers-color-scheme:light)').matches?'light':'dark'),el=document.getElementById('profiler-toolbar'),tog=document.getElementById('profiler-toolbar-toggle');if(c){el.style.cssText='animation:none!important;transform:translateX(calc(100% + 44px))';tog.dataset.collapsed='true';}el.setAttribute('data-theme',theme);tog.setAttribute('data-theme',theme);})();</script>
176
+ <script src="#{Profiler::Assets.url("profiler-toolbar.js")}" defer#{nonce_attr}></script>
165
177
  HTML
166
178
  end
167
179
 
@@ -176,7 +188,7 @@ module Profiler
176
188
 
177
189
  # Thermal design system, self-contained CSS for the injected toolbar.
178
190
  # Variables are defined on #profiler-toolbar to avoid polluting the host app.
179
- def toolbar_styles
191
+ def self.build_toolbar_styles
180
192
  <<~'CSS'
181
193
  .profiler-root {
182
194
  --pf-bg: #080b10;
@@ -324,6 +336,26 @@ module Profiler
324
336
  }
325
337
  #profiler-toolbar .profiler-toolbar-logo:hover { color: var(--pf-amber-h); }
326
338
 
339
+ /* Error state: the profile could not be loaded, or a component failed. */
340
+ #profiler-toolbar .profiler-toolbar-error {
341
+ display: inline-flex;
342
+ align-items: center;
343
+ gap: 10px;
344
+ padding: 0 13px;
345
+ }
346
+ #profiler-toolbar .profiler-toolbar-retry {
347
+ font-family: var(--pf-mono);
348
+ font-size: 11px;
349
+ color: var(--pf-amber);
350
+ background: transparent;
351
+ border: 1px solid var(--pf-border-s);
352
+ border-radius: 3px;
353
+ padding: 2px 8px;
354
+ cursor: pointer;
355
+ }
356
+ #profiler-toolbar .profiler-toolbar-retry:hover:not(:disabled) { color: var(--pf-amber-h); background: var(--pf-amber-bg); }
357
+ #profiler-toolbar .profiler-toolbar-retry:disabled { cursor: default; opacity: .6; }
358
+
327
359
  /* ── Toggle button (fixed, always visible) ───────────────────────── */
328
360
  #profiler-toolbar-toggle {
329
361
  position: fixed;
@@ -347,7 +379,7 @@ module Profiler
347
379
  animation: pfPop 380ms ease-out both;
348
380
  transform: none;
349
381
  }
350
- .profiler-toggle-icon {
382
+ #profiler-toolbar-toggle .profiler-toggle-icon {
351
383
  display: inline-block;
352
384
  transition: transform 280ms cubic-bezier(.4,0,.2,1);
353
385
  }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.35.2"
4
+ VERSION = "0.36.0"
5
5
 
6
6
  GEM_ROOT = File.expand_path("../..", __dir__)
7
7
 
data/lib/profiler.rb CHANGED
@@ -273,6 +273,7 @@ require_relative "profiler/collectors/mailer_collector"
273
273
  require_relative "profiler/storage/token"
274
274
  require_relative "profiler/storage/private_files"
275
275
  require_relative "profiler/storage/unavailable"
276
+ require_relative "profiler/assets"
276
277
  require_relative "profiler/ajax_data"
277
278
  require_relative "profiler/env_override_store"
278
279
  require_relative "profiler/instrumentation/thread_context_propagation"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.35.2
4
+ version: 0.36.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sébastien Duplessy
@@ -145,6 +145,7 @@ files:
145
145
  - lib/profiler.rb
146
146
  - lib/profiler/ajax_data.rb
147
147
  - lib/profiler/allocation_counter.rb
148
+ - lib/profiler/assets.rb
148
149
  - lib/profiler/body_content.rb
149
150
  - lib/profiler/boot_env.rb
150
151
  - lib/profiler/cluster/master_client.rb