rails-profiler 0.31.5 → 0.31.6

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: 87fc21dec17ab7627d4430f49b042e53061efb33034d7309ea07aac6eb83ac5d
4
- data.tar.gz: 3e64027bb71fa5ce3d2a23a23cf1525f0e5a55c508c019f1398c35c705f20434
3
+ metadata.gz: f8c9d9f31f3f32dc5ada20eb78eb6bc205c0985e084942edf18b283cfafe81be
4
+ data.tar.gz: 49fb9afa0dd2fa52033f151b077ec971db08fe45818fe64448559ba95f4face3
5
5
  SHA512:
6
- metadata.gz: 596fda2dad5ffa7e2a7c98a2b749c03901df7748d26776203a30d88a86b842e5956d92453b3847a3cf856a7afee5c4511f02ce83070a6dd26d0ef4d1e6784b5b
7
- data.tar.gz: b70a833918e15474ac410c182763f13e03e0352e39cd7a0ed066c68a47f2f86762e7ef01fcd5246bfe47d391d86c03556929f66dbb84f0098f0bc43c43e9b415
6
+ metadata.gz: 79bd6095dd2deb184ee662c398183d5e63b9b9af30f5bda76b1806353adce5137627c2e33afc5c95aadc451dd540a9d243c1cc1df3eb72eccc2001331362581f
7
+ data.tar.gz: bd488d9da999d2f54be42b4002030f88bb51b069c956b78c85f0aadc4e87faa3f6ec3a2bb26c9921b73bea0fbaa84e16fd8e3f5967cfc8276ada75fa44a9b331
data/CHANGELOG.md CHANGED
@@ -19,6 +19,36 @@ every commit of every tag interval is accounted for one way or the other.
19
19
 
20
20
  ## [Unreleased]
21
21
 
22
+ ## [0.31.6] - 2026-10-05
23
+
24
+ <!-- stamped -->
25
+
26
+ ### Fixed
27
+
28
+ - **Storage:** With `config.compress_bodies` (on by default), a text body larger than
29
+ `config.compress_body_threshold` (10 KB) is now stored gzip+base64, as the README said: the
30
+ profile was decompressed again before every save, so the file, Redis and memory stores kept the
31
+ body as text, next to the compressed copy the request collector keeps, and the compression only
32
+ cost time. A 132 KB HTML page now takes 86 KB in those stores instead of 171 KB (273 KB without
33
+ compression); SQLite, which only keeps that copy, 44 KB instead of 36 KB at the faster level.
34
+ Compressing it costs 1.6 ms on the request instead of 4.1 ms (the fastest zlib level), saving
35
+ the profile 0.2 ms instead of 1.2 ms; the bodies are decompressed only when a profile is shown,
36
+ about 0.7 ms. The profile page, its API, the job, console and test APIs, the lists the cluster
37
+ proxy reads and the MCP tools still give the bodies as text; the toolbar, which shows no body,
38
+ gets them as stored (86 KB instead of 171 KB for that page).
39
+ - **Storage:** The profile list summaries kept by the file, SQLite and Redis stores no longer
40
+ carry the first 500 characters of the request and response bodies. Summaries written by 0.31.4
41
+ and 0.31.5 keep them until their profiles are evicted; no list shows them.
42
+ - **MCP:** The `curl` command of `get_profile_detail` carries the request body as it was sent: a
43
+ body larger than `compress_body_threshold` came out as its compressed base64 form, and a binary
44
+ body as base64 text. A binary body is now left out of the command.
45
+ - **Upgrading:** the profiles already stored, all as text, are read as before, with no migration.
46
+ An earlier version reading a store written by this one shows the compressed bodies as text too,
47
+ and a cluster master and its slaves may run either version: the profile API they read still
48
+ gives the bodies as text. A body that cannot be decompressed is shown as it is stored, never as
49
+ an error. To keep the bodies readable as text in the stored files or Redis keys, set
50
+ `config.compress_bodies = false`.
51
+
22
52
  ## [0.31.5] - 2026-10-05
23
53
 
24
54
  <!-- stamped -->
@@ -14,7 +14,7 @@ module Profiler
14
14
  return render json: { error: "Console profile not found" }, status: :not_found
15
15
  end
16
16
 
17
- render json: profile.to_h
17
+ render json: profile.to_h(decode_bodies: true)
18
18
  end
19
19
 
20
20
  def destroy
@@ -14,7 +14,7 @@ module Profiler
14
14
  return render json: { error: "Job profile not found" }, status: :not_found
15
15
  end
16
16
 
17
- render json: profile.to_h.merge(
17
+ render json: profile.to_h(decode_bodies: true).merge(
18
18
  child_jobs: build_child_jobs(profile),
19
19
  parent_profile: build_parent_summary(profile)
20
20
  )
@@ -28,7 +28,7 @@ module Profiler
28
28
  # The route table and ENV are the process's, not stored in the profile
29
29
  Profiler::ProcessSnapshot.hydrate(profile)
30
30
 
31
- render json: profile.to_h.merge(
31
+ render json: profile.to_h(decode_bodies: true).merge(
32
32
  child_jobs: build_child_jobs(profile),
33
33
  parent_profile: build_parent_summary(profile)
34
34
  )
@@ -14,7 +14,7 @@ module Profiler
14
14
  return render json: { error: "Test profile not found" }, status: :not_found
15
15
  end
16
16
 
17
- render json: profile.to_h
17
+ render json: profile.to_h(decode_bodies: true)
18
18
  end
19
19
 
20
20
  def destroy
@@ -19,6 +19,7 @@ module Profiler
19
19
  # The route table and ENV are the process's, not stored in the profile
20
20
  Profiler::ProcessSnapshot.hydrate(profile)
21
21
 
22
+ # The bodies as stored (gzip+base64 past compress_body_threshold): the toolbar shows none.
22
23
  render json: {
23
24
  profile: profile.to_h.merge(child_jobs: build_child_jobs(profile)),
24
25
  events_cursor: events_cursor
@@ -74,9 +74,10 @@ module Profiler
74
74
  render_page(Profiler.storage.list(limit: limit + 1, offset: offset, type: type, summary: summary), limit, offset)
75
75
  end
76
76
 
77
+ # The bodies as text: the cluster proxy lists whole profiles (all_types), a summary has none.
77
78
  def render_page(profiles, limit, offset)
78
79
  render json: {
79
- profiles: profiles.first(limit).map(&:to_h),
80
+ profiles: profiles.first(limit).map { |profile| profile.to_h(decode_bodies: true) },
80
81
  limit: limit,
81
82
  offset: offset,
82
83
  has_more: profiles.size > limit
@@ -25,7 +25,7 @@ module Profiler
25
25
  # The route table and ENV are the process's, not stored in the profile
26
26
  Profiler::ProcessSnapshot.hydrate(@profile)
27
27
 
28
- @profile_data = @profile.to_h.merge(
28
+ @profile_data = @profile.to_h(decode_bodies: true).merge(
29
29
  child_jobs: build_child_jobs(@profile),
30
30
  parent_profile: build_parent_summary(@profile)
31
31
  )
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "base64"
4
- require "zlib"
3
+ require_relative "../models/profile"
5
4
 
6
5
  module Profiler
7
6
  module MCP
@@ -19,13 +18,7 @@ module Profiler
19
18
  return " *(binary, base64 encoded)*" if encoding == "base64"
20
19
  return nil if body.nil? || body.empty?
21
20
 
22
- if encoding == "gzip+base64"
23
- begin
24
- body = Zlib::Inflate.inflate(Base64.strict_decode64(body))
25
- rescue Zlib::Error, ArgumentError
26
- # fall through with original body
27
- end
28
- end
21
+ body, = Models::Profile.decode_body(body, encoding)
29
22
 
30
23
  if params["save_bodies"]
31
24
  path = FileCache.save(token, name, body)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../slave_support"
4
4
  require_relative "../../process_snapshot"
5
+ require_relative "../../models/profile"
5
6
 
6
7
  require "shellwords"
7
8
  require "cgi"
@@ -666,7 +667,10 @@ module Profiler
666
667
  def self.generate_curl(profile, req_data)
667
668
  headers = req_data&.dig("headers") || {}
668
669
  params = req_data&.dig("params") || {}
669
- req_body = req_data&.dig("request_body")
670
+ # As captured: a binary body cannot be typed on a command line.
671
+ req_body, req_encoding = Models::Profile.decode_body(req_data&.dig("request_body"),
672
+ req_data&.dig("request_body_encoding"))
673
+ req_body = nil unless req_encoding.nil? || req_encoding == "text"
670
674
 
671
675
  parts = ["curl -X #{profile.method}"]
672
676
 
@@ -117,9 +117,15 @@ module Profiler
117
117
  end
118
118
  private :collector_metadata
119
119
 
120
- def to_h
121
- req_body, req_enc = decode_body(@request_body, @request_body_encoding)
122
- resp_body, resp_enc = decode_body(@response_body, @response_body_encoding)
120
+ # The profile as the stores keep it: a body compressed by compress_bodies stays gzip+base64.
121
+ # decode_bodies: true gives the bodies as text, for the responses that show them.
122
+ def to_h(decode_bodies: false)
123
+ req_body, req_enc = @request_body, @request_body_encoding
124
+ resp_body, resp_enc = @response_body, @response_body_encoding
125
+ if decode_bodies
126
+ req_body, req_enc = self.class.decode_body(req_body, req_enc)
127
+ resp_body, resp_enc = self.class.decode_body(resp_body, resp_enc)
128
+ end
123
129
 
124
130
  {
125
131
  profile_type: @profile_type,
@@ -155,6 +161,7 @@ module Profiler
155
161
  }
156
162
  end
157
163
 
164
+ # The stored form, as #to_h.
158
165
  def to_json(*args)
159
166
  to_h.to_json(*args)
160
167
  end
@@ -212,6 +219,18 @@ module Profiler
212
219
  profile
213
220
  end
214
221
 
222
+ # A body kept gzip+base64 as text, with its encoding then "text"; any other body as it is.
223
+ # One that cannot be decompressed (cut, damaged) is returned as it is: unreadable at worst.
224
+ def self.decode_body(body, encoding)
225
+ return [body, encoding] unless encoding == "gzip+base64"
226
+ return [body, encoding] if body.nil? || body.empty?
227
+
228
+ decoded = Zlib::Inflate.inflate(Base64.strict_decode64(body))
229
+ [decoded.force_encoding(Encoding::UTF_8).scrub, "text"]
230
+ rescue Zlib::Error, ArgumentError
231
+ [body, encoding]
232
+ end
233
+
215
234
  def self.deep_stringify_keys(obj)
216
235
  case obj
217
236
  when Hash
@@ -235,7 +254,8 @@ module Profiler
235
254
  else
236
255
  text = Redaction.filter_body(raw, content_type).encode("UTF-8", invalid: :replace, undef: :replace)
237
256
  if compress_body?(text)
238
- { body: Base64.strict_encode64(Zlib::Deflate.deflate(text)), encoding: "gzip+base64" }
257
+ # The fastest level: paid on the request, a quarter larger than the default level.
258
+ { body: Base64.strict_encode64(Zlib::Deflate.deflate(text, Zlib::BEST_SPEED)), encoding: "gzip+base64" }
239
259
  else
240
260
  { body: text, encoding: "text" }
241
261
  end
@@ -253,16 +273,6 @@ module Profiler
253
273
  text.bytesize > Profiler.configuration.compress_body_threshold
254
274
  end
255
275
 
256
- def decode_body(body, encoding)
257
- return [body, encoding] unless encoding == "gzip+base64"
258
- return [body, encoding] if body.nil? || body.empty?
259
-
260
- decoded = Zlib::Inflate.inflate(Base64.strict_decode64(body))
261
- [decoded, "text"]
262
- rescue Zlib::Error, ArgumentError
263
- [body, encoding]
264
- end
265
-
266
276
  def binary_content_type?(ct)
267
277
  ct.to_s.match?(%r{image/(?!svg)|application/(?:pdf|octet-stream|zip)|audio/|video/})
268
278
  end
@@ -7,16 +7,19 @@ module Profiler
7
7
  # What the profile lists show of a profile, for the stores to keep apart from the profile
8
8
  # itself: the top-level fields without the bodies, params, headers and tabs, and for each
9
9
  # collector its scalar values only (database.total_queries, job.queue, test.test_name...),
10
- # nested lists and hashes left out, strings cut at MAX_SCALAR_LENGTH (a console return value,
11
- # an exception message). Built from Profile#to_h, after the collectors and the
12
- # redaction have run: a summary is a subset of the stored profile (a long string cut to its
13
- # start) and holds no value the profile does not.
10
+ # nested lists and hashes and the request collector's copy of the bodies left out, strings
11
+ # cut at MAX_SCALAR_LENGTH (a console return value, an exception message). Built from
12
+ # Profile#to_h, after the collectors and the redaction have run: a summary is a subset of the
13
+ # stored profile (a long string cut to its start) and holds no value the profile does not.
14
14
  module Summary
15
15
  MAX_SCALAR_LENGTH = 500
16
16
 
17
17
  DROPPED = %i[params headers response_headers request_body request_body_encoding
18
18
  response_body response_body_encoding tabs].freeze
19
19
 
20
+ # The request collector keeps a copy of the bodies: no more listed than the profile's own.
21
+ DROPPED_SCALARS = %w[request_body request_body_encoding response_body response_body_encoding].freeze
22
+
20
23
  module_function
21
24
 
22
25
  # From a Profile, or the Hash its #to_h returns.
@@ -37,6 +40,7 @@ module Profiler
37
40
  def scalars(values)
38
41
  values.each_with_object({}) do |(key, value), kept|
39
42
  next unless scalar?(value)
43
+ next if DROPPED_SCALARS.include?(key.to_s)
40
44
 
41
45
  kept[key.to_s] = value.is_a?(String) && value.length > MAX_SCALAR_LENGTH ? value[0, MAX_SCALAR_LENGTH] : value
42
46
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.31.5"
4
+ VERSION = "0.31.6"
5
5
  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.31.5
4
+ version: 0.31.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sébastien Duplessy