rails-profiler 0.32.2 → 0.33.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 +4 -4
- data/CHANGELOG.md +57 -0
- data/app/assets/builds/profiler-toolbar.js +46 -13
- data/app/assets/builds/profiler.css +13 -0
- data/app/assets/builds/profiler.js +23 -12
- data/lib/profiler/collectors/ajax_collector.rb +22 -1
- data/lib/profiler/job_profiler.rb +11 -0
- data/lib/profiler/mcp/server.rb +1 -1
- data/lib/profiler/mcp/tools/get_profile_ajax.rb +1 -0
- data/lib/profiler/middleware/toolbar_injector.rb +4 -0
- data/lib/profiler/models/profile.rb +1 -1
- data/lib/profiler/storage/base_store.rb +8 -1
- data/lib/profiler/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4dc81c1f51aaa36f9a01703962763e6526c95ea44a86aec0f6580a46c6239bfe
|
|
4
|
+
data.tar.gz: 1555289065c0d81d6267b21ddb9d3ae2c2c0716e1290f33e30df30590ff4b8c9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 967050b5160cb75653481fd86444696cb81fa84c46aa7faa34064a3c281c0acaa5ec08d4029a8b1aca2525da8ffb5d0033b934025d885a8b53e969faca151293
|
|
7
|
+
data.tar.gz: e63d49ccaf002fb7b4911920fc3e279597451fa276d669f557fde971e13e2855cbc347cc16bf254ff2d995b14ba0c18454e929910326a41d0104ee83db49c327
|
data/CHANGELOG.md
CHANGED
|
@@ -19,6 +19,63 @@ every commit of every tag interval is accounted for one way or the other.
|
|
|
19
19
|
|
|
20
20
|
## [Unreleased]
|
|
21
21
|
|
|
22
|
+
## [0.33.0] - 2026-10-14
|
|
23
|
+
|
|
24
|
+
<!-- stamped -->
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **AJAX:** Requests made by Turbo (Hotwire) for a `<turbo-frame>`, and requests answered with
|
|
29
|
+
Turbo Stream actions, carry a `FRAME` or `STREAM` badge in the AJAX panel of the toolbar and in
|
|
30
|
+
the AJAX tab of the dashboard. The AJAX data of a page in the API gives each request a `turbo`
|
|
31
|
+
field: `frame`, `stream` or `null`. The MCP tool `get_profile_ajax` gives the same kind on a
|
|
32
|
+
`Turbo` line of each Turbo request, and none for a plain fetch. The Request tab of a frame
|
|
33
|
+
request shows its `Turbo-Frame` header.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **AJAX:** A Turbo Drive visit is no longer listed as an AJAX request of the page it leaves,
|
|
38
|
+
and neither is a link Turbo prefetches on hover: they are the next page, with their own
|
|
39
|
+
profile. The requests a page reached by a Drive visit makes are linked to that page instead of
|
|
40
|
+
to the first page of the session.
|
|
41
|
+
- **Toolbar:** After a Turbo Drive visit, the toolbar of the previous page is taken down: it no
|
|
42
|
+
longer keeps checking for updates of the previous profile, and `Alt+P` toggles the toolbar
|
|
43
|
+
once instead of once per page visited.
|
|
44
|
+
|
|
45
|
+
### Limitations
|
|
46
|
+
|
|
47
|
+
- **Toolbar:** A Turbo 8 page refresh with morphing (`turbo_refreshes_with method: :morph`, or
|
|
48
|
+
a `broadcasts_refreshes` model) is not handled: it empties the toolbar, which keeps checking
|
|
49
|
+
for updates of the profile of the page as first loaded, and the requests made after it stay
|
|
50
|
+
linked to that profile. This predates this release and is left for a later one.
|
|
51
|
+
- **Toolbar:** After going back in the browser history, two toolbar containers are left in the
|
|
52
|
+
page, only one of them visible. This predates this release too.
|
|
53
|
+
|
|
54
|
+
## [0.32.3] - 2026-10-14
|
|
55
|
+
|
|
56
|
+
<!-- stamped -->
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- **Jobs:** the toolbar of a request now sees the jobs it enqueued start and end after its
|
|
61
|
+
response, without a page reload. A job enqueued by a request is saved as `running` as soon as
|
|
62
|
+
it starts, so it appears in the `child_jobs` of that request while it runs, and every save of
|
|
63
|
+
the job tells the request's toolbar that its profile changed, so it reads the job again when it
|
|
64
|
+
ends, `completed` or `failed`. Without Redis storage, this reaches the toolbar only when the job
|
|
65
|
+
runs in a process serving the profiler's pages (the `async` or `inline` adapters); a job run by
|
|
66
|
+
another process is still listed on the next reload.
|
|
67
|
+
With several Puma workers and no Redis, the toolbar sees a job start and end only when its
|
|
68
|
+
polling happens to reach the worker that ran the job.
|
|
69
|
+
- **Jobs:** a job that is still running is shown as running, in the JOB panel of the toolbar and
|
|
70
|
+
in the Background Jobs list of `/_profiler`: a turning ◐ with `running` and no duration, a blue
|
|
71
|
+
**Running** badge in the list, instead of a green check mark and a duration of 0 ms that read as
|
|
72
|
+
an instant success. The page of the job itself and the Jobs tab of a profile do the same: a blue
|
|
73
|
+
**Running** badge and no duration. The JOB item of the toolbar is blue while one of its jobs runs, the list has
|
|
74
|
+
a **Running** status filter, and its **Completed** filter no longer includes running jobs. The
|
|
75
|
+
`status` filter of the MCP tool `query_jobs` accepts `running`. A job killed while it performs
|
|
76
|
+
(`SIGKILL`, out of memory, container stopped) never records its end, so it stays `running` in the
|
|
77
|
+
toolbar and the list for good.
|
|
78
|
+
|
|
22
79
|
## [0.32.2] - 2026-10-13
|
|
23
80
|
|
|
24
81
|
<!-- stamped -->
|
|
@@ -3356,6 +3356,10 @@
|
|
|
3356
3356
|
};
|
|
3357
3357
|
return map[method] || "";
|
|
3358
3358
|
}
|
|
3359
|
+
var TURBO_TITLES = {
|
|
3360
|
+
frame: "Turbo Frame request",
|
|
3361
|
+
stream: "Turbo Stream response"
|
|
3362
|
+
};
|
|
3359
3363
|
function AjaxPanel({ ajaxData }) {
|
|
3360
3364
|
return /* @__PURE__ */ u3(k, { children: [
|
|
3361
3365
|
/* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-header", children: [
|
|
@@ -3373,6 +3377,7 @@
|
|
|
3373
3377
|
return /* @__PURE__ */ u3("div", { class: `profiler-ajax-card ${statusCls}`, children: /* @__PURE__ */ u3("div", { class: "profiler-ajax-card__row", children: [
|
|
3374
3378
|
/* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2", children: [
|
|
3375
3379
|
/* @__PURE__ */ u3("span", { class: `badge ${methodBadgeClass(req.method)}`, children: req.method }),
|
|
3380
|
+
req.turbo && /* @__PURE__ */ u3("span", { class: "badge badge-default", title: TURBO_TITLES[req.turbo], children: req.turbo.toUpperCase() }),
|
|
3376
3381
|
/* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--truncate", children: req.path })
|
|
3377
3382
|
] }),
|
|
3378
3383
|
/* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2", children: [
|
|
@@ -3723,7 +3728,23 @@
|
|
|
3723
3728
|
}
|
|
3724
3729
|
|
|
3725
3730
|
// app/assets/typescript/profiler/components/toolbar/panels/JobsPanel.tsx
|
|
3731
|
+
function JobStatus({ job }) {
|
|
3732
|
+
if (job.status === "running") {
|
|
3733
|
+
return /* @__PURE__ */ u3("strong", { class: "profiler-text--xs profiler-text--mono profiler-text--info", children: [
|
|
3734
|
+
/* @__PURE__ */ u3("span", { class: "profiler-spinner", "aria-hidden": "true", children: "\u25D0" }),
|
|
3735
|
+
" running"
|
|
3736
|
+
] });
|
|
3737
|
+
}
|
|
3738
|
+
const failed = job.status === "failed";
|
|
3739
|
+
return /* @__PURE__ */ u3("strong", { class: `profiler-text--xs profiler-text--mono ${failed ? "profiler-text--error" : "profiler-text--success"}`, children: [
|
|
3740
|
+
failed ? "\u2717" : "\u2713",
|
|
3741
|
+
" ",
|
|
3742
|
+
job.duration?.toFixed(0),
|
|
3743
|
+
"ms"
|
|
3744
|
+
] });
|
|
3745
|
+
}
|
|
3726
3746
|
function JobsPanel({ jobs }) {
|
|
3747
|
+
const running = jobs.filter((j3) => j3.status === "running").length;
|
|
3727
3748
|
const completed = jobs.filter((j3) => j3.status === "completed").length;
|
|
3728
3749
|
const failed = jobs.filter((j3) => j3.status === "failed").length;
|
|
3729
3750
|
const preview = jobs.slice(0, 5);
|
|
@@ -3739,9 +3760,13 @@
|
|
|
3739
3760
|
] }),
|
|
3740
3761
|
/* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-content", children: [
|
|
3741
3762
|
/* @__PURE__ */ u3("div", { class: "profiler-section__header", children: "Summary" }),
|
|
3763
|
+
running > 0 && /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
|
|
3764
|
+
/* @__PURE__ */ u3("span", { children: "Running" }),
|
|
3765
|
+
/* @__PURE__ */ u3("strong", { class: "profiler-text--info", children: running })
|
|
3766
|
+
] }),
|
|
3742
3767
|
/* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
|
|
3743
3768
|
/* @__PURE__ */ u3("span", { children: "Completed" }),
|
|
3744
|
-
/* @__PURE__ */ u3("strong", { class: "profiler-text--success", children: completed })
|
|
3769
|
+
/* @__PURE__ */ u3("strong", { class: completed > 0 ? "profiler-text--success" : "profiler-text--muted", children: completed })
|
|
3745
3770
|
] }),
|
|
3746
3771
|
/* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
|
|
3747
3772
|
/* @__PURE__ */ u3("span", { children: "Failed" }),
|
|
@@ -3750,12 +3775,7 @@
|
|
|
3750
3775
|
/* @__PURE__ */ u3("div", { class: "profiler-section__header profiler-mt-3", children: "Last jobs" }),
|
|
3751
3776
|
preview.map((job, i3) => /* @__PURE__ */ u3("div", { class: "profiler-toolbar-panel-row", children: [
|
|
3752
3777
|
/* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--mono", children: job.job_class }),
|
|
3753
|
-
/* @__PURE__ */ u3(
|
|
3754
|
-
job.status === "failed" ? "\u2717" : "\u2713",
|
|
3755
|
-
" ",
|
|
3756
|
-
job.duration?.toFixed(0),
|
|
3757
|
-
"ms"
|
|
3758
|
-
] })
|
|
3778
|
+
/* @__PURE__ */ u3(JobStatus, { job })
|
|
3759
3779
|
] }, i3)),
|
|
3760
3780
|
remaining > 0 && /* @__PURE__ */ u3("div", { class: "profiler-more", children: [
|
|
3761
3781
|
"+ ",
|
|
@@ -4102,13 +4122,14 @@
|
|
|
4102
4122
|
ToolbarItem,
|
|
4103
4123
|
{
|
|
4104
4124
|
href: `/_profiler/profiles/${token}?tab=jobs`,
|
|
4105
|
-
className: childJobs.some((j3) => j3.status === "failed") ? "profiler-text--error" : "profiler-text--success",
|
|
4125
|
+
className: childJobs.some((j3) => j3.status === "failed") ? "profiler-text--error" : childJobs.some((j3) => j3.status === "running") ? "profiler-text--info" : "profiler-text--success",
|
|
4106
4126
|
panelLarge: true,
|
|
4107
4127
|
panel: /* @__PURE__ */ u3(JobsPanel, { jobs: childJobs }),
|
|
4108
4128
|
children: [
|
|
4109
4129
|
/* @__PURE__ */ u3("span", { class: "profiler-text--muted profiler-text--xs", children: "JOB" }),
|
|
4110
4130
|
/* @__PURE__ */ u3("span", { children: childJobs.length }),
|
|
4111
|
-
childJobs.some((j3) => j3.status === "failed") && /* @__PURE__ */ u3("span", { class: "profiler-text--error profiler-text--xs", children: "\u2717" })
|
|
4131
|
+
childJobs.some((j3) => j3.status === "failed") && /* @__PURE__ */ u3("span", { class: "profiler-text--error profiler-text--xs", children: "\u2717" }),
|
|
4132
|
+
childJobs.some((j3) => j3.status === "running") && /* @__PURE__ */ u3("span", { class: "profiler-spinner profiler-text--info profiler-text--xs", "aria-hidden": "true", children: "\u25D0" })
|
|
4112
4133
|
]
|
|
4113
4134
|
}
|
|
4114
4135
|
),
|
|
@@ -4351,14 +4372,26 @@
|
|
|
4351
4372
|
if (!el || !toggleEl) return;
|
|
4352
4373
|
const token = el.dataset.token;
|
|
4353
4374
|
if (!token) return;
|
|
4375
|
+
window.__PROFILER_TOOLBAR_TEARDOWN__?.();
|
|
4376
|
+
const listeners = new AbortController();
|
|
4377
|
+
const { signal } = listeners;
|
|
4378
|
+
const teardown = () => {
|
|
4379
|
+
listeners.abort();
|
|
4380
|
+
J(null, el);
|
|
4381
|
+
if (window.__PROFILER_TOOLBAR_TEARDOWN__ === teardown) delete window.__PROFILER_TOOLBAR_TEARDOWN__;
|
|
4382
|
+
};
|
|
4383
|
+
window.__PROFILER_TOOLBAR_TEARDOWN__ = teardown;
|
|
4384
|
+
document.addEventListener("turbo:load", () => {
|
|
4385
|
+
if (!el.isConnected) teardown();
|
|
4386
|
+
}, { signal });
|
|
4354
4387
|
const themeEls = [el, toggleEl];
|
|
4355
4388
|
applyTheme(themeEls);
|
|
4356
4389
|
window.addEventListener("storage", (e3) => {
|
|
4357
4390
|
if (e3.key === "profiler-theme") applyTheme(themeEls);
|
|
4358
|
-
});
|
|
4391
|
+
}, { signal });
|
|
4359
4392
|
window.addEventListener("profiler:theme-change", ((e3) => {
|
|
4360
4393
|
if (e3.detail?.theme) themeEls.forEach((elem) => elem.setAttribute("data-theme", e3.detail.theme));
|
|
4361
|
-
}));
|
|
4394
|
+
}), { signal });
|
|
4362
4395
|
let isCollapsed = localStorage.getItem("profiler-toolbar-collapsed") === "true";
|
|
4363
4396
|
toggleEl.dataset.collapsed = String(isCollapsed);
|
|
4364
4397
|
if (isCollapsed && !el.style.transform) {
|
|
@@ -4390,13 +4423,13 @@
|
|
|
4390
4423
|
}, ANIM_MS + 50);
|
|
4391
4424
|
}
|
|
4392
4425
|
}
|
|
4393
|
-
toggleEl.addEventListener("click", toggleCollapse);
|
|
4426
|
+
toggleEl.addEventListener("click", toggleCollapse, { signal });
|
|
4394
4427
|
document.addEventListener("keydown", (e3) => {
|
|
4395
4428
|
if (e3.altKey && e3.key === "p") {
|
|
4396
4429
|
e3.preventDefault();
|
|
4397
4430
|
toggleCollapse();
|
|
4398
4431
|
}
|
|
4399
|
-
});
|
|
4432
|
+
}, { signal });
|
|
4400
4433
|
J(
|
|
4401
4434
|
/* @__PURE__ */ u3(QueryClientProvider, { client: toolbarQueryClient, children: /* @__PURE__ */ u3(ToolbarMount, { token }) }),
|
|
4402
4435
|
el
|
|
@@ -606,6 +606,11 @@ tr:hover .btn-row-delete {
|
|
|
606
606
|
transform: rotate(360deg);
|
|
607
607
|
}
|
|
608
608
|
}
|
|
609
|
+
.profiler-spinner {
|
|
610
|
+
display: inline-block;
|
|
611
|
+
animation: spin 1s linear infinite;
|
|
612
|
+
}
|
|
613
|
+
|
|
609
614
|
@keyframes pulse {
|
|
610
615
|
0%, 100% {
|
|
611
616
|
opacity: 1;
|
|
@@ -1558,6 +1563,10 @@ a.profiler-toolbar-item {
|
|
|
1558
1563
|
color: var(--profiler-error) !important;
|
|
1559
1564
|
}
|
|
1560
1565
|
|
|
1566
|
+
.profiler-text--info {
|
|
1567
|
+
color: var(--profiler-info) !important;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1561
1570
|
.profiler-text--muted {
|
|
1562
1571
|
color: var(--profiler-text-muted) !important;
|
|
1563
1572
|
}
|
|
@@ -1578,6 +1587,10 @@ a.profiler-toolbar-item {
|
|
|
1578
1587
|
background: var(--profiler-success);
|
|
1579
1588
|
}
|
|
1580
1589
|
|
|
1590
|
+
.profiler-toolbar-item.profiler-text--info::after {
|
|
1591
|
+
background: var(--profiler-info);
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1581
1594
|
.profiler-toolbar-hoverable {
|
|
1582
1595
|
position: relative;
|
|
1583
1596
|
}
|
|
@@ -5569,6 +5569,11 @@
|
|
|
5569
5569
|
if (duration >= 100) return "badge-warning";
|
|
5570
5570
|
return "badge-success";
|
|
5571
5571
|
}
|
|
5572
|
+
function jobStatusOf(p3) {
|
|
5573
|
+
const status = p3.collectors_data?.job?.status;
|
|
5574
|
+
if (status) return status;
|
|
5575
|
+
return p3.status === 500 ? "failed" : "completed";
|
|
5576
|
+
}
|
|
5572
5577
|
function formatTime(iso) {
|
|
5573
5578
|
return new Date(iso).toLocaleTimeString("en", { hour12: false });
|
|
5574
5579
|
}
|
|
@@ -5827,8 +5832,7 @@
|
|
|
5827
5832
|
}) : filteredProfiles;
|
|
5828
5833
|
const filteredJobs = jobs.filter((p3) => {
|
|
5829
5834
|
if (jobSearch && !p3.path.toLowerCase().includes(jobSearch.toLowerCase())) return false;
|
|
5830
|
-
if (jobStatus
|
|
5831
|
-
if (jobStatus === "completed" && p3.status === 500) return false;
|
|
5835
|
+
if (jobStatus && jobStatusOf(p3) !== jobStatus) return false;
|
|
5832
5836
|
if (jobDuration) {
|
|
5833
5837
|
if (jobDuration === "lt100" ? p3.duration >= 100 : p3.duration < parseInt(jobDuration)) return false;
|
|
5834
5838
|
}
|
|
@@ -6104,6 +6108,7 @@
|
|
|
6104
6108
|
),
|
|
6105
6109
|
/* @__PURE__ */ u3("select", { class: "profiler-filter-select", value: jobStatus, onChange: (e3) => setJobStatus(e3.target.value), children: [
|
|
6106
6110
|
/* @__PURE__ */ u3("option", { value: "", children: "All Statuses" }),
|
|
6111
|
+
/* @__PURE__ */ u3("option", { value: "running", children: "Running" }),
|
|
6107
6112
|
/* @__PURE__ */ u3("option", { value: "completed", children: "Completed" }),
|
|
6108
6113
|
/* @__PURE__ */ u3("option", { value: "failed", children: "Failed" })
|
|
6109
6114
|
] }),
|
|
@@ -6147,7 +6152,7 @@
|
|
|
6147
6152
|
] }) }),
|
|
6148
6153
|
/* @__PURE__ */ u3("tbody", { children: sortedJobs.map((p3) => {
|
|
6149
6154
|
const jobData = p3.collectors_data?.job;
|
|
6150
|
-
const
|
|
6155
|
+
const status = jobStatusOf(p3);
|
|
6151
6156
|
return /* @__PURE__ */ u3("tr", { children: [
|
|
6152
6157
|
/* @__PURE__ */ u3("td", { children: formatTime(p3.started_at) }),
|
|
6153
6158
|
/* @__PURE__ */ u3("td", { children: [
|
|
@@ -6155,11 +6160,14 @@
|
|
|
6155
6160
|
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" })
|
|
6156
6161
|
] }),
|
|
6157
6162
|
/* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--mono", children: jobData?.queue || "-" }) }),
|
|
6158
|
-
/* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: durationClass(p3.duration), children: [
|
|
6163
|
+
/* @__PURE__ */ u3("td", { children: status === "running" ? /* @__PURE__ */ u3("span", { class: "profiler-text--muted", children: "-" }) : /* @__PURE__ */ u3("span", { class: durationClass(p3.duration), children: [
|
|
6159
6164
|
p3.duration.toFixed(2),
|
|
6160
6165
|
" ms"
|
|
6161
6166
|
] }) }),
|
|
6162
|
-
/* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class:
|
|
6167
|
+
/* @__PURE__ */ u3("td", { children: status === "running" ? /* @__PURE__ */ u3("span", { class: "badge-info", children: [
|
|
6168
|
+
/* @__PURE__ */ u3("span", { class: "profiler-spinner", "aria-hidden": "true", children: "\u25D0" }),
|
|
6169
|
+
" Running"
|
|
6170
|
+
] }) : /* @__PURE__ */ u3("span", { class: status === "failed" ? "badge-error" : "badge-success", children: status === "failed" ? "\u2717 Failed" : "\u2713 Completed" }) }),
|
|
6163
6171
|
/* @__PURE__ */ u3("td", { children: jobData?.executions ?? "-" }),
|
|
6164
6172
|
/* @__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" }) }),
|
|
6165
6173
|
/* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("button", { class: "btn-row-delete", onClick: () => deleteJob2(p3.token), title: "Delete", children: "\xD7" }) })
|
|
@@ -8165,6 +8173,10 @@
|
|
|
8165
8173
|
}
|
|
8166
8174
|
|
|
8167
8175
|
// app/assets/typescript/profiler/components/dashboard/tabs/AjaxTab.tsx
|
|
8176
|
+
var TURBO_TITLES = {
|
|
8177
|
+
frame: "Turbo Frame request",
|
|
8178
|
+
stream: "Turbo Stream response"
|
|
8179
|
+
};
|
|
8168
8180
|
function AjaxTab({ ajaxData }) {
|
|
8169
8181
|
if (!ajaxData?.requests?.length) {
|
|
8170
8182
|
return /* @__PURE__ */ u3("div", { class: "profiler-empty", children: [
|
|
@@ -8217,6 +8229,7 @@
|
|
|
8217
8229
|
/* @__PURE__ */ u3("div", { class: "profiler-ajax-card__row", children: [
|
|
8218
8230
|
/* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-3", children: [
|
|
8219
8231
|
/* @__PURE__ */ u3("span", { class: `profiler-ajax-card__method badge-${methodBadge(req.method)}`, children: req.method }),
|
|
8232
|
+
req.turbo && /* @__PURE__ */ u3("span", { class: "badge-default", title: TURBO_TITLES[req.turbo], children: req.turbo.toUpperCase() }),
|
|
8220
8233
|
/* @__PURE__ */ u3("strong", { class: "profiler-ajax-card__path", children: req.path })
|
|
8221
8234
|
] }),
|
|
8222
8235
|
/* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2", children: [
|
|
@@ -8631,10 +8644,10 @@
|
|
|
8631
8644
|
jobs.map((job, index) => /* @__PURE__ */ u3("div", { class: `profiler-ajax-card profiler-ajax-card--${job.status === "completed" ? "success" : job.status === "failed" ? "error" : "default"}`, children: [
|
|
8632
8645
|
/* @__PURE__ */ u3("div", { class: "profiler-ajax-card__row", children: [
|
|
8633
8646
|
/* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-3", children: [
|
|
8634
|
-
/* @__PURE__ */ u3("span", { class: `badge-${job.status === "completed" ? "success" : job.status === "failed" ? "error" : "warning"}`, children: job.status ?? "unknown" }),
|
|
8647
|
+
/* @__PURE__ */ u3("span", { class: `badge-${job.status === "completed" ? "success" : job.status === "failed" ? "error" : job.status === "running" ? "info" : "warning"}`, children: job.status ?? "unknown" }),
|
|
8635
8648
|
/* @__PURE__ */ u3("strong", { class: "profiler-ajax-card__path", children: job.job_class })
|
|
8636
8649
|
] }),
|
|
8637
|
-
/* @__PURE__ */ u3("span", { class: job.duration >= 1e3 ? "badge-error" : job.duration >= 200 ? "badge-warning" : "badge-success", children: [
|
|
8650
|
+
job.status === "running" ? /* @__PURE__ */ u3("span", { class: "badge-info", children: "running" }) : /* @__PURE__ */ u3("span", { class: job.duration >= 1e3 ? "badge-error" : job.duration >= 200 ? "badge-warning" : "badge-success", children: [
|
|
8638
8651
|
job.duration?.toFixed(2),
|
|
8639
8652
|
" ms"
|
|
8640
8653
|
] })
|
|
@@ -9063,6 +9076,7 @@
|
|
|
9063
9076
|
const [activeTab, setActiveTab] = d2(hasException ? "exception" : defaultTab);
|
|
9064
9077
|
const jobData = cd["job"];
|
|
9065
9078
|
const isFailed = jobData?.status === "failed";
|
|
9079
|
+
const isRunning = jobData?.status === "running";
|
|
9066
9080
|
const parent = profile.parent_profile;
|
|
9067
9081
|
const handleTabClick = (tab) => (e3) => {
|
|
9068
9082
|
e3.preventDefault();
|
|
@@ -9082,14 +9096,11 @@
|
|
|
9082
9096
|
/* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-4 profiler-mt-2", children: [
|
|
9083
9097
|
/* @__PURE__ */ u3("span", { children: [
|
|
9084
9098
|
"Duration: ",
|
|
9085
|
-
/* @__PURE__ */ u3("strong", { children:
|
|
9086
|
-
profile.duration.toFixed(2),
|
|
9087
|
-
" ms"
|
|
9088
|
-
] })
|
|
9099
|
+
/* @__PURE__ */ u3("strong", { children: isRunning ? "running" : `${profile.duration.toFixed(2)} ms` })
|
|
9089
9100
|
] }),
|
|
9090
9101
|
/* @__PURE__ */ u3("span", { children: [
|
|
9091
9102
|
"Status: ",
|
|
9092
|
-
/* @__PURE__ */ u3("strong", { children: /* @__PURE__ */ u3("span", { class: `badge-${isFailed ? "error" : "success"}`, children: isFailed ? "Failed" : "Completed" }) })
|
|
9103
|
+
/* @__PURE__ */ u3("strong", { children: /* @__PURE__ */ u3("span", { class: `badge-${isFailed ? "error" : isRunning ? "info" : "success"}`, children: isFailed ? "Failed" : isRunning ? "Running" : "Completed" }) })
|
|
9093
9104
|
] }),
|
|
9094
9105
|
allocatedObjects(profile) != null && /* @__PURE__ */ u3("span", { title: ALLOCATIONS_HINT, children: [
|
|
9095
9106
|
"Allocations: ",
|
|
@@ -105,9 +105,30 @@ module Profiler
|
|
|
105
105
|
"method" => profile.method,
|
|
106
106
|
"status" => profile.status,
|
|
107
107
|
"duration" => profile.duration,
|
|
108
|
-
"started_at" => profile.started_at&.iso8601
|
|
108
|
+
"started_at" => profile.started_at&.iso8601,
|
|
109
|
+
"turbo" => turbo_kind(profile)
|
|
109
110
|
}
|
|
110
111
|
end
|
|
112
|
+
|
|
113
|
+
TURBO_STREAM_MIME = "text/vnd.turbo-stream.html"
|
|
114
|
+
|
|
115
|
+
# "frame" for a request a <turbo-frame> made (Turbo sends its id in Turbo-Frame), "stream"
|
|
116
|
+
# for one answered with Turbo Stream actions, nil for any other fetch or XHR.
|
|
117
|
+
def turbo_kind(profile)
|
|
118
|
+
return "frame" if header_value(profile.headers, "Turbo-Frame").to_s != ""
|
|
119
|
+
return "stream" if header_value(profile.response_headers, "Content-Type").to_s.start_with?(TURBO_STREAM_MIME)
|
|
120
|
+
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Header names keep the case they were saved with: Content-Type from Rack 2, content-type
|
|
125
|
+
# from Rack 3.
|
|
126
|
+
def header_value(headers, name)
|
|
127
|
+
return nil unless headers.respond_to?(:each)
|
|
128
|
+
|
|
129
|
+
headers.each { |key, value| return value if key.to_s.casecmp?(name) }
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
111
132
|
end
|
|
112
133
|
end
|
|
113
134
|
end
|
|
@@ -72,6 +72,7 @@ module Profiler
|
|
|
72
72
|
return block.call unless Collectors::Lifecycle.subscribe_all(collectors, "JobProfiler")
|
|
73
73
|
|
|
74
74
|
exception_collector = collectors.find { |c| c.is_a?(Collectors::ExceptionCollector) }
|
|
75
|
+
save_running(profile, job_collector) if @parent_token
|
|
75
76
|
|
|
76
77
|
allocations_before = AllocationCounter.current if Profiler.configuration.track_memory
|
|
77
78
|
|
|
@@ -118,6 +119,16 @@ module Profiler
|
|
|
118
119
|
|
|
119
120
|
private
|
|
120
121
|
|
|
122
|
+
# A job enqueued by a request runs after that request's response: saved now, running, the
|
|
123
|
+
# toolbar of the request lists it before it ends. The save at the end replaces it.
|
|
124
|
+
def save_running(profile, job_collector)
|
|
125
|
+
job_collector.collect
|
|
126
|
+
profile.duration = 0.0
|
|
127
|
+
Profiler.save_profile(profile, from: "JobProfiler")
|
|
128
|
+
rescue StandardError => e
|
|
129
|
+
Profiler.log_error("JobProfiler: could not record job #{@job_class} as running", e)
|
|
130
|
+
end
|
|
131
|
+
|
|
121
132
|
def sanitize_arguments(args)
|
|
122
133
|
return [] unless args
|
|
123
134
|
|
data/lib/profiler/mcp/server.rb
CHANGED
|
@@ -230,7 +230,7 @@ module Profiler
|
|
|
230
230
|
input_schema: {
|
|
231
231
|
properties: {
|
|
232
232
|
queue: { type: "string", description: "Filter by queue name" },
|
|
233
|
-
status: { type: "string", description: "Filter by status (completed, failed)" },
|
|
233
|
+
status: { type: "string", description: "Filter by status (running, completed, failed). A job enqueued by a request is listed as running while it performs." },
|
|
234
234
|
limit: { type: "number", description: "Maximum number of results (default 20)" },
|
|
235
235
|
fields: { type: "array", items: { type: "string" }, description: "Columns to include. Valid values: time, job_class, queue, status, duration, token. Omit for all." },
|
|
236
236
|
cursor: { type: "string", description: "Pagination cursor: ISO8601 timestamp of the last item seen. Returns jobs older than this." },
|
|
@@ -61,6 +61,7 @@ module Profiler
|
|
|
61
61
|
lines << "- **Path:** #{req['path']}"
|
|
62
62
|
lines << "- **Status:** #{req['status']}"
|
|
63
63
|
lines << "- **Duration:** #{req['duration'].round(2)} ms"
|
|
64
|
+
lines << "- **Turbo:** #{req['turbo']}" if req['turbo']
|
|
64
65
|
lines << "- **Token:** #{req['token']}" if req['token']
|
|
65
66
|
lines << "- **Started At:** #{req['started_at']}" if req['started_at']
|
|
66
67
|
end
|
|
@@ -233,6 +233,8 @@ module Profiler
|
|
|
233
233
|
from { transform: translateY(100%); opacity: 0; }
|
|
234
234
|
to { transform: translateY(0); opacity: 1; }
|
|
235
235
|
}
|
|
236
|
+
@keyframes pfSpin { to { transform: rotate(360deg); } }
|
|
237
|
+
#profiler-toolbar .profiler-spinner { display: inline-block; animation: pfSpin 1s linear infinite; }
|
|
236
238
|
@keyframes pfPop {
|
|
237
239
|
0% { transform: scaleX(1); }
|
|
238
240
|
15% { transform: scaleX(1.18); }
|
|
@@ -303,11 +305,13 @@ module Profiler
|
|
|
303
305
|
#profiler-toolbar .profiler-text--error::after { background: var(--pf-error); }
|
|
304
306
|
#profiler-toolbar .profiler-text--warning::after { background: var(--pf-warning); }
|
|
305
307
|
#profiler-toolbar .profiler-text--success::after { background: var(--pf-success); }
|
|
308
|
+
#profiler-toolbar .profiler-text--info::after { background: var(--pf-info); }
|
|
306
309
|
|
|
307
310
|
/* Semantic colors */
|
|
308
311
|
#profiler-toolbar .profiler-text--success { color: var(--pf-success) !important; }
|
|
309
312
|
#profiler-toolbar .profiler-text--warning { color: var(--pf-warning) !important; }
|
|
310
313
|
#profiler-toolbar .profiler-text--error { color: var(--pf-error) !important; }
|
|
314
|
+
#profiler-toolbar .profiler-text--info { color: var(--pf-info) !important; }
|
|
311
315
|
#profiler-toolbar .profiler-text--accent { color: var(--pf-amber) !important; }
|
|
312
316
|
#profiler-toolbar .profiler-text--muted { color: var(--pf-muted) !important; }
|
|
313
317
|
|
|
@@ -346,7 +346,7 @@ module Profiler
|
|
|
346
346
|
Accept Accept-Charset Accept-Encoding Accept-Language
|
|
347
347
|
Authorization Cache-Control Connection Content-Length Content-Type
|
|
348
348
|
Cookie Host If-Modified-Since If-None-Match Origin
|
|
349
|
-
Referer User-Agent
|
|
349
|
+
Referer User-Agent Turbo-Frame
|
|
350
350
|
].freeze
|
|
351
351
|
|
|
352
352
|
def extract_headers(env)
|
|
@@ -6,13 +6,15 @@ module Profiler
|
|
|
6
6
|
module Storage
|
|
7
7
|
class BaseStore
|
|
8
8
|
# Public interface: persists the profile then records the save, for the pages showing it
|
|
9
|
-
# to see it changed (Profiler::SSE).
|
|
9
|
+
# to see it changed (Profiler::SSE). A job's save is also recorded for the profile that
|
|
10
|
+
# enqueued it: the toolbar of that request lists its jobs, and has to see them start and end.
|
|
10
11
|
# Subclasses implement #do_save, not #save. The token must be one the gem issued
|
|
11
12
|
# (Storage::Token); every other public method answers "not found" for any other token.
|
|
12
13
|
def save(token, profile)
|
|
13
14
|
Token.validate!(token)
|
|
14
15
|
result = do_save(token, profile)
|
|
15
16
|
broadcast_event(token)
|
|
17
|
+
broadcast_event(profile.parent_token) if job_with_parent?(profile)
|
|
16
18
|
result
|
|
17
19
|
end
|
|
18
20
|
|
|
@@ -53,6 +55,11 @@ module Profiler
|
|
|
53
55
|
|
|
54
56
|
private
|
|
55
57
|
|
|
58
|
+
# Not for an AJAX request linked to its page: the toolbar reads those on its own links.
|
|
59
|
+
def job_with_parent?(profile)
|
|
60
|
+
profile.profile_type == "job" && Token.valid?(profile.parent_token)
|
|
61
|
+
end
|
|
62
|
+
|
|
56
63
|
def broadcast_event(token)
|
|
57
64
|
Profiler::SSE.current.broadcast(token)
|
|
58
65
|
rescue StandardError => e
|
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.33.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-10-
|
|
11
|
+
date: 2026-10-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|