rails-profiler 0.19.1 → 0.20.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 713e1b01bcd916191e3ae2b89124f4637eeb95d7b9837d350d2ee76a575a2f14
4
- data.tar.gz: 9f1becf53b9b39145a4ebdf81981793aeb5f0494cce1e6872facdcf11c0a597a
3
+ metadata.gz: 1f2b977ebc11075e9d00ae802a9d8f038819907c18b17073cf6b3a0fa437ba29
4
+ data.tar.gz: 4565ab7a98d1a8992cfc3bde0646c6dea355b64d79ab66849c99cab2fff024a3
5
5
  SHA512:
6
- metadata.gz: 298e772fd273b734126c095e57186e36d7f6fbaf26c7bb7db9899d02a38431231e4ecbde42f0b1a103deac26c29f48a596a065b5dd93f16a900b24d66e69a280
7
- data.tar.gz: 78f23d51a52d0467e3acf9d912cbcbeebec03cf245632d88c7306d00d7c00645dda2be0a6208f3e58f4c815ea3a9efe0cb4e6783a3c6c4be44b784fd72e35f37
6
+ metadata.gz: 48c0c092e6d9145ed04651fbc3e924fa7f3e02d79da737cc6a052e0d9d7b41c590651c086a941d76a983eef29358ee0817b1fd078c41d0a7edd750a948a191f2
7
+ data.tar.gz: 7de21cc82a590ff838ed5130970e6947c69e4910ff2ebb5c93431f4d4deba93389cdf446e370fe50a9fce7097526b16ffcd442a18bea82ef29716b0638746028
@@ -1027,7 +1027,7 @@
1027
1027
  second: "2-digit"
1028
1028
  });
1029
1029
  }
1030
- function ToolbarApp({ profile, token }) {
1030
+ function ToolbarApp({ profile, token, currentVersion }) {
1031
1031
  const cd = profile.collectors_data || {};
1032
1032
  const requestData = cd["request"];
1033
1033
  const dbData = cd["database"];
@@ -1291,6 +1291,21 @@
1291
1291
  ]
1292
1292
  }
1293
1293
  ),
1294
+ profile.gem_version && profile.gem_version !== currentVersion ? /* @__PURE__ */ u3(
1295
+ "a",
1296
+ {
1297
+ href: `/_profiler/profiles/${token}`,
1298
+ class: "profiler-toolbar-item profiler-text--warning",
1299
+ title: `Captur\xE9 avec v${profile.gem_version} \u2014 actuel : v${currentVersion}`,
1300
+ children: [
1301
+ "\u26A0\uFE0F v",
1302
+ profile.gem_version
1303
+ ]
1304
+ }
1305
+ ) : currentVersion ? /* @__PURE__ */ u3("span", { class: "profiler-toolbar-item", style: "cursor:default", children: [
1306
+ "v",
1307
+ currentVersion
1308
+ ] }) : null,
1294
1309
  /* @__PURE__ */ u3("a", { href: "/_profiler", class: "profiler-toolbar-item", children: "\u2B21 Profiler" })
1295
1310
  ] });
1296
1311
  }
@@ -1306,6 +1321,7 @@
1306
1321
  if (!el) return;
1307
1322
  const token = el.dataset.token;
1308
1323
  if (!token) return;
1324
+ const currentVersion = el.dataset.version ?? "";
1309
1325
  applyTheme(el);
1310
1326
  window.addEventListener("storage", (e3) => {
1311
1327
  if (e3.key === "profiler-theme") applyTheme(el);
@@ -1314,7 +1330,7 @@
1314
1330
  if (e3.detail?.theme) el.setAttribute("data-theme", e3.detail.theme);
1315
1331
  }));
1316
1332
  const renderToolbar = (profile) => {
1317
- J(/* @__PURE__ */ u3(ToolbarApp, { profile, token }), el);
1333
+ J(/* @__PURE__ */ u3(ToolbarApp, { profile, token, currentVersion }), el);
1318
1334
  applyTheme(el);
1319
1335
  };
1320
1336
  const loadAndRender = () => {
@@ -521,6 +521,34 @@ tr:hover .btn-row-delete {
521
521
  border: 1px solid var(--profiler-border);
522
522
  }
523
523
 
524
+ .profiler-version-badge {
525
+ display: inline-flex;
526
+ align-items: center;
527
+ padding: 1px 6px;
528
+ border-radius: var(--profiler-radius-sm);
529
+ font-family: var(--profiler-font-mono);
530
+ font-size: var(--profiler-text-xs);
531
+ color: var(--profiler-text-muted);
532
+ background: var(--profiler-bg-lighter);
533
+ border: 1px solid var(--profiler-border);
534
+ }
535
+
536
+ .profiler-version-warn {
537
+ margin-left: 6px;
538
+ cursor: help;
539
+ font-size: var(--profiler-text-xs);
540
+ }
541
+
542
+ .profiler-version-mismatch {
543
+ margin-top: var(--profiler-space-2);
544
+ padding: var(--profiler-space-2) var(--profiler-space-3);
545
+ border-radius: var(--profiler-radius-sm);
546
+ font-size: var(--profiler-text-sm);
547
+ background: var(--profiler-warning-bg);
548
+ color: var(--profiler-warning);
549
+ border: 1px solid rgba(251, 146, 60, 0.3);
550
+ }
551
+
524
552
  @keyframes fadeInDown {
525
553
  from {
526
554
  opacity: 0;
@@ -396,6 +396,11 @@
396
396
  return "function" == typeof t3 ? t3(n2) : t3;
397
397
  }
398
398
 
399
+ // app/assets/typescript/profiler/dashboard/utils.ts
400
+ function getGemVersion() {
401
+ return document.querySelector('meta[name="profiler-version"]')?.content ?? "";
402
+ }
403
+
399
404
  // app/assets/typescript/profiler/components/dashboard/tabs/shared/utils.ts
400
405
  function methodBadge(method) {
401
406
  const map = { GET: "info", POST: "success", PUT: "warning", PATCH: "warning", DELETE: "error" };
@@ -1635,6 +1640,7 @@
1635
1640
  { key: "has_exception", label: "Has exception" }
1636
1641
  ];
1637
1642
  function ProfileList() {
1643
+ const currentVersion = getGemVersion();
1638
1644
  const params = new URLSearchParams(window.location.search);
1639
1645
  const initialSection = () => {
1640
1646
  const s3 = params.get("section");
@@ -1982,7 +1988,11 @@
1982
1988
  /* @__PURE__ */ u3("span", { class: "h1-emoji", children: "\u{1F50D}" }),
1983
1989
  " Rails Profiler"
1984
1990
  ] }),
1985
- /* @__PURE__ */ u3("p", { children: "Recent profiled requests and jobs" })
1991
+ /* @__PURE__ */ u3("p", { children: "Recent profiled requests and jobs" }),
1992
+ currentVersion && /* @__PURE__ */ u3("span", { class: "profiler-version-badge", children: [
1993
+ "v",
1994
+ currentVersion
1995
+ ] })
1986
1996
  ] }),
1987
1997
  /* @__PURE__ */ u3("div", { class: "profiler-panel profiler-mb-6", children: [
1988
1998
  /* @__PURE__ */ u3("div", { class: "tabs", children: [
@@ -2092,7 +2102,10 @@
2092
2102
  /* @__PURE__ */ u3("tbody", { children: sortedProfiles.map((p3) => /* @__PURE__ */ u3("tr", { children: [
2093
2103
  /* @__PURE__ */ u3("td", { children: formatTime(p3.started_at) }),
2094
2104
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: methodClass(p3.method), children: p3.method }) }),
2095
- /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("a", { href: `${BASE}/profiles/${p3.token}`, children: p3.path }) }),
2105
+ /* @__PURE__ */ u3("td", { children: [
2106
+ /* @__PURE__ */ u3("a", { href: `${BASE}/profiles/${p3.token}`, children: p3.path }),
2107
+ p3.gem_version && p3.gem_version !== currentVersion && /* @__PURE__ */ u3("span", { class: "profiler-version-warn", title: `Captur\xE9 avec v${p3.gem_version} (actuel : v${currentVersion})`, children: "\u26A0\uFE0F" })
2108
+ ] }),
2096
2109
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: durationClass(p3.duration), children: [
2097
2110
  p3.duration.toFixed(2),
2098
2111
  " ms"
@@ -2169,7 +2182,10 @@
2169
2182
  const isFailed = p3.status === 500;
2170
2183
  return /* @__PURE__ */ u3("tr", { children: [
2171
2184
  /* @__PURE__ */ u3("td", { children: formatTime(p3.started_at) }),
2172
- /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("a", { href: `${BASE}/profiles/${p3.token}`, children: p3.path }) }),
2185
+ /* @__PURE__ */ u3("td", { children: [
2186
+ /* @__PURE__ */ u3("a", { href: `${BASE}/profiles/${p3.token}`, children: p3.path }),
2187
+ p3.gem_version && p3.gem_version !== currentVersion && /* @__PURE__ */ u3("span", { class: "profiler-version-warn", title: `Captur\xE9 avec v${p3.gem_version} (actuel : v${currentVersion})`, children: "\u26A0\uFE0F" })
2188
+ ] }),
2173
2189
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--mono", children: jobData?.queue || "-" }) }),
2174
2190
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: durationClass(p3.duration), children: [
2175
2191
  p3.duration.toFixed(2),
@@ -4482,7 +4498,17 @@
4482
4498
  " MB"
4483
4499
  ] })
4484
4500
  ] }),
4485
- /* @__PURE__ */ u3("span", { style: "color:var(--profiler-text-muted)", children: new Date(profile.started_at).toLocaleString("en", { hour12: false, month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit" }) })
4501
+ /* @__PURE__ */ u3("span", { style: "color:var(--profiler-text-muted)", children: new Date(profile.started_at).toLocaleString("en", { hour12: false, month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit" }) }),
4502
+ profile.gem_version && /* @__PURE__ */ u3("span", { class: "profiler-version-badge", children: [
4503
+ "v",
4504
+ profile.gem_version
4505
+ ] })
4506
+ ] }),
4507
+ profile.gem_version && profile.gem_version !== getGemVersion() && /* @__PURE__ */ u3("div", { class: "profiler-version-mismatch", children: [
4508
+ "\u26A0\uFE0F Profil captur\xE9 avec la version ",
4509
+ /* @__PURE__ */ u3("strong", { children: profile.gem_version }),
4510
+ " \u2014 version actuelle : ",
4511
+ /* @__PURE__ */ u3("strong", { children: getGemVersion() })
4486
4512
  ] })
4487
4513
  ] }),
4488
4514
  /* @__PURE__ */ u3("div", { class: "profiler-panel profiler-mb-6", children: [
@@ -4628,8 +4654,18 @@
4628
4654
  (profile.memory / 1024 / 1024).toFixed(2),
4629
4655
  " MB"
4630
4656
  ] })
4657
+ ] }),
4658
+ profile.gem_version && /* @__PURE__ */ u3("span", { class: "profiler-version-badge", children: [
4659
+ "v",
4660
+ profile.gem_version
4631
4661
  ] })
4632
4662
  ] }),
4663
+ profile.gem_version && profile.gem_version !== getGemVersion() && /* @__PURE__ */ u3("div", { class: "profiler-version-mismatch", children: [
4664
+ "\u26A0\uFE0F Profil captur\xE9 avec la version ",
4665
+ /* @__PURE__ */ u3("strong", { children: profile.gem_version }),
4666
+ " \u2014 version actuelle : ",
4667
+ /* @__PURE__ */ u3("strong", { children: getGemVersion() })
4668
+ ] }),
4633
4669
  parent && /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2 profiler-mt-2 profiler-text--sm", children: [
4634
4670
  /* @__PURE__ */ u3("span", { style: "color:var(--profiler-text-muted)", children: "Triggered by:" }),
4635
4671
  parent.profile_type === "http" ? /* @__PURE__ */ u3("span", { children: [
@@ -5,6 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <%= csrf_meta_tags %>
7
7
  <%= csp_meta_tag %>
8
+ <meta name="profiler-version" content="<%= Profiler::VERSION %>">
8
9
 
9
10
  <link rel="stylesheet" href="/_profiler/assets/profiler.css">
10
11
  <script src="/_profiler/assets/profiler.js" defer></script>
@@ -5,6 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <%= csrf_meta_tags %>
7
7
  <%= csp_meta_tag %>
8
+ <meta name="profiler-version" content="<%= Profiler::VERSION %>">
8
9
 
9
10
  <link rel="stylesheet" href="/_profiler/assets/profiler.css">
10
11
  <script src="/_profiler/assets/profiler.js" defer></script>
@@ -25,6 +25,24 @@ module Profiler
25
25
  # Fallback: body may be passed as the 2nd argument and only applied
26
26
  # to req inside super via req.set_body_internal(body)
27
27
  req_body = body.to_s if req_body.empty? && body
28
+ # Fallback: libraries like RestClient set body_stream instead of body
29
+ # and pass nil as the 2nd argument to Net::HTTP#request.
30
+ # Read the stream content, then restore it so the actual request works.
31
+ # RestClient::Payload doesn't implement #rewind, so we fall back to
32
+ # replacing body_stream with a fresh StringIO.
33
+ if req_body.empty? && req.body_stream
34
+ begin
35
+ stream = req.body_stream
36
+ req_body = stream.read.to_s
37
+ if stream.respond_to?(:rewind)
38
+ stream.rewind
39
+ else
40
+ req.body_stream = StringIO.new(req_body)
41
+ end
42
+ rescue
43
+ req_body = ""
44
+ end
45
+ end
28
46
  req_headers = req.to_hash.transform_values { |v| v.join(", ") }
29
47
  request_id = SecureRandom.hex(8)
30
48
  started_at = Time.now.iso8601(3)
@@ -105,9 +123,6 @@ module Profiler
105
123
 
106
124
  SKIP_HOSTS = %w[127.0.0.1 localhost ::1].freeze
107
125
 
108
- TEXT_BODY_LIMIT = 512 * 1024 # 512 KB
109
- BINARY_BODY_LIMIT = 256 * 1024 # 256 KB (before base64)
110
-
111
126
  TEXT_CONTENT_TYPES = /\A(text\/|application\/(json|xml|xhtml|javascript|x-www-form-urlencoded)|image\/svg)/i
112
127
  BINARY_CONTENT_TYPES = /\A(image\/|application\/pdf|application\/octet-stream|application\/zip|audio\/|video\/)/i
113
128
 
@@ -143,12 +158,10 @@ module Profiler
143
158
  mime = content_type.split(";").first.to_s.strip
144
159
 
145
160
  if mime.match?(BINARY_CONTENT_TYPES)
146
- truncated = body.byteslice(0, BINARY_BODY_LIMIT) || ""
147
- { body: Base64.strict_encode64(truncated.b), encoding: "base64" }
161
+ { body: Base64.strict_encode64(body.b), encoding: "base64" }
148
162
  else
149
- # Text (including unknown content types)
150
163
  text = body.encode("UTF-8", invalid: :replace, undef: :replace, replace: "?")
151
- { body: text.byteslice(0, TEXT_BODY_LIMIT), encoding: "text" }
164
+ { body: text, encoding: "text" }
152
165
  end
153
166
  end
154
167
 
@@ -50,6 +50,7 @@ module Profiler
50
50
  def run(&block)
51
51
  profile = Models::Profile.new
52
52
  profile.profile_type = "job"
53
+ profile.gem_version = Profiler::VERSION
53
54
  profile.path = @job_class
54
55
  profile.method = "JOB"
55
56
  profile.parent_token = @parent_token if @parent_token
@@ -60,7 +60,7 @@ module Profiler
60
60
  lines += section_views(profile) if want.("views")
61
61
  lines += section_cache(profile) if want.("cache")
62
62
  lines += section_ajax(profile) if want.("ajax")
63
- lines += section_http(profile) if want.("http")
63
+ lines += section_http(profile, params) if want.("http")
64
64
  lines += section_routes(profile) if want.("routes")
65
65
  lines += section_dumps(profile) if want.("dumps")
66
66
  lines += section_related_jobs(profile) if want.("related_jobs")
@@ -76,6 +76,13 @@ module Profiler
76
76
  lines << "**Duration:** #{profile.duration.round(2)} ms"
77
77
  lines << "**Memory:** #{(profile.memory / 1024.0 / 1024.0).round(2)} MB" if profile.memory
78
78
  lines << "**Time:** #{profile.started_at}"
79
+ if profile.gem_version
80
+ if profile.gem_version != Profiler::VERSION
81
+ lines << "**Gem Version:** #{profile.gem_version} ⚠️ (current: #{Profiler::VERSION})"
82
+ else
83
+ lines << "**Gem Version:** #{profile.gem_version}"
84
+ end
85
+ end
79
86
  lines << "**Parent Token:** #{profile.parent_token}" if profile.parent_token
80
87
  lines << ""
81
88
  lines
@@ -325,7 +332,7 @@ module Profiler
325
332
  lines
326
333
  end
327
334
 
328
- def self.section_http(profile)
335
+ def self.section_http(profile, params = {})
329
336
  lines = []
330
337
  http_data = profile.collector_data("http")
331
338
  return lines unless http_data && http_data["total_requests"].to_i > 0
@@ -339,10 +346,39 @@ module Profiler
339
346
 
340
347
  if http_data["requests"] && !http_data["requests"].empty?
341
348
  lines << "### Request List"
342
- http_data["requests"].each do |req|
349
+ http_data["requests"].each_with_index do |req, index|
343
350
  flag = req["duration"] >= threshold ? " [SLOW]" : ""
344
351
  err = req["status"] >= 400 || req["status"] == 0 ? " [ERROR]" : ""
345
- lines << "- **#{req['method']} #{req['url']}** — #{req['status'] == 0 ? 'error' : req['status']} — #{req['duration'].round(2)} ms#{flag}#{err}"
352
+ lines << "\n**#{index + 1}. #{req['method']} #{req['url']}** — #{req['status'] == 0 ? 'error' : req['status']} — #{req['duration'].round(2)} ms#{flag}#{err}"
353
+
354
+ if req["request_body"] && !req["request_body"].empty?
355
+ lines << "**Request Body:**"
356
+ formatted = BodyFormatter.format_body(
357
+ profile.token,
358
+ "http_#{index}_request_body",
359
+ req["request_body"],
360
+ req["request_body_encoding"],
361
+ params
362
+ )
363
+ lines << formatted if formatted
364
+ end
365
+
366
+ if req["response_body"] && !req["response_body"].empty?
367
+ lines << "**Response Body:**"
368
+ formatted = BodyFormatter.format_body(
369
+ profile.token,
370
+ "http_#{index}_response_body",
371
+ req["response_body"],
372
+ req["response_body_encoding"],
373
+ params
374
+ )
375
+ lines << formatted if formatted
376
+ end
377
+
378
+ if req["backtrace"] && !req["backtrace"].empty?
379
+ lines << "_Backtrace:_"
380
+ req["backtrace"].each { |frame| lines << " #{frame}" }
381
+ end
346
382
  end
347
383
  lines << ""
348
384
  end
@@ -4,7 +4,7 @@ module Profiler
4
4
  module MCP
5
5
  module Tools
6
6
  class QueryJobs
7
- ALL_FIELDS = %w[time job_class queue status duration token parent_token].freeze
7
+ ALL_FIELDS = %w[time job_class queue status duration gem_version token parent_token].freeze
8
8
 
9
9
  def self.call(params)
10
10
  limit = params["limit"]&.to_i || 20
@@ -64,6 +64,9 @@ module Profiler
64
64
  when "queue" then job_data["queue"] || "-"
65
65
  when "status" then job_data["status"] || "-"
66
66
  when "duration" then "#{profile.duration.round(2)}ms"
67
+ when "gem_version"
68
+ v = profile.gem_version || "-"
69
+ v != Profiler::VERSION ? "#{v} ⚠️" : v
67
70
  when "token" then profile.token.to_s
68
71
  when "parent_token" then profile.parent_token || "-"
69
72
  end
@@ -20,6 +20,7 @@ module Profiler
20
20
  collectors = nil
21
21
 
22
22
  profile = Models::Profile.new(build_request(env))
23
+ profile.gem_version = Profiler::VERSION
23
24
  Profiler::CurrentContext.token = profile.token
24
25
 
25
26
  # Capture request body before app processes it
@@ -62,7 +62,7 @@ module Profiler
62
62
  def toolbar_html
63
63
  <<~HTML
64
64
  #{ajax_interceptor_script}
65
- <div id="profiler-toolbar" data-token="#{@token}"></div>
65
+ <div id="profiler-toolbar" data-token="#{@token}" data-version="#{Profiler::VERSION}"></div>
66
66
  <script src="/_profiler/assets/profiler-toolbar.js" defer#{nonce_attr}></script>
67
67
  <style>#{toolbar_styles}</style>
68
68
  HTML
@@ -13,7 +13,8 @@ module Profiler
13
13
  :response_headers, :collectors_data, :collectors_metadata,
14
14
  :parent_token, :is_ajax, :profile_type,
15
15
  :request_body, :request_body_encoding,
16
- :response_body, :response_body_encoding
16
+ :response_body, :response_body_encoding,
17
+ :gem_version
17
18
 
18
19
  def initialize(request = nil)
19
20
  @token = SecureRandom.hex(16)
@@ -32,9 +33,6 @@ module Profiler
32
33
  end
33
34
  end
34
35
 
35
- TEXT_BODY_LIMIT = 512 * 1024
36
- BINARY_BODY_LIMIT = 256 * 1024
37
-
38
36
  def set_bodies(request_body:, response_body:, req_content_type:, resp_content_type:)
39
37
  req = process_body(request_body, req_content_type)
40
38
  resp = process_body(response_body, resp_content_type)
@@ -79,6 +77,7 @@ module Profiler
79
77
 
80
78
  {
81
79
  profile_type: @profile_type,
80
+ gem_version: @gem_version,
82
81
  token: @token,
83
82
  path: @path,
84
83
  method: @method,
@@ -130,6 +129,7 @@ module Profiler
130
129
  profile.parent_token = data[:parent_token]
131
130
  profile.is_ajax = data[:is_ajax] || false
132
131
  profile.profile_type = data[:profile_type] || "http"
132
+ profile.gem_version = data[:gem_version]
133
133
 
134
134
  # Convert collectors_data keys to strings recursively for consistency
135
135
  profile.collectors_data = (data[:collectors_data] || {}).transform_keys(&:to_s).transform_values do |value|
@@ -159,10 +159,9 @@ module Profiler
159
159
  return { body: nil, encoding: "text" } if raw.nil? || raw.empty?
160
160
 
161
161
  if binary_content_type?(content_type)
162
- truncated = raw.b[0, BINARY_BODY_LIMIT]
163
- { body: Base64.strict_encode64(truncated), encoding: "base64" }
162
+ { body: Base64.strict_encode64(raw.b), encoding: "base64" }
164
163
  else
165
- text = raw.encode("UTF-8", invalid: :replace, undef: :replace)[0, TEXT_BODY_LIMIT]
164
+ text = raw.encode("UTF-8", invalid: :replace, undef: :replace)
166
165
  if compress_body?(text)
167
166
  { body: Base64.strict_encode64(Zlib::Deflate.deflate(text)), encoding: "gzip+base64" }
168
167
  else
@@ -51,17 +51,18 @@ module Profiler
51
51
  @db.execute(
52
52
  <<~SQL,
53
53
  INSERT OR REPLACE INTO profiler_profiles (
54
- token, profile_type, path, method, status, duration, memory,
54
+ token, profile_type, gem_version, path, method, status, duration, memory,
55
55
  started_at, finished_at, parent_token, is_ajax,
56
56
  tabs, params, headers, response_headers, collectors_meta
57
57
  ) VALUES (
58
- :token, :profile_type, :path, :method, :status, :duration, :memory,
58
+ :token, :profile_type, :gem_version, :path, :method, :status, :duration, :memory,
59
59
  :started_at, :finished_at, :parent_token, :is_ajax,
60
60
  :tabs, :params, :headers, :response_headers, :collectors_meta
61
61
  )
62
62
  SQL
63
63
  token: token,
64
64
  profile_type: data[:profile_type] || "http",
65
+ gem_version: data[:gem_version],
65
66
  path: data[:path],
66
67
  method: data[:method],
67
68
  status: data[:status],
@@ -176,6 +177,7 @@ module Profiler
176
177
  CREATE TABLE IF NOT EXISTS profiler_profiles (
177
178
  token TEXT PRIMARY KEY,
178
179
  profile_type TEXT NOT NULL DEFAULT 'http',
180
+ gem_version TEXT,
179
181
  path TEXT,
180
182
  method TEXT,
181
183
  status INTEGER,
@@ -202,6 +204,12 @@ module Profiler
202
204
  CREATE INDEX IF NOT EXISTS idx_profiler_profile_type
203
205
  ON profiler_profiles(profile_type);
204
206
  SQL
207
+
208
+ begin
209
+ @db.execute("ALTER TABLE profiler_profiles ADD COLUMN gem_version TEXT")
210
+ rescue SQLite3::Exception
211
+ # column already exists
212
+ end
205
213
  end
206
214
 
207
215
  def row_to_profile(row, load_blobs: true)
@@ -223,6 +231,7 @@ module Profiler
223
231
  Models::Profile.from_hash(
224
232
  token: row["token"],
225
233
  profile_type: row["profile_type"],
234
+ gem_version: row["gem_version"],
226
235
  path: row["path"],
227
236
  method: row["method"],
228
237
  status: row["status"],
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.19.1"
4
+ VERSION = "0.20.0"
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.19.1
4
+ version: 0.20.0
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-04-17 00:00:00.000000000 Z
11
+ date: 2026-05-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails