rails-profiler 0.35.0 → 0.35.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1bdfb0831a95ebd47c1f2d396cf1180ff1d2143385082127f16c9b32e6cb0422
4
- data.tar.gz: f286f70eb7ba2fcb9c69a4fc5bb54d9a90df9d957baa04e102d7bc67d0eb9476
3
+ metadata.gz: d04dbf2927645f4fbec0524e6171a4086a5cbc54206a3595a903229611941a5b
4
+ data.tar.gz: 4caba281e20e0a097204b6107e90be8ec74450f91bb96c26bbfac28737006fed
5
5
  SHA512:
6
- metadata.gz: 7f59ab05dc3296b8068ce6a9c117c3ff48cf2a56ee057b01dd74404ab0de00391864eb2eedafb65efcbe4aea7b332d9c8c5edeb326a706841d36ef5f387c25ee
7
- data.tar.gz: a730e9c3c3cc44835f535075f74cb7584d306009a7087b8c3d69360f4896110a494d42de787d5d8bf87893e5446308c67f16e96b601b2d0b344fa0525e5c9613
6
+ metadata.gz: e911c0a48a0f9e3f558e665cc1defac6c55a038f0d2f0620a6c445e41c5b3fce41bb4c8df07efa3135d382afad79d1c86d3e78b41966c800b954d67487c2cd68
7
+ data.tar.gz: 2962593dd780fad90a9b1ca9db957e0a8555e137acff0de9260fa3b57b9997c6b08e5e3f0a52f69cce5908c7cbd07aca3200dbcc0d8f22cf64f3405fe3547858
data/CHANGELOG.md CHANGED
@@ -19,6 +19,59 @@ every commit of every tag interval is accounted for one way or the other.
19
19
 
20
20
  ## [Unreleased]
21
21
 
22
+ ## [0.35.2] - 2026-10-17
23
+
24
+ <!-- stamped -->
25
+
26
+ ### Changed
27
+
28
+ - **Performance:** The patch on `Thread#initialize` that carries the profiling context into new
29
+ threads is now installed by the railtie, only when the profiler is `enabled`; a disabled profiler
30
+ no longer touches `Thread`.
31
+
32
+ ### Fixed
33
+
34
+ - **Cluster:** A worker forked from a process that had already started the cluster client (Puma in
35
+ cluster mode with `preload_app!`, a preloaded Unicorn) now gets its own heartbeat thread and
36
+ registers again with the master, through a `Process._fork` hook (Ruby 3.1 and later). Where the
37
+ hook is not available, call `Profiler::Cluster::MasterClient.restart_after_fork` from Puma's
38
+ `on_worker_boot`; see `docs/cluster.md`.
39
+ - **HTTP and request bodies:** A body is now kept as base64 when its bytes are binary (a NUL byte,
40
+ invalid UTF-8, mostly control characters), whatever `Content-Type` it was served with, and `font/`,
41
+ `application/x-protobuf` and `application/gzip` are recognized as binary types. A binary string
42
+ is no longer turned into `?` characters. A body whose `Content-Type` declares a charset other than UTF-8 (`ISO-8859-1`...) stays text, decoded from that charset. A `Net::HTTP` response read as a stream (`read_body`
43
+ with a block) is recorded with no body and `response_body_not_captured`, instead of the `inspect`
44
+ of the stream adapter.
45
+ - **Version:** `Profiler.version`, the version recorded in profiles and announced by the MCP server,
46
+ comes from `git describe` when the gem is mounted by path from a git checkout, so
47
+ `gem_version` is no longer always `0.0.0` there. A published gem keeps the version it was built
48
+ with, and `0.0.0` remains the fallback.
49
+ - **MCP:** The `profiler-mcp` executable reads the profiles the application's file storage wrote
50
+ under `tmp/rails-profiler` in the current directory; it read an empty memory store of its own and
51
+ always answered that there were no profiles.
52
+ - **Profile pages:** the `profiler-version` meta tag of the page layouts now reports `Profiler.version`, the same value the API returns, instead of the raw `Profiler::VERSION` constant. When the gem is mounted from a git checkout, every profile no longer shows a false "different version" warning.
53
+
54
+ ## [0.35.1] - 2026-10-17
55
+
56
+ <!-- stamped -->
57
+
58
+ ### Fixed
59
+
60
+ - **Release:** The version a merge publishes is now read from the subject line of each commit only,
61
+ never from its body, and `BREAKING-CHANGE:` (with a hyphen) publishes a major version just as
62
+ `BREAKING CHANGE:` already did.
63
+ - **Canary:** A prerelease is now numbered after the version its branch is going to publish, not
64
+ after the last release, so RubyGems no longer sorts a recent canary as older than the release it
65
+ precedes.
66
+
67
+ ### Security
68
+
69
+ - **Release:** `rake release` and its subtasks now refuse to run from a developer machine and point
70
+ to the `release` CI job, the only publisher. `rake build` and `rake install` keep working.
71
+ - **CI:** The `canary` job no longer publishes a prerelease on every push to a `feature/`,
72
+ `bugfix/`, `hotfix/`, `fix/` or `breaking/` branch; it is now manual and publishes only when
73
+ someone plays it.
74
+
22
75
  ## [0.35.0] - 2026-10-17
23
76
 
24
77
  <!-- stamped -->
@@ -74,6 +127,7 @@ every commit of every tag interval is accounted for one way or the other.
74
127
  (`charset=utf-7`, `charset=internal`) no longer loses its profile; the body is read as UTF-8 with
75
128
  invalid bytes replaced.
76
129
 
130
+
77
131
  ## [0.34.4] - 2026-10-16
78
132
 
79
133
  <!-- stamped -->
@@ -6,7 +6,7 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1">
7
7
  <%= csrf_meta_tags %>
8
8
  <%= csp_meta_tag %>
9
- <meta name="profiler-version" content="<%= Profiler::VERSION %>">
9
+ <meta name="profiler-version" content="<%= Profiler.version %>">
10
10
  <meta name="profiler-name" content="<%= Profiler.configuration.resolved_name %>">
11
11
  <meta name="profiler-is-master" content="<%= Profiler.configuration.master? %>">
12
12
 
@@ -6,7 +6,7 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1">
7
7
  <%= csrf_meta_tags %>
8
8
  <%= csp_meta_tag %>
9
- <meta name="profiler-version" content="<%= Profiler::VERSION %>">
9
+ <meta name="profiler-version" content="<%= Profiler.version %>">
10
10
 
11
11
  <link rel="stylesheet" href="/_profiler/assets/profiler.css">
12
12
  <script src="/_profiler/assets/profiler.js" defer></script>
data/exe/profiler-mcp CHANGED
@@ -4,10 +4,17 @@
4
4
  # The MCP server over stdio, without the application: its initializers do not run here, so
5
5
  # config.mcp_read_only set there has no effect. Set PROFILER_MCP_READ_ONLY=1 in the environment the
6
6
  # MCP client gives this process to leave out the write tools (Profiler::MCP::Server.read_only?).
7
- # `bundle exec rake profiler:mcp` boots the application and reads both.
7
+ # `bundle exec rake profiler:mcp` boots the application and reads both. It reads the profiles of
8
+ # the file storage under tmp/rails-profiler in the current directory: run it from the application's
9
+ # root. An application on another storage is read through `rake profiler:mcp` or the HTTP endpoint.
8
10
 
9
11
  require "bundler/setup"
10
12
  require "profiler"
11
13
  require "profiler/mcp/server"
12
14
 
15
+ # Without the application there is no initializer to pick a storage, and the default is :memory:
16
+ # a store of this process alone, which never holds a profile the application wrote. Read the files
17
+ # the application's file storage wrote under ./tmp/rails-profiler, from the application's root.
18
+ Profiler.configure { |config| config.storage = :file }
19
+
13
20
  Profiler::MCP::Server.new.start(transport: :stdio)
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Profiler
4
+ # Tells a body that is text from one that is not, from the bytes themselves and not only from
5
+ # the Content-Type the sender declared: a binary payload served as text/plain is common, and
6
+ # forcing it into UTF-8 loses bytes it cannot get back.
7
+ module BodyContent
8
+ BINARY_CONTENT_TYPES = %r{
9
+ \A(?:image/(?!svg)|audio/|video/|font/|
10
+ application/(?:pdf|octet-stream|zip|gzip|x-gzip|x-protobuf|protobuf|x-msgpack|msgpack|
11
+ wasm|x-font|font-|vnd\.ms-fontobject|x-tar|x-7z-compressed|x-bzip2|zstd))
12
+ }xi
13
+
14
+ # Enough to tell: the head of a body is where a magic number or a NUL byte shows up.
15
+ SAMPLE_BYTES = 8192
16
+ # A text holds few control characters: tab, newline, carriage return and escape set aside.
17
+ MAX_CONTROL_RATIO = 0.05
18
+
19
+ module_function
20
+
21
+ def binary_type?(content_type)
22
+ content_type.to_s.split(";").first.to_s.strip.match?(BINARY_CONTENT_TYPES)
23
+ end
24
+
25
+ # True for a body that cannot be kept as text: a NUL byte, bytes that are not UTF-8 (a cut
26
+ # in the middle of the last character excepted), or mostly control characters.
27
+ #
28
+ # A body whose Content-Type declares a charset other than UTF-8 is not judged on its UTF-8
29
+ # validity: its high bytes belong to that charset (ISO-8859-1, Windows-1252...) and are
30
+ # decoded from it afterwards. The NUL byte and control character tests still apply to it.
31
+ def binary_bytes?(body, content_type = nil)
32
+ return false if body.nil? || body.empty?
33
+
34
+ sample = body.byteslice(0, SAMPLE_BYTES).dup.force_encoding(Encoding::UTF_8)
35
+ return true if sample.include?("\0")
36
+
37
+ if !sample.valid_encoding? && !foreign_charset?(content_type)
38
+ # The sample may end inside a multi-byte character, whichever way it was cut.
39
+ sample = (1..3).filter_map { |n| sample.byteslice(0, sample.bytesize - n)&.force_encoding(Encoding::UTF_8) }
40
+ .find(&:valid_encoding?)
41
+ return true if sample.nil?
42
+ end
43
+
44
+ sample = sample.scrub("") unless sample.valid_encoding?
45
+ controls = sample.count("\x01-\x08\x0B\x0C\x0E-\x1A\x1C-\x1F\x7F")
46
+ sample.length.positive? && controls.fdiv(sample.length) > MAX_CONTROL_RATIO
47
+ end
48
+
49
+ def binary?(body, content_type)
50
+ binary_type?(content_type) || binary_bytes?(body, content_type)
51
+ end
52
+
53
+ def foreign_charset?(content_type)
54
+ charset = content_type.to_s[/charset\s*=\s*"?([^\s";]+)/i, 1]
55
+ !charset.nil? && !charset.match?(/\A(?:utf-?8|us-ascii)\z/i)
56
+ end
57
+ end
58
+ end
@@ -8,7 +8,38 @@ require_relative "security"
8
8
  module Profiler
9
9
  module Cluster
10
10
  class MasterClient
11
+ # Process._fork (Ruby 3.1+) runs in the parent and in the child alike. A thread does not
12
+ # survive a fork, so a worker forked from a process that had started the client (Puma in
13
+ # cluster mode with preload_app!, a preloaded Unicorn) would never heartbeat.
14
+ module ForkHook
15
+ def _fork
16
+ pid = super
17
+ MasterClient.restart_after_fork if pid.zero?
18
+ pid
19
+ end
20
+ end
21
+
22
+ class << self
23
+ # The client the railtie started in this process, for the fork hook to restart.
24
+ attr_accessor :current
25
+
26
+ def install_fork_hook!
27
+ return false if @fork_hook_installed || !Process.respond_to?(:_fork)
28
+
29
+ Process.singleton_class.prepend(ForkHook)
30
+ @fork_hook_installed = true
31
+ end
32
+
33
+ def restart_after_fork
34
+ current&.restart_after_fork
35
+ rescue => e
36
+ Profiler.log_warn("Cluster: could not restart the heartbeat after a fork", e)
37
+ end
38
+ end
39
+
11
40
  def start
41
+ self.class.current = self
42
+ self.class.install_fork_hook!
12
43
  @registered = false
13
44
  begin
14
45
  register!
@@ -16,6 +47,17 @@ module Profiler
16
47
  rescue => e
17
48
  log_warn("could not register with master at startup, will retry in the heartbeat loop", e)
18
49
  end
50
+ @started = true
51
+ start_heartbeat_thread
52
+ end
53
+
54
+ # In the child of a fork: the heartbeat thread of the parent is gone and the parent's
55
+ # registration is not the child's own, so register again from the new thread.
56
+ # Does nothing while a heartbeat thread is alive, nor before start.
57
+ def restart_after_fork
58
+ return if @started.nil? || @thread&.alive?
59
+
60
+ @registered = false
19
61
  start_heartbeat_thread
20
62
  end
21
63
 
@@ -61,7 +103,9 @@ module Profiler
61
103
  end
62
104
 
63
105
  def start_heartbeat_thread
64
- Thread.new do
106
+ return if @thread&.alive?
107
+
108
+ @thread = Thread.new do
65
109
  interval = Profiler.configuration.cluster_heartbeat_interval
66
110
  loop do
67
111
  sleep interval
@@ -38,7 +38,7 @@ module Profiler
38
38
  def run(&block)
39
39
  profile = Models::Profile.new
40
40
  profile.profile_type = "console"
41
- profile.gem_version = Profiler::VERSION
41
+ profile.gem_version = Profiler.version
42
42
  profile.path = Redaction.truncate(@expression, 200)
43
43
  profile.method = "CONSOLE"
44
44
 
@@ -6,6 +6,7 @@ require "zlib"
6
6
  require "stringio"
7
7
  require "securerandom"
8
8
  require_relative "../redaction"
9
+ require_relative "../body_content"
9
10
  require_relative "../configuration"
10
11
 
11
12
  module Profiler
@@ -59,7 +60,10 @@ module Profiler
59
60
  response = super
60
61
 
61
62
  duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round(2)
62
- resp_body_raw = response.body.to_s
63
+ # read_body with a block hands each chunk to the block and keeps none: the response then
64
+ # holds a Net::ReadAdapter, whose to_s is an inspect and whose bytes are gone.
65
+ streamed = NetHttpInstrumentation.streamed_body?(response)
66
+ resp_body_raw = streamed ? "" : response.body.to_s
63
67
  resp_content_encoding = response["content-encoding"].to_s.strip.downcase
64
68
  resp_body = NetHttpInstrumentation.decompress_body(resp_body_raw, resp_content_encoding, limit)
65
69
  resp_truncated = limit ? resp_body.bytesize > limit : false
@@ -76,8 +80,9 @@ module Profiler
76
80
  response_headers: Redaction.filter_headers(response.to_hash.transform_values { |v| v.join(", ") }),
77
81
  response_body: processed_resp[:body],
78
82
  response_body_encoding: processed_resp[:encoding],
79
- response_size: resp_body_raw.bytesize,
80
- response_body_truncated: resp_truncated
83
+ response_size: streamed ? (declared_length(response) || 0) : resp_body_raw.bytesize,
84
+ response_body_truncated: resp_truncated,
85
+ response_body_not_captured: streamed
81
86
  )
82
87
 
83
88
  fg = Thread.current[:profiler_flamegraph_collector]
@@ -96,6 +101,11 @@ module Profiler
96
101
 
97
102
  private
98
103
 
104
+ def declared_length(response)
105
+ length = response["content-length"].to_s
106
+ length.match?(/\A\d+\z/) ? length.to_i : nil
107
+ end
108
+
99
109
  def build_url(host, port, path, ssl)
100
110
  scheme = ssl ? "https" : "http"
101
111
  standard_port = (ssl && port == 443) || (!ssl && port == 80)
@@ -110,7 +120,7 @@ module Profiler
110
120
  deprecate_constant :SKIP_HOSTS
111
121
 
112
122
  TEXT_CONTENT_TYPES = /\A(text\/|application\/(json|xml|xhtml|javascript|x-www-form-urlencoded)|image\/svg)/i
113
- BINARY_CONTENT_TYPES = /\A(image\/|application\/pdf|application\/octet-stream|application\/zip|audio\/|video\/)/i
123
+ BINARY_CONTENT_TYPES = BodyContent::BINARY_CONTENT_TYPES
114
124
 
115
125
  def self.install!
116
126
  return if @installed
@@ -255,12 +265,15 @@ module Profiler
255
265
  inflater&.close
256
266
  end
257
267
 
268
+ # A response read with read_body(&block) was consumed as a stream: its body is gone.
269
+ def self.streamed_body?(response)
270
+ defined?(Net::ReadAdapter) && response.body.is_a?(Net::ReadAdapter)
271
+ end
272
+
258
273
  def self.process_body(body, content_type)
259
274
  return { body: nil, encoding: "text" } if body.nil? || body.empty?
260
275
 
261
- mime = content_type.split(";").first.to_s.strip
262
-
263
- if mime.match?(BINARY_CONTENT_TYPES)
276
+ if BodyContent.binary?(body, content_type)
264
277
  # Masked on the raw bytes, before the encoding hides them from any later search.
265
278
  { body: Base64.strict_encode64(Redaction.hide_credentials(body.b)), encoding: "base64" }
266
279
  else
@@ -37,8 +37,15 @@ module Profiler
37
37
  super(*args, &wrapper)
38
38
  end
39
39
  ruby2_keywords :initialize
40
+
41
+ # Called by the railtie once the application has decided `enabled`: the patch is on
42
+ # Thread#initialize for every thread of the process, so a disabled profiler leaves it out.
43
+ def self.install!
44
+ return if @installed
45
+
46
+ Thread.prepend(self)
47
+ @installed = true
48
+ end
40
49
  end
41
50
  end
42
51
  end
43
-
44
- Thread.prepend(Profiler::Instrumentation::ThreadContextPropagation)
@@ -55,7 +55,7 @@ module Profiler
55
55
  def run(&block)
56
56
  profile = Models::Profile.new
57
57
  profile.profile_type = "job"
58
- profile.gem_version = Profiler::VERSION
58
+ profile.gem_version = Profiler.version
59
59
  profile.path = @job_class
60
60
  profile.method = "JOB"
61
61
  profile.parent_token = @parent_token if @parent_token
@@ -46,7 +46,7 @@ module Profiler
46
46
 
47
47
  @server = ::MCP::Server.new(
48
48
  name: "rails-profiler",
49
- version: Profiler::VERSION,
49
+ version: Profiler.version,
50
50
  tools: tools,
51
51
  resources: resources
52
52
  )
@@ -98,8 +98,8 @@ module Profiler
98
98
  lines << "**Allocated objects:** #{profile.allocated_objects}" if profile.allocated_objects
99
99
  lines << "**Time:** #{profile.started_at}"
100
100
  if profile.gem_version
101
- if profile.gem_version != Profiler::VERSION
102
- lines << "**Gem Version:** #{profile.gem_version} ⚠️ (current: #{Profiler::VERSION})"
101
+ if profile.gem_version != Profiler.version
102
+ lines << "**Gem Version:** #{profile.gem_version} ⚠️ (current: #{Profiler.version})"
103
103
  else
104
104
  lines << "**Gem Version:** #{profile.gem_version}"
105
105
  end
@@ -91,6 +91,9 @@ module Profiler
91
91
  lines << "- _Request body not captured: a stream that cannot be rewound is sent unread._"
92
92
  end
93
93
  lines << "- _Request body truncated: only the first bytes were kept (max_captured_body_bytes)._" if req["request_body_truncated"]
94
+ if req["response_body_not_captured"]
95
+ lines << "- _Response body not captured: it was read as a stream (read_body with a block)._"
96
+ end
94
97
  lines << "- _Response body truncated: only the first bytes were kept (max_captured_body_bytes)._" if req["response_body_truncated"]
95
98
  lines << "- **Error:** #{req['error']}" if req["error"]
96
99
 
@@ -73,7 +73,7 @@ module Profiler
73
73
  when "duration" then profile.duration ? "#{profile.duration.round(2)}ms" : "-"
74
74
  when "gem_version"
75
75
  v = profile.gem_version || "-"
76
- v != Profiler::VERSION ? "#{v} ⚠️" : v
76
+ v != Profiler.version ? "#{v} ⚠️" : v
77
77
  when "token" then profile.token.to_s
78
78
  when "parent_token" then profile.parent_token || "-"
79
79
  end
@@ -94,7 +94,10 @@ module Profiler
94
94
  timed_out = false
95
95
 
96
96
  loop do
97
- break if Time.now > deadline && (timed_out = true)
97
+ if Time.now > deadline
98
+ timed_out = true
99
+ break
100
+ end
98
101
  run_data = proxy.get_json("/_profiler/api/test_runner/runs/#{run_id}")
99
102
  break if Profiler::TestRunner::RunStore::TERMINAL_STATUSES.include?(run_data["status"])
100
103
  sleep 2
@@ -29,7 +29,7 @@ module Profiler
29
29
  begin
30
30
  begin
31
31
  profile = Models::Profile.new(build_request(env), params: false)
32
- profile.gem_version = Profiler::VERSION
32
+ profile.gem_version = Profiler.version
33
33
  Profiler::CurrentContext.token = profile.token
34
34
 
35
35
  # Capture request body before app processes it
@@ -5,6 +5,7 @@ require "securerandom"
5
5
  require "json"
6
6
  require "zlib"
7
7
  require_relative "../redaction"
8
+ require_relative "../body_content"
8
9
  require_relative "../allocation_counter"
9
10
 
10
11
  module Profiler
@@ -256,7 +257,7 @@ module Profiler
256
257
  def process_body(raw, content_type)
257
258
  return { body: nil, encoding: "text" } if raw.nil? || raw.empty?
258
259
 
259
- if binary_content_type?(content_type)
260
+ if BodyContent.binary?(raw, content_type)
260
261
  # Masked on the raw bytes, before the encoding hides them from any later search. A
261
262
  # compressed format (zip, png, gzip...) does not hold the secret as it is.
262
263
  { body: Base64.strict_encode64(Redaction.hide_credentials(raw.b)), encoding: "base64" }
@@ -315,10 +316,6 @@ module Profiler
315
316
  text.bytesize > Profiler.configuration.compress_body_threshold
316
317
  end
317
318
 
318
- def binary_content_type?(ct)
319
- ct.to_s.match?(%r{image/(?!svg)|application/(?:pdf|octet-stream|zip)|audio/|video/})
320
- end
321
-
322
319
  LEGACY_FILTERED_PARAMS = %w[password password_confirmation token secret].freeze
323
320
 
324
321
  # The key added to params cut at max_captured_body_bytes, so that the cut shows wherever
@@ -77,6 +77,13 @@ module Profiler
77
77
  app.middleware.delete Profiler::Middleware::CorsMiddleware
78
78
  end
79
79
 
80
+ # Thread#initialize is patched for every thread of the process: only when the profiler is on.
81
+ initializer "profiler.install_thread_context_propagation" do
82
+ next unless Profiler.configuration.enabled
83
+
84
+ Profiler::Instrumentation::ThreadContextPropagation.install!
85
+ end
86
+
80
87
  initializer "profiler.setup_test_profiler" do
81
88
  next unless Profiler.configuration.enabled && Profiler.configuration.track_tests
82
89
 
@@ -1,5 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.35.0"
4
+ VERSION = "0.35.2"
5
+
6
+ GEM_ROOT = File.expand_path("../..", __dir__)
7
+
8
+ # The version the gem reports (profiles, the MCP server). A published gem has it written into
9
+ # VERSION by the release job, and that is what this returns. VERSION stays at 0.0.0 on the
10
+ # default branch, so a gem mounted by path from a git checkout derives it from `git describe`
11
+ # instead; anything else (no git, no checkout, no tag, an installed gem) keeps VERSION.
12
+ def self.version
13
+ return @version if defined?(@version)
14
+
15
+ @version = VERSION == "0.0.0" ? (checkout_version || VERSION) : VERSION
16
+ end
17
+
18
+ # "0.34.2" on a tag, "0.34.2-5-g1a2b3c4" five commits after it. Nil unless the gem lives in
19
+ # its own git checkout: the .git next to lib/ is what tells it from a copy someone else's
20
+ # repository happens to contain.
21
+ def self.checkout_version
22
+ return nil unless File.exist?(File.join(GEM_ROOT, ".git")) && File.directory?(File.join(GEM_ROOT, "lib"))
23
+
24
+ described = IO.popen(["git", "-C", GEM_ROOT, "describe", "--tags", "--match", "v[0-9]*"],
25
+ err: File::NULL, &:read)
26
+ described = described.to_s.strip.delete_prefix("v")
27
+ $?&.success? && described.match?(/\A\d+\.\d+\.\d+/) ? described : nil
28
+ rescue SystemCallError, IOError
29
+ nil
30
+ end
5
31
  end
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.0
4
+ version: 0.35.2
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/body_content.rb
148
149
  - lib/profiler/boot_env.rb
149
150
  - lib/profiler/cluster/master_client.rb
150
151
  - lib/profiler/cluster/security.rb