rails-profiler 0.34.2 → 0.34.4

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: b6365d7a34d882d20ca0257247629d6099917508ad78cecc96726dc6788c7a75
4
- data.tar.gz: e7dffe134ddcac64202aae83bb73963837068ae258b1cdc1d959e78504b31861
3
+ metadata.gz: d4912dd474a748e657c35111e3224d83500e75960ef2592e5cd652155dc62617
4
+ data.tar.gz: d0d4cb0f34f25d98f5be08875bdc59ec4cc84894f7cbe0b512fd1ecd153dda4b
5
5
  SHA512:
6
- metadata.gz: 8912bad06bc0824942c4e696d9fd408dff3a76238af92faaa7c262815e477e3850fe90c14d9acd6ceced45271180c990c8d6d597cf591d998eae48edf3dc9e11
7
- data.tar.gz: aeb7cec816ce9de90ab3dcc72629d36c6f9b1ff69bfa618416e72fa5cbb6301cae5a5d12dd09d6d0a4c247c76095c397777dad9f42a18bbf5aaafa7d375b7248
6
+ metadata.gz: 3ca18ad3dd582e68c2bb935a5ace8628dc466a4999fa655e54117879548d543c5a291ec98765eb25456b0ff8fa774ee0f98f939544a346aa9b3368d37c421f72
7
+ data.tar.gz: e144011d2fd93d37aa3166748bf2d3671d8d246325a475cf13d756b188dd99999ef6f95b232fd36eca444c91e24e2715c1902695697ea1930407bc932b2e952d
data/CHANGELOG.md CHANGED
@@ -19,6 +19,44 @@ every commit of every tag interval is accounted for one way or the other.
19
19
 
20
20
  ## [Unreleased]
21
21
 
22
+ ## [0.34.4] - 2026-10-16
23
+
24
+ <!-- stamped -->
25
+
26
+ ### Fixed
27
+
28
+ - **Sidekiq:** A job run through the ActiveJob adapter is no longer profiled twice. It reaches the
29
+ Sidekiq middleware wrapped (`job["wrapped"]`) and is already profiled by the ActiveJob
30
+ `around_perform` callback, so the middleware now leaves it alone when ActiveJob is instrumented
31
+ and the same work is recorded once.
32
+ - **Logs:** The log buffer behind `CaptureLogger` is now guarded by a `Mutex`, so lines recorded
33
+ from several threads at once (a streamed body iterated by another thread, work handed the buffer)
34
+ keep the byte count and the stored lines consistent.
35
+ - **Flame graph and views:** Nested durations are counted once. The total of the flame graph and
36
+ the total of the views and partials now cover each moment a single time, so a template that
37
+ renders partials, or a controller action that runs queries, is no longer added to what it
38
+ contains.
39
+
40
+ ## [0.34.3] - 2026-10-15
41
+
42
+ <!-- stamped -->
43
+
44
+ ### Changed
45
+
46
+ - **Test suite:** `bundle exec rspec` now measures the line coverage of the gem's Ruby code with
47
+ SimpleCov, and the CI fails below 92.0%, the level measured on Ruby 3.3 once the MCP
48
+ tools, the function profiler, the storage base class and the cluster client got specs of their
49
+ own. A run of a few spec files is held to no floor outside the CI.
50
+ - **Test suite:** The suite loads and passes under any locale, `LANG=C` included: `spec_helper.rb`
51
+ reads files as UTF-8 whatever the locale, and a `.rspec` file loads it for every spec. The specs
52
+ of the masking of sensitive data now live together under `spec/redaction/`.
53
+
54
+ ### Fixed
55
+
56
+ - **Middleware:** The default `skip_paths` are now anchored to the whole path, so a page such as `/users/favicon.ico.html` is no longer skipped by mistake, and the Sprockets assets (`/assets`) and Action Cable (`/cable`) requests are no longer profiled. A `skip_paths` list you set yourself is used as given.
57
+ - **Mailers:** Visiting the mailer previews (`/rails/mailers`) no longer records phantom delivered emails in the profile.
58
+ - **Security:** An `authorize_with` block that raises is now treated as a denial: the error is logged server side and the request is served unprofiled, instead of breaking every request of the application.
59
+
22
60
  ## [0.34.2] - 2026-10-15
23
61
 
24
62
  <!-- stamped -->
@@ -124,6 +124,24 @@ module Profiler
124
124
 
125
125
  protected
126
126
 
127
+ # The time covered by +intervals+ ([started, finished] pairs of Time or Float seconds), in
128
+ # milliseconds, each moment counted once: a template that renders partials, or a controller
129
+ # action that runs queries, is not added to what it contains.
130
+ def covered_ms(intervals)
131
+ covered = 0.0
132
+ current_end = nil
133
+ intervals.sort_by(&:first).each do |started, finished|
134
+ if current_end.nil? || started >= current_end
135
+ covered += finished - started
136
+ current_end = finished
137
+ elsif finished > current_end
138
+ covered += finished - current_end
139
+ current_end = finished
140
+ end
141
+ end
142
+ (covered * 1000).round(2)
143
+ end
144
+
127
145
  def store_data(data)
128
146
  @data = data
129
147
  @profile.add_collector_data(name, data)
@@ -139,7 +139,7 @@ module Profiler
139
139
 
140
140
  store_data({
141
141
  total_events: @events.size,
142
- total_duration: @events.empty? ? 0 : @events.map(&:duration).sum.round(2),
142
+ total_duration: @events.empty? ? 0 : covered_ms(@events.map { |e| [e.started_at, e.finished_at] }),
143
143
  root_events: root_events.map(&:to_h)
144
144
  })
145
145
  end
@@ -20,13 +20,22 @@ module Profiler
20
20
  @bytes = 0
21
21
  @dropped = 0
22
22
  @truncated = false
23
+ @mutex = Mutex.new
23
24
  end
24
25
 
25
26
  def truncated?
26
27
  @truncated
27
28
  end
28
29
 
30
+ # A streamed body iterated by another thread, or work handed the buffer, can record while
31
+ # the request's own thread does: the byte count and the lines are updated together.
29
32
  def record(entry)
33
+ @mutex.synchronize { record_unlocked(entry) }
34
+ end
35
+
36
+ private
37
+
38
+ def record_unlocked(entry)
30
39
  message = entry[:message]
31
40
  if @limit
32
41
  room = @limit - @bytes
@@ -316,8 +316,9 @@ module Profiler
316
316
  end
317
317
 
318
318
  def rails_preview_request?
319
- path = Thread.current[:profiler_request_path]
320
- path&.start_with?("/rails/mailers")
319
+ # The profile of the request being served carries its path; the thread-local is kept
320
+ # for a caller that sets it by hand.
321
+ [profile&.path, Thread.current[:profiler_request_path]].any? { |path| path.to_s.match?(%r{\A/rails/mailers(/|\z)}) }
321
322
  end
322
323
 
323
324
  def to_path(class_name)
@@ -9,6 +9,7 @@ module Profiler
9
9
  super
10
10
  @views = []
11
11
  @partials = []
12
+ @intervals = []
12
13
  @subscriptions = []
13
14
  end
14
15
 
@@ -36,6 +37,7 @@ module Profiler
36
37
 
37
38
  @subscriptions << subscribe_notification("render_template.action_view") do |name, started, finished, unique_id, payload|
38
39
  duration = ((finished - started) * 1000).round(2)
40
+ @intervals << [started, finished]
39
41
  @views << {
40
42
  identifier: payload[:identifier],
41
43
  layout: payload[:layout],
@@ -45,6 +47,7 @@ module Profiler
45
47
 
46
48
  @subscriptions << subscribe_notification("render_partial.action_view") do |name, started, finished, unique_id, payload|
47
49
  duration = ((finished - started) * 1000).round(2)
50
+ @intervals << [started, finished]
48
51
  @partials << {
49
52
  identifier: payload[:identifier],
50
53
  duration: duration
@@ -60,7 +63,7 @@ module Profiler
60
63
  partials: @partials,
61
64
  total_views: @views.size,
62
65
  total_partials: @partials.size,
63
- total_duration: (@views + @partials).sum { |v| v[:duration] }.round(2)
66
+ total_duration: covered_ms(@intervals)
64
67
  }
65
68
 
66
69
  store_data(data)
@@ -76,7 +79,7 @@ module Profiler
76
79
  end
77
80
 
78
81
  def toolbar_summary
79
- total_duration = (@views + @partials).sum { |v| v[:duration] }.round(2)
82
+ total_duration = covered_ms(@intervals)
80
83
 
81
84
  {
82
85
  text: "#{@views.size} views, #{@partials.size} partials (#{total_duration}ms)",
@@ -65,7 +65,8 @@ module Profiler
65
65
  @storage = :memory
66
66
  @storage_options = {}
67
67
  @collectors = []
68
- @skip_paths = [%r{^/_profiler}, /\.well-known/, /favicon\.ico/, /manifest\.json/]
68
+ @skip_paths = [%r{\A/_profiler}, %r{\A/\.well-known/}, %r{\A/favicon\.ico\z}, %r{\A(?:/[^/]+)*/manifest\.json\z},
69
+ %r{\A/assets/}, %r{\A/cable(/|\z)}]
69
70
  @slow_query_threshold = 100 # milliseconds
70
71
  @max_queries_warning = 50
71
72
  # Where each query comes from: :first_and_slow captures the caller the first time a
@@ -284,7 +285,7 @@ module Profiler
284
285
  LocalRequest.warn_once(reason)
285
286
  false
286
287
  when :allow_authorized
287
- @authorize_block ? @authorize_block.call(request) : false
288
+ run_authorize_block(request)
288
289
  else
289
290
  false
290
291
  end
@@ -373,5 +374,16 @@ module Profiler
373
374
  raise ArgumentError, "config.mcp_allowed_hosts: #{pattern.inspect} cannot be matched against the whole " \
374
375
  "host name (#{e.message})"
375
376
  end
377
+
378
+ # An exception in the application's own authorization code is a denial, never an error
379
+ # for the request: it would otherwise break every request. The detail stays in the log.
380
+ def run_authorize_block(request)
381
+ return false unless @authorize_block
382
+
383
+ @authorize_block.call(request) ? true : false
384
+ rescue StandardError => e
385
+ Profiler.log_error("authorize_with block raised, treating the request as unauthorized", e)
386
+ false
387
+ end
376
388
  end
377
389
  end
@@ -12,6 +12,11 @@ module Profiler
12
12
  class SidekiqMiddleware
13
13
  def call(worker, job, queue, &block)
14
14
  Profiler.env_override_store.apply!
15
+ # A job run through the ActiveJob adapter reaches Sidekiq wrapped (job["wrapped"] names the
16
+ # real class) and is profiled by ActiveJobInstrumentation's around_perform: profiling the
17
+ # wrapper too would record the same work twice.
18
+ return block.call if job["wrapped"] && active_job_profiled?
19
+
15
20
  Profiler::JobProfiler.profile(
16
21
  job_class: job["class"],
17
22
  job_id: job["jid"],
@@ -22,6 +27,13 @@ module Profiler
22
27
  &block
23
28
  )
24
29
  end
30
+
31
+ private
32
+
33
+ def active_job_profiled?
34
+ defined?(ActiveJob::Base) && defined?(Profiler::Instrumentation::ActiveJobInstrumentation) &&
35
+ ActiveJob::Base.include?(Profiler::Instrumentation::ActiveJobInstrumentation)
36
+ end
25
37
  end
26
38
  end
27
39
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.34.2"
4
+ VERSION = "0.34.4"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.34.2
4
+ version: 0.34.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sébastien Duplessy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-15 00:00:00.000000000 Z
11
+ date: 2026-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails