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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 403d9ff1c8dc1f63d91b2ae71490ab29ef762da12f035ca1cd566d2e036083e5
4
- data.tar.gz: d63bf66a090688c87efbcfa4c84140c25c0d611f1f760c3c4ff8c24410ce98f1
3
+ metadata.gz: 4dc81c1f51aaa36f9a01703962763e6526c95ea44a86aec0f6580a46c6239bfe
4
+ data.tar.gz: 1555289065c0d81d6267b21ddb9d3ae2c2c0716e1290f33e30df30590ff4b8c9
5
5
  SHA512:
6
- metadata.gz: db9b3d2fbc40310a01d3bde8675f218931bcdb35409c677ae1fc9ca8b490fe0103aba880db1bee5a16d6dda96ebc5caa7763cecc50e3638b3ee74eb60c4a35d4
7
- data.tar.gz: fe872b2809ec74b20f449cbafbfe3ee440f78ad54e909e0415ce90c222206c8108d191689366655ddcdaf1bbca83cdda489f51e41f59d190fc25fb43945c936b
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("strong", { class: `profiler-text--xs profiler-text--mono ${job.status === "failed" ? "profiler-text--error" : "profiler-text--success"}`, children: [
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 === "failed" && p3.status !== 500) return false;
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 isFailed = p3.status === 500;
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: isFailed ? "badge-error" : "badge-success", children: isFailed ? "\u2717 Failed" : "\u2713 Completed" }) }),
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
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.32.2"
4
+ VERSION = "0.33.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.32.2
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-13 00:00:00.000000000 Z
11
+ date: 2026-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails