rails-profiler 0.12.0 → 0.13.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: b25edc08059907d5374748ac0624d804685b83189faabc4bae8b19cc65b0bed9
4
- data.tar.gz: a5e8268d4e59540a91d7fc2b27c44bbb492e221e2ac2f4446c2ad095a2473245
3
+ metadata.gz: e968972f60bc4764a4b61648179a5bf618f2d1f2a68b9d43f724c4e14f3c2794
4
+ data.tar.gz: 70536b9f1ed2c5d04b698657ae9f948abad17cbc9fa202125311092b77d94005
5
5
  SHA512:
6
- metadata.gz: af7137fb8650d7a40d2291ad66dc4fe25466c77ad2ebb83489f15191114a1a290b6a4eb47e5760744e991212ec0ed05a146d1d9450f4c7d7cbcbd394629ae3b1
7
- data.tar.gz: 197f4016c350ed157b690d52ff44c83ef4ef2c5ae69f7906ad55ec8b44416fb606e41b66e3d0d84124ec378cb2961ac0b6c8bcde08e55c1e45c488648889cea8
6
+ metadata.gz: 8e9bb63e8ac85773680d3f83daff09b18a8c440e5272bac926e10d1dd30142969b5442168965711e40eddd9882c05ac592323f0a46410fa6390a3f70064e8521
7
+ data.tar.gz: 16bc718c65e6fa50493e9cdc1cc87ae51264f1873cb68baac0db52915a22eb8c383c52523941ab7d2f7d14f86fad2cb6996b74e4e5c175fa7a4e5da3a75272cb
@@ -1957,6 +1957,52 @@ a.profiler-toolbar-item.profiler-text--warning::after {
1957
1957
  opacity: 1;
1958
1958
  }
1959
1959
 
1960
+ @keyframes profiler-shimmer {
1961
+ 0% {
1962
+ background-position: 200% 0;
1963
+ }
1964
+ 100% {
1965
+ background-position: -200% 0;
1966
+ }
1967
+ }
1968
+ .profiler-skeleton__row {
1969
+ display: flex;
1970
+ align-items: center;
1971
+ gap: 20px;
1972
+ padding: 14px 20px;
1973
+ border-bottom: 1px solid var(--profiler-border);
1974
+ }
1975
+ .profiler-skeleton__row:last-child {
1976
+ border-bottom: none;
1977
+ }
1978
+ .profiler-skeleton__cell {
1979
+ height: 12px;
1980
+ border-radius: 3px;
1981
+ background: linear-gradient(90deg, var(--profiler-bg-lighter, #e5e7eb) 25%, var(--profiler-bg-light, #f3f4f6) 50%, var(--profiler-bg-lighter, #e5e7eb) 75%);
1982
+ background-size: 200% 100%;
1983
+ animation: profiler-shimmer 1.4s infinite linear;
1984
+ }
1985
+ .profiler-skeleton__cell--xs {
1986
+ width: 40px;
1987
+ flex-shrink: 0;
1988
+ }
1989
+ .profiler-skeleton__cell--sm {
1990
+ width: 72px;
1991
+ flex-shrink: 0;
1992
+ }
1993
+ .profiler-skeleton__cell--md {
1994
+ width: 120px;
1995
+ flex-shrink: 0;
1996
+ }
1997
+ .profiler-skeleton__cell--lg {
1998
+ width: 180px;
1999
+ flex-shrink: 0;
2000
+ }
2001
+ .profiler-skeleton__cell--flex {
2002
+ flex: 1;
2003
+ min-width: 60px;
2004
+ }
2005
+
1960
2006
  .profiler-timeline {
1961
2007
  margin: 24px 0;
1962
2008
  padding: 24px;
@@ -496,6 +496,63 @@
496
496
  }
497
497
  var PREVIEW_LIMIT = 500;
498
498
  var CSV_ROW_LIMIT = 10;
499
+ function categoryMime(category) {
500
+ const map = {
501
+ json: "application/json",
502
+ xml: "application/xml",
503
+ csv: "text/csv",
504
+ html: "text/html",
505
+ svg: "image/svg+xml"
506
+ };
507
+ return map[category] || "text/plain";
508
+ }
509
+ function mimeToExt(mime) {
510
+ const m3 = mime.split(";")[0].trim().toLowerCase();
511
+ const map = {
512
+ "application/json": ".json",
513
+ "application/ld+json": ".jsonld",
514
+ "application/xml": ".xml",
515
+ "text/xml": ".xml",
516
+ "text/csv": ".csv",
517
+ "application/csv": ".csv",
518
+ "text/html": ".html",
519
+ "text/plain": ".txt",
520
+ "text/css": ".css",
521
+ "application/javascript": ".js",
522
+ "text/javascript": ".js",
523
+ "image/svg+xml": ".svg",
524
+ "image/png": ".png",
525
+ "image/jpeg": ".jpg",
526
+ "image/gif": ".gif",
527
+ "image/webp": ".webp",
528
+ "image/avif": ".avif",
529
+ "application/pdf": ".pdf",
530
+ "application/zip": ".zip",
531
+ "application/gzip": ".gz",
532
+ "application/octet-stream": ".bin"
533
+ };
534
+ return map[m3] || ".bin";
535
+ }
536
+ function CopyButton({ text }) {
537
+ const [copied, setCopied] = d2(false);
538
+ function copy() {
539
+ navigator.clipboard.writeText(text).then(() => {
540
+ setCopied(true);
541
+ setTimeout(() => setCopied(false), 2e3);
542
+ });
543
+ }
544
+ return /* @__PURE__ */ u3("button", { onClick: copy, class: "profiler-body-download-btn profiler-text--xs", style: "cursor:pointer", children: copied ? "Copied!" : "Copy" });
545
+ }
546
+ function DownloadTextButton({ text, mime }) {
547
+ const [url, setUrl] = d2(null);
548
+ y2(() => {
549
+ const objectUrl = URL.createObjectURL(new Blob([text], { type: mime }));
550
+ setUrl(objectUrl);
551
+ return () => URL.revokeObjectURL(objectUrl);
552
+ }, [text, mime]);
553
+ if (!url) return null;
554
+ return /* @__PURE__ */ u3("a", { href: url, download: `body${mimeToExt(mime)}`, class: "profiler-body-download-btn profiler-text--xs", children: "Download" });
555
+ }
499
556
  function SmartBodyPreview({ body, encoding, headers }) {
500
557
  const [expanded, setExpanded] = d2(false);
501
558
  const [objectUrl, setObjectUrl] = d2(null);
@@ -513,17 +570,21 @@
513
570
  if (!body) return /* @__PURE__ */ u3("span", { class: "profiler-text--muted profiler-text--xs", children: "empty" });
514
571
  if (encoding === "base64") {
515
572
  const mime = Object.entries(headers).find(([k3]) => k3.toLowerCase() === "content-type")?.[1]?.split(";")[0].trim() || "application/octet-stream";
516
- const filename = mime.replace("/", "_").replace(/[^a-z0-9_]/gi, "") + "_download";
573
+ const filename = `body${mimeToExt(mime)}`;
517
574
  return /* @__PURE__ */ u3("div", { class: "profiler-body-binary", children: [
518
575
  objectUrl && (category === "image" || category === "svg") && /* @__PURE__ */ u3("img", { src: objectUrl, alt: "response preview", style: "max-width:100%;max-height:300px;display:block;margin-bottom:8px;border-radius:4px;border:1px solid var(--profiler-border)" }),
519
576
  objectUrl && category === "pdf" && /* @__PURE__ */ u3("iframe", { src: objectUrl, class: "profiler-body-preview-frame", title: "PDF preview" }),
520
- objectUrl && /* @__PURE__ */ u3("a", { href: objectUrl, download: filename, class: "profiler-body-download-btn profiler-text--xs", children: [
521
- "Download ",
522
- mime
577
+ /* @__PURE__ */ u3("div", { style: "display:flex;gap:8px;margin-top:4px", children: [
578
+ objectUrl && /* @__PURE__ */ u3("a", { href: objectUrl, download: filename, class: "profiler-body-download-btn profiler-text--xs", children: [
579
+ "Download ",
580
+ mime
581
+ ] }),
582
+ /* @__PURE__ */ u3(CopyButton, { text: body })
523
583
  ] }),
524
584
  !objectUrl && /* @__PURE__ */ u3("span", { class: "profiler-text--muted profiler-text--xs", children: "Loading preview\u2026" })
525
585
  ] });
526
586
  }
587
+ const actualMime = Object.entries(headers).find(([k3]) => k3.toLowerCase() === "content-type")?.[1]?.split(";")[0].trim() || categoryMime(category);
527
588
  if (category === "csv") {
528
589
  const rows = parseCsv(body);
529
590
  const header = rows[0] || [];
@@ -531,6 +592,10 @@
531
592
  const visible = expanded ? dataRows : dataRows.slice(0, CSV_ROW_LIMIT);
532
593
  const hasMore = dataRows.length > CSV_ROW_LIMIT;
533
594
  return /* @__PURE__ */ u3("div", { children: [
595
+ /* @__PURE__ */ u3("div", { style: "display:flex;gap:8px;margin-bottom:6px", children: [
596
+ /* @__PURE__ */ u3(CopyButton, { text: body }),
597
+ /* @__PURE__ */ u3(DownloadTextButton, { text: body, mime: actualMime })
598
+ ] }),
534
599
  /* @__PURE__ */ u3("div", { style: "overflow-x:auto", children: /* @__PURE__ */ u3("table", { class: "profiler-body-csv", children: [
535
600
  /* @__PURE__ */ u3("thead", { children: /* @__PURE__ */ u3("tr", { children: header.map((h3, i3) => /* @__PURE__ */ u3("th", { children: h3 }, i3)) }) }),
536
601
  /* @__PURE__ */ u3("tbody", { children: visible.map((row, i3) => /* @__PURE__ */ u3("tr", { children: row.map((cell, j3) => /* @__PURE__ */ u3("td", { children: cell }, j3)) }, i3)) })
@@ -550,6 +615,10 @@
550
615
  const preview = expanded ? formatted : formatted.slice(0, PREVIEW_LIMIT);
551
616
  const truncated = formatted.length > PREVIEW_LIMIT && !expanded;
552
617
  return /* @__PURE__ */ u3("div", { children: [
618
+ /* @__PURE__ */ u3("div", { style: "display:flex;gap:8px;margin-bottom:6px", children: [
619
+ /* @__PURE__ */ u3(CopyButton, { text: formatted }),
620
+ /* @__PURE__ */ u3(DownloadTextButton, { text: formatted, mime: actualMime })
621
+ ] }),
553
622
  /* @__PURE__ */ u3("pre", { class: "profiler-code profiler-text--xs", style: "white-space:pre-wrap;word-break:break-all;margin:0", children: [
554
623
  preview,
555
624
  truncated ? "\u2026" : ""
@@ -565,11 +634,101 @@
565
634
  )
566
635
  ] });
567
636
  }
637
+ function waterfallBarColor(status, duration) {
638
+ if (status === 0 || status >= 500) return "var(--profiler-error, #ef4444)";
639
+ if (status >= 400) return "var(--profiler-warning, #f59e0b)";
640
+ if (duration >= 500) return "var(--profiler-warning, #f59e0b)";
641
+ return "var(--profiler-success, #22c55e)";
642
+ }
643
+ function WaterfallView({ requests }) {
644
+ const timed = requests.filter((r3) => r3.started_at).map((r3) => ({ ...r3, startMs: new Date(r3.started_at).getTime() }));
645
+ if (!timed.length) {
646
+ return /* @__PURE__ */ u3("div", { class: "profiler-text--muted profiler-text--sm", children: "No timing data available (started_at missing)." });
647
+ }
648
+ const minStart = Math.min(...timed.map((r3) => r3.startMs));
649
+ const maxEnd = Math.max(...timed.map((r3) => r3.startMs + r3.duration));
650
+ const totalSpan = maxEnd - minStart || 1;
651
+ const ticks = [0, 0.25, 0.5, 0.75, 1].map((f4) => ({
652
+ pct: f4 * 100,
653
+ label: `${Math.round(f4 * totalSpan)}ms`
654
+ }));
655
+ return /* @__PURE__ */ u3("div", { children: [
656
+ /* @__PURE__ */ u3("div", { style: "display:flex;position:relative;margin-left:200px;margin-bottom:4px;height:16px", children: ticks.map((t3) => /* @__PURE__ */ u3("div", { style: `position:absolute;left:${t3.pct}%;font-size:10px;color:var(--profiler-text-muted);transform:translateX(-50%)`, children: t3.label }, t3.pct)) }),
657
+ /* @__PURE__ */ u3("div", { style: "position:relative", children: [
658
+ ticks.map((t3) => /* @__PURE__ */ u3("div", { style: `position:absolute;left:calc(200px + ${t3.pct}% * (100% - 200px) / 100);top:0;bottom:0;width:1px;background:var(--profiler-border);opacity:0.5;pointer-events:none` }, t3.pct)),
659
+ timed.map((req, i3) => {
660
+ const left = (req.startMs - minStart) / totalSpan * 100;
661
+ const width = Math.max(req.duration / totalSpan * 100, 0.5);
662
+ const path = req.url.replace(/^https?:\/\/[^/]+/, "") || req.url;
663
+ const color = waterfallBarColor(req.status, req.duration);
664
+ return /* @__PURE__ */ u3("div", { style: "display:flex;align-items:center;gap:0;margin-bottom:3px;height:22px", children: [
665
+ /* @__PURE__ */ u3(
666
+ "div",
667
+ {
668
+ title: req.url,
669
+ style: "width:200px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;color:var(--profiler-text-muted);padding-right:8px;text-align:right",
670
+ children: [
671
+ /* @__PURE__ */ u3("span", { style: `font-size:10px;font-weight:600;margin-right:4px;color:${color}`, children: req.method }),
672
+ path
673
+ ]
674
+ }
675
+ ),
676
+ /* @__PURE__ */ u3("div", { style: "flex:1;position:relative;height:14px;background:var(--profiler-bg-lighter);border-radius:2px", children: /* @__PURE__ */ u3(
677
+ "div",
678
+ {
679
+ style: `position:absolute;left:${left}%;width:${width}%;min-width:2px;height:100%;background:${color};border-radius:2px;opacity:0.85`,
680
+ title: `${req.duration.toFixed(2)}ms \xB7 ${req.status || "ERR"}`
681
+ }
682
+ ) }),
683
+ /* @__PURE__ */ u3("div", { style: "width:52px;text-align:right;font-size:11px;color:var(--profiler-text-muted);padding-left:6px;flex-shrink:0", children: [
684
+ req.duration.toFixed(0),
685
+ "ms"
686
+ ] })
687
+ ] }, i3);
688
+ })
689
+ ] }),
690
+ /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "margin-top:8px", children: [
691
+ "Total span: ",
692
+ totalSpan.toFixed(0),
693
+ "ms \xB7 ",
694
+ timed.length,
695
+ " request",
696
+ timed.length !== 1 ? "s" : ""
697
+ ] })
698
+ ] });
699
+ }
700
+ function buildCurl(req) {
701
+ const parts = [`curl -X ${req.method}`];
702
+ const headers = req.request_headers || {};
703
+ for (const [k3, v3] of Object.entries(headers)) {
704
+ parts.push(` -H ${JSON.stringify(`${k3}: ${v3}`)}`);
705
+ }
706
+ if (req.request_body && req.request_body_encoding !== "base64") {
707
+ parts.push(` -d ${JSON.stringify(req.request_body)}`);
708
+ }
709
+ parts.push(` ${JSON.stringify(req.url)}`);
710
+ return parts.join(" \\\n");
711
+ }
568
712
  function HttpRequestDetail({ req, index, threshold }) {
569
713
  const [open, setOpen] = d2(false);
714
+ const [copiedUrl, setCopiedUrl] = d2(false);
715
+ const [copiedCurl, setCopiedCurl] = d2(false);
570
716
  const isError = req.status >= 400 || req.status === 0;
571
717
  const isSlow = req.duration >= threshold;
572
718
  const cardCls = isError ? "profiler-ajax-card--error" : isSlow ? "profiler-ajax-card--warning" : "profiler-ajax-card--success";
719
+ function copyUrl(e3) {
720
+ e3.stopPropagation();
721
+ navigator.clipboard.writeText(req.url).then(() => {
722
+ setCopiedUrl(true);
723
+ setTimeout(() => setCopiedUrl(false), 2e3);
724
+ });
725
+ }
726
+ function copyCurl() {
727
+ navigator.clipboard.writeText(buildCurl(req)).then(() => {
728
+ setCopiedCurl(true);
729
+ setTimeout(() => setCopiedCurl(false), 2e3);
730
+ });
731
+ }
573
732
  return /* @__PURE__ */ u3("div", { class: `profiler-ajax-card ${cardCls}`, style: "margin-bottom:8px", children: [
574
733
  /* @__PURE__ */ u3(
575
734
  "div",
@@ -578,10 +737,20 @@
578
737
  style: "cursor:pointer;user-select:none",
579
738
  onClick: () => setOpen((o3) => !o3),
580
739
  children: [
581
- /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-3", children: [
740
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-3", style: "min-width:0;flex:1", children: [
582
741
  /* @__PURE__ */ u3("span", { style: "font-size:11px;color:var(--profiler-muted)", children: open ? "\u25BE" : "\u25B8" }),
583
742
  /* @__PURE__ */ u3("span", { class: `profiler-ajax-card__method badge-${methodBadge(req.method)}`, children: req.method }),
584
- /* @__PURE__ */ u3("strong", { class: "profiler-ajax-card__path", style: "word-break:break-all", children: req.url })
743
+ /* @__PURE__ */ u3("strong", { class: "profiler-ajax-card__path", style: "word-break:break-all", children: req.url }),
744
+ /* @__PURE__ */ u3(
745
+ "button",
746
+ {
747
+ onClick: copyUrl,
748
+ class: "profiler-body-download-btn profiler-text--xs",
749
+ style: "flex-shrink:0;cursor:pointer",
750
+ title: "Copy URL",
751
+ children: copiedUrl ? "Copied!" : "Copy URL"
752
+ }
753
+ )
585
754
  ] }),
586
755
  /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2", style: "flex-shrink:0", children: [
587
756
  /* @__PURE__ */ u3("span", { class: `badge-${statusBadge(req.status)}`, children: req.status === 0 ? "ERR" : req.status }),
@@ -604,6 +773,7 @@
604
773
  ] }),
605
774
  req.error && /* @__PURE__ */ u3("div", { class: "profiler-ajax-card__row", children: /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--error", children: req.error }) }),
606
775
  open && /* @__PURE__ */ u3("div", { style: "padding:12px 4px 4px;border-top:1px solid rgba(0,0,0,0.08);margin-top:8px", children: [
776
+ /* @__PURE__ */ u3("div", { style: "margin-bottom:12px", children: /* @__PURE__ */ u3("button", { onClick: copyCurl, class: "profiler-body-download-btn profiler-text--xs", style: "cursor:pointer", children: copiedCurl ? "Copied!" : "Copy as cURL" }) }),
607
777
  /* @__PURE__ */ u3("div", { style: "margin-bottom:16px", children: [
608
778
  /* @__PURE__ */ u3("div", { class: "profiler-text--sm", style: "font-weight:600;margin-bottom:8px;color:var(--profiler-muted)", children: "REQUEST" }),
609
779
  /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted", style: "margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px", children: "Headers" }),
@@ -688,13 +858,19 @@
688
858
  ] })
689
859
  ] }),
690
860
  /* @__PURE__ */ u3("h3", { class: "profiler-text--lg profiler-mb-3", children: "Requests" }),
691
- /* @__PURE__ */ u3("p", { class: "profiler-text--xs profiler-text--muted profiler-mb-3", children: "Click a request to expand headers and body." }),
692
- httpData.requests.map((req, index) => /* @__PURE__ */ u3(HttpRequestDetail, { req, index, threshold }, index))
861
+ /* @__PURE__ */ u3(WaterfallView, { requests: httpData.requests }),
862
+ /* @__PURE__ */ u3("div", { style: "margin-top:16px", children: [
863
+ /* @__PURE__ */ u3("p", { class: "profiler-text--xs profiler-text--muted profiler-mb-3", children: "Click a request to expand headers and body." }),
864
+ httpData.requests.map((req, index) => /* @__PURE__ */ u3(HttpRequestDetail, { req, index, threshold }, index))
865
+ ] })
693
866
  ] });
694
867
  }
695
868
 
696
869
  // app/assets/typescript/profiler/components/ProfileList.tsx
697
870
  var BASE = "/_profiler";
871
+ function TableSkeleton({ cols, rows = 6 }) {
872
+ return /* @__PURE__ */ u3("div", { children: Array.from({ length: rows }).map((_2, i3) => /* @__PURE__ */ u3("div", { class: "profiler-skeleton__row", children: cols.map((size, j3) => /* @__PURE__ */ u3("div", { class: `profiler-skeleton__cell profiler-skeleton__cell--${size}` }, j3)) }, i3)) });
873
+ }
698
874
  function methodClass(method) {
699
875
  const map = { GET: "badge-info", POST: "badge-success", PUT: "badge-warning", PATCH: "badge-warning", DELETE: "badge-error" };
700
876
  return map[method] || "badge-default";
@@ -734,7 +910,7 @@
734
910
  const col = params.get("sort");
735
911
  const dir = params.get("dir");
736
912
  return {
737
- col: col === "duration" || col === "memory" || col === "status" || col === "queries" ? col : null,
913
+ col: col === "date" || col === "duration" || col === "memory" || col === "status" || col === "queries" ? col : null,
738
914
  dir: dir === "desc" ? "desc" : "asc"
739
915
  };
740
916
  };
@@ -766,6 +942,7 @@
766
942
  return PRESETS.some((pr) => pr.key === p3) ? p3 : "";
767
943
  });
768
944
  const [httpSort, setHttpSort] = d2(initialSort);
945
+ const [jobSort, setJobSort] = d2({ col: null, dir: "asc" });
769
946
  const [jobSearch, setJobSearch] = d2("");
770
947
  const [jobStatus, setJobStatus] = d2("");
771
948
  const [jobDuration, setJobDuration] = d2("");
@@ -808,6 +985,11 @@
808
985
  (prev) => prev.col === col ? { col, dir: prev.dir === "asc" ? "desc" : "asc" } : { col, dir: "asc" }
809
986
  );
810
987
  };
988
+ const toggleJobSort = (col) => {
989
+ setJobSort(
990
+ (prev) => prev.col === col ? { col, dir: prev.dir === "asc" ? "desc" : "asc" } : { col, dir: "asc" }
991
+ );
992
+ };
811
993
  const togglePreset = (key) => {
812
994
  setHttpPreset((prev) => prev === key ? "" : key);
813
995
  };
@@ -873,6 +1055,7 @@
873
1055
  setJobSearch("");
874
1056
  setJobStatus("");
875
1057
  setJobDuration("");
1058
+ setJobSort({ col: null, dir: "asc" });
876
1059
  setOutboundSearch("");
877
1060
  setOutboundMethod("");
878
1061
  setOutboundStatus("");
@@ -955,6 +1138,10 @@
955
1138
  return true;
956
1139
  });
957
1140
  const sortedProfiles = httpSort.col ? [...filteredProfiles].sort((a3, b) => {
1141
+ if (httpSort.col === "date") {
1142
+ const diff = new Date(a3.started_at).getTime() - new Date(b.started_at).getTime();
1143
+ return httpSort.dir === "asc" ? diff : -diff;
1144
+ }
958
1145
  let av, bv;
959
1146
  switch (httpSort.col) {
960
1147
  case "duration":
@@ -987,6 +1174,26 @@
987
1174
  }
988
1175
  return true;
989
1176
  });
1177
+ const sortedJobs = jobSort.col ? [...filteredJobs].sort((a3, b) => {
1178
+ if (jobSort.col === "date") {
1179
+ const diff = new Date(a3.started_at).getTime() - new Date(b.started_at).getTime();
1180
+ return jobSort.dir === "asc" ? diff : -diff;
1181
+ }
1182
+ let av, bv;
1183
+ switch (jobSort.col) {
1184
+ case "duration":
1185
+ av = a3.duration;
1186
+ bv = b.duration;
1187
+ break;
1188
+ case "status":
1189
+ av = a3.status;
1190
+ bv = b.status;
1191
+ break;
1192
+ default:
1193
+ return 0;
1194
+ }
1195
+ return jobSort.dir === "asc" ? av - bv : bv - av;
1196
+ }) : filteredJobs;
990
1197
  const filteredOutbound = outboundRequests.filter((req) => {
991
1198
  if (outboundSearch && !req.url.toLowerCase().includes(outboundSearch.toLowerCase())) return false;
992
1199
  if (outboundMethod && req.method !== outboundMethod) return false;
@@ -1003,9 +1210,9 @@
1003
1210
  const httpFiltersActive = !!(httpSearch || httpMethod || httpStatus || httpDuration || httpPreset);
1004
1211
  const jobFiltersActive = !!(jobSearch || jobStatus || jobDuration);
1005
1212
  const outboundFiltersActive = !!(outboundSearch || outboundMethod || outboundStatus);
1006
- const sortIcon = (col) => {
1007
- if (httpSort.col !== col) return /* @__PURE__ */ u3("span", { class: "sort-icon sort-icon--idle", children: "\u21C5" });
1008
- return /* @__PURE__ */ u3("span", { class: "sort-icon sort-icon--active", children: httpSort.dir === "asc" ? "\u25B2" : "\u25BC" });
1213
+ const sortIcon = (activeCol, dir, col) => {
1214
+ if (activeCol !== col) return /* @__PURE__ */ u3("span", { class: "sort-icon sort-icon--idle", children: "\u21C5" });
1215
+ return /* @__PURE__ */ u3("span", { class: "sort-icon sort-icon--active", children: dir === "asc" ? "\u25B2" : "\u25BC" });
1009
1216
  };
1010
1217
  return /* @__PURE__ */ u3("div", { class: "container", children: [
1011
1218
  /* @__PURE__ */ u3("div", { class: "header", children: [
@@ -1031,7 +1238,7 @@
1031
1238
  }, children: "Outbound HTTP" })
1032
1239
  ] }),
1033
1240
  /* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
1034
- section === "http" && (loadingHttp ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "Loading..." }) }) : error ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: error }) }) : profiles.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: [
1241
+ section === "http" && (loadingHttp ? /* @__PURE__ */ u3(TableSkeleton, { cols: ["sm", "xs", "flex", "sm", "sm", "sm", "xs", "sm"] }) : error ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: error }) }) : profiles.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: [
1035
1242
  /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "No profiles found" }),
1036
1243
  /* @__PURE__ */ u3("p", { class: "profiler-empty__description", children: "Make some requests to your application to see profiling data" })
1037
1244
  ] }) : /* @__PURE__ */ u3(k, { children: [
@@ -1090,24 +1297,27 @@
1090
1297
  ] }),
1091
1298
  filteredProfiles.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: [
1092
1299
  /* @__PURE__ */ u3("thead", { children: /* @__PURE__ */ u3("tr", { children: [
1093
- /* @__PURE__ */ u3("th", { children: "Time" }),
1300
+ /* @__PURE__ */ u3("th", { class: `sortable${httpSort.col === "date" ? " sortable--active" : ""}`, onClick: () => toggleHttpSort("date"), children: [
1301
+ "Time ",
1302
+ sortIcon(httpSort.col, httpSort.dir, "date")
1303
+ ] }),
1094
1304
  /* @__PURE__ */ u3("th", { children: "Method" }),
1095
1305
  /* @__PURE__ */ u3("th", { children: "Path" }),
1096
1306
  /* @__PURE__ */ u3("th", { class: `sortable${httpSort.col === "duration" ? " sortable--active" : ""}`, onClick: () => toggleHttpSort("duration"), children: [
1097
1307
  "Duration ",
1098
- sortIcon("duration")
1308
+ sortIcon(httpSort.col, httpSort.dir, "duration")
1099
1309
  ] }),
1100
1310
  /* @__PURE__ */ u3("th", { class: `sortable${httpSort.col === "queries" ? " sortable--active" : ""}`, onClick: () => toggleHttpSort("queries"), children: [
1101
1311
  "Queries ",
1102
- sortIcon("queries")
1312
+ sortIcon(httpSort.col, httpSort.dir, "queries")
1103
1313
  ] }),
1104
1314
  /* @__PURE__ */ u3("th", { class: `sortable${httpSort.col === "memory" ? " sortable--active" : ""}`, onClick: () => toggleHttpSort("memory"), children: [
1105
1315
  "Memory ",
1106
- sortIcon("memory")
1316
+ sortIcon(httpSort.col, httpSort.dir, "memory")
1107
1317
  ] }),
1108
1318
  /* @__PURE__ */ u3("th", { class: `sortable${httpSort.col === "status" ? " sortable--active" : ""}`, onClick: () => toggleHttpSort("status"), children: [
1109
1319
  "Status ",
1110
- sortIcon("status")
1320
+ sortIcon(httpSort.col, httpSort.dir, "status")
1111
1321
  ] }),
1112
1322
  /* @__PURE__ */ u3("th", { children: "Token" }),
1113
1323
  /* @__PURE__ */ u3("th", {})
@@ -1129,7 +1339,7 @@
1129
1339
  ] }),
1130
1340
  httpHasMore && !httpFiltersActive && /* @__PURE__ */ u3("div", { class: "profiler-load-more", children: /* @__PURE__ */ u3("button", { class: "btn btn-secondary", onClick: loadMoreHttp, disabled: httpLoadingMore, children: httpLoadingMore ? "Loading\u2026" : "Load more" }) })
1131
1341
  ] })),
1132
- section === "jobs" && (loadingJobs ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "Loading..." }) }) : jobsError ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: jobsError }) }) : jobs.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: [
1342
+ section === "jobs" && (loadingJobs ? /* @__PURE__ */ u3(TableSkeleton, { cols: ["sm", "flex", "md", "sm", "xs", "xs", "sm"] }) : jobsError ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: jobsError }) }) : jobs.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: [
1133
1343
  /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "No job profiles found" }),
1134
1344
  /* @__PURE__ */ u3("p", { class: "profiler-empty__description", children: "Run background jobs in your application to see profiling data" })
1135
1345
  ] }) : /* @__PURE__ */ u3(k, { children: [
@@ -1169,16 +1379,25 @@
1169
1379
  ] }),
1170
1380
  filteredJobs.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: [
1171
1381
  /* @__PURE__ */ u3("thead", { children: /* @__PURE__ */ u3("tr", { children: [
1172
- /* @__PURE__ */ u3("th", { children: "Time" }),
1382
+ /* @__PURE__ */ u3("th", { class: `sortable${jobSort.col === "date" ? " sortable--active" : ""}`, onClick: () => toggleJobSort("date"), children: [
1383
+ "Time ",
1384
+ sortIcon(jobSort.col, jobSort.dir, "date")
1385
+ ] }),
1173
1386
  /* @__PURE__ */ u3("th", { children: "Job Class" }),
1174
1387
  /* @__PURE__ */ u3("th", { children: "Queue" }),
1175
- /* @__PURE__ */ u3("th", { children: "Duration" }),
1176
- /* @__PURE__ */ u3("th", { children: "Status" }),
1388
+ /* @__PURE__ */ u3("th", { class: `sortable${jobSort.col === "duration" ? " sortable--active" : ""}`, onClick: () => toggleJobSort("duration"), children: [
1389
+ "Duration ",
1390
+ sortIcon(jobSort.col, jobSort.dir, "duration")
1391
+ ] }),
1392
+ /* @__PURE__ */ u3("th", { class: `sortable${jobSort.col === "status" ? " sortable--active" : ""}`, onClick: () => toggleJobSort("status"), children: [
1393
+ "Status ",
1394
+ sortIcon(jobSort.col, jobSort.dir, "status")
1395
+ ] }),
1177
1396
  /* @__PURE__ */ u3("th", { children: "Executions" }),
1178
1397
  /* @__PURE__ */ u3("th", { children: "Token" }),
1179
1398
  /* @__PURE__ */ u3("th", {})
1180
1399
  ] }) }),
1181
- /* @__PURE__ */ u3("tbody", { children: filteredJobs.map((p3) => {
1400
+ /* @__PURE__ */ u3("tbody", { children: sortedJobs.map((p3) => {
1182
1401
  const jobData = p3.collectors_data?.job;
1183
1402
  const isFailed = p3.status === 500;
1184
1403
  return /* @__PURE__ */ u3("tr", { children: [
@@ -1198,7 +1417,7 @@
1198
1417
  ] }),
1199
1418
  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" }) })
1200
1419
  ] })),
1201
- section === "outbound" && (loadingOutbound ? /* @__PURE__ */ u3("div", { class: "profiler-empty", children: /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "Loading..." }) }) : 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: [
1420
+ 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: [
1202
1421
  /* @__PURE__ */ u3("div", { class: "profiler-empty__title", children: "No outbound HTTP requests found" }),
1203
1422
  /* @__PURE__ */ u3("p", { class: "profiler-empty__description", children: "Make requests to external services to see outbound HTTP data" })
1204
1423
  ] }) : /* @__PURE__ */ u3(k, { children: [
@@ -1264,22 +1483,7 @@
1264
1483
  }
1265
1484
 
1266
1485
  // app/assets/typescript/profiler/components/dashboard/tabs/RequestTab.tsx
1267
- function tryFormatJson(text) {
1268
- try {
1269
- return JSON.stringify(JSON.parse(text), null, 2);
1270
- } catch {
1271
- return text;
1272
- }
1273
- }
1274
- function BodyBlock({ body, encoding }) {
1275
- if (!body) return null;
1276
- if (encoding === "base64") {
1277
- return /* @__PURE__ */ u3("p", { class: "profiler-text--muted profiler-text--sm", children: "[binary content, base64-encoded]" });
1278
- }
1279
- const formatted = tryFormatJson(body);
1280
- return /* @__PURE__ */ u3("pre", { class: "profiler-code profiler-text--xs", children: formatted });
1281
- }
1282
- function buildCurl(profile) {
1486
+ function buildCurl2(profile) {
1283
1487
  const headers = profile.headers ?? {};
1284
1488
  const params = profile.params ?? {};
1285
1489
  const reqBody = profile.request_body;
@@ -1308,7 +1512,7 @@
1308
1512
  }
1309
1513
  function RequestTab({ profile }) {
1310
1514
  const [copied, setCopied] = d2(false);
1311
- const curl = buildCurl(profile);
1515
+ const curl = buildCurl2(profile);
1312
1516
  const routeData = profile.collectors_data?.request ?? {};
1313
1517
  function copyToClipboard() {
1314
1518
  navigator.clipboard.writeText(curl).then(() => {
@@ -1368,7 +1572,14 @@
1368
1572
  ] }),
1369
1573
  profile.request_body && /* @__PURE__ */ u3(k, { children: [
1370
1574
  /* @__PURE__ */ u3("h2", { class: "profiler-section__header profiler-mt-6", children: "Request Body" }),
1371
- /* @__PURE__ */ u3(BodyBlock, { body: profile.request_body, encoding: profile.request_body_encoding })
1575
+ /* @__PURE__ */ u3(
1576
+ SmartBodyPreview,
1577
+ {
1578
+ body: profile.request_body,
1579
+ encoding: profile.request_body_encoding,
1580
+ headers: profile.headers ?? {}
1581
+ }
1582
+ )
1372
1583
  ] }),
1373
1584
  profile.response_headers && Object.keys(profile.response_headers).length > 0 && /* @__PURE__ */ u3(k, { children: [
1374
1585
  /* @__PURE__ */ u3("h2", { class: "profiler-section__header profiler-mt-6", children: "Response Headers" }),
@@ -1379,7 +1590,14 @@
1379
1590
  ] }),
1380
1591
  profile.response_body && /* @__PURE__ */ u3(k, { children: [
1381
1592
  /* @__PURE__ */ u3("h2", { class: "profiler-section__header profiler-mt-6", children: "Response Body" }),
1382
- /* @__PURE__ */ u3(BodyBlock, { body: profile.response_body, encoding: profile.response_body_encoding })
1593
+ /* @__PURE__ */ u3(
1594
+ SmartBodyPreview,
1595
+ {
1596
+ body: profile.response_body,
1597
+ encoding: profile.response_body_encoding,
1598
+ headers: profile.response_headers ?? {}
1599
+ }
1600
+ )
1383
1601
  ] }),
1384
1602
  /* @__PURE__ */ u3("h2", { class: "profiler-section__header profiler-mt-6", children: "Curl Command" }),
1385
1603
  /* @__PURE__ */ u3("div", { style: "position: relative;", children: [
@@ -1461,6 +1679,14 @@
1461
1679
  ] });
1462
1680
  }
1463
1681
  function ExplainModal({ state, onClose }) {
1682
+ y2(() => {
1683
+ if (!state.open) return;
1684
+ const handler = (e3) => {
1685
+ if (e3.key === "Escape") onClose();
1686
+ };
1687
+ document.addEventListener("keydown", handler);
1688
+ return () => document.removeEventListener("keydown", handler);
1689
+ }, [state.open]);
1464
1690
  if (!state.open) return null;
1465
1691
  const renderResult = () => {
1466
1692
  if (state.loading) return /* @__PURE__ */ u3("div", { class: "profiler-text--muted", children: "Running EXPLAIN ANALYZE\u2026" });
@@ -1571,7 +1797,7 @@
1571
1797
  ] }),
1572
1798
  n1Groups.length > 0 && /* @__PURE__ */ u3("div", { class: "profiler-alert-banner profiler-alert-banner--warning profiler-mb-4", children: [
1573
1799
  /* @__PURE__ */ u3("span", { class: "profiler-alert-banner__icon", children: "\u26A0\uFE0F" }),
1574
- /* @__PURE__ */ u3("div", { children: [
1800
+ /* @__PURE__ */ u3("div", { style: "flex:1", children: [
1575
1801
  /* @__PURE__ */ u3("strong", { children: "Potential N+1 detected" }),
1576
1802
  " \u2014 ",
1577
1803
  n1Groups.length,
@@ -1581,7 +1807,19 @@
1581
1807
  n1Groups.reduce((sum, g2) => sum + g2.indices.length, 0),
1582
1808
  " times total",
1583
1809
  /* @__PURE__ */ u3("div", { class: "profiler-text--xs profiler-text--muted profiler-mt-1", children: "Queries causing N+1 are highlighted below. Expand each group to see the call stack." })
1584
- ] })
1810
+ ] }),
1811
+ n1Groups.length > 1 && /* @__PURE__ */ u3(
1812
+ "button",
1813
+ {
1814
+ class: "profiler-btn profiler-btn--sm",
1815
+ style: "flex-shrink:0;align-self:flex-start",
1816
+ onClick: () => {
1817
+ const allOpen = n1Groups.every((g2) => openBacktraces.has(g2.pattern));
1818
+ setOpenBacktraces(allOpen ? /* @__PURE__ */ new Set() : new Set(n1Groups.map((g2) => g2.pattern)));
1819
+ },
1820
+ children: n1Groups.every((g2) => openBacktraces.has(g2.pattern)) ? "Collapse all" : "Expand all"
1821
+ }
1822
+ )
1585
1823
  ] }),
1586
1824
  n1Groups.map((group) => /* @__PURE__ */ u3("div", { class: "profiler-n1-group profiler-mb-4", children: [
1587
1825
  /* @__PURE__ */ u3("div", { class: "profiler-n1-group__header", onClick: () => toggleBacktrace(group.pattern), children: [
@@ -3035,6 +3273,7 @@
3035
3273
  }
3036
3274
  function LogsTab({ logData }) {
3037
3275
  const [filter, setFilter] = d2("ALL");
3276
+ const [search, setSearch] = d2("");
3038
3277
  if (!logData?.logs?.length) {
3039
3278
  return /* @__PURE__ */ u3("div", { class: "profiler-empty", children: [
3040
3279
  /* @__PURE__ */ u3("div", { class: "profiler-empty__icon", children: "\u{1F4CB}" }),
@@ -3047,7 +3286,11 @@
3047
3286
  ] });
3048
3287
  }
3049
3288
  const levels = ["ALL", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"];
3050
- const filtered = filter === "ALL" ? logData.logs : logData.logs.filter((l3) => l3.level === filter);
3289
+ const filtered = logData.logs.filter((l3) => {
3290
+ if (filter !== "ALL" && l3.level !== filter) return false;
3291
+ if (search && !l3.message.toLowerCase().includes(search.toLowerCase())) return false;
3292
+ return true;
3293
+ });
3051
3294
  return /* @__PURE__ */ u3(k, { children: [
3052
3295
  /* @__PURE__ */ u3("h2", { class: "profiler-section__header", children: [
3053
3296
  "Log Messages (",
@@ -3064,19 +3307,32 @@
3064
3307
  /* @__PURE__ */ u3("strong", { class: "profiler-text--warning", children: logData.warnings })
3065
3308
  ] })
3066
3309
  ] }),
3067
- /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2 profiler-mb-4", children: levels.map((level) => /* @__PURE__ */ u3(
3068
- "button",
3069
- {
3070
- onClick: () => setFilter(level),
3071
- class: `btn btn-sm ${filter === level ? "btn-primary" : "btn-secondary"}`,
3072
- children: level
3073
- },
3074
- level
3075
- )) }),
3310
+ /* @__PURE__ */ u3("div", { class: "profiler-action-bar profiler-mb-4", children: [
3311
+ /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2", children: levels.map((level) => /* @__PURE__ */ u3(
3312
+ "button",
3313
+ {
3314
+ onClick: () => setFilter(level),
3315
+ class: `btn btn-sm ${filter === level ? "btn-primary" : "btn-secondary"}`,
3316
+ children: level
3317
+ },
3318
+ level
3319
+ )) }),
3320
+ /* @__PURE__ */ u3(
3321
+ "input",
3322
+ {
3323
+ type: "text",
3324
+ class: "profiler-filter-input",
3325
+ placeholder: "Search messages\u2026",
3326
+ value: search,
3327
+ onInput: (e3) => setSearch(e3.target.value),
3328
+ style: "width:200px"
3329
+ }
3330
+ )
3331
+ ] }),
3076
3332
  filtered.length === 0 ? /* @__PURE__ */ u3("div", { class: "profiler-text--muted profiler-text--sm", children: [
3077
- "No ",
3078
- filter,
3079
- " messages."
3333
+ "No messages match",
3334
+ search ? ` "${search}"` : "",
3335
+ "."
3080
3336
  ] }) : filtered.map((entry, index) => /* @__PURE__ */ u3("div", { class: "profiler-query-card profiler-mb-2", children: [
3081
3337
  /* @__PURE__ */ u3("div", { class: "profiler-query-card__header", children: [
3082
3338
  /* @__PURE__ */ u3(
@@ -3368,7 +3624,8 @@
3368
3624
  (profile.memory / 1024 / 1024).toFixed(2),
3369
3625
  " MB"
3370
3626
  ] })
3371
- ] })
3627
+ ] }),
3628
+ /* @__PURE__ */ u3("span", { style: "color:var(--profiler-text-muted)", children: new Date(profile.started_at).toLocaleString("en", { hour12: false, month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit" }) })
3372
3629
  ] })
3373
3630
  ] }),
3374
3631
  /* @__PURE__ */ u3("div", { class: "profiler-panel profiler-mb-6", children: [
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.12.0"
4
+ VERSION = "0.13.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.12.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sébastien Duplessy