rails-profiler 0.24.0 → 0.25.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: 7470de2ae63f1df27d88a861cfc022dbd4a7323e69195d7d0987dab1ada89689
4
- data.tar.gz: eb002b0b052032a60e1f348cde049f8957bee12f6305a81c3081c8217f036d22
3
+ metadata.gz: cbaa1466083b900023ccc95cdf77675a4dc80b67618db16155756b3d62325690
4
+ data.tar.gz: 8084c6725e5427bc3cec40d58b47c01604fa30b5aee2def8b13ff21c67907b6a
5
5
  SHA512:
6
- metadata.gz: a6dea6a9037b7d031699b1604722de35dcdc0ed59116c346ad4b627c986ac1af532740f0c7293321a3ccd468c6f0e5c44ef7765ea93ea2b7897948a93474b622
7
- data.tar.gz: 9c8bd2f0a48dfd3670a4089c5b4a29f00943d7ed282bb4b30e2a58bb198812203385680b0be0352df4a46d53668679377c964def5385f1c29c3d12cbc0467469
6
+ metadata.gz: 4141c0e5cb65245f961be688edcb8d38ccd8a2db98a64fbfacc74fc50fa87aec180298f268978cbafaa6dc06751fc9cb17b57784f0c7f44a773489b3306a2d8f
7
+ data.tar.gz: 2b2b3139162749df458dc874c2e68be2e0daf96c8fae29054f809d34b4469babeac0c630f7846f8b1e59d1a0cbd4120d2bd99ba4971142a1498d171ade98042f
@@ -1644,7 +1644,7 @@
1644
1644
  const params = new URLSearchParams(window.location.search);
1645
1645
  const initialSection = () => {
1646
1646
  const s3 = params.get("section");
1647
- return s3 === "http" || s3 === "jobs" || s3 === "outbound" || s3 === "env" ? s3 : "http";
1647
+ return s3 === "http" || s3 === "jobs" || s3 === "console" || s3 === "outbound" || s3 === "env" ? s3 : "http";
1648
1648
  };
1649
1649
  const initialSort = () => {
1650
1650
  const col = params.get("sort");
@@ -1663,12 +1663,18 @@
1663
1663
  const [jobOffset, setJobOffset] = d2(0);
1664
1664
  const [jobHasMore, setJobHasMore] = d2(false);
1665
1665
  const [jobLoadingMore, setJobLoadingMore] = d2(false);
1666
+ const [consoles, setConsoles] = d2([]);
1667
+ const [consoleOffset, setConsoleOffset] = d2(0);
1668
+ const [consoleHasMore, setConsoleHasMore] = d2(false);
1669
+ const [consoleLoadingMore, setConsoleLoadingMore] = d2(false);
1666
1670
  const [outboundRequests, setOutboundRequests] = d2([]);
1667
1671
  const [loadingHttp, setLoadingHttp] = d2(initialSection() === "http");
1668
1672
  const [loadingJobs, setLoadingJobs] = d2(initialSection() === "jobs");
1673
+ const [loadingConsole, setLoadingConsole] = d2(initialSection() === "console");
1669
1674
  const [loadingOutbound, setLoadingOutbound] = d2(initialSection() === "outbound");
1670
1675
  const [error, setError] = d2(null);
1671
1676
  const [jobsError, setJobsError] = d2(null);
1677
+ const [consoleError, setConsoleError] = d2(null);
1672
1678
  const [outboundError, setOutboundError] = d2(null);
1673
1679
  const [envData, setEnvData] = d2(void 0);
1674
1680
  const [loadingEnv, setLoadingEnv] = d2(initialSection() === "env");
@@ -1684,9 +1690,12 @@
1684
1690
  });
1685
1691
  const [httpSort, setHttpSort] = d2(initialSort);
1686
1692
  const [jobSort, setJobSort] = d2({ col: null, dir: "asc" });
1693
+ const [consoleSort, setConsoleSort] = d2({ col: null, dir: "asc" });
1687
1694
  const [jobSearch, setJobSearch] = d2("");
1688
1695
  const [jobStatus, setJobStatus] = d2("");
1689
1696
  const [jobDuration, setJobDuration] = d2("");
1697
+ const [consoleSearch, setConsoleSearch] = d2("");
1698
+ const [consoleStatus, setConsoleStatus] = d2("");
1690
1699
  const [outboundSearch, setOutboundSearch] = d2("");
1691
1700
  const [outboundMethod, setOutboundMethod] = d2("");
1692
1701
  const [outboundStatus, setOutboundStatus] = d2("");
@@ -1740,6 +1749,15 @@
1740
1749
  setJobLoadingMore(false);
1741
1750
  }).catch(() => setJobLoadingMore(false));
1742
1751
  };
1752
+ const loadMoreConsole = () => {
1753
+ setConsoleLoadingMore(true);
1754
+ fetch(`${BASE}/api/console?limit=50&offset=${consoleOffset}`).then((res) => res.json()).then((data) => {
1755
+ setConsoles((prev) => [...prev, ...data.profiles]);
1756
+ setConsoleOffset((prev) => prev + data.profiles.length);
1757
+ setConsoleHasMore(data.has_more);
1758
+ setConsoleLoadingMore(false);
1759
+ }).catch(() => setConsoleLoadingMore(false));
1760
+ };
1743
1761
  const handleSectionChange = (s3) => {
1744
1762
  if (s3 === section) {
1745
1763
  refreshSection(s3);
@@ -1760,6 +1778,9 @@
1760
1778
  setJobStatus("");
1761
1779
  setJobDuration("");
1762
1780
  setJobSort({ col: null, dir: "asc" });
1781
+ setConsoleSearch("");
1782
+ setConsoleStatus("");
1783
+ setConsoleSort({ col: null, dir: "asc" });
1763
1784
  setOutboundSearch("");
1764
1785
  setOutboundMethod("");
1765
1786
  setOutboundStatus("");
@@ -1780,6 +1801,11 @@
1780
1801
  setJobs((prev) => prev.filter((p3) => p3.token !== token));
1781
1802
  });
1782
1803
  };
1804
+ const deleteConsole = (token) => {
1805
+ fetch(`${BASE}/api/console/${token}`, { method: "DELETE" }).then(() => {
1806
+ setConsoles((prev) => prev.filter((p3) => p3.token !== token));
1807
+ });
1808
+ };
1783
1809
  const clearProfiles = () => {
1784
1810
  if (!window.confirm("Delete all HTTP profiles?")) return;
1785
1811
  fetch(`${BASE}/api/profiles/clear`, { method: "DELETE" }).then(() => {
@@ -1792,14 +1818,22 @@
1792
1818
  setJobs([]);
1793
1819
  });
1794
1820
  };
1821
+ const clearConsole = () => {
1822
+ if (!window.confirm("Delete all console profiles?")) return;
1823
+ fetch(`${BASE}/api/console/clear`, { method: "DELETE" }).then(() => {
1824
+ setConsoles([]);
1825
+ });
1826
+ };
1795
1827
  const clearAll = () => {
1796
- if (!window.confirm("Delete all HTTP and job profiles?")) return;
1828
+ if (!window.confirm("Delete all HTTP, job and console profiles?")) return;
1797
1829
  Promise.all([
1798
1830
  fetch(`${BASE}/api/profiles/clear`, { method: "DELETE" }),
1799
- fetch(`${BASE}/api/jobs/clear`, { method: "DELETE" })
1831
+ fetch(`${BASE}/api/jobs/clear`, { method: "DELETE" }),
1832
+ fetch(`${BASE}/api/console/clear`, { method: "DELETE" })
1800
1833
  ]).then(() => {
1801
1834
  setProfiles([]);
1802
1835
  setJobs([]);
1836
+ setConsoles([]);
1803
1837
  });
1804
1838
  };
1805
1839
  const refreshSection = (s3) => {
@@ -1825,6 +1859,17 @@
1825
1859
  setJobsError("Failed to load job profiles");
1826
1860
  setLoadingJobs(false);
1827
1861
  });
1862
+ } else if (s3 === "console") {
1863
+ setLoadingConsole(true);
1864
+ fetch(`${BASE}/api/console?limit=50&offset=0`).then((res) => res.json()).then((data) => {
1865
+ setConsoles(data.profiles);
1866
+ setConsoleOffset(data.profiles.length);
1867
+ setConsoleHasMore(data.has_more);
1868
+ setLoadingConsole(false);
1869
+ }).catch(() => {
1870
+ setConsoleError("Failed to load console profiles");
1871
+ setLoadingConsole(false);
1872
+ });
1828
1873
  } else if (s3 === "outbound") {
1829
1874
  setLoadingOutbound(true);
1830
1875
  fetch(`${BASE}/api/outbound_http`).then((res) => res.json()).then((data) => {
@@ -1846,6 +1891,11 @@
1846
1891
  }
1847
1892
  };
1848
1893
  const refresh = () => refreshSection(section);
1894
+ const toggleConsoleSort = (col) => {
1895
+ setConsoleSort(
1896
+ (prev) => prev.col === col ? { col, dir: prev.dir === "asc" ? "desc" : "asc" } : { col, dir: "asc" }
1897
+ );
1898
+ };
1849
1899
  const tabClass = (s3) => `tab${section === s3 ? " active" : ""}`;
1850
1900
  const filteredProfiles = profiles.filter((p3) => {
1851
1901
  if (httpSearch && !p3.path.toLowerCase().includes(httpSearch.toLowerCase())) return false;
@@ -1933,8 +1983,35 @@
1933
1983
  }
1934
1984
  return true;
1935
1985
  });
1986
+ const filteredConsoles = consoles.filter((p3) => {
1987
+ if (consoleSearch && !p3.path.toLowerCase().includes(consoleSearch.toLowerCase())) return false;
1988
+ if (consoleStatus === "error" && p3.status !== 500) return false;
1989
+ if (consoleStatus === "ok" && p3.status === 500) return false;
1990
+ return true;
1991
+ });
1992
+ const sortedConsoles = consoleSort.col ? [...filteredConsoles].sort((a3, b) => {
1993
+ if (consoleSort.col === "date") {
1994
+ const diff = new Date(a3.started_at).getTime() - new Date(b.started_at).getTime();
1995
+ return consoleSort.dir === "asc" ? diff : -diff;
1996
+ }
1997
+ let av, bv;
1998
+ switch (consoleSort.col) {
1999
+ case "duration":
2000
+ av = a3.duration;
2001
+ bv = b.duration;
2002
+ break;
2003
+ case "status":
2004
+ av = a3.status;
2005
+ bv = b.status;
2006
+ break;
2007
+ default:
2008
+ return 0;
2009
+ }
2010
+ return consoleSort.dir === "asc" ? av - bv : bv - av;
2011
+ }) : filteredConsoles;
1936
2012
  const httpFiltersActive = !!(httpSearch || httpMethod || httpStatus || httpDuration || httpPreset);
1937
2013
  const jobFiltersActive = !!(jobSearch || jobStatus || jobDuration);
2014
+ const consoleFiltersActive = !!(consoleSearch || consoleStatus);
1938
2015
  const outboundFiltersActive = !!(outboundSearch || outboundMethod || outboundStatus);
1939
2016
  const sortIcon = (activeCol, dir, col) => {
1940
2017
  if (activeCol !== col) return /* @__PURE__ */ u3("span", { class: "sort-icon sort-icon--idle", children: "\u21C5" });
@@ -1962,6 +2039,10 @@
1962
2039
  e3.preventDefault();
1963
2040
  handleSectionChange("jobs");
1964
2041
  }, children: "Background Jobs" }),
2042
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("console"), onClick: (e3) => {
2043
+ e3.preventDefault();
2044
+ handleSectionChange("console");
2045
+ }, children: "Console" }),
1965
2046
  /* @__PURE__ */ u3("div", { style: "flex: 1" }),
1966
2047
  /* @__PURE__ */ u3("a", { href: "#", class: tabClass("outbound"), onClick: (e3) => {
1967
2048
  e3.preventDefault();
@@ -2160,6 +2241,83 @@
2160
2241
  ] }),
2161
2242
  jobHasMore && !jobFiltersActive && /* @__PURE__ */ u3("div", { class: "profiler-load-more", children: /* @__PURE__ */ u3("button", { class: "btn btn-secondary", onClick: loadMoreJobs, disabled: jobLoadingMore, children: jobLoadingMore ? "Loading\u2026" : "Load more" }) })
2162
2243
  ] })),
2244
+ section === "console" && (loadingConsole ? /* @__PURE__ */ u3(TableSkeleton, { cols: ["sm", "flex", "sm", "sm", "xs", "sm"] }) : consoleError ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: consoleError }) }) : consoles.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: [
2245
+ /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "No console profiles found" }),
2246
+ /* @__PURE__ */ u3("p", { class: "profiler-empty__description", children: [
2247
+ "Run expressions in ",
2248
+ /* @__PURE__ */ u3("code", { children: "rails console" }),
2249
+ " to see profiling data"
2250
+ ] })
2251
+ ] }) : /* @__PURE__ */ u3(k, { children: [
2252
+ /* @__PURE__ */ u3("div", { class: "profiler-action-bar profiler-mb-3", children: [
2253
+ /* @__PURE__ */ u3("div", { class: "profiler-filter-group", children: [
2254
+ /* @__PURE__ */ u3(
2255
+ "input",
2256
+ {
2257
+ type: "text",
2258
+ class: "profiler-filter-input",
2259
+ placeholder: "Search expression\u2026",
2260
+ value: consoleSearch,
2261
+ onInput: (e3) => setConsoleSearch(e3.target.value)
2262
+ }
2263
+ ),
2264
+ /* @__PURE__ */ u3("select", { class: "profiler-filter-select", value: consoleStatus, onChange: (e3) => setConsoleStatus(e3.target.value), children: [
2265
+ /* @__PURE__ */ u3("option", { value: "", children: "All Statuses" }),
2266
+ /* @__PURE__ */ u3("option", { value: "ok", children: "OK" }),
2267
+ /* @__PURE__ */ u3("option", { value: "error", children: "Error" })
2268
+ ] })
2269
+ ] }),
2270
+ /* @__PURE__ */ u3("div", { class: "profiler-filter-group", children: [
2271
+ consoleFiltersActive && /* @__PURE__ */ u3("span", { class: "profiler-filter-count", children: [
2272
+ filteredConsoles.length,
2273
+ " / ",
2274
+ consoles.length
2275
+ ] }),
2276
+ /* @__PURE__ */ u3("button", { class: `btn-refresh${loadingConsole ? " btn-refresh--spinning" : ""}`, onClick: refresh, disabled: loadingConsole, title: "Refresh", children: "\u21BA" }),
2277
+ /* @__PURE__ */ u3("button", { class: "btn btn-danger btn-sm", onClick: clearConsole, title: "Delete console profiles", children: "Clear" }),
2278
+ /* @__PURE__ */ u3("button", { class: "btn btn-danger btn-sm", onClick: clearAll, title: "Delete all profiles", children: "Clear All" })
2279
+ ] })
2280
+ ] }),
2281
+ filteredConsoles.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "No results match filters" }) }) : /* @__PURE__ */ u3("table", { children: [
2282
+ /* @__PURE__ */ u3("thead", { children: /* @__PURE__ */ u3("tr", { children: [
2283
+ /* @__PURE__ */ u3("th", { class: `sortable${consoleSort.col === "date" ? " sortable--active" : ""}`, onClick: () => toggleConsoleSort("date"), children: [
2284
+ "Time ",
2285
+ sortIcon(consoleSort.col, consoleSort.dir, "date")
2286
+ ] }),
2287
+ /* @__PURE__ */ u3("th", { children: "Expression" }),
2288
+ /* @__PURE__ */ u3("th", { class: `sortable${consoleSort.col === "duration" ? " sortable--active" : ""}`, onClick: () => toggleConsoleSort("duration"), children: [
2289
+ "Duration ",
2290
+ sortIcon(consoleSort.col, consoleSort.dir, "duration")
2291
+ ] }),
2292
+ /* @__PURE__ */ u3("th", { children: "SQL" }),
2293
+ /* @__PURE__ */ u3("th", { class: `sortable${consoleSort.col === "status" ? " sortable--active" : ""}`, onClick: () => toggleConsoleSort("status"), children: [
2294
+ "Status ",
2295
+ sortIcon(consoleSort.col, consoleSort.dir, "status")
2296
+ ] }),
2297
+ /* @__PURE__ */ u3("th", { children: "Token" }),
2298
+ /* @__PURE__ */ u3("th", {})
2299
+ ] }) }),
2300
+ /* @__PURE__ */ u3("tbody", { children: sortedConsoles.map((p3) => {
2301
+ const isError = p3.status === 500;
2302
+ return /* @__PURE__ */ u3("tr", { children: [
2303
+ /* @__PURE__ */ u3("td", { children: formatTime(p3.started_at) }),
2304
+ /* @__PURE__ */ u3("td", { children: [
2305
+ /* @__PURE__ */ u3("a", { href: `${BASE}/profiles/${p3.token}`, class: "profiler-text--mono", style: "font-size:0.85em;", children: p3.path.length > 60 ? p3.path.slice(0, 60) + "\u2026" : p3.path }),
2306
+ 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" })
2307
+ ] }),
2308
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: durationClass(p3.duration), children: [
2309
+ p3.duration.toFixed(2),
2310
+ " ms"
2311
+ ] }) }),
2312
+ /* @__PURE__ */ u3("td", { children: p3.collectors_data?.database?.total_queries ?? "\u2014" }),
2313
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: isError ? "badge-error" : "badge-success", children: isError ? "\u2717 Error" : "\u2713 OK" }) }),
2314
+ /* @__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" }) }),
2315
+ /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("button", { class: "btn-row-delete", onClick: () => deleteConsole(p3.token), title: "Delete", children: "\xD7" }) })
2316
+ ] }, p3.token);
2317
+ }) })
2318
+ ] }),
2319
+ consoleHasMore && !consoleFiltersActive && /* @__PURE__ */ u3("div", { class: "profiler-load-more", children: /* @__PURE__ */ u3("button", { class: "btn btn-secondary", onClick: loadMoreConsole, disabled: consoleLoadingMore, children: consoleLoadingMore ? "Loading\u2026" : "Load more" }) })
2320
+ ] })),
2163
2321
  section === "outbound" && (loadingOutbound ? /* @__PURE__ */ u3(TableSkeleton, { cols: ["sm", "xs", "flex", "sm", "xs"], rows: 4 }) : outboundError ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: outboundError }) }) : outboundRequests.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: [
2164
2322
  /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "No outbound HTTP requests found" }),
2165
2323
  /* @__PURE__ */ u3("p", { class: "profiler-empty__description", children: "Make requests to external services to see outbound HTTP data" })
@@ -4928,6 +5086,106 @@
4928
5086
  ] });
4929
5087
  }
4930
5088
 
5089
+ // app/assets/typescript/profiler/components/dashboard/tabs/ConsoleTab.tsx
5090
+ function ConsoleTab({ data }) {
5091
+ return /* @__PURE__ */ u3("div", { class: "profiler-p-4", children: [
5092
+ /* @__PURE__ */ u3("div", { class: "profiler-mb-6", children: [
5093
+ /* @__PURE__ */ u3("h3", { class: "profiler-section-title profiler-mb-2", children: "Expression" }),
5094
+ /* @__PURE__ */ u3("pre", { class: "profiler-code-block profiler-code-block--full", children: data.expression })
5095
+ ] }),
5096
+ data.return_value !== void 0 && /* @__PURE__ */ u3("div", { children: [
5097
+ /* @__PURE__ */ u3("h3", { class: "profiler-section-title profiler-mb-2", children: "Return value" }),
5098
+ /* @__PURE__ */ u3("pre", { class: "profiler-code-block profiler-code-block--full", children: data.return_value })
5099
+ ] })
5100
+ ] });
5101
+ }
5102
+
5103
+ // app/assets/typescript/profiler/components/dashboard/ConsoleProfileDashboard.tsx
5104
+ function ConsoleProfileDashboard({ profile, initialTab, embedded }) {
5105
+ const cd = profile.collectors_data || {};
5106
+ const hasHttp = cd["http"]?.total_requests > 0;
5107
+ const hasDumps = (cd["dump"]?.count ?? 0) > 0;
5108
+ const hasLogs = (cd["logs"]?.total ?? 0) > 0;
5109
+ const hasException = !!cd["exception"]?.exception_class;
5110
+ const consoleData = cd["console"];
5111
+ const validTabs = ["console", "database", "cache", "http", "dump", "logs", "exception", "env", "timeline"];
5112
+ const defaultTab = validTabs.includes(initialTab) ? initialTab : "console";
5113
+ const [activeTab, setActiveTab] = d2(hasException ? "exception" : defaultTab);
5114
+ const isFailed = profile.status === 500;
5115
+ const handleTabClick = (tab) => (e3) => {
5116
+ e3.preventDefault();
5117
+ setActiveTab(tab);
5118
+ const url = new URL(window.location.href);
5119
+ url.searchParams.set("tab", tab);
5120
+ history.pushState(null, "", url.toString());
5121
+ };
5122
+ const tabClass = (key) => `tab${activeTab === key ? " active" : ""}`;
5123
+ return /* @__PURE__ */ u3("div", { class: "container", children: [
5124
+ /* @__PURE__ */ u3("div", { class: "header", children: [
5125
+ /* @__PURE__ */ u3("h1", { children: /* @__PURE__ */ u3("a", { href: "/_profiler?section=console", children: [
5126
+ /* @__PURE__ */ u3("span", { class: "h1-emoji", children: ">_" }),
5127
+ " Console Profile"
5128
+ ] }) }),
5129
+ /* @__PURE__ */ u3("p", { style: "font-family: monospace; word-break: break-all", children: profile.path }),
5130
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-4 profiler-mt-2", children: [
5131
+ /* @__PURE__ */ u3("span", { children: [
5132
+ "Duration: ",
5133
+ /* @__PURE__ */ u3("strong", { children: [
5134
+ profile.duration.toFixed(2),
5135
+ " ms"
5136
+ ] })
5137
+ ] }),
5138
+ /* @__PURE__ */ u3("span", { children: [
5139
+ "Status: ",
5140
+ /* @__PURE__ */ u3("strong", { children: /* @__PURE__ */ u3("span", { class: `badge-${isFailed ? "error" : "success"}`, children: isFailed ? "Error" : "OK" }) })
5141
+ ] }),
5142
+ profile.memory != null && /* @__PURE__ */ u3("span", { children: [
5143
+ "Memory: ",
5144
+ /* @__PURE__ */ u3("strong", { children: [
5145
+ (profile.memory / 1024 / 1024).toFixed(2),
5146
+ " MB"
5147
+ ] })
5148
+ ] }),
5149
+ profile.gem_version && /* @__PURE__ */ u3("span", { class: "profiler-version-badge", children: [
5150
+ "v",
5151
+ profile.gem_version
5152
+ ] })
5153
+ ] }),
5154
+ profile.gem_version && profile.gem_version !== getGemVersion() && /* @__PURE__ */ u3("div", { class: "profiler-version-mismatch", children: [
5155
+ "\u26A0\uFE0F Profil captur\xE9 avec la version ",
5156
+ /* @__PURE__ */ u3("strong", { children: profile.gem_version }),
5157
+ " \u2014 version actuelle : ",
5158
+ /* @__PURE__ */ u3("strong", { children: getGemVersion() })
5159
+ ] })
5160
+ ] }),
5161
+ /* @__PURE__ */ u3("div", { class: "profiler-panel", children: [
5162
+ /* @__PURE__ */ u3("div", { class: "tabs", children: [
5163
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("console"), onClick: handleTabClick("console"), children: "Console" }),
5164
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("database"), onClick: handleTabClick("database"), children: "Database" }),
5165
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("cache"), onClick: handleTabClick("cache"), children: "Cache" }),
5166
+ hasHttp && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("http"), onClick: handleTabClick("http"), children: "HTTP" }),
5167
+ hasDumps && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("dump"), onClick: handleTabClick("dump"), children: "Dumps" }),
5168
+ hasLogs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("logs"), onClick: handleTabClick("logs"), children: "Logs" }),
5169
+ hasException && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("exception"), onClick: handleTabClick("exception"), children: "Exception" }),
5170
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("env"), onClick: handleTabClick("env"), children: "Env" }),
5171
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("timeline"), onClick: handleTabClick("timeline"), children: "Timeline" })
5172
+ ] }),
5173
+ /* @__PURE__ */ u3("div", { class: "profiler-p-0 tab-content active", children: [
5174
+ activeTab === "console" && consoleData && /* @__PURE__ */ u3(ConsoleTab, { data: consoleData }),
5175
+ activeTab === "database" && /* @__PURE__ */ u3(DatabaseTab, { dbData: cd["database"], token: profile.token }),
5176
+ activeTab === "cache" && /* @__PURE__ */ u3(CacheTab, { cacheData: cd["cache"] }),
5177
+ activeTab === "http" && hasHttp && /* @__PURE__ */ u3(HttpTab, { httpData: cd["http"] }),
5178
+ activeTab === "dump" && hasDumps && /* @__PURE__ */ u3(DumpsTab, { dumpData: cd["dump"] }),
5179
+ activeTab === "logs" && hasLogs && /* @__PURE__ */ u3(LogsTab, { logData: cd["logs"] }),
5180
+ activeTab === "exception" && hasException && /* @__PURE__ */ u3(ExceptionTab, { exceptionData: cd["exception"] }),
5181
+ activeTab === "env" && /* @__PURE__ */ u3(EnvTab, { envData: cd["env"], readOnly: true }),
5182
+ activeTab === "timeline" && /* @__PURE__ */ u3(FlameGraphTab, { flamegraphData: cd["flamegraph"], perfData: cd["performance"], functionProfileData: cd["function_profile"] })
5183
+ ] })
5184
+ ] }),
5185
+ !embedded && /* @__PURE__ */ u3("div", { class: "profiler-mt-6", children: /* @__PURE__ */ u3("a", { href: "/_profiler", style: "color: var(--profiler-accent);", children: "\u2190 Back to profiles" }) })
5186
+ ] });
5187
+ }
5188
+
4931
5189
  // app/assets/typescript/profiler/timeline.ts
4932
5190
  function initTimeline(container) {
4933
5191
  console.log("Initializing profiler timeline");
@@ -5189,6 +5447,8 @@ Duration: ${event.duration.toFixed(2)}ms`;
5189
5447
  const embedded = showEl.dataset.embedded === "true";
5190
5448
  if (profile.profile_type === "job") {
5191
5449
  J(/* @__PURE__ */ u3(JobProfileDashboard, { profile, initialTab: tab, embedded }), showEl);
5450
+ } else if (profile.profile_type === "console") {
5451
+ J(/* @__PURE__ */ u3(ConsoleProfileDashboard, { profile, initialTab: tab, embedded }), showEl);
5192
5452
  } else {
5193
5453
  J(/* @__PURE__ */ u3(ProfileDashboard, { profile, initialTab: tab, embedded }), showEl);
5194
5454
  }
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Profiler
4
+ module Api
5
+ class ConsoleController < ApplicationController
6
+ skip_before_action :verify_authenticity_token
7
+
8
+ def index
9
+ limit = (params[:limit] || 50).to_i
10
+ offset = (params[:offset] || 0).to_i
11
+ all = Profiler.storage.list(limit: 1000, offset: 0)
12
+ console = all.select { |p| p.profile_type == "console" }
13
+ page = console.drop(offset).first(limit + 1)
14
+ render json: {
15
+ profiles: page.first(limit).map(&:to_h),
16
+ limit: limit,
17
+ offset: offset,
18
+ has_more: page.size > limit
19
+ }
20
+ end
21
+
22
+ def show
23
+ profile = Profiler.storage.load(params[:id])
24
+
25
+ unless profile && profile.profile_type == "console"
26
+ return render json: { error: "Console profile not found" }, status: :not_found
27
+ end
28
+
29
+ render json: profile.to_h
30
+ end
31
+
32
+ def destroy
33
+ profile = Profiler.storage.load(params[:id])
34
+ return render json: { error: "Console profile not found" }, status: :not_found unless profile&.profile_type == "console"
35
+
36
+ Profiler.storage.delete(params[:id])
37
+ head :no_content
38
+ end
39
+
40
+ def clear
41
+ Profiler.storage.clear(type: "console")
42
+ head :no_content
43
+ end
44
+ end
45
+ end
46
+ end
@@ -9,7 +9,7 @@ module Profiler
9
9
  limit = (params[:limit] || 50).to_i
10
10
  offset = (params[:offset] || 0).to_i
11
11
  all = Profiler.storage.list(limit: 1000, offset: 0)
12
- http = all.reject { |p| p.profile_type == "job" }
12
+ http = all.select { |p| p.profile_type == "http" }
13
13
  page = http.drop(offset).first(limit + 1)
14
14
  render json: {
15
15
  profiles: page.first(limit).map(&:to_h),
data/config/routes.rb CHANGED
@@ -29,6 +29,9 @@ Profiler::Engine.routes.draw do
29
29
  resources :jobs, only: [:index, :show, :destroy] do
30
30
  collection { delete :clear }
31
31
  end
32
+ resources :console, only: [:index, :show, :destroy] do
33
+ collection { delete :clear }
34
+ end
32
35
  resources :outbound_http, only: [:index]
33
36
  get "toolbar/:token", to: "toolbar#show"
34
37
  post "ajax/link", to: "ajax#link"
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_collector"
4
+
5
+ module Profiler
6
+ module Collectors
7
+ class ConsoleCollector < BaseCollector
8
+ def initialize(profile, expression:)
9
+ super(profile)
10
+ @expression = expression
11
+ @return_value = nil
12
+ @return_value_captured = false
13
+ end
14
+
15
+ def set_return_value(value)
16
+ @return_value = value.inspect.slice(0, 10_000)
17
+ @return_value_captured = true
18
+ rescue
19
+ @return_value = "(uninspectable)"
20
+ @return_value_captured = true
21
+ end
22
+
23
+ def icon
24
+ ">_"
25
+ end
26
+
27
+ def priority
28
+ 5
29
+ end
30
+
31
+ def tab_config
32
+ {
33
+ key: "console",
34
+ label: "Console",
35
+ icon: icon,
36
+ priority: priority,
37
+ enabled: true,
38
+ default_active: true
39
+ }
40
+ end
41
+
42
+ def collect
43
+ data = { expression: @expression }
44
+ data[:return_value] = @return_value if @return_value_captured
45
+ store_data(data)
46
+ end
47
+
48
+ def has_data?
49
+ @expression.to_s.length > 0
50
+ end
51
+
52
+ def toolbar_summary
53
+ { text: @expression.to_s[0, 30], color: "blue" }
54
+ end
55
+ end
56
+ end
57
+ end
@@ -11,6 +11,7 @@ module Profiler
11
11
  :track_ajax, :ajax_skip_paths,
12
12
  :track_http, :slow_http_threshold, :http_skip_hosts,
13
13
  :track_jobs,
14
+ :track_console,
14
15
  :track_mailers, :capture_mail_body, :sanitize_mailer_recipients, :mailer_skip_actions,
15
16
  :compress_bodies, :compress_body_threshold
16
17
 
@@ -42,6 +43,7 @@ module Profiler
42
43
  @slow_http_threshold = 500 # milliseconds
43
44
  @http_skip_hosts = []
44
45
  @track_jobs = true
46
+ @track_console = true
45
47
  @track_mailers = true
46
48
  @capture_mail_body = false
47
49
  @sanitize_mailer_recipients = false
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "models/profile"
4
+ require_relative "current_context"
5
+ require_relative "collectors/console_collector"
6
+ require_relative "collectors/database_collector"
7
+ require_relative "collectors/cache_collector"
8
+ require_relative "collectors/http_collector"
9
+ require_relative "collectors/dump_collector"
10
+ require_relative "collectors/log_collector"
11
+ require_relative "collectors/exception_collector"
12
+ require_relative "collectors/env_collector"
13
+ require_relative "collectors/flamegraph_collector"
14
+
15
+ module Profiler
16
+ class ConsoleProfiler
17
+ CONSOLE_COLLECTOR_CLASSES = [
18
+ Collectors::DatabaseCollector,
19
+ Collectors::CacheCollector,
20
+ Collectors::HttpCollector,
21
+ Collectors::DumpCollector,
22
+ Collectors::LogCollector,
23
+ Collectors::ExceptionCollector,
24
+ Collectors::EnvCollector,
25
+ Collectors::FlameGraphCollector
26
+ ].freeze
27
+
28
+ def self.profile(expression:, &block)
29
+ return block.call unless Profiler.enabled? && Profiler.configuration.track_console
30
+
31
+ new(expression: expression).run(&block)
32
+ end
33
+
34
+ def initialize(expression:)
35
+ @expression = expression
36
+ end
37
+
38
+ def run(&block)
39
+ profile = Models::Profile.new
40
+ profile.profile_type = "console"
41
+ profile.gem_version = Profiler::VERSION
42
+ profile.path = @expression.length > 200 ? "#{@expression[0, 200]}..." : @expression
43
+ profile.method = "CONSOLE"
44
+
45
+ console_collector = Collectors::ConsoleCollector.new(profile, expression: @expression)
46
+ collectors = [console_collector] + CONSOLE_COLLECTOR_CLASSES.map { |klass| klass.new(profile) }
47
+ collectors.each { |c| c.subscribe if c.respond_to?(:subscribe) }
48
+
49
+ exception_collector = collectors.find { |c| c.is_a?(Collectors::ExceptionCollector) }
50
+
51
+ memory_before = current_memory if Profiler.configuration.track_memory
52
+
53
+ console_status = "completed"
54
+
55
+ previous_token = Profiler::CurrentContext.token
56
+ Profiler::CurrentContext.token = profile.token
57
+ result = nil
58
+ begin
59
+ result = block.call
60
+ console_collector.set_return_value(result)
61
+ result
62
+ rescue => e
63
+ console_status = "failed"
64
+ exception_collector&.capture(e)
65
+ raise
66
+ ensure
67
+ Profiler::CurrentContext.token = previous_token
68
+ if Profiler.configuration.track_memory
69
+ profile.memory = current_memory - memory_before
70
+ end
71
+
72
+ profile.finish(console_status == "completed" ? 200 : 500)
73
+
74
+ collectors.each do |collector|
75
+ begin
76
+ collector.collect if collector.respond_to?(:collect)
77
+ profile.add_collector_metadata(collector)
78
+ rescue => e
79
+ warn "Profiler ConsoleProfiler: Collector #{collector.class} failed: #{e.message}"
80
+ end
81
+ end
82
+
83
+ Profiler.storage.save(profile.token, profile)
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def current_memory
90
+ return 0 unless defined?(GC.stat)
91
+
92
+ stats = GC.stat
93
+ if stats.key?(:total_allocated_size)
94
+ stats[:total_allocated_size]
95
+ elsif stats.key?(:total_allocated_objects)
96
+ stats[:total_allocated_objects] * 40
97
+ else
98
+ 0
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Profiler
4
+ module Instrumentation
5
+ module IrbInstrumentation
6
+ IRB_SKIP_COMMANDS = %w[exit quit exit! irb help].freeze
7
+
8
+ def evaluate(line, line_no, *args)
9
+ Profiler.env_override_store.apply!
10
+ stripped = line.to_s.strip
11
+ skip = !Profiler.enabled? ||
12
+ !Profiler.configuration.track_console ||
13
+ stripped.empty? ||
14
+ IRB_SKIP_COMMANDS.include?(stripped)
15
+ return super if skip
16
+
17
+ Profiler::ConsoleProfiler.profile(expression: stripped) { super }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -81,6 +81,14 @@ module Profiler
81
81
  end
82
82
  end
83
83
 
84
+ console do
85
+ next unless Profiler.configuration.enabled && Profiler.configuration.track_console
86
+
87
+ require_relative "console_profiler"
88
+ require_relative "instrumentation/irb_instrumentation"
89
+ IRB::Context.prepend(Profiler::Instrumentation::IrbInstrumentation)
90
+ end
91
+
84
92
  rake_tasks do
85
93
  load "profiler/tasks/profiler.rake"
86
94
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.24.0"
4
+ VERSION = "0.25.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.24.0
4
+ version: 0.25.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sébastien Duplessy
@@ -107,6 +107,7 @@ files:
107
107
  - app/assets/builds/profiler.css
108
108
  - app/assets/builds/profiler.js
109
109
  - app/controllers/profiler/api/ajax_controller.rb
110
+ - app/controllers/profiler/api/console_controller.rb
110
111
  - app/controllers/profiler/api/env_vars_controller.rb
111
112
  - app/controllers/profiler/api/explain_controller.rb
112
113
  - app/controllers/profiler/api/function_profiling_controller.rb
@@ -127,6 +128,7 @@ files:
127
128
  - lib/profiler/collectors/ajax_collector.rb
128
129
  - lib/profiler/collectors/base_collector.rb
129
130
  - lib/profiler/collectors/cache_collector.rb
131
+ - lib/profiler/collectors/console_collector.rb
130
132
  - lib/profiler/collectors/database_collector.rb
131
133
  - lib/profiler/collectors/dump_collector.rb
132
134
  - lib/profiler/collectors/env_collector.rb
@@ -142,11 +144,13 @@ files:
142
144
  - lib/profiler/collectors/routes_collector.rb
143
145
  - lib/profiler/collectors/view_collector.rb
144
146
  - lib/profiler/configuration.rb
147
+ - lib/profiler/console_profiler.rb
145
148
  - lib/profiler/current_context.rb
146
149
  - lib/profiler/engine.rb
147
150
  - lib/profiler/env_override_store.rb
148
151
  - lib/profiler/explain_runner.rb
149
152
  - lib/profiler/instrumentation/active_job_instrumentation.rb
153
+ - lib/profiler/instrumentation/irb_instrumentation.rb
150
154
  - lib/profiler/instrumentation/net_http_instrumentation.rb
151
155
  - lib/profiler/instrumentation/sidekiq_middleware.rb
152
156
  - lib/profiler/instrumentation/thread_context_propagation.rb