rails-profiler 0.34.6 → 0.35.1
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 +4 -4
- data/CHANGELOG.md +29 -0
- data/app/assets/builds/profiler-toolbar.js +1 -1
- data/app/assets/builds/profiler.js +22 -22
- data/app/assets/images/icon.png +0 -0
- data/app/controllers/profiler/api/cluster_controller.rb +1 -1
- data/app/controllers/profiler/assets_controller.rb +5 -0
- data/app/controllers/profiler/profiles_controller.rb +2 -2
- data/app/views/layouts/profiler/application.html.erb +1 -0
- data/app/views/layouts/profiler/embedded.html.erb +2 -1
- data/config/routes.rb +1 -0
- data/lib/profiler/collectors/function_profiler_collector.rb +1 -1
- data/lib/profiler/collectors/mailer_collector.rb +1 -1
- data/lib/profiler/collectors/routes_collector.rb +1 -1
- data/lib/profiler/mcp/server.rb +2 -2
- data/lib/profiler/mcp/tools/explain_query.rb +1 -1
- data/lib/profiler/mcp/tools/get_profile_detail.rb +7 -7
- data/lib/profiler/mcp/tools/run_tests.rb +1 -1
- data/lib/profiler/middleware/toolbar_injector.rb +2 -2
- data/lib/profiler/railtie.rb +1 -1
- data/lib/profiler/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: deae1ffa2327cdd3603f1451a376444412a495cd544920c4ff7b91b53275a169
|
|
4
|
+
data.tar.gz: 661c22ece7d423f81a12f283c5daafcca147b1bf8661ffa1a96ea7a42d962451
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 668ccb4d779af6e835701b59df419eaf91daed287ccc3087dd45a74ad9e7e6895bc7c37c36d152e06d1f3ddbcc68181b539c1eb1dea98e3a5421c1bb35eac2b3
|
|
7
|
+
data.tar.gz: 3511797c44acfea198604714c262eb8555a983f7e8392abc6788041dd9e18ff5ce29cb4814d6f64a364c6a921c89790a33e1bf3dc4eef085aa6bbaa877c7e8ad
|
data/CHANGELOG.md
CHANGED
|
@@ -19,6 +19,35 @@ every commit of every tag interval is accounted for one way or the other.
|
|
|
19
19
|
|
|
20
20
|
## [Unreleased]
|
|
21
21
|
|
|
22
|
+
## [0.35.1] - 2026-10-17
|
|
23
|
+
|
|
24
|
+
<!-- stamped -->
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Release:** The version a merge publishes is now read from the subject line of each commit only,
|
|
29
|
+
never from its body, and `BREAKING-CHANGE:` (with a hyphen) publishes a major version just as
|
|
30
|
+
`BREAKING CHANGE:` already did.
|
|
31
|
+
- **Canary:** A prerelease is now numbered after the version its branch is going to publish, not
|
|
32
|
+
after the last release, so RubyGems no longer sorts a recent canary as older than the release it
|
|
33
|
+
precedes.
|
|
34
|
+
|
|
35
|
+
### Security
|
|
36
|
+
|
|
37
|
+
- **Release:** `rake release` and its subtasks now refuse to run from a developer machine and point
|
|
38
|
+
to the `release` CI job, the only publisher. `rake build` and `rake install` keep working.
|
|
39
|
+
- **CI:** The `canary` job no longer publishes a prerelease on every push to a `feature/`,
|
|
40
|
+
`bugfix/`, `hotfix/`, `fix/` or `breaking/` branch; it is now manual and publishes only when
|
|
41
|
+
someone plays it.
|
|
42
|
+
|
|
43
|
+
## [0.35.0] - 2026-10-17
|
|
44
|
+
|
|
45
|
+
<!-- stamped -->
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- The dashboard pages now carry the Chrome extension's icon as their favicon, served at `/_profiler/assets/icon.png`.
|
|
50
|
+
|
|
22
51
|
## [0.34.6] - 2026-10-16
|
|
23
52
|
|
|
24
53
|
<!-- stamped -->
|
|
@@ -4098,7 +4098,7 @@
|
|
|
4098
4098
|
panel: /* @__PURE__ */ u3(RoutesPanel, { routesData }),
|
|
4099
4099
|
children: [
|
|
4100
4100
|
/* @__PURE__ */ u3("span", { class: "profiler-text--muted profiler-text--xs", children: "ROUTE" }),
|
|
4101
|
-
/* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--mono", children: routesData.matched?.pattern ?? "
|
|
4101
|
+
/* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--mono", children: routesData.matched?.pattern ?? "-" })
|
|
4102
4102
|
]
|
|
4103
4103
|
}
|
|
4104
4104
|
),
|
|
@@ -4033,7 +4033,7 @@
|
|
|
4033
4033
|
return "warning";
|
|
4034
4034
|
}
|
|
4035
4035
|
function formatBytes(bytes) {
|
|
4036
|
-
if (bytes < 0) return "
|
|
4036
|
+
if (bytes < 0) return "-";
|
|
4037
4037
|
if (bytes < 1024) return `${bytes} B`;
|
|
4038
4038
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
4039
4039
|
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
@@ -4682,7 +4682,7 @@
|
|
|
4682
4682
|
setVariables(data.variables);
|
|
4683
4683
|
setTotal(data.total);
|
|
4684
4684
|
setOverrides(data.overrides ?? {});
|
|
4685
|
-
showFlash("success", `Refreshed
|
|
4685
|
+
showFlash("success", `Refreshed - ${data.total} variables`);
|
|
4686
4686
|
return data;
|
|
4687
4687
|
} catch (e3) {
|
|
4688
4688
|
showFlash("error", e3.message ?? "Failed to refresh");
|
|
@@ -4956,10 +4956,10 @@
|
|
|
4956
4956
|
] })
|
|
4957
4957
|
] }),
|
|
4958
4958
|
forceReadOnly ? /* @__PURE__ */ u3("div", { style: "background:var(--profiler-bg-subtle,rgba(0,0,0,0.03));border:1px solid var(--profiler-border);border-radius:6px;padding:8px 12px;margin-bottom:16px;font-size:12px;color:var(--profiler-text-muted);", children: [
|
|
4959
|
-
"Snapshot taken at request time
|
|
4959
|
+
"Snapshot taken at request time - modify env vars in ",
|
|
4960
4960
|
/* @__PURE__ */ u3("a", { href: "/_profiler?section=env", style: "color:var(--profiler-accent);", children: "/_profiler?section=env" }),
|
|
4961
4961
|
"."
|
|
4962
|
-
] }) : /* @__PURE__ */ u3("div", { style: "background:var(--profiler-warning-bg,rgba(245,158,11,0.1));border:1px solid var(--profiler-warning,#f59e0b);border-radius:6px;padding:8px 12px;margin-bottom:16px;font-size:12px;color:var(--profiler-warning,#f59e0b);", children: "\u26A0 Changes affect the current process only
|
|
4962
|
+
] }) : /* @__PURE__ */ u3("div", { style: "background:var(--profiler-warning-bg,rgba(245,158,11,0.1));border:1px solid var(--profiler-warning,#f59e0b);border-radius:6px;padding:8px 12px;margin-bottom:16px;font-size:12px;color:var(--profiler-warning,#f59e0b);", children: "\u26A0 Changes affect the current process only - for development use." }),
|
|
4963
4963
|
flash && /* @__PURE__ */ u3("div", { style: `background:${flash.type === "success" ? "var(--profiler-success-bg,rgba(34,197,94,0.1))" : "var(--profiler-error-bg,rgba(239,68,68,0.08))"};border:1px solid ${flash.type === "success" ? "var(--profiler-success,#22c55e)" : "var(--profiler-error,#ef4444)"};border-radius:6px;padding:6px 12px;margin-bottom:12px;font-size:12px;color:${flash.type === "success" ? "var(--profiler-success,#22c55e)" : "var(--profiler-error,#ef4444)"};`, children: [
|
|
4964
4964
|
flash.type === "success" ? "\u2713" : "\u2717",
|
|
4965
4965
|
" ",
|
|
@@ -6100,7 +6100,7 @@
|
|
|
6100
6100
|
p3.duration.toFixed(2),
|
|
6101
6101
|
" ms"
|
|
6102
6102
|
] }) }),
|
|
6103
|
-
/* @__PURE__ */ u3("td", { children: p3.collectors_data?.database?.total_queries ?? "
|
|
6103
|
+
/* @__PURE__ */ u3("td", { children: p3.collectors_data?.database?.total_queries ?? "-" }),
|
|
6104
6104
|
/* @__PURE__ */ u3("td", { children: formatAllocations(allocatedObjects(p3)) }),
|
|
6105
6105
|
/* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: statusClass(p3.status), children: p3.status }) }),
|
|
6106
6106
|
/* @__PURE__ */ u3("td", { class: "profiler-text--xs profiler-text--mono profiler-text--muted", children: /* @__PURE__ */ u3("button", { class: "token-copy", onClick: () => copyToken(p3.token), title: "Copy full token", children: copiedToken === p3.token ? "\u2713" : p3.token.substring(0, 8) + "\u2026" }) }),
|
|
@@ -6263,7 +6263,7 @@
|
|
|
6263
6263
|
p3.duration.toFixed(2),
|
|
6264
6264
|
" ms"
|
|
6265
6265
|
] }) }),
|
|
6266
|
-
/* @__PURE__ */ u3("td", { children: p3.collectors_data?.database?.total_queries ?? "
|
|
6266
|
+
/* @__PURE__ */ u3("td", { children: p3.collectors_data?.database?.total_queries ?? "-" }),
|
|
6267
6267
|
/* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: isError ? "badge-error" : "badge-success", children: isError ? "\u2717 Error" : "\u2713 OK" }) }),
|
|
6268
6268
|
/* @__PURE__ */ u3("td", { class: "profiler-text--xs profiler-text--mono profiler-text--muted", children: /* @__PURE__ */ u3("button", { class: "token-copy", onClick: () => copyToken(p3.token), title: "Copy full token", children: copiedToken === p3.token ? "\u2713" : p3.token.substring(0, 8) + "\u2026" }) }),
|
|
6269
6269
|
/* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("button", { class: "btn-row-delete", onClick: () => deleteConsole2(p3.token), title: "Delete", children: "\xD7" }) })
|
|
@@ -6346,7 +6346,7 @@
|
|
|
6346
6346
|
p3.duration.toFixed(2),
|
|
6347
6347
|
" ms"
|
|
6348
6348
|
] }) }),
|
|
6349
|
-
/* @__PURE__ */ u3("td", { children: p3.collectors_data?.database?.total_queries ?? "
|
|
6349
|
+
/* @__PURE__ */ u3("td", { children: p3.collectors_data?.database?.total_queries ?? "-" }),
|
|
6350
6350
|
/* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: status === "failed" ? "badge-error" : status === "pending" ? "badge-warning" : "badge-success", children: status === "failed" ? "\u2717 Failed" : status === "pending" ? "\u23F8 Pending" : "\u2713 Passed" }) }),
|
|
6351
6351
|
/* @__PURE__ */ u3("td", { class: "profiler-text--xs profiler-text--mono profiler-text--muted", children: /* @__PURE__ */ u3("button", { class: "token-copy", onClick: () => copyToken(p3.token), title: "Copy full token", children: copiedToken === p3.token ? "\u2713" : p3.token.substring(0, 8) + "\u2026" }) }),
|
|
6352
6352
|
/* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("button", { class: "btn-row-delete", onClick: () => deleteTest2(p3.token), title: "Delete", children: "\xD7" }) })
|
|
@@ -6741,7 +6741,7 @@
|
|
|
6741
6741
|
/* @__PURE__ */ u3("span", { class: "profiler-alert-banner__icon", children: "\u26A0\uFE0F" }),
|
|
6742
6742
|
/* @__PURE__ */ u3("div", { style: "flex:1", children: [
|
|
6743
6743
|
/* @__PURE__ */ u3("strong", { children: "Potential N+1 detected" }),
|
|
6744
|
-
"
|
|
6744
|
+
" - ",
|
|
6745
6745
|
n1Groups.length,
|
|
6746
6746
|
" pattern",
|
|
6747
6747
|
n1Groups.length > 1 ? "s" : "",
|
|
@@ -7814,7 +7814,7 @@
|
|
|
7814
7814
|
class: `profiler-fn-profiling__mode-btn${mode === m3 ? " profiler-fn-profiling__mode-btn--active" : ""}`,
|
|
7815
7815
|
onClick: () => mode !== m3 && onModeChange(m3),
|
|
7816
7816
|
disabled: modeUpdating,
|
|
7817
|
-
title: m3 === "full" ? "Exhaustive TracePoint
|
|
7817
|
+
title: m3 === "full" ? "Exhaustive TracePoint - timing + allocations + memory bytes" : "StackProf sampling - very low overhead",
|
|
7818
7818
|
children: m3.charAt(0).toUpperCase() + m3.slice(1)
|
|
7819
7819
|
},
|
|
7820
7820
|
m3
|
|
@@ -7864,7 +7864,7 @@
|
|
|
7864
7864
|
": statistical sampling with the stackprof gem, to add to the application's Gemfile; very low overhead (<1%), on by default once stackprof is installed.",
|
|
7865
7865
|
" ",
|
|
7866
7866
|
/* @__PURE__ */ u3("strong", { children: "Full" }),
|
|
7867
|
-
": exhaustive TracePoint tracing with memory bytes
|
|
7867
|
+
": exhaustive TracePoint tracing with memory bytes - significant overhead."
|
|
7868
7868
|
] }),
|
|
7869
7869
|
enabled && stackprofMissing && /* @__PURE__ */ u3("p", { class: "profiler-fn-profiling__hint", children: [
|
|
7870
7870
|
"No data: sampling needs the ",
|
|
@@ -7884,7 +7884,7 @@
|
|
|
7884
7884
|
data.frame_cap_reached && /* @__PURE__ */ u3("div", { class: "profiler-fn-profiling__cap-warning", children: [
|
|
7885
7885
|
"\u26A0 Frame cap reached (",
|
|
7886
7886
|
data.max_frames,
|
|
7887
|
-
' frames)
|
|
7887
|
+
' frames) - call tree is truncated. Increase "Max frames" to capture more.'
|
|
7888
7888
|
] }),
|
|
7889
7889
|
/* @__PURE__ */ u3("div", { class: "profiler-flamegraph__stats", style: { marginTop: "0.75rem" }, children: [
|
|
7890
7890
|
/* @__PURE__ */ u3("div", { class: "stat-item", children: [
|
|
@@ -8581,8 +8581,8 @@
|
|
|
8581
8581
|
filtered.map((route, i3) => /* @__PURE__ */ u3("div", { class: `profiler-flex-table__row${route.matched ? " profiler-flex-table__row--matched" : ""}`, children: [
|
|
8582
8582
|
/* @__PURE__ */ u3("span", { class: "profiler-flex-table__cell--fixed", children: /* @__PURE__ */ u3(VerbBadge, { verb: route.verb }) }),
|
|
8583
8583
|
/* @__PURE__ */ u3("span", { class: "profiler-flex-table__cell profiler-text--mono profiler-text--xs", title: route.pattern, children: route.pattern }),
|
|
8584
|
-
/* @__PURE__ */ u3("span", { class: "profiler-flex-table__cell profiler-flex-table__cell--muted profiler-text--mono profiler-text--xs", title: route.name ? `${route.name}_path` : "", children: route.name ? `${route.name}_path` : "
|
|
8585
|
-
/* @__PURE__ */ u3("span", { class: "profiler-flex-table__cell profiler-flex-table__cell--muted profiler-text--mono profiler-text--xs", title: route.controller_action ?? "", children: route.controller_action ?? "
|
|
8584
|
+
/* @__PURE__ */ u3("span", { class: "profiler-flex-table__cell profiler-flex-table__cell--muted profiler-text--mono profiler-text--xs", title: route.name ? `${route.name}_path` : "", children: route.name ? `${route.name}_path` : "-" }),
|
|
8585
|
+
/* @__PURE__ */ u3("span", { class: "profiler-flex-table__cell profiler-flex-table__cell--muted profiler-text--mono profiler-text--xs", title: route.controller_action ?? "", children: route.controller_action ?? "-" })
|
|
8586
8586
|
] }, i3))
|
|
8587
8587
|
] })
|
|
8588
8588
|
] });
|
|
@@ -8692,7 +8692,7 @@
|
|
|
8692
8692
|
const hasText = !!email.body_text;
|
|
8693
8693
|
if (!email.body_captured) {
|
|
8694
8694
|
return /* @__PURE__ */ u3("div", { class: "profiler-mt-3 profiler-text--xs profiler-text--muted", children: [
|
|
8695
|
-
"Body not captured
|
|
8695
|
+
"Body not captured - enable ",
|
|
8696
8696
|
/* @__PURE__ */ u3("code", { children: "config.capture_mail_body = true" }),
|
|
8697
8697
|
" in your profiler initializer"
|
|
8698
8698
|
] });
|
|
@@ -8778,7 +8778,7 @@
|
|
|
8778
8778
|
] }),
|
|
8779
8779
|
/* @__PURE__ */ u3("div", { class: "profiler-kv-row", children: [
|
|
8780
8780
|
/* @__PURE__ */ u3("span", { children: "Render" }),
|
|
8781
|
-
/* @__PURE__ */ u3("span", { class: "profiler-query-card__duration", children: email.duration_ms != null ? `${email.duration_ms} ms` : "
|
|
8781
|
+
/* @__PURE__ */ u3("span", { class: "profiler-query-card__duration", children: email.duration_ms != null ? `${email.duration_ms} ms` : "-" })
|
|
8782
8782
|
] }),
|
|
8783
8783
|
email.delivery_ms != null && /* @__PURE__ */ u3("div", { class: "profiler-kv-row", children: [
|
|
8784
8784
|
/* @__PURE__ */ u3("span", { children: "Delivery" }),
|
|
@@ -8810,7 +8810,7 @@
|
|
|
8810
8810
|
email.action
|
|
8811
8811
|
] }),
|
|
8812
8812
|
email.subject && /* @__PURE__ */ u3("span", { class: "profiler-text--muted", children: [
|
|
8813
|
-
"
|
|
8813
|
+
" - ",
|
|
8814
8814
|
email.subject.slice(0, 60),
|
|
8815
8815
|
email.subject.length > 60 ? "\u2026" : ""
|
|
8816
8816
|
] })
|
|
@@ -8891,7 +8891,7 @@
|
|
|
8891
8891
|
/* @__PURE__ */ u3("span", { class: "profiler-alert-banner__icon", children: "\u26A0\uFE0F" }),
|
|
8892
8892
|
/* @__PURE__ */ u3("div", { style: { flex: 1 }, children: [
|
|
8893
8893
|
/* @__PURE__ */ u3("strong", { children: "Send loop detected" }),
|
|
8894
|
-
"
|
|
8894
|
+
" - ",
|
|
8895
8895
|
mailerData.loop_warnings.length,
|
|
8896
8896
|
" pattern",
|
|
8897
8897
|
mailerData.loop_warnings.length > 1 ? "s" : "",
|
|
@@ -8900,7 +8900,7 @@
|
|
|
8900
8900
|
] }),
|
|
8901
8901
|
mailerData.truncated && /* @__PURE__ */ u3("div", { class: "profiler-alert-banner profiler-alert-banner--warning profiler-mb-4", children: [
|
|
8902
8902
|
/* @__PURE__ */ u3("span", { class: "profiler-alert-banner__icon", children: "\u26A0\uFE0F" }),
|
|
8903
|
-
/* @__PURE__ */ u3("span", { children: "Showing first 50 emails only
|
|
8903
|
+
/* @__PURE__ */ u3("span", { children: "Showing first 50 emails only - additional emails were truncated" })
|
|
8904
8904
|
] }),
|
|
8905
8905
|
mailerData.emails.length > 0 && /* @__PURE__ */ u3(k, { children: [
|
|
8906
8906
|
/* @__PURE__ */ u3("h3", { class: "profiler-text--lg profiler-mt-4 profiler-mb-3", children: [
|
|
@@ -8912,7 +8912,7 @@
|
|
|
8912
8912
|
mailerData.queued && mailerData.queued.length > 0 && /* @__PURE__ */ u3(k, { children: [
|
|
8913
8913
|
/* @__PURE__ */ u3("h3", { class: "profiler-text--lg profiler-mt-4 profiler-mb-2", children: [
|
|
8914
8914
|
"Queued",
|
|
8915
|
-
/* @__PURE__ */ u3("span", { class: "profiler-text--muted profiler-text--sm", style: { fontWeight: "normal", marginLeft: "8px" }, children: "deliver_later
|
|
8915
|
+
/* @__PURE__ */ u3("span", { class: "profiler-text--muted profiler-text--sm", style: { fontWeight: "normal", marginLeft: "8px" }, children: "deliver_later - will be sent in a background job" })
|
|
8916
8916
|
] }),
|
|
8917
8917
|
mailerData.queued.map((email, i3) => /* @__PURE__ */ u3(QueuedRow, { email }, `q-${i3}`))
|
|
8918
8918
|
] }),
|
|
@@ -8979,7 +8979,7 @@
|
|
|
8979
8979
|
profile.gem_version && profile.gem_version !== getGemVersion() && /* @__PURE__ */ u3("div", { class: "profiler-version-mismatch", children: [
|
|
8980
8980
|
"\u26A0\uFE0F Profil captur\xE9 avec la version ",
|
|
8981
8981
|
/* @__PURE__ */ u3("strong", { children: profile.gem_version }),
|
|
8982
|
-
"
|
|
8982
|
+
" - version actuelle : ",
|
|
8983
8983
|
/* @__PURE__ */ u3("strong", { children: getGemVersion() })
|
|
8984
8984
|
] })
|
|
8985
8985
|
] }),
|
|
@@ -9133,7 +9133,7 @@
|
|
|
9133
9133
|
profile.gem_version && profile.gem_version !== getGemVersion() && /* @__PURE__ */ u3("div", { class: "profiler-version-mismatch", children: [
|
|
9134
9134
|
"\u26A0\uFE0F Profil captur\xE9 avec la version ",
|
|
9135
9135
|
/* @__PURE__ */ u3("strong", { children: profile.gem_version }),
|
|
9136
|
-
"
|
|
9136
|
+
" - version actuelle : ",
|
|
9137
9137
|
/* @__PURE__ */ u3("strong", { children: getGemVersion() })
|
|
9138
9138
|
] }),
|
|
9139
9139
|
parent && /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2 profiler-mt-2 profiler-text--sm", children: [
|
|
@@ -9262,7 +9262,7 @@
|
|
|
9262
9262
|
profile.gem_version && profile.gem_version !== getGemVersion() && /* @__PURE__ */ u3("div", { class: "profiler-version-mismatch", children: [
|
|
9263
9263
|
"\u26A0\uFE0F Profil captur\xE9 avec la version ",
|
|
9264
9264
|
/* @__PURE__ */ u3("strong", { children: profile.gem_version }),
|
|
9265
|
-
"
|
|
9265
|
+
" - version actuelle : ",
|
|
9266
9266
|
/* @__PURE__ */ u3("strong", { children: getGemVersion() })
|
|
9267
9267
|
] })
|
|
9268
9268
|
] }),
|
|
Binary file
|
|
@@ -27,7 +27,7 @@ module Profiler
|
|
|
27
27
|
name = params[:name].to_s
|
|
28
28
|
entry = Profiler.slave_registry.heartbeat(name)
|
|
29
29
|
if entry.nil?
|
|
30
|
-
render json: { error: "Unknown slave
|
|
30
|
+
render json: { error: "Unknown slave - please re-register" }, status: :not_found
|
|
31
31
|
else
|
|
32
32
|
render json: { ok: true }
|
|
33
33
|
end
|
|
@@ -22,6 +22,11 @@ module Profiler
|
|
|
22
22
|
serve("profiler.css", "text/css")
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
def icon
|
|
26
|
+
expires_in 1.day, public: true
|
|
27
|
+
send_file Profiler::Engine.root.join("app", "assets", "images", "icon.png"), type: "image/png", disposition: "inline"
|
|
28
|
+
end
|
|
29
|
+
|
|
25
30
|
private
|
|
26
31
|
|
|
27
32
|
def serve(name, content_type)
|
|
@@ -83,13 +83,13 @@ module Profiler
|
|
|
83
83
|
# Sets @resolved_storage to the storage that owns the profile so that
|
|
84
84
|
# secondary lookups (child jobs, parent, AJAX) go to the same node.
|
|
85
85
|
def resolve_profile(token)
|
|
86
|
-
# Local storage
|
|
86
|
+
# Local storage, fast path, covers master-owned profiles
|
|
87
87
|
if (profile = Profiler.storage.load(token))
|
|
88
88
|
@resolved_storage = Profiler.storage
|
|
89
89
|
return profile
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
-
# Cache hit
|
|
92
|
+
# Cache hit, skip fan-out if we already know which slave has this token
|
|
93
93
|
if (cached_slave = Profiler.token_cache.fetch(token))
|
|
94
94
|
entry = Profiler.slave_registry.all.find { |e| e[:name] == cached_slave }
|
|
95
95
|
if entry && entry[:status] == "online"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<html data-theme="<%= params[:theme].presence&.then { |t| %w[light dark].include?(t) ? t : 'dark' } || 'dark' %>">
|
|
3
3
|
<head>
|
|
4
4
|
<title>Rails Profiler</title>
|
|
5
|
+
<link rel="icon" type="image/png" href="/_profiler/assets/icon.png">
|
|
5
6
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
7
|
<%= csrf_meta_tags %>
|
|
7
8
|
<%= csp_meta_tag %>
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
<script src="/_profiler/assets/profiler.js" defer></script>
|
|
12
13
|
|
|
13
14
|
<style>
|
|
14
|
-
/* Embedded overrides
|
|
15
|
+
/* Embedded overrides, tighter layout, no page chrome */
|
|
15
16
|
body { overflow-y: auto; }
|
|
16
17
|
.container { padding: 16px 20px; }
|
|
17
18
|
</style>
|
data/config/routes.rb
CHANGED
|
@@ -26,6 +26,7 @@ Profiler::Engine.routes.draw do
|
|
|
26
26
|
get "assets/profiler-toolbar.js", to: "assets#toolbar_js"
|
|
27
27
|
get "assets/profiler.js", to: "assets#main_js"
|
|
28
28
|
get "assets/profiler.css", to: "assets#main_css"
|
|
29
|
+
get "assets/icon.png", to: "assets#icon"
|
|
29
30
|
|
|
30
31
|
get "test_runner", to: "test_runner#index"
|
|
31
32
|
|
|
@@ -173,7 +173,7 @@ module Profiler
|
|
|
173
173
|
app_root = app_root_path
|
|
174
174
|
|
|
175
175
|
# Partition frames: app frames vs GC frame
|
|
176
|
-
# Exclude Ruby meta-frames (<main>, <class:Foo>, #<Class:0x...> generated templates)
|
|
176
|
+
# Exclude Ruby meta-frames (<main>, <class:Foo>, #<Class:0x...> generated templates), noise
|
|
177
177
|
gc_frame_ids = frames_meta.each_with_object(Set.new) { |(id, f), s| s << id if f[:name] == GC_FRAME_NAME }
|
|
178
178
|
app_frame_ids = frames_meta.each_with_object(Set.new) do |(id, f), s|
|
|
179
179
|
s << id if f[:file]&.start_with?(app_root) && !f[:name].to_s.match?(/\A[<#]/)
|
|
@@ -310,7 +310,7 @@ module Profiler
|
|
|
310
310
|
@loop_warnings << {
|
|
311
311
|
key: key,
|
|
312
312
|
count: count,
|
|
313
|
-
message: "#{key} was called #{count} times in a single request
|
|
313
|
+
message: "#{key} was called #{count} times in a single request - possible send loop"
|
|
314
314
|
}
|
|
315
315
|
end
|
|
316
316
|
end
|
data/lib/profiler/mcp/server.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Profiler
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def rack_app
|
|
36
|
-
# Memoized at class level
|
|
36
|
+
# Memoized at class level, survives route reloads in development
|
|
37
37
|
@rack_app ||= lambda do |env|
|
|
38
38
|
HttpGuard.call(env) || instance.http_transport.handle_request(Rack::Request.new(env))
|
|
39
39
|
end
|
|
@@ -230,7 +230,7 @@ module Profiler
|
|
|
230
230
|
),
|
|
231
231
|
define_tool(
|
|
232
232
|
name: "get_profile_mailers",
|
|
233
|
-
description: "Get detailed mailer activity for a profile: delivered emails, errors, and queued deliveries
|
|
233
|
+
description: "Get detailed mailer activity for a profile: delivered emails, errors, and queued deliveries - including email bodies when capture_mail_body is enabled.",
|
|
234
234
|
input_schema: {
|
|
235
235
|
properties: {
|
|
236
236
|
token: { type: "string", description: "Profile token, or 'latest' for the most recent profile (required)" },
|
|
@@ -33,7 +33,7 @@ module Profiler
|
|
|
33
33
|
data = Profiler::ExplainRunner.run(token, query_index.to_i)
|
|
34
34
|
|
|
35
35
|
lines = []
|
|
36
|
-
lines << "# EXPLAIN ANALYZE
|
|
36
|
+
lines << "# EXPLAIN ANALYZE - Query ##{query_index}"
|
|
37
37
|
lines << "Adapter: `#{data[:adapter]}`\n"
|
|
38
38
|
|
|
39
39
|
if data[:format] == "json"
|
|
@@ -313,7 +313,7 @@ module Profiler
|
|
|
313
313
|
if view_data["views"] && !view_data["views"].empty?
|
|
314
314
|
lines << "### Templates"
|
|
315
315
|
view_data["views"].each do |view|
|
|
316
|
-
lines << "- `#{view['identifier']}`
|
|
316
|
+
lines << "- `#{view['identifier']}` - #{view['duration'].round(2)} ms"
|
|
317
317
|
end
|
|
318
318
|
lines << ""
|
|
319
319
|
end
|
|
@@ -321,7 +321,7 @@ module Profiler
|
|
|
321
321
|
if view_data["partials"] && !view_data["partials"].empty?
|
|
322
322
|
lines << "### Partials"
|
|
323
323
|
view_data["partials"].each do |partial|
|
|
324
|
-
lines << "- `#{partial['identifier']}`
|
|
324
|
+
lines << "- `#{partial['identifier']}` - #{partial['duration'].round(2)} ms"
|
|
325
325
|
end
|
|
326
326
|
lines << ""
|
|
327
327
|
end
|
|
@@ -343,7 +343,7 @@ module Profiler
|
|
|
343
343
|
lines << "### Cache Reads"
|
|
344
344
|
cache_data["reads"].each do |op|
|
|
345
345
|
hit_label = op["hit"] ? "HIT" : "MISS"
|
|
346
|
-
lines << "- [#{hit_label}] `#{op['key']}`
|
|
346
|
+
lines << "- [#{hit_label}] `#{op['key']}` - #{op['duration'].round(2)} ms"
|
|
347
347
|
end
|
|
348
348
|
lines << ""
|
|
349
349
|
end
|
|
@@ -351,7 +351,7 @@ module Profiler
|
|
|
351
351
|
if cache_data["writes"] && !cache_data["writes"].empty?
|
|
352
352
|
lines << "### Cache Writes"
|
|
353
353
|
cache_data["writes"].each do |op|
|
|
354
|
-
lines << "- `#{op['key']}`
|
|
354
|
+
lines << "- `#{op['key']}` - #{op['duration'].round(2)} ms"
|
|
355
355
|
end
|
|
356
356
|
lines << ""
|
|
357
357
|
end
|
|
@@ -359,7 +359,7 @@ module Profiler
|
|
|
359
359
|
if cache_data["deletes"] && !cache_data["deletes"].empty?
|
|
360
360
|
lines << "### Cache Deletes"
|
|
361
361
|
cache_data["deletes"].each do |op|
|
|
362
|
-
lines << "- `#{op['key']}`
|
|
362
|
+
lines << "- `#{op['key']}` - #{op['duration'].round(2)} ms"
|
|
363
363
|
end
|
|
364
364
|
lines << ""
|
|
365
365
|
end
|
|
@@ -378,7 +378,7 @@ module Profiler
|
|
|
378
378
|
if ajax_data["requests"] && !ajax_data["requests"].empty?
|
|
379
379
|
lines << "### Request List"
|
|
380
380
|
ajax_data["requests"].each do |req|
|
|
381
|
-
lines << "- **#{req['method']} #{req['path']}**
|
|
381
|
+
lines << "- **#{req['method']} #{req['path']}** - #{req['status']} - #{req['duration'].round(2)} ms (token: #{req['token']})"
|
|
382
382
|
end
|
|
383
383
|
lines << ""
|
|
384
384
|
end
|
|
@@ -402,7 +402,7 @@ module Profiler
|
|
|
402
402
|
http_data["requests"].each_with_index do |req, index|
|
|
403
403
|
flag = req["duration"] >= threshold ? " [SLOW]" : ""
|
|
404
404
|
err = req["status"] >= 400 || req["status"] == 0 ? " [ERROR]" : ""
|
|
405
|
-
lines << "\n**#{index + 1}. #{req['method']} #{req['url']}**
|
|
405
|
+
lines << "\n**#{index + 1}. #{req['method']} #{req['url']}** - #{req['status'] == 0 ? 'error' : req['status']} - #{req['duration'].round(2)} ms#{flag}#{err}"
|
|
406
406
|
|
|
407
407
|
if req["request_body"] && !req["request_body"].empty?
|
|
408
408
|
lines << "**Request Body:**"
|
|
@@ -143,7 +143,7 @@ module Profiler
|
|
|
143
143
|
|
|
144
144
|
if timed_out
|
|
145
145
|
lines << ""
|
|
146
|
-
lines << "> ⚠ Timed out
|
|
146
|
+
lines << "> ⚠ Timed out - run is still in progress. Use run ID `#{run.id}` to check later."
|
|
147
147
|
end
|
|
148
148
|
|
|
149
149
|
lines << "\n## Output (last #{output.length} chars)"
|
|
@@ -174,7 +174,7 @@ module Profiler
|
|
|
174
174
|
@nonce ? " nonce=\"#{ERB::Util.html_escape(@nonce)}\"" : ""
|
|
175
175
|
end
|
|
176
176
|
|
|
177
|
-
# Thermal design system
|
|
177
|
+
# Thermal design system, self-contained CSS for the injected toolbar.
|
|
178
178
|
# Variables are defined on #profiler-toolbar to avoid polluting the host app.
|
|
179
179
|
def toolbar_styles
|
|
180
180
|
<<~'CSS'
|
|
@@ -278,7 +278,7 @@ module Profiler
|
|
|
278
278
|
transition: color var(--pf-tf), background var(--pf-tf);
|
|
279
279
|
cursor: default;
|
|
280
280
|
}
|
|
281
|
-
/* Amber bottom indicator
|
|
281
|
+
/* Amber bottom indicator, springy easing */
|
|
282
282
|
#profiler-toolbar .profiler-toolbar-item::after {
|
|
283
283
|
content: '';
|
|
284
284
|
position: absolute;
|
data/lib/profiler/railtie.rb
CHANGED
|
@@ -117,7 +117,7 @@ module Profiler
|
|
|
117
117
|
end
|
|
118
118
|
|
|
119
119
|
initializer "profiler.start_cluster_client" do
|
|
120
|
-
# Check slave? inside on_load
|
|
120
|
+
# Check slave? inside on_load, the app's own initializers (config/initializers/profiler.rb)
|
|
121
121
|
# set master_url AFTER railtie initializers run, so the check must happen after_initialize.
|
|
122
122
|
ActiveSupport.on_load(:after_initialize) do
|
|
123
123
|
next unless Profiler.configuration.enabled
|
data/lib/profiler/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.35.1
|
|
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-
|
|
11
|
+
date: 2026-10-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -115,6 +115,7 @@ files:
|
|
|
115
115
|
- app/assets/builds/profiler-toolbar.js
|
|
116
116
|
- app/assets/builds/profiler.css
|
|
117
117
|
- app/assets/builds/profiler.js
|
|
118
|
+
- app/assets/images/icon.png
|
|
118
119
|
- app/assets/javascript/profiler-ajax-interceptor.js
|
|
119
120
|
- app/controllers/profiler/api/ajax_controller.rb
|
|
120
121
|
- app/controllers/profiler/api/cluster_controller.rb
|