rails-profiler 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/builds/profiler.css +46 -0
- data/app/assets/builds/profiler.js +397 -64
- data/app/controllers/profiler/api/jobs_controller.rb +4 -1
- data/app/controllers/profiler/api/profiles_controller.rb +4 -1
- data/app/controllers/profiler/application_controller.rb +46 -0
- data/app/controllers/profiler/profiles_controller.rb +5 -0
- data/app/views/profiler/profiles/show.html.erb +1 -1
- data/lib/profiler/current_context.rb +17 -0
- data/lib/profiler/instrumentation/active_job_instrumentation.rb +16 -0
- data/lib/profiler/instrumentation/sidekiq_middleware.rb +8 -0
- data/lib/profiler/job_profiler.rb +10 -3
- data/lib/profiler/mcp/tools/get_profile_detail.rb +47 -1
- data/lib/profiler/mcp/tools/query_jobs.rb +8 -7
- data/lib/profiler/middleware/profiler_middleware.rb +3 -0
- data/lib/profiler/railtie.rb +5 -0
- data/lib/profiler/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 44b1fd924fe2eb7f0483bc36f0e3956e81535da3ce39e60f6e29c091194f22ce
|
|
4
|
+
data.tar.gz: c840abbc0813c4e8c9516c507138e4ead25e9980f5c02a3306461b1a308ff6e3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d5307f599d3822291a99d931cacb1dc70218e32a120e136cebe98790e89f803e0ad84a5520b6961279c88048a7c2262c91600d770f07639cb5e31be32ff3e5a
|
|
7
|
+
data.tar.gz: 341cfa488dd91fe1b566dfc7c0a4d28dd6ab42e5fb0eb874250ca4b0482e3bb154d398c1c0919fef09a812a1cae7f897542b5702eb097c83e80e534cbbf777bd
|
|
@@ -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
|
|
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
|
-
|
|
521
|
-
"
|
|
522
|
-
|
|
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(
|
|
692
|
-
|
|
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 (
|
|
1008
|
-
return /* @__PURE__ */ u3("span", { class: "sort-icon sort-icon--active", children:
|
|
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(
|
|
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", {
|
|
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(
|
|
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", {
|
|
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", {
|
|
1176
|
-
|
|
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:
|
|
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(
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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-
|
|
3068
|
-
"
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
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
|
-
|
|
3079
|
-
"
|
|
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(
|
|
@@ -3321,6 +3577,47 @@
|
|
|
3321
3577
|
] });
|
|
3322
3578
|
}
|
|
3323
3579
|
|
|
3580
|
+
// app/assets/typescript/profiler/components/dashboard/tabs/JobsTab.tsx
|
|
3581
|
+
function JobsTab({ jobs }) {
|
|
3582
|
+
if (!jobs?.length) {
|
|
3583
|
+
return /* @__PURE__ */ u3("div", { class: "profiler-empty", children: [
|
|
3584
|
+
/* @__PURE__ */ u3("div", { class: "profiler-empty__icon", children: "\u2699\uFE0F" }),
|
|
3585
|
+
/* @__PURE__ */ u3("h3", { class: "profiler-empty__title", children: "No jobs triggered" }),
|
|
3586
|
+
/* @__PURE__ */ u3("p", { class: "profiler-empty__description", children: "Background jobs enqueued during this request will appear here." })
|
|
3587
|
+
] });
|
|
3588
|
+
}
|
|
3589
|
+
return /* @__PURE__ */ u3(k, { children: [
|
|
3590
|
+
/* @__PURE__ */ u3("h2", { class: "profiler-section__header", children: [
|
|
3591
|
+
"Background Jobs (",
|
|
3592
|
+
jobs.length,
|
|
3593
|
+
")"
|
|
3594
|
+
] }),
|
|
3595
|
+
jobs.map((job, index) => /* @__PURE__ */ u3("div", { class: `profiler-ajax-card profiler-ajax-card--${job.status === "completed" ? "success" : job.status === "failed" ? "error" : "default"}`, children: [
|
|
3596
|
+
/* @__PURE__ */ u3("div", { class: "profiler-ajax-card__row", children: [
|
|
3597
|
+
/* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-3", children: [
|
|
3598
|
+
/* @__PURE__ */ u3("span", { class: `badge-${job.status === "completed" ? "success" : job.status === "failed" ? "error" : "warning"}`, children: job.status ?? "unknown" }),
|
|
3599
|
+
/* @__PURE__ */ u3("strong", { class: "profiler-ajax-card__path", children: job.job_class })
|
|
3600
|
+
] }),
|
|
3601
|
+
/* @__PURE__ */ u3("span", { class: job.duration >= 1e3 ? "badge-error" : job.duration >= 200 ? "badge-warning" : "badge-success", children: [
|
|
3602
|
+
job.duration?.toFixed(2),
|
|
3603
|
+
" ms"
|
|
3604
|
+
] })
|
|
3605
|
+
] }),
|
|
3606
|
+
/* @__PURE__ */ u3("div", { class: "profiler-ajax-card__row", children: [
|
|
3607
|
+
/* @__PURE__ */ u3("span", { class: "profiler-ajax-card__time profiler-text--muted", children: [
|
|
3608
|
+
job.queue && /* @__PURE__ */ u3("span", { children: [
|
|
3609
|
+
"Queue: ",
|
|
3610
|
+
/* @__PURE__ */ u3("strong", { children: job.queue }),
|
|
3611
|
+
" \xB7 "
|
|
3612
|
+
] }),
|
|
3613
|
+
new Date(job.started_at).toLocaleTimeString("en", { hour12: false })
|
|
3614
|
+
] }),
|
|
3615
|
+
/* @__PURE__ */ u3("a", { href: `/_profiler/profiles/${job.token}`, class: "profiler-text--sm", style: "color: var(--profiler-accent);", children: "View Job \u2192" })
|
|
3616
|
+
] })
|
|
3617
|
+
] }, index))
|
|
3618
|
+
] });
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3324
3621
|
// app/assets/typescript/profiler/components/dashboard/ProfileDashboard.tsx
|
|
3325
3622
|
function ProfileDashboard({ profile, initialTab, embedded }) {
|
|
3326
3623
|
const cd = profile.collectors_data || {};
|
|
@@ -3330,6 +3627,7 @@
|
|
|
3330
3627
|
const hasLogs = (cd["logs"]?.count ?? 0) > 0;
|
|
3331
3628
|
const hasRoutes = (cd["routes"]?.total ?? 0) > 0;
|
|
3332
3629
|
const hasI18n = (cd["i18n"]?.total ?? 0) > 0;
|
|
3630
|
+
const hasJobs = (profile.child_jobs?.length ?? 0) > 0;
|
|
3333
3631
|
const [activeTab, setActiveTab] = d2(hasException ? "exception" : initialTab);
|
|
3334
3632
|
const handleTabClick = (tab) => (e3) => {
|
|
3335
3633
|
e3.preventDefault();
|
|
@@ -3368,7 +3666,8 @@
|
|
|
3368
3666
|
(profile.memory / 1024 / 1024).toFixed(2),
|
|
3369
3667
|
" MB"
|
|
3370
3668
|
] })
|
|
3371
|
-
] })
|
|
3669
|
+
] }),
|
|
3670
|
+
/* @__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
3671
|
] })
|
|
3373
3672
|
] }),
|
|
3374
3673
|
/* @__PURE__ */ u3("div", { class: "profiler-panel profiler-mb-6", children: [
|
|
@@ -3384,7 +3683,12 @@
|
|
|
3384
3683
|
/* @__PURE__ */ u3("a", { href: "#", class: tabClass("cache"), onClick: handleTabClick("cache"), children: "Cache" }),
|
|
3385
3684
|
hasLogs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("logs"), onClick: handleTabClick("logs"), children: "Logs" }),
|
|
3386
3685
|
hasRoutes && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("routes"), onClick: handleTabClick("routes"), children: "Routes" }),
|
|
3387
|
-
hasI18n && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("i18n"), onClick: handleTabClick("i18n"), children: "I18n" })
|
|
3686
|
+
hasI18n && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("i18n"), onClick: handleTabClick("i18n"), children: "I18n" }),
|
|
3687
|
+
hasJobs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("jobs"), onClick: handleTabClick("jobs"), children: [
|
|
3688
|
+
"Jobs (",
|
|
3689
|
+
profile.child_jobs.length,
|
|
3690
|
+
")"
|
|
3691
|
+
] })
|
|
3388
3692
|
] }),
|
|
3389
3693
|
/* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
|
|
3390
3694
|
activeTab === "exception" && /* @__PURE__ */ u3(ExceptionTab, { exceptionData: cd["exception"] }),
|
|
@@ -3398,7 +3702,8 @@
|
|
|
3398
3702
|
activeTab === "cache" && /* @__PURE__ */ u3(CacheTab, { cacheData: cd["cache"] }),
|
|
3399
3703
|
activeTab === "logs" && /* @__PURE__ */ u3(LogsTab, { logData: cd["logs"] }),
|
|
3400
3704
|
activeTab === "routes" && /* @__PURE__ */ u3(RoutesTab, { routesData: cd["routes"] }),
|
|
3401
|
-
activeTab === "i18n" && /* @__PURE__ */ u3(I18nTab, { i18nData: cd["i18n"] })
|
|
3705
|
+
activeTab === "i18n" && /* @__PURE__ */ u3(I18nTab, { i18nData: cd["i18n"] }),
|
|
3706
|
+
activeTab === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs })
|
|
3402
3707
|
] })
|
|
3403
3708
|
] }),
|
|
3404
3709
|
!embedded && /* @__PURE__ */ u3("div", { class: "profiler-mt-6", children: /* @__PURE__ */ u3("a", { href: "/_profiler", style: "color: var(--profiler-accent);", children: "\u2190 Back to profiles" }) })
|
|
@@ -3461,13 +3766,15 @@
|
|
|
3461
3766
|
|
|
3462
3767
|
// app/assets/typescript/profiler/components/dashboard/JobProfileDashboard.tsx
|
|
3463
3768
|
function JobProfileDashboard({ profile, initialTab, embedded }) {
|
|
3464
|
-
const validTabs = ["job", "database", "cache", "http"];
|
|
3769
|
+
const validTabs = ["job", "database", "cache", "http", "jobs"];
|
|
3465
3770
|
const defaultTab = validTabs.includes(initialTab) ? initialTab : "job";
|
|
3466
3771
|
const [activeTab, setActiveTab] = d2(defaultTab);
|
|
3467
3772
|
const cd = profile.collectors_data || {};
|
|
3468
3773
|
const hasHttp = cd["http"]?.total_requests > 0;
|
|
3774
|
+
const hasJobs = (profile.child_jobs?.length ?? 0) > 0;
|
|
3469
3775
|
const jobData = cd["job"];
|
|
3470
3776
|
const isFailed = jobData?.status === "failed";
|
|
3777
|
+
const parent = profile.parent_profile;
|
|
3471
3778
|
const handleTabClick = (tab) => (e3) => {
|
|
3472
3779
|
e3.preventDefault();
|
|
3473
3780
|
setActiveTab(tab);
|
|
@@ -3502,6 +3809,26 @@
|
|
|
3502
3809
|
" MB"
|
|
3503
3810
|
] })
|
|
3504
3811
|
] })
|
|
3812
|
+
] }),
|
|
3813
|
+
parent && /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2 profiler-mt-2 profiler-text--sm", children: [
|
|
3814
|
+
/* @__PURE__ */ u3("span", { style: "color:var(--profiler-text-muted)", children: "Triggered by:" }),
|
|
3815
|
+
parent.profile_type === "http" ? /* @__PURE__ */ u3("span", { children: [
|
|
3816
|
+
/* @__PURE__ */ u3("strong", { children: parent.method }),
|
|
3817
|
+
" ",
|
|
3818
|
+
parent.path,
|
|
3819
|
+
" \xB7 ",
|
|
3820
|
+
parent.http_status,
|
|
3821
|
+
" \xB7 ",
|
|
3822
|
+
parent.duration?.toFixed(2),
|
|
3823
|
+
" ms"
|
|
3824
|
+
] }) : /* @__PURE__ */ u3("span", { children: [
|
|
3825
|
+
"\u2699\uFE0F ",
|
|
3826
|
+
/* @__PURE__ */ u3("strong", { children: parent.path }),
|
|
3827
|
+
" \xB7 ",
|
|
3828
|
+
parent.duration?.toFixed(2),
|
|
3829
|
+
" ms"
|
|
3830
|
+
] }),
|
|
3831
|
+
/* @__PURE__ */ u3("a", { href: `/_profiler/profiles/${parent.token}`, style: "color: var(--profiler-accent);", children: "View \u2192" })
|
|
3505
3832
|
] })
|
|
3506
3833
|
] }),
|
|
3507
3834
|
/* @__PURE__ */ u3("div", { class: "profiler-panel profiler-mb-6", children: [
|
|
@@ -3509,13 +3836,19 @@
|
|
|
3509
3836
|
/* @__PURE__ */ u3("a", { href: "#", class: tabClass("job"), onClick: handleTabClick("job"), children: "Job" }),
|
|
3510
3837
|
/* @__PURE__ */ u3("a", { href: "#", class: tabClass("database"), onClick: handleTabClick("database"), children: "Database" }),
|
|
3511
3838
|
/* @__PURE__ */ u3("a", { href: "#", class: tabClass("cache"), onClick: handleTabClick("cache"), children: "Cache" }),
|
|
3512
|
-
hasHttp && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("http"), onClick: handleTabClick("http"), children: "Outbound HTTP" })
|
|
3839
|
+
hasHttp && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("http"), onClick: handleTabClick("http"), children: "Outbound HTTP" }),
|
|
3840
|
+
hasJobs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("jobs"), onClick: handleTabClick("jobs"), children: [
|
|
3841
|
+
"Jobs (",
|
|
3842
|
+
profile.child_jobs.length,
|
|
3843
|
+
")"
|
|
3844
|
+
] })
|
|
3513
3845
|
] }),
|
|
3514
3846
|
/* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
|
|
3515
3847
|
activeTab === "job" && /* @__PURE__ */ u3(JobTab, { jobData: cd["job"] }),
|
|
3516
3848
|
activeTab === "database" && /* @__PURE__ */ u3(DatabaseTab, { dbData: cd["database"], token: profile.token }),
|
|
3517
3849
|
activeTab === "cache" && /* @__PURE__ */ u3(CacheTab, { cacheData: cd["cache"] }),
|
|
3518
|
-
activeTab === "http" && /* @__PURE__ */ u3(HttpTab, { httpData: cd["http"] })
|
|
3850
|
+
activeTab === "http" && /* @__PURE__ */ u3(HttpTab, { httpData: cd["http"] }),
|
|
3851
|
+
activeTab === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs })
|
|
3519
3852
|
] })
|
|
3520
3853
|
] }),
|
|
3521
3854
|
!embedded && /* @__PURE__ */ u3("div", { class: "profiler-mt-6", children: /* @__PURE__ */ u3("a", { href: "/_profiler", style: "color: var(--profiler-accent);", children: "\u2190 Back to profiles" }) })
|
|
@@ -26,7 +26,10 @@ module Profiler
|
|
|
26
26
|
return render json: { error: "Job profile not found" }, status: :not_found
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
render json: profile.to_h
|
|
29
|
+
render json: profile.to_h.merge(
|
|
30
|
+
child_jobs: build_child_jobs(profile),
|
|
31
|
+
parent_profile: build_parent_summary(profile)
|
|
32
|
+
)
|
|
30
33
|
end
|
|
31
34
|
|
|
32
35
|
def destroy
|
|
@@ -29,7 +29,10 @@ module Profiler
|
|
|
29
29
|
# Recalculate AJAX collector data (since AJAX requests happen after page load)
|
|
30
30
|
recalculate_ajax_data(profile)
|
|
31
31
|
|
|
32
|
-
render json: profile.to_h
|
|
32
|
+
render json: profile.to_h.merge(
|
|
33
|
+
child_jobs: build_child_jobs(profile),
|
|
34
|
+
parent_profile: build_parent_summary(profile)
|
|
35
|
+
)
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
def destroy
|
|
@@ -15,5 +15,51 @@ module Profiler
|
|
|
15
15
|
render plain: "Profiler is disabled", status: :forbidden
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
|
+
|
|
19
|
+
def build_child_jobs(profile)
|
|
20
|
+
Profiler.storage.find_by_parent(profile.token)
|
|
21
|
+
.select { |p| p.profile_type == "job" }
|
|
22
|
+
.map do |j|
|
|
23
|
+
job_data = j.collector_data("job") || {}
|
|
24
|
+
{
|
|
25
|
+
token: j.token,
|
|
26
|
+
job_class: j.path,
|
|
27
|
+
job_id: job_data["job_id"],
|
|
28
|
+
queue: job_data["queue"],
|
|
29
|
+
status: job_data["status"],
|
|
30
|
+
duration: j.duration,
|
|
31
|
+
started_at: j.started_at&.iso8601
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def build_parent_summary(profile)
|
|
37
|
+
return nil unless profile.parent_token
|
|
38
|
+
|
|
39
|
+
parent = Profiler.storage.load(profile.parent_token)
|
|
40
|
+
return nil unless parent
|
|
41
|
+
|
|
42
|
+
if parent.profile_type == "job"
|
|
43
|
+
job_data = parent.collector_data("job") || {}
|
|
44
|
+
{
|
|
45
|
+
token: parent.token,
|
|
46
|
+
profile_type: "job",
|
|
47
|
+
path: parent.path,
|
|
48
|
+
status: job_data["status"],
|
|
49
|
+
duration: parent.duration,
|
|
50
|
+
started_at: parent.started_at&.iso8601
|
|
51
|
+
}
|
|
52
|
+
else
|
|
53
|
+
{
|
|
54
|
+
token: parent.token,
|
|
55
|
+
profile_type: "http",
|
|
56
|
+
method: parent.method,
|
|
57
|
+
path: parent.path,
|
|
58
|
+
http_status: parent.status,
|
|
59
|
+
duration: parent.duration,
|
|
60
|
+
started_at: parent.started_at&.iso8601
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
end
|
|
18
64
|
end
|
|
19
65
|
end
|
|
@@ -23,6 +23,11 @@ module Profiler
|
|
|
23
23
|
# Recalculate AJAX collector data (since AJAX requests happen after page load)
|
|
24
24
|
recalculate_ajax_data(@profile)
|
|
25
25
|
|
|
26
|
+
@profile_data = @profile.to_h.merge(
|
|
27
|
+
child_jobs: build_child_jobs(@profile),
|
|
28
|
+
parent_profile: build_parent_summary(@profile)
|
|
29
|
+
)
|
|
30
|
+
|
|
26
31
|
@embedded = params[:embed] == "true"
|
|
27
32
|
|
|
28
33
|
render layout: @embedded ? "profiler/embedded" : "profiler/application"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Profiler
|
|
4
|
+
module CurrentContext
|
|
5
|
+
def self.token
|
|
6
|
+
Thread.current[:profiler_token]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.token=(value)
|
|
10
|
+
Thread.current[:profiler_token] = value
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.clear
|
|
14
|
+
Thread.current[:profiler_token] = nil
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -6,6 +6,12 @@ module Profiler
|
|
|
6
6
|
extend ActiveSupport::Concern
|
|
7
7
|
|
|
8
8
|
included do
|
|
9
|
+
attr_accessor :profiler_parent_token
|
|
10
|
+
|
|
11
|
+
before_enqueue do |job|
|
|
12
|
+
job.profiler_parent_token = Profiler::CurrentContext.token
|
|
13
|
+
end
|
|
14
|
+
|
|
9
15
|
around_perform do |job, block|
|
|
10
16
|
Profiler::JobProfiler.profile(
|
|
11
17
|
job_class: job.class.name,
|
|
@@ -13,10 +19,20 @@ module Profiler
|
|
|
13
19
|
queue: job.queue_name,
|
|
14
20
|
arguments: job.arguments,
|
|
15
21
|
executions: job.executions - 1,
|
|
22
|
+
parent_token: job.profiler_parent_token,
|
|
16
23
|
&block
|
|
17
24
|
)
|
|
18
25
|
end
|
|
19
26
|
end
|
|
27
|
+
|
|
28
|
+
def serialize
|
|
29
|
+
super.merge("profiler_parent_token" => profiler_parent_token)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def deserialize(job_data)
|
|
33
|
+
super
|
|
34
|
+
self.profiler_parent_token = job_data["profiler_parent_token"]
|
|
35
|
+
end
|
|
20
36
|
end
|
|
21
37
|
end
|
|
22
38
|
end
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Profiler
|
|
4
4
|
module Instrumentation
|
|
5
|
+
class SidekiqClientMiddleware
|
|
6
|
+
def call(_worker_class, job, _queue, _redis_pool)
|
|
7
|
+
job["profiler_parent_token"] = Profiler::CurrentContext.token
|
|
8
|
+
yield
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
5
12
|
class SidekiqMiddleware
|
|
6
13
|
def call(worker, job, queue, &block)
|
|
7
14
|
Profiler::JobProfiler.profile(
|
|
@@ -10,6 +17,7 @@ module Profiler
|
|
|
10
17
|
queue: queue,
|
|
11
18
|
arguments: job["args"],
|
|
12
19
|
executions: job["retry_count"].to_i,
|
|
20
|
+
parent_token: job["profiler_parent_token"],
|
|
13
21
|
&block
|
|
14
22
|
)
|
|
15
23
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "models/profile"
|
|
4
|
+
require_relative "current_context"
|
|
4
5
|
require_relative "collectors/job_collector"
|
|
5
6
|
require_relative "collectors/database_collector"
|
|
6
7
|
require_relative "collectors/cache_collector"
|
|
@@ -14,7 +15,7 @@ module Profiler
|
|
|
14
15
|
Collectors::HttpCollector
|
|
15
16
|
].freeze
|
|
16
17
|
|
|
17
|
-
def self.profile(job_class:, job_id:, queue:, arguments:, executions:, &block)
|
|
18
|
+
def self.profile(job_class:, job_id:, queue:, arguments:, executions:, parent_token: nil, &block)
|
|
18
19
|
return block.call unless Profiler.enabled? && Profiler.configuration.track_jobs
|
|
19
20
|
|
|
20
21
|
new(
|
|
@@ -22,16 +23,18 @@ module Profiler
|
|
|
22
23
|
job_id: job_id,
|
|
23
24
|
queue: queue,
|
|
24
25
|
arguments: arguments,
|
|
25
|
-
executions: executions
|
|
26
|
+
executions: executions,
|
|
27
|
+
parent_token: parent_token
|
|
26
28
|
).run(&block)
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
def initialize(job_class:, job_id:, queue:, arguments:, executions:)
|
|
31
|
+
def initialize(job_class:, job_id:, queue:, arguments:, executions:, parent_token: nil)
|
|
30
32
|
@job_class = job_class
|
|
31
33
|
@job_id = job_id
|
|
32
34
|
@queue = queue
|
|
33
35
|
@arguments = arguments
|
|
34
36
|
@executions = executions
|
|
37
|
+
@parent_token = parent_token
|
|
35
38
|
end
|
|
36
39
|
|
|
37
40
|
def run(&block)
|
|
@@ -39,6 +42,7 @@ module Profiler
|
|
|
39
42
|
profile.profile_type = "job"
|
|
40
43
|
profile.path = @job_class
|
|
41
44
|
profile.method = "JOB"
|
|
45
|
+
profile.parent_token = @parent_token if @parent_token
|
|
42
46
|
|
|
43
47
|
job_collector = Collectors::JobCollector.new(profile, {
|
|
44
48
|
job_class: @job_class,
|
|
@@ -56,6 +60,8 @@ module Profiler
|
|
|
56
60
|
job_status = "completed"
|
|
57
61
|
error_message = nil
|
|
58
62
|
|
|
63
|
+
previous_token = Profiler::CurrentContext.token
|
|
64
|
+
Profiler::CurrentContext.token = profile.token
|
|
59
65
|
begin
|
|
60
66
|
result = block.call
|
|
61
67
|
result
|
|
@@ -64,6 +70,7 @@ module Profiler
|
|
|
64
70
|
error_message = "#{e.class}: #{e.message}"
|
|
65
71
|
raise
|
|
66
72
|
ensure
|
|
73
|
+
Profiler::CurrentContext.token = previous_token
|
|
67
74
|
if Profiler.configuration.track_memory
|
|
68
75
|
profile.memory = current_memory - memory_before
|
|
69
76
|
end
|
|
@@ -63,17 +63,21 @@ module Profiler
|
|
|
63
63
|
lines += section_http(profile) if want.("http")
|
|
64
64
|
lines += section_routes(profile) if want.("routes")
|
|
65
65
|
lines += section_dumps(profile) if want.("dumps")
|
|
66
|
+
lines += section_related_jobs(profile) if want.("related_jobs")
|
|
66
67
|
lines.join("\n")
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
def self.section_overview(profile)
|
|
70
71
|
lines = []
|
|
71
72
|
lines << "# Profile Details: #{profile.token}\n"
|
|
73
|
+
lines << "**Type:** #{profile.profile_type == 'job' ? 'Job' : 'HTTP Request'}"
|
|
72
74
|
lines << "**Request:** #{profile.method} #{profile.path}"
|
|
73
75
|
lines << "**Status:** #{profile.status}"
|
|
74
76
|
lines << "**Duration:** #{profile.duration.round(2)} ms"
|
|
75
77
|
lines << "**Memory:** #{(profile.memory / 1024.0 / 1024.0).round(2)} MB" if profile.memory
|
|
76
|
-
lines << "**Time:** #{profile.started_at}
|
|
78
|
+
lines << "**Time:** #{profile.started_at}"
|
|
79
|
+
lines << "**Parent Token:** #{profile.parent_token}" if profile.parent_token
|
|
80
|
+
lines << ""
|
|
77
81
|
lines
|
|
78
82
|
end
|
|
79
83
|
|
|
@@ -386,6 +390,48 @@ module Profiler
|
|
|
386
390
|
lines
|
|
387
391
|
end
|
|
388
392
|
|
|
393
|
+
def self.section_related_jobs(profile)
|
|
394
|
+
lines = []
|
|
395
|
+
|
|
396
|
+
# Parent info
|
|
397
|
+
if profile.parent_token
|
|
398
|
+
parent = Profiler.storage.load(profile.parent_token)
|
|
399
|
+
if parent
|
|
400
|
+
lines << "## Triggered By"
|
|
401
|
+
if parent.profile_type == "job"
|
|
402
|
+
job_data = parent.collector_data("job") || {}
|
|
403
|
+
lines << "- **Type:** Job"
|
|
404
|
+
lines << "- **Class:** #{job_data['job_class'] || parent.path}"
|
|
405
|
+
lines << "- **Status:** #{job_data['status']}"
|
|
406
|
+
lines << "- **Duration:** #{parent.duration.round(2)} ms"
|
|
407
|
+
lines << "- **Token:** #{parent.token}"
|
|
408
|
+
else
|
|
409
|
+
lines << "- **Type:** HTTP Request"
|
|
410
|
+
lines << "- **Request:** #{parent.method} #{parent.path}"
|
|
411
|
+
lines << "- **Status:** #{parent.status}"
|
|
412
|
+
lines << "- **Duration:** #{parent.duration.round(2)} ms"
|
|
413
|
+
lines << "- **Token:** #{parent.token}"
|
|
414
|
+
end
|
|
415
|
+
lines << ""
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# Child jobs
|
|
420
|
+
child_jobs = Profiler.storage.find_by_parent(profile.token).select { |p| p.profile_type == "job" }
|
|
421
|
+
return lines if child_jobs.empty?
|
|
422
|
+
|
|
423
|
+
lines << "## Child Jobs (#{child_jobs.size})"
|
|
424
|
+
lines << ""
|
|
425
|
+
lines << "| Job Class | Status | Duration | Token |"
|
|
426
|
+
lines << "|-----------|--------|----------|-------|"
|
|
427
|
+
child_jobs.each do |job|
|
|
428
|
+
job_data = job.collector_data("job") || {}
|
|
429
|
+
lines << "| #{job_data['job_class'] || job.path} | #{job_data['status'] || '-'} | #{job.duration.round(2)} ms | #{job.token} |"
|
|
430
|
+
end
|
|
431
|
+
lines << ""
|
|
432
|
+
lines
|
|
433
|
+
end
|
|
434
|
+
|
|
389
435
|
def self.generate_curl(profile, req_data)
|
|
390
436
|
headers = req_data&.dig("headers") || {}
|
|
391
437
|
params = req_data&.dig("params") || {}
|
|
@@ -4,7 +4,7 @@ module Profiler
|
|
|
4
4
|
module MCP
|
|
5
5
|
module Tools
|
|
6
6
|
class QueryJobs
|
|
7
|
-
ALL_FIELDS = %w[time job_class queue status duration token].freeze
|
|
7
|
+
ALL_FIELDS = %w[time job_class queue status duration token parent_token].freeze
|
|
8
8
|
|
|
9
9
|
def self.call(params)
|
|
10
10
|
limit = params["limit"]&.to_i || 20
|
|
@@ -59,12 +59,13 @@ module Profiler
|
|
|
59
59
|
job_data = profile.collector_data("job") || {}
|
|
60
60
|
row = fields.map do |f|
|
|
61
61
|
case f
|
|
62
|
-
when "time"
|
|
63
|
-
when "job_class"
|
|
64
|
-
when "queue"
|
|
65
|
-
when "status"
|
|
66
|
-
when "duration"
|
|
67
|
-
when "token"
|
|
62
|
+
when "time" then profile.started_at.strftime("%H:%M:%S")
|
|
63
|
+
when "job_class" then job_data["job_class"] || profile.path
|
|
64
|
+
when "queue" then job_data["queue"] || "-"
|
|
65
|
+
when "status" then job_data["status"] || "-"
|
|
66
|
+
when "duration" then "#{profile.duration.round(2)}ms"
|
|
67
|
+
when "token" then profile.token.to_s
|
|
68
|
+
when "parent_token" then profile.parent_token || "-"
|
|
68
69
|
end
|
|
69
70
|
end
|
|
70
71
|
lines << "| #{row.join(' | ')} |"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../models/profile"
|
|
4
|
+
require_relative "../current_context"
|
|
4
5
|
require_relative "toolbar_injector"
|
|
5
6
|
|
|
6
7
|
module Profiler
|
|
@@ -14,6 +15,7 @@ module Profiler
|
|
|
14
15
|
return @app.call(env) unless should_profile?(env)
|
|
15
16
|
|
|
16
17
|
profile = Models::Profile.new(build_request(env))
|
|
18
|
+
Profiler::CurrentContext.token = profile.token
|
|
17
19
|
|
|
18
20
|
# Capture request body before app processes it
|
|
19
21
|
req_body_raw = read_rack_input(env)
|
|
@@ -64,6 +66,7 @@ module Profiler
|
|
|
64
66
|
|
|
65
67
|
# Store profile
|
|
66
68
|
Profiler.storage.save(profile.token, profile)
|
|
69
|
+
Profiler::CurrentContext.clear
|
|
67
70
|
|
|
68
71
|
# Add profiler token header
|
|
69
72
|
headers["X-Profiler-Token"] = profile.token
|
data/lib/profiler/railtie.rb
CHANGED
|
@@ -62,6 +62,11 @@ module Profiler
|
|
|
62
62
|
chain.add Profiler::Instrumentation::SidekiqMiddleware
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
|
+
Sidekiq.configure_client do |config|
|
|
66
|
+
config.client_middleware do |chain|
|
|
67
|
+
chain.add Profiler::Instrumentation::SidekiqClientMiddleware
|
|
68
|
+
end
|
|
69
|
+
end
|
|
65
70
|
end
|
|
66
71
|
|
|
67
72
|
if defined?(ActiveJob::Base)
|
data/lib/profiler/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sébastien Duplessy
|
|
@@ -139,6 +139,7 @@ files:
|
|
|
139
139
|
- lib/profiler/collectors/routes_collector.rb
|
|
140
140
|
- lib/profiler/collectors/view_collector.rb
|
|
141
141
|
- lib/profiler/configuration.rb
|
|
142
|
+
- lib/profiler/current_context.rb
|
|
142
143
|
- lib/profiler/engine.rb
|
|
143
144
|
- lib/profiler/explain_runner.rb
|
|
144
145
|
- lib/profiler/instrumentation/active_job_instrumentation.rb
|