rails-profiler 0.13.0 → 0.15.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.js +638 -5
- data/app/controllers/profiler/api/env_vars_controller.rb +33 -0
- 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/config/routes.rb +1 -0
- data/lib/profiler/collectors/env_collector.rb +38 -0
- 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
- data/lib/profiler.rb +1 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4f9f680d793888c215c4b0203f3f4ceb538d415de05916400730fd86561e1c84
|
|
4
|
+
data.tar.gz: d0d944230d9e29d4fea24b54c6ea7134d1994e6624c1e7d0994cd7900a716406
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b46d2fd2ecc46a051a5616115a0c2ef8b4da155b009ac5734e5e44fe355e14b2b30c4cdeaf52721d403436eebb5076ca6b3c6c3343407d326cdae17394607df7
|
|
7
|
+
data.tar.gz: fc1e2d5d01929200d5fe4f705db509fce7daed7ed13f8a474b31196e397bf797b912a85add2c0f2ae662f3d90b94f65e8dcb383ea142f1706420dfe68b4a961f
|
|
@@ -3577,6 +3577,602 @@
|
|
|
3577
3577
|
] });
|
|
3578
3578
|
}
|
|
3579
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
|
+
|
|
3621
|
+
// app/assets/typescript/profiler/components/dashboard/tabs/EnvTab.tsx
|
|
3622
|
+
function detectType(value) {
|
|
3623
|
+
if (value === "") return { label: "empty", color: "var(--profiler-text-muted)" };
|
|
3624
|
+
if (/^(true|false|yes|no)$/i.test(value)) return { label: "bool", color: "#a78bfa" };
|
|
3625
|
+
if (/^\d+$/.test(value)) return { label: "int", color: "var(--profiler-success,#22c55e)" };
|
|
3626
|
+
if (/^\d+\.\d+$/.test(value)) return { label: "float", color: "var(--profiler-success,#22c55e)" };
|
|
3627
|
+
return { label: "string", color: "var(--profiler-text-muted)" };
|
|
3628
|
+
}
|
|
3629
|
+
function getPrefix(key) {
|
|
3630
|
+
const idx = key.indexOf("_");
|
|
3631
|
+
return idx > 0 ? key.slice(0, idx) : "OTHER";
|
|
3632
|
+
}
|
|
3633
|
+
function parseEnvFile(content) {
|
|
3634
|
+
const result = {};
|
|
3635
|
+
for (const raw of content.split("\n")) {
|
|
3636
|
+
const line = raw.trim();
|
|
3637
|
+
if (!line || line.startsWith("#")) continue;
|
|
3638
|
+
const eq = line.indexOf("=");
|
|
3639
|
+
if (eq < 1) continue;
|
|
3640
|
+
const key = line.slice(0, eq).trim();
|
|
3641
|
+
let value = line.slice(eq + 1);
|
|
3642
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
3643
|
+
value = value.slice(1, -1);
|
|
3644
|
+
}
|
|
3645
|
+
result[key] = value;
|
|
3646
|
+
}
|
|
3647
|
+
return result;
|
|
3648
|
+
}
|
|
3649
|
+
function loadUnmasked() {
|
|
3650
|
+
try {
|
|
3651
|
+
const raw = localStorage.getItem("profiler_unmasked_env_keys");
|
|
3652
|
+
return raw ? new Set(JSON.parse(raw)) : /* @__PURE__ */ new Set();
|
|
3653
|
+
} catch {
|
|
3654
|
+
return /* @__PURE__ */ new Set();
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
function saveUnmasked(keys) {
|
|
3658
|
+
try {
|
|
3659
|
+
localStorage.setItem("profiler_unmasked_env_keys", JSON.stringify([...keys]));
|
|
3660
|
+
} catch {
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
function TypeBadgeComp({ value }) {
|
|
3664
|
+
const badge = detectType(value);
|
|
3665
|
+
if (!badge) return null;
|
|
3666
|
+
return /* @__PURE__ */ u3("span", { style: {
|
|
3667
|
+
display: "inline-block",
|
|
3668
|
+
padding: "0 5px",
|
|
3669
|
+
borderRadius: "3px",
|
|
3670
|
+
fontSize: "10px",
|
|
3671
|
+
fontWeight: 600,
|
|
3672
|
+
fontFamily: "monospace",
|
|
3673
|
+
border: `1px solid ${badge.color}`,
|
|
3674
|
+
color: badge.color,
|
|
3675
|
+
marginRight: "6px",
|
|
3676
|
+
verticalAlign: "middle",
|
|
3677
|
+
lineHeight: "16px",
|
|
3678
|
+
flexShrink: 0
|
|
3679
|
+
}, children: badge.label });
|
|
3680
|
+
}
|
|
3681
|
+
function isBool(value) {
|
|
3682
|
+
return /^(true|false|yes|no)$/i.test(value);
|
|
3683
|
+
}
|
|
3684
|
+
function isQuoted(value) {
|
|
3685
|
+
if (value === "") return false;
|
|
3686
|
+
if (isBool(value)) return false;
|
|
3687
|
+
if (/^\d+(\.\d+)?$/.test(value)) return false;
|
|
3688
|
+
return true;
|
|
3689
|
+
}
|
|
3690
|
+
async function patchEnvVar(key, value) {
|
|
3691
|
+
const res = await fetch("/_profiler/api/env_vars", {
|
|
3692
|
+
method: "PATCH",
|
|
3693
|
+
headers: { "Content-Type": "application/json" },
|
|
3694
|
+
body: JSON.stringify({ key, value })
|
|
3695
|
+
});
|
|
3696
|
+
if (!res.ok) {
|
|
3697
|
+
const body = await res.json().catch(() => ({}));
|
|
3698
|
+
throw new Error(body.error ?? `Request failed (${res.status})`);
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
function EnvTab({ envData }) {
|
|
3702
|
+
const initial = T2(() => ({ ...envData?.variables ?? {} }), []);
|
|
3703
|
+
const [variables, setVariables] = d2(initial);
|
|
3704
|
+
const [total, setTotal] = d2(envData?.total ?? 0);
|
|
3705
|
+
const [search, setSearch] = d2("");
|
|
3706
|
+
const [collapsedGroups, setCollapsedGroups] = d2(/* @__PURE__ */ new Set());
|
|
3707
|
+
const [editingKey, setEditingKey] = d2(null);
|
|
3708
|
+
const [editValue, setEditValue] = d2("");
|
|
3709
|
+
const [newKey, setNewKey] = d2("");
|
|
3710
|
+
const [newValue, setNewValue] = d2("");
|
|
3711
|
+
const [flash, setFlash] = d2(null);
|
|
3712
|
+
const [saving, setSaving] = d2(false);
|
|
3713
|
+
const [refreshing, setRefreshing] = d2(false);
|
|
3714
|
+
const [unmaskedKeys, setUnmaskedKeys] = d2(loadUnmasked);
|
|
3715
|
+
const [copiedId, setCopiedId] = d2(null);
|
|
3716
|
+
const [readOnly, setReadOnly] = d2(false);
|
|
3717
|
+
const [showImport, setShowImport] = d2(false);
|
|
3718
|
+
const [importContent, setImportContent] = d2("");
|
|
3719
|
+
const editInputRef = A2(null);
|
|
3720
|
+
y2(() => {
|
|
3721
|
+
if (editingKey !== null) editInputRef.current?.focus();
|
|
3722
|
+
}, [editingKey]);
|
|
3723
|
+
y2(() => {
|
|
3724
|
+
refresh();
|
|
3725
|
+
}, []);
|
|
3726
|
+
const showFlash = (type, message) => {
|
|
3727
|
+
setFlash({ type, message });
|
|
3728
|
+
setTimeout(() => setFlash(null), 3e3);
|
|
3729
|
+
};
|
|
3730
|
+
const toggleUnmask = (key) => {
|
|
3731
|
+
setUnmaskedKeys((prev) => {
|
|
3732
|
+
const next = new Set(prev);
|
|
3733
|
+
if (next.has(key)) next.delete(key);
|
|
3734
|
+
else next.add(key);
|
|
3735
|
+
saveUnmasked(next);
|
|
3736
|
+
return next;
|
|
3737
|
+
});
|
|
3738
|
+
};
|
|
3739
|
+
const copy = async (text, id) => {
|
|
3740
|
+
try {
|
|
3741
|
+
await navigator.clipboard.writeText(text);
|
|
3742
|
+
setCopiedId(id);
|
|
3743
|
+
setTimeout(() => setCopiedId(null), 1500);
|
|
3744
|
+
} catch {
|
|
3745
|
+
showFlash("error", "Clipboard access denied");
|
|
3746
|
+
}
|
|
3747
|
+
};
|
|
3748
|
+
const toggleGroup = (prefix) => {
|
|
3749
|
+
setCollapsedGroups((prev) => {
|
|
3750
|
+
const next = new Set(prev);
|
|
3751
|
+
if (next.has(prefix)) next.delete(prefix);
|
|
3752
|
+
else next.add(prefix);
|
|
3753
|
+
return next;
|
|
3754
|
+
});
|
|
3755
|
+
};
|
|
3756
|
+
const refresh = async () => {
|
|
3757
|
+
setRefreshing(true);
|
|
3758
|
+
try {
|
|
3759
|
+
const res = await fetch("/_profiler/api/env_vars");
|
|
3760
|
+
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
|
3761
|
+
const data = await res.json();
|
|
3762
|
+
setVariables(data.variables);
|
|
3763
|
+
setTotal(data.total);
|
|
3764
|
+
showFlash("success", `Refreshed \u2014 ${data.total} variables`);
|
|
3765
|
+
} catch (e3) {
|
|
3766
|
+
showFlash("error", e3.message ?? "Failed to refresh");
|
|
3767
|
+
} finally {
|
|
3768
|
+
setRefreshing(false);
|
|
3769
|
+
}
|
|
3770
|
+
};
|
|
3771
|
+
const exportEnv = () => {
|
|
3772
|
+
const entries = filteredEntries;
|
|
3773
|
+
const content = entries.map(([k3, v3]) => `${k3}=${v3}`).join("\n") + "\n";
|
|
3774
|
+
const blob = new Blob([content], { type: "text/plain" });
|
|
3775
|
+
const url = URL.createObjectURL(blob);
|
|
3776
|
+
const a3 = document.createElement("a");
|
|
3777
|
+
a3.href = url;
|
|
3778
|
+
a3.download = ".env";
|
|
3779
|
+
a3.click();
|
|
3780
|
+
URL.revokeObjectURL(url);
|
|
3781
|
+
showFlash("success", `Exported ${entries.length} variables`);
|
|
3782
|
+
};
|
|
3783
|
+
const importPreview = T2(() => parseEnvFile(importContent), [importContent]);
|
|
3784
|
+
const importCount = Object.keys(importPreview).length;
|
|
3785
|
+
const importEnv = async () => {
|
|
3786
|
+
const entries = Object.entries(importPreview);
|
|
3787
|
+
if (!entries.length) return;
|
|
3788
|
+
setSaving(true);
|
|
3789
|
+
try {
|
|
3790
|
+
await Promise.all(entries.map(([k3, v3]) => patchEnvVar(k3, v3)));
|
|
3791
|
+
setVariables((prev) => ({ ...prev, ...importPreview }));
|
|
3792
|
+
setImportContent("");
|
|
3793
|
+
setShowImport(false);
|
|
3794
|
+
showFlash("success", `Imported ${entries.length} variables`);
|
|
3795
|
+
} catch (e3) {
|
|
3796
|
+
showFlash("error", e3.message ?? "Import failed");
|
|
3797
|
+
} finally {
|
|
3798
|
+
setSaving(false);
|
|
3799
|
+
}
|
|
3800
|
+
};
|
|
3801
|
+
const startEdit = (key) => {
|
|
3802
|
+
setEditingKey(key);
|
|
3803
|
+
setEditValue(variables[key] ?? "");
|
|
3804
|
+
};
|
|
3805
|
+
const cancelEdit = () => {
|
|
3806
|
+
setEditingKey(null);
|
|
3807
|
+
setEditValue("");
|
|
3808
|
+
};
|
|
3809
|
+
const saveEdit = async () => {
|
|
3810
|
+
if (!editingKey) return;
|
|
3811
|
+
setSaving(true);
|
|
3812
|
+
try {
|
|
3813
|
+
await patchEnvVar(editingKey, editValue);
|
|
3814
|
+
setVariables((prev) => ({ ...prev, [editingKey]: editValue }));
|
|
3815
|
+
showFlash("success", `${editingKey} updated`);
|
|
3816
|
+
setEditingKey(null);
|
|
3817
|
+
} catch (e3) {
|
|
3818
|
+
showFlash("error", e3.message ?? "Failed to update");
|
|
3819
|
+
} finally {
|
|
3820
|
+
setSaving(false);
|
|
3821
|
+
}
|
|
3822
|
+
};
|
|
3823
|
+
const deleteVar = async (key) => {
|
|
3824
|
+
setSaving(true);
|
|
3825
|
+
try {
|
|
3826
|
+
await patchEnvVar(key, null);
|
|
3827
|
+
setVariables((prev) => {
|
|
3828
|
+
const n2 = { ...prev };
|
|
3829
|
+
delete n2[key];
|
|
3830
|
+
return n2;
|
|
3831
|
+
});
|
|
3832
|
+
setUnmaskedKeys((prev) => {
|
|
3833
|
+
const n2 = new Set(prev);
|
|
3834
|
+
n2.delete(key);
|
|
3835
|
+
saveUnmasked(n2);
|
|
3836
|
+
return n2;
|
|
3837
|
+
});
|
|
3838
|
+
showFlash("success", `${key} deleted`);
|
|
3839
|
+
} catch (e3) {
|
|
3840
|
+
showFlash("error", e3.message ?? "Failed to delete");
|
|
3841
|
+
} finally {
|
|
3842
|
+
setSaving(false);
|
|
3843
|
+
}
|
|
3844
|
+
};
|
|
3845
|
+
const toggleBool = async (key, current) => {
|
|
3846
|
+
const next = /^(true|yes)$/i.test(current) ? "false" : "true";
|
|
3847
|
+
setSaving(true);
|
|
3848
|
+
try {
|
|
3849
|
+
await patchEnvVar(key, next);
|
|
3850
|
+
setVariables((prev) => ({ ...prev, [key]: next }));
|
|
3851
|
+
} catch (e3) {
|
|
3852
|
+
showFlash("error", e3.message ?? "Failed to update");
|
|
3853
|
+
} finally {
|
|
3854
|
+
setSaving(false);
|
|
3855
|
+
}
|
|
3856
|
+
};
|
|
3857
|
+
const addVar = async () => {
|
|
3858
|
+
const key = newKey.trim();
|
|
3859
|
+
if (!key) return;
|
|
3860
|
+
setSaving(true);
|
|
3861
|
+
try {
|
|
3862
|
+
await patchEnvVar(key, newValue);
|
|
3863
|
+
setVariables((prev) => ({ ...prev, [key]: newValue }));
|
|
3864
|
+
setNewKey("");
|
|
3865
|
+
setNewValue("");
|
|
3866
|
+
showFlash("success", `${key} added`);
|
|
3867
|
+
} catch (e3) {
|
|
3868
|
+
showFlash("error", e3.message ?? "Failed to add");
|
|
3869
|
+
} finally {
|
|
3870
|
+
setSaving(false);
|
|
3871
|
+
}
|
|
3872
|
+
};
|
|
3873
|
+
const handleEditKeyDown = (e3) => {
|
|
3874
|
+
if (e3.key === "Enter") saveEdit();
|
|
3875
|
+
if (e3.key === "Escape") cancelEdit();
|
|
3876
|
+
};
|
|
3877
|
+
const allEntries = Object.entries(variables);
|
|
3878
|
+
const filteredEntries = T2(() => {
|
|
3879
|
+
const q2 = search.toLowerCase().trim();
|
|
3880
|
+
if (!q2) return allEntries;
|
|
3881
|
+
return allEntries.filter(
|
|
3882
|
+
([k3, v3]) => k3.toLowerCase().includes(q2) || v3.toLowerCase().includes(q2)
|
|
3883
|
+
);
|
|
3884
|
+
}, [variables, search]);
|
|
3885
|
+
const groups = T2(() => {
|
|
3886
|
+
const map = {};
|
|
3887
|
+
for (const entry of filteredEntries) {
|
|
3888
|
+
const p3 = getPrefix(entry[0]);
|
|
3889
|
+
(map[p3] ?? (map[p3] = [])).push(entry);
|
|
3890
|
+
}
|
|
3891
|
+
return Object.entries(map).sort(
|
|
3892
|
+
([a3], [b]) => a3 === "OTHER" ? 1 : b === "OTHER" ? -1 : a3.localeCompare(b)
|
|
3893
|
+
);
|
|
3894
|
+
}, [filteredEntries]);
|
|
3895
|
+
const allRevealed = filteredEntries.every(([k3]) => unmaskedKeys.has(k3));
|
|
3896
|
+
const toggleAllMasks = () => {
|
|
3897
|
+
const keys = filteredEntries.map(([k3]) => k3);
|
|
3898
|
+
setUnmaskedKeys((prev) => {
|
|
3899
|
+
const next = new Set(prev);
|
|
3900
|
+
if (allRevealed) {
|
|
3901
|
+
keys.forEach((k3) => next.delete(k3));
|
|
3902
|
+
} else {
|
|
3903
|
+
keys.forEach((k3) => next.add(k3));
|
|
3904
|
+
}
|
|
3905
|
+
saveUnmasked(next);
|
|
3906
|
+
return next;
|
|
3907
|
+
});
|
|
3908
|
+
};
|
|
3909
|
+
const inputStyle = "width:100%;font-family:monospace;font-size:12px;padding:2px 6px;border:1px solid var(--profiler-border);border-radius:4px;background:var(--profiler-bg);color:var(--profiler-text);";
|
|
3910
|
+
const iconBtn = "background:none;border:none;cursor:pointer;font-size:12px;padding:0 3px;color:var(--profiler-text-muted);opacity:0.7;flex-shrink:0;";
|
|
3911
|
+
const headerBtn = "background:none;border:1px solid var(--profiler-border);border-radius:4px;cursor:pointer;color:var(--profiler-text-muted);font-size:11px;padding:3px 8px;";
|
|
3912
|
+
return /* @__PURE__ */ u3(k, { children: [
|
|
3913
|
+
/* @__PURE__ */ u3("div", { class: "profiler-flex profiler-mb-4", style: "align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;", children: [
|
|
3914
|
+
/* @__PURE__ */ u3("h2", { class: "profiler-section__header", style: "margin:0;", children: "Environment Variables" }),
|
|
3915
|
+
/* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2", children: [
|
|
3916
|
+
/* @__PURE__ */ u3("button", { onClick: refresh, disabled: refreshing, style: headerBtn, children: refreshing ? "\u2026" : "\u21BA Refresh" }),
|
|
3917
|
+
/* @__PURE__ */ u3("button", { onClick: () => setShowImport((v3) => !v3), style: `${headerBtn}${showImport ? "border-color:var(--profiler-accent);color:var(--profiler-accent);" : ""}`, children: "\u2B06 Import" }),
|
|
3918
|
+
/* @__PURE__ */ u3("button", { onClick: exportEnv, style: headerBtn, children: "\u2B07 Export" }),
|
|
3919
|
+
/* @__PURE__ */ u3("button", { onClick: toggleAllMasks, style: headerBtn, children: allRevealed ? "\u{1F648} Mask all" : "\u{1F441} Reveal all" }),
|
|
3920
|
+
/* @__PURE__ */ u3(
|
|
3921
|
+
"button",
|
|
3922
|
+
{
|
|
3923
|
+
onClick: () => setReadOnly((v3) => !v3),
|
|
3924
|
+
title: readOnly ? "Unlock editing" : "Lock editing",
|
|
3925
|
+
style: `${headerBtn}${readOnly ? "border-color:var(--profiler-error,#ef4444);color:var(--profiler-error,#ef4444);" : ""}`,
|
|
3926
|
+
children: readOnly ? "\u{1F512} Locked" : "\u{1F513} Edit"
|
|
3927
|
+
}
|
|
3928
|
+
)
|
|
3929
|
+
] })
|
|
3930
|
+
] }),
|
|
3931
|
+
/* @__PURE__ */ u3("div", { style: "background:var(--profiler-warning-bg,rgba(245,158,11,0.1));border:1px solid var(--profiler-warning,#f59e0b);border-radius:6px;padding:8px 12px;margin-bottom:16px;font-size:12px;color:var(--profiler-warning,#f59e0b);", children: "\u26A0 Changes affect the current process only \u2014 for development use." }),
|
|
3932
|
+
flash && /* @__PURE__ */ u3("div", { style: `background:${flash.type === "success" ? "var(--profiler-success-bg,rgba(34,197,94,0.1))" : "var(--profiler-error-bg,rgba(239,68,68,0.08))"};border:1px solid ${flash.type === "success" ? "var(--profiler-success,#22c55e)" : "var(--profiler-error,#ef4444)"};border-radius:6px;padding:6px 12px;margin-bottom:12px;font-size:12px;color:${flash.type === "success" ? "var(--profiler-success,#22c55e)" : "var(--profiler-error,#ef4444)"};`, children: [
|
|
3933
|
+
flash.type === "success" ? "\u2713" : "\u2717",
|
|
3934
|
+
" ",
|
|
3935
|
+
flash.message
|
|
3936
|
+
] }),
|
|
3937
|
+
showImport && /* @__PURE__ */ u3("div", { style: "border:1px solid var(--profiler-border);border-radius:6px;padding:12px;margin-bottom:16px;", children: [
|
|
3938
|
+
/* @__PURE__ */ u3("p", { class: "profiler-text--xs profiler-text--muted", style: "margin:0 0 8px;", children: [
|
|
3939
|
+
"Paste the contents of a ",
|
|
3940
|
+
/* @__PURE__ */ u3("code", { children: ".env" }),
|
|
3941
|
+
" file. Comments and blank lines are ignored."
|
|
3942
|
+
] }),
|
|
3943
|
+
/* @__PURE__ */ u3(
|
|
3944
|
+
"textarea",
|
|
3945
|
+
{
|
|
3946
|
+
value: importContent,
|
|
3947
|
+
onInput: (e3) => setImportContent(e3.target.value),
|
|
3948
|
+
placeholder: "APP_NAME=MyApp\nFEATURE_BETA=true\n# comment",
|
|
3949
|
+
rows: 6,
|
|
3950
|
+
style: "width:100%;font-family:monospace;font-size:12px;padding:6px 8px;border:1px solid var(--profiler-border);border-radius:4px;background:var(--profiler-bg);color:var(--profiler-text);resize:vertical;box-sizing:border-box;"
|
|
3951
|
+
}
|
|
3952
|
+
),
|
|
3953
|
+
/* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2 profiler-mt-2", style: "align-items:center;", children: [
|
|
3954
|
+
importCount > 0 && /* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--muted", children: [
|
|
3955
|
+
importCount,
|
|
3956
|
+
" variable",
|
|
3957
|
+
importCount !== 1 ? "s" : "",
|
|
3958
|
+
" to import"
|
|
3959
|
+
] }),
|
|
3960
|
+
/* @__PURE__ */ u3(
|
|
3961
|
+
"button",
|
|
3962
|
+
{
|
|
3963
|
+
onClick: importEnv,
|
|
3964
|
+
disabled: saving || importCount === 0,
|
|
3965
|
+
style: "background:var(--profiler-accent);border:none;cursor:pointer;color:#fff;font-size:11px;padding:4px 12px;border-radius:4px;margin-left:auto;",
|
|
3966
|
+
children: "Apply"
|
|
3967
|
+
}
|
|
3968
|
+
)
|
|
3969
|
+
] })
|
|
3970
|
+
] }),
|
|
3971
|
+
/* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-4 profiler-mb-4", children: [
|
|
3972
|
+
/* @__PURE__ */ u3("div", { class: "profiler-stat-card", children: [
|
|
3973
|
+
/* @__PURE__ */ u3("div", { class: "profiler-stat-card__value", children: total }),
|
|
3974
|
+
/* @__PURE__ */ u3("div", { class: "profiler-stat-card__label", children: "Total vars" })
|
|
3975
|
+
] }),
|
|
3976
|
+
search && /* @__PURE__ */ u3("div", { class: "profiler-stat-card", children: [
|
|
3977
|
+
/* @__PURE__ */ u3("div", { class: "profiler-stat-card__value", children: filteredEntries.length }),
|
|
3978
|
+
/* @__PURE__ */ u3("div", { class: "profiler-stat-card__label", children: "Matching" })
|
|
3979
|
+
] })
|
|
3980
|
+
] }),
|
|
3981
|
+
/* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2 profiler-mb-3", children: /* @__PURE__ */ u3(
|
|
3982
|
+
"input",
|
|
3983
|
+
{
|
|
3984
|
+
type: "text",
|
|
3985
|
+
class: "profiler-filter-input",
|
|
3986
|
+
placeholder: "Filter by key or value\u2026",
|
|
3987
|
+
value: search,
|
|
3988
|
+
onInput: (e3) => setSearch(e3.target.value),
|
|
3989
|
+
style: "flex:1;"
|
|
3990
|
+
}
|
|
3991
|
+
) }),
|
|
3992
|
+
/* @__PURE__ */ u3("table", { class: "profiler-table", children: [
|
|
3993
|
+
/* @__PURE__ */ u3("thead", { children: /* @__PURE__ */ u3("tr", { children: [
|
|
3994
|
+
/* @__PURE__ */ u3("th", { style: "width:32%", children: "Key" }),
|
|
3995
|
+
/* @__PURE__ */ u3("th", { children: "Value" }),
|
|
3996
|
+
!readOnly && /* @__PURE__ */ u3("th", { style: "width:120px;text-align:right;" })
|
|
3997
|
+
] }) }),
|
|
3998
|
+
/* @__PURE__ */ u3("tbody", { children: [
|
|
3999
|
+
groups.map(([prefix, entries]) => {
|
|
4000
|
+
const collapsed = collapsedGroups.has(prefix);
|
|
4001
|
+
return /* @__PURE__ */ u3(k, { children: [
|
|
4002
|
+
/* @__PURE__ */ u3(
|
|
4003
|
+
"tr",
|
|
4004
|
+
{
|
|
4005
|
+
onClick: () => toggleGroup(prefix),
|
|
4006
|
+
style: "cursor:pointer;background:var(--profiler-bg-subtle,rgba(0,0,0,0.03));",
|
|
4007
|
+
children: /* @__PURE__ */ u3("td", { colspan: readOnly ? 2 : 3, style: "padding:4px 8px;", children: [
|
|
4008
|
+
/* @__PURE__ */ u3("span", { style: "font-size:11px;font-weight:600;color:var(--profiler-text-muted);letter-spacing:0.05em;font-family:monospace;", children: [
|
|
4009
|
+
collapsed ? "\u25B6" : "\u25BC",
|
|
4010
|
+
" ",
|
|
4011
|
+
prefix,
|
|
4012
|
+
"_"
|
|
4013
|
+
] }),
|
|
4014
|
+
/* @__PURE__ */ u3("span", { style: "font-size:10px;color:var(--profiler-text-muted);margin-left:6px;", children: entries.length })
|
|
4015
|
+
] })
|
|
4016
|
+
},
|
|
4017
|
+
`group-${prefix}`
|
|
4018
|
+
),
|
|
4019
|
+
!collapsed && entries.map(([key, value]) => {
|
|
4020
|
+
const unmasked = unmaskedKeys.has(key);
|
|
4021
|
+
const isEditing = editingKey === key;
|
|
4022
|
+
const isCopiedVal = copiedId === key;
|
|
4023
|
+
const isCopiedKey = copiedId === `__key__${key}`;
|
|
4024
|
+
const wasModified = initial[key] !== void 0 && initial[key] !== value;
|
|
4025
|
+
const wasAdded = initial[key] === void 0;
|
|
4026
|
+
const diffStyle = wasModified ? "border-left:3px solid #f59e0b;" : wasAdded ? "border-left:3px solid var(--profiler-success,#22c55e);" : "";
|
|
4027
|
+
return /* @__PURE__ */ u3("tr", { style: diffStyle, children: [
|
|
4028
|
+
/* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("div", { class: "profiler-flex", style: "align-items:center;gap:4px;", children: [
|
|
4029
|
+
/* @__PURE__ */ u3("code", { class: "profiler-text--xs profiler-text--mono", children: key }),
|
|
4030
|
+
/* @__PURE__ */ u3(
|
|
4031
|
+
"button",
|
|
4032
|
+
{
|
|
4033
|
+
onClick: () => copy(key, `__key__${key}`),
|
|
4034
|
+
title: "Copy key",
|
|
4035
|
+
style: `${iconBtn}${isCopiedKey ? "color:var(--profiler-success,#22c55e);opacity:1;" : ""}`,
|
|
4036
|
+
children: isCopiedKey ? "\u2713" : "\u2398"
|
|
4037
|
+
}
|
|
4038
|
+
)
|
|
4039
|
+
] }) }),
|
|
4040
|
+
/* @__PURE__ */ u3("td", { children: isEditing ? /* @__PURE__ */ u3("div", { class: "profiler-flex", style: "align-items:center;gap:2px;", children: [
|
|
4041
|
+
isQuoted(editValue) && /* @__PURE__ */ u3("span", { style: "font-family:monospace;font-size:12px;color:var(--profiler-text-muted);opacity:0.5;flex-shrink:0;", children: '"' }),
|
|
4042
|
+
/* @__PURE__ */ u3(
|
|
4043
|
+
"input",
|
|
4044
|
+
{
|
|
4045
|
+
ref: editInputRef,
|
|
4046
|
+
type: "text",
|
|
4047
|
+
value: editValue,
|
|
4048
|
+
onInput: (e3) => setEditValue(e3.target.value),
|
|
4049
|
+
onKeyDown: handleEditKeyDown,
|
|
4050
|
+
disabled: saving,
|
|
4051
|
+
style: "flex:1;font-family:monospace;font-size:12px;padding:2px 6px;border:1px solid var(--profiler-accent);border-radius:4px;background:var(--profiler-bg);color:var(--profiler-text);"
|
|
4052
|
+
}
|
|
4053
|
+
),
|
|
4054
|
+
isQuoted(editValue) && /* @__PURE__ */ u3("span", { style: "font-family:monospace;font-size:12px;color:var(--profiler-text-muted);opacity:0.5;flex-shrink:0;", children: '"' })
|
|
4055
|
+
] }) : isBool(value) ? /* @__PURE__ */ u3(k, { children: [
|
|
4056
|
+
/* @__PURE__ */ u3("div", { class: "profiler-flex", style: "align-items:center;gap:8px;", children: [
|
|
4057
|
+
/* @__PURE__ */ u3(TypeBadgeComp, { value }),
|
|
4058
|
+
/* @__PURE__ */ u3(
|
|
4059
|
+
"button",
|
|
4060
|
+
{
|
|
4061
|
+
onClick: () => !readOnly && !saving && toggleBool(key, value),
|
|
4062
|
+
disabled: saving || readOnly,
|
|
4063
|
+
title: `Click to set ${/^(true|yes)$/i.test(value) ? "false" : "true"}`,
|
|
4064
|
+
style: `position:relative;display:inline-block;width:36px;height:20px;border-radius:10px;border:none;cursor:${readOnly || saving ? "default" : "pointer"};padding:0;transition:background 0.2s;background:${/^(true|yes)$/i.test(value) ? "var(--profiler-accent)" : "var(--profiler-border,#d1d5db)"};flex-shrink:0;`,
|
|
4065
|
+
children: /* @__PURE__ */ u3("span", { style: `position:absolute;top:3px;width:14px;height:14px;border-radius:50%;background:#fff;transition:left 0.2s;left:${/^(true|yes)$/i.test(value) ? "19px" : "3px"};` })
|
|
4066
|
+
}
|
|
4067
|
+
),
|
|
4068
|
+
/* @__PURE__ */ u3("span", { class: "profiler-text--xs profiler-text--mono", style: "color:var(--profiler-text-muted);", children: value }),
|
|
4069
|
+
/* @__PURE__ */ u3(
|
|
4070
|
+
"button",
|
|
4071
|
+
{
|
|
4072
|
+
onClick: () => copy(value, key),
|
|
4073
|
+
title: "Copy value",
|
|
4074
|
+
style: `${iconBtn}${isCopiedVal ? "color:var(--profiler-success,#22c55e);opacity:1;" : ""}`,
|
|
4075
|
+
children: isCopiedVal ? "\u2713" : "\u2398"
|
|
4076
|
+
}
|
|
4077
|
+
)
|
|
4078
|
+
] }),
|
|
4079
|
+
wasModified && /* @__PURE__ */ u3("div", { style: "font-size:10px;color:#f59e0b;margin-top:2px;font-family:monospace;", children: [
|
|
4080
|
+
"was: ",
|
|
4081
|
+
initial[key]
|
|
4082
|
+
] })
|
|
4083
|
+
] }) : /* @__PURE__ */ u3(k, { children: [
|
|
4084
|
+
/* @__PURE__ */ u3("div", { class: "profiler-flex", style: "align-items:center;gap:4px;", children: [
|
|
4085
|
+
/* @__PURE__ */ u3(TypeBadgeComp, { value }),
|
|
4086
|
+
/* @__PURE__ */ u3(
|
|
4087
|
+
"span",
|
|
4088
|
+
{
|
|
4089
|
+
class: "profiler-text--xs profiler-text--mono",
|
|
4090
|
+
style: "word-break:break-all;flex:1;",
|
|
4091
|
+
children: unmasked ? isQuoted(value) ? /* @__PURE__ */ u3(k, { children: [
|
|
4092
|
+
/* @__PURE__ */ u3("span", { style: "color:var(--profiler-text-muted);opacity:0.5;", children: '"' }),
|
|
4093
|
+
value,
|
|
4094
|
+
/* @__PURE__ */ u3("span", { style: "color:var(--profiler-text-muted);opacity:0.5;", children: '"' })
|
|
4095
|
+
] }) : value : "\u2022".repeat(Math.min(value.length, 20))
|
|
4096
|
+
}
|
|
4097
|
+
),
|
|
4098
|
+
/* @__PURE__ */ u3(
|
|
4099
|
+
"button",
|
|
4100
|
+
{
|
|
4101
|
+
onClick: () => toggleUnmask(key),
|
|
4102
|
+
title: unmasked ? "Hide value" : "Show value",
|
|
4103
|
+
style: `${iconBtn}${unmasked ? "opacity:1;color:var(--profiler-accent);" : ""}`,
|
|
4104
|
+
children: unmasked ? "\u{1F441}" : "\u{1F648}"
|
|
4105
|
+
}
|
|
4106
|
+
),
|
|
4107
|
+
/* @__PURE__ */ u3(
|
|
4108
|
+
"button",
|
|
4109
|
+
{
|
|
4110
|
+
onClick: () => copy(value, key),
|
|
4111
|
+
title: "Copy value",
|
|
4112
|
+
style: `${iconBtn}${isCopiedVal ? "color:var(--profiler-success,#22c55e);opacity:1;" : ""}`,
|
|
4113
|
+
children: isCopiedVal ? "\u2713" : "\u2398"
|
|
4114
|
+
}
|
|
4115
|
+
)
|
|
4116
|
+
] }),
|
|
4117
|
+
wasModified && /* @__PURE__ */ u3("div", { style: "font-size:10px;color:#f59e0b;margin-top:2px;font-family:monospace;", children: [
|
|
4118
|
+
"was: ",
|
|
4119
|
+
unmasked ? isQuoted(initial[key]) ? `"${initial[key]}"` : initial[key] : "\u2022".repeat(Math.min(initial[key].length, 20))
|
|
4120
|
+
] })
|
|
4121
|
+
] }) }),
|
|
4122
|
+
!readOnly && /* @__PURE__ */ u3("td", { style: "text-align:right;white-space:nowrap;", children: isEditing ? /* @__PURE__ */ u3(k, { children: [
|
|
4123
|
+
/* @__PURE__ */ u3("button", { onClick: saveEdit, disabled: saving, title: "Save", style: "background:none;border:none;cursor:pointer;color:var(--profiler-success,#22c55e);font-size:14px;padding:0 4px;", children: "\u2713" }),
|
|
4124
|
+
/* @__PURE__ */ u3("button", { onClick: cancelEdit, disabled: saving, title: "Cancel", style: "background:none;border:none;cursor:pointer;color:var(--profiler-text-muted);font-size:14px;padding:0 4px;", children: "\u2717" })
|
|
4125
|
+
] }) : isBool(value) ? /* @__PURE__ */ u3(k, { children: [
|
|
4126
|
+
/* @__PURE__ */ u3("button", { onClick: () => startEdit(key), disabled: saving, style: "background:none;border:none;cursor:pointer;color:var(--profiler-accent);font-size:11px;padding:0 4px;", children: "Edit" }),
|
|
4127
|
+
/* @__PURE__ */ u3("button", { onClick: () => deleteVar(key), disabled: saving, style: "background:none;border:none;cursor:pointer;color:var(--profiler-error,#ef4444);font-size:11px;padding:0 4px;", children: "Delete" })
|
|
4128
|
+
] }) : /* @__PURE__ */ u3(k, { children: [
|
|
4129
|
+
/* @__PURE__ */ u3("button", { onClick: () => startEdit(key), disabled: saving, style: "background:none;border:none;cursor:pointer;color:var(--profiler-accent);font-size:11px;padding:0 4px;", children: "Edit" }),
|
|
4130
|
+
/* @__PURE__ */ u3("button", { onClick: () => deleteVar(key), disabled: saving, style: "background:none;border:none;cursor:pointer;color:var(--profiler-error,#ef4444);font-size:11px;padding:0 4px;", children: "Delete" })
|
|
4131
|
+
] }) })
|
|
4132
|
+
] }, key);
|
|
4133
|
+
})
|
|
4134
|
+
] });
|
|
4135
|
+
}),
|
|
4136
|
+
!readOnly && /* @__PURE__ */ u3("tr", { children: [
|
|
4137
|
+
/* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3(
|
|
4138
|
+
"input",
|
|
4139
|
+
{
|
|
4140
|
+
type: "text",
|
|
4141
|
+
value: newKey,
|
|
4142
|
+
onInput: (e3) => setNewKey(e3.target.value),
|
|
4143
|
+
onKeyDown: (e3) => e3.key === "Enter" && addVar(),
|
|
4144
|
+
placeholder: "NEW_KEY",
|
|
4145
|
+
disabled: saving,
|
|
4146
|
+
style: inputStyle
|
|
4147
|
+
}
|
|
4148
|
+
) }),
|
|
4149
|
+
/* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3(
|
|
4150
|
+
"input",
|
|
4151
|
+
{
|
|
4152
|
+
type: "text",
|
|
4153
|
+
value: newValue,
|
|
4154
|
+
onInput: (e3) => setNewValue(e3.target.value),
|
|
4155
|
+
onKeyDown: (e3) => e3.key === "Enter" && addVar(),
|
|
4156
|
+
placeholder: "value",
|
|
4157
|
+
disabled: saving,
|
|
4158
|
+
style: inputStyle
|
|
4159
|
+
}
|
|
4160
|
+
) }),
|
|
4161
|
+
/* @__PURE__ */ u3("td", { style: "text-align:right;", children: /* @__PURE__ */ u3(
|
|
4162
|
+
"button",
|
|
4163
|
+
{
|
|
4164
|
+
onClick: addVar,
|
|
4165
|
+
disabled: saving || !newKey.trim(),
|
|
4166
|
+
style: "background:var(--profiler-accent);border:none;cursor:pointer;color:#fff;font-size:11px;padding:3px 8px;border-radius:4px;",
|
|
4167
|
+
children: "Add"
|
|
4168
|
+
}
|
|
4169
|
+
) })
|
|
4170
|
+
] })
|
|
4171
|
+
] })
|
|
4172
|
+
] })
|
|
4173
|
+
] });
|
|
4174
|
+
}
|
|
4175
|
+
|
|
3580
4176
|
// app/assets/typescript/profiler/components/dashboard/ProfileDashboard.tsx
|
|
3581
4177
|
function ProfileDashboard({ profile, initialTab, embedded }) {
|
|
3582
4178
|
const cd = profile.collectors_data || {};
|
|
@@ -3586,6 +4182,7 @@
|
|
|
3586
4182
|
const hasLogs = (cd["logs"]?.count ?? 0) > 0;
|
|
3587
4183
|
const hasRoutes = (cd["routes"]?.total ?? 0) > 0;
|
|
3588
4184
|
const hasI18n = (cd["i18n"]?.total ?? 0) > 0;
|
|
4185
|
+
const hasJobs = (profile.child_jobs?.length ?? 0) > 0;
|
|
3589
4186
|
const [activeTab, setActiveTab] = d2(hasException ? "exception" : initialTab);
|
|
3590
4187
|
const handleTabClick = (tab) => (e3) => {
|
|
3591
4188
|
e3.preventDefault();
|
|
@@ -3641,7 +4238,13 @@
|
|
|
3641
4238
|
/* @__PURE__ */ u3("a", { href: "#", class: tabClass("cache"), onClick: handleTabClick("cache"), children: "Cache" }),
|
|
3642
4239
|
hasLogs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("logs"), onClick: handleTabClick("logs"), children: "Logs" }),
|
|
3643
4240
|
hasRoutes && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("routes"), onClick: handleTabClick("routes"), children: "Routes" }),
|
|
3644
|
-
hasI18n && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("i18n"), onClick: handleTabClick("i18n"), children: "I18n" })
|
|
4241
|
+
hasI18n && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("i18n"), onClick: handleTabClick("i18n"), children: "I18n" }),
|
|
4242
|
+
hasJobs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("jobs"), onClick: handleTabClick("jobs"), children: [
|
|
4243
|
+
"Jobs (",
|
|
4244
|
+
profile.child_jobs.length,
|
|
4245
|
+
")"
|
|
4246
|
+
] }),
|
|
4247
|
+
/* @__PURE__ */ u3("a", { href: "#", class: tabClass("env"), onClick: handleTabClick("env"), children: "Env" })
|
|
3645
4248
|
] }),
|
|
3646
4249
|
/* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
|
|
3647
4250
|
activeTab === "exception" && /* @__PURE__ */ u3(ExceptionTab, { exceptionData: cd["exception"] }),
|
|
@@ -3655,7 +4258,9 @@
|
|
|
3655
4258
|
activeTab === "cache" && /* @__PURE__ */ u3(CacheTab, { cacheData: cd["cache"] }),
|
|
3656
4259
|
activeTab === "logs" && /* @__PURE__ */ u3(LogsTab, { logData: cd["logs"] }),
|
|
3657
4260
|
activeTab === "routes" && /* @__PURE__ */ u3(RoutesTab, { routesData: cd["routes"] }),
|
|
3658
|
-
activeTab === "i18n" && /* @__PURE__ */ u3(I18nTab, { i18nData: cd["i18n"] })
|
|
4261
|
+
activeTab === "i18n" && /* @__PURE__ */ u3(I18nTab, { i18nData: cd["i18n"] }),
|
|
4262
|
+
activeTab === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs }),
|
|
4263
|
+
activeTab === "env" && /* @__PURE__ */ u3(EnvTab, { envData: cd["env"] })
|
|
3659
4264
|
] })
|
|
3660
4265
|
] }),
|
|
3661
4266
|
!embedded && /* @__PURE__ */ u3("div", { class: "profiler-mt-6", children: /* @__PURE__ */ u3("a", { href: "/_profiler", style: "color: var(--profiler-accent);", children: "\u2190 Back to profiles" }) })
|
|
@@ -3718,13 +4323,15 @@
|
|
|
3718
4323
|
|
|
3719
4324
|
// app/assets/typescript/profiler/components/dashboard/JobProfileDashboard.tsx
|
|
3720
4325
|
function JobProfileDashboard({ profile, initialTab, embedded }) {
|
|
3721
|
-
const validTabs = ["job", "database", "cache", "http"];
|
|
4326
|
+
const validTabs = ["job", "database", "cache", "http", "jobs"];
|
|
3722
4327
|
const defaultTab = validTabs.includes(initialTab) ? initialTab : "job";
|
|
3723
4328
|
const [activeTab, setActiveTab] = d2(defaultTab);
|
|
3724
4329
|
const cd = profile.collectors_data || {};
|
|
3725
4330
|
const hasHttp = cd["http"]?.total_requests > 0;
|
|
4331
|
+
const hasJobs = (profile.child_jobs?.length ?? 0) > 0;
|
|
3726
4332
|
const jobData = cd["job"];
|
|
3727
4333
|
const isFailed = jobData?.status === "failed";
|
|
4334
|
+
const parent = profile.parent_profile;
|
|
3728
4335
|
const handleTabClick = (tab) => (e3) => {
|
|
3729
4336
|
e3.preventDefault();
|
|
3730
4337
|
setActiveTab(tab);
|
|
@@ -3759,6 +4366,26 @@
|
|
|
3759
4366
|
" MB"
|
|
3760
4367
|
] })
|
|
3761
4368
|
] })
|
|
4369
|
+
] }),
|
|
4370
|
+
parent && /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2 profiler-mt-2 profiler-text--sm", children: [
|
|
4371
|
+
/* @__PURE__ */ u3("span", { style: "color:var(--profiler-text-muted)", children: "Triggered by:" }),
|
|
4372
|
+
parent.profile_type === "http" ? /* @__PURE__ */ u3("span", { children: [
|
|
4373
|
+
/* @__PURE__ */ u3("strong", { children: parent.method }),
|
|
4374
|
+
" ",
|
|
4375
|
+
parent.path,
|
|
4376
|
+
" \xB7 ",
|
|
4377
|
+
parent.http_status,
|
|
4378
|
+
" \xB7 ",
|
|
4379
|
+
parent.duration?.toFixed(2),
|
|
4380
|
+
" ms"
|
|
4381
|
+
] }) : /* @__PURE__ */ u3("span", { children: [
|
|
4382
|
+
"\u2699\uFE0F ",
|
|
4383
|
+
/* @__PURE__ */ u3("strong", { children: parent.path }),
|
|
4384
|
+
" \xB7 ",
|
|
4385
|
+
parent.duration?.toFixed(2),
|
|
4386
|
+
" ms"
|
|
4387
|
+
] }),
|
|
4388
|
+
/* @__PURE__ */ u3("a", { href: `/_profiler/profiles/${parent.token}`, style: "color: var(--profiler-accent);", children: "View \u2192" })
|
|
3762
4389
|
] })
|
|
3763
4390
|
] }),
|
|
3764
4391
|
/* @__PURE__ */ u3("div", { class: "profiler-panel profiler-mb-6", children: [
|
|
@@ -3766,13 +4393,19 @@
|
|
|
3766
4393
|
/* @__PURE__ */ u3("a", { href: "#", class: tabClass("job"), onClick: handleTabClick("job"), children: "Job" }),
|
|
3767
4394
|
/* @__PURE__ */ u3("a", { href: "#", class: tabClass("database"), onClick: handleTabClick("database"), children: "Database" }),
|
|
3768
4395
|
/* @__PURE__ */ u3("a", { href: "#", class: tabClass("cache"), onClick: handleTabClick("cache"), children: "Cache" }),
|
|
3769
|
-
hasHttp && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("http"), onClick: handleTabClick("http"), children: "Outbound HTTP" })
|
|
4396
|
+
hasHttp && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("http"), onClick: handleTabClick("http"), children: "Outbound HTTP" }),
|
|
4397
|
+
hasJobs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("jobs"), onClick: handleTabClick("jobs"), children: [
|
|
4398
|
+
"Jobs (",
|
|
4399
|
+
profile.child_jobs.length,
|
|
4400
|
+
")"
|
|
4401
|
+
] })
|
|
3770
4402
|
] }),
|
|
3771
4403
|
/* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
|
|
3772
4404
|
activeTab === "job" && /* @__PURE__ */ u3(JobTab, { jobData: cd["job"] }),
|
|
3773
4405
|
activeTab === "database" && /* @__PURE__ */ u3(DatabaseTab, { dbData: cd["database"], token: profile.token }),
|
|
3774
4406
|
activeTab === "cache" && /* @__PURE__ */ u3(CacheTab, { cacheData: cd["cache"] }),
|
|
3775
|
-
activeTab === "http" && /* @__PURE__ */ u3(HttpTab, { httpData: cd["http"] })
|
|
4407
|
+
activeTab === "http" && /* @__PURE__ */ u3(HttpTab, { httpData: cd["http"] }),
|
|
4408
|
+
activeTab === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs })
|
|
3776
4409
|
] })
|
|
3777
4410
|
] }),
|
|
3778
4411
|
!embedded && /* @__PURE__ */ u3("div", { class: "profiler-mt-6", children: /* @__PURE__ */ u3("a", { href: "/_profiler", style: "color: var(--profiler-accent);", children: "\u2190 Back to profiles" }) })
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Profiler
|
|
4
|
+
module Api
|
|
5
|
+
class EnvVarsController < ApplicationController
|
|
6
|
+
skip_before_action :verify_authenticity_token
|
|
7
|
+
|
|
8
|
+
def show
|
|
9
|
+
variables = ENV.to_h.sort.to_h
|
|
10
|
+
render json: { variables: variables, total: variables.size }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def update
|
|
14
|
+
key = params[:key].to_s.strip
|
|
15
|
+
|
|
16
|
+
if key.blank?
|
|
17
|
+
render json: { error: "Key cannot be blank" }, status: :unprocessable_entity
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
value = params[:value]
|
|
22
|
+
|
|
23
|
+
if value.nil? || value.to_s.empty?
|
|
24
|
+
ENV.delete(key)
|
|
25
|
+
render json: { key: key, value: nil, deleted: true }
|
|
26
|
+
else
|
|
27
|
+
ENV[key] = value.to_s
|
|
28
|
+
render json: { key: key, value: ENV[key] }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -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"
|
data/config/routes.rb
CHANGED
|
@@ -34,5 +34,6 @@ Profiler::Engine.routes.draw do
|
|
|
34
34
|
post "ajax/link", to: "ajax#link"
|
|
35
35
|
post "explain", to: "explain#create"
|
|
36
36
|
resource :function_profiling, only: [:show, :update], controller: "function_profiling"
|
|
37
|
+
resource :env_vars, only: [:show, :update], controller: "env_vars"
|
|
37
38
|
end
|
|
38
39
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_collector"
|
|
4
|
+
|
|
5
|
+
module Profiler
|
|
6
|
+
module Collectors
|
|
7
|
+
class EnvCollector < BaseCollector
|
|
8
|
+
def icon
|
|
9
|
+
"⚙️"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def priority
|
|
13
|
+
90
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def tab_config
|
|
17
|
+
{
|
|
18
|
+
key: "env",
|
|
19
|
+
label: "Env",
|
|
20
|
+
icon: icon,
|
|
21
|
+
priority: priority,
|
|
22
|
+
enabled: true,
|
|
23
|
+
default_active: false
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def collect
|
|
28
|
+
variables = ENV.to_h.sort.to_h
|
|
29
|
+
store_data({ variables: variables, total: variables.size })
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def toolbar_summary
|
|
33
|
+
data = panel_content
|
|
34
|
+
{ text: "#{data[:total] || 0} vars", color: "gray" }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -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
data/lib/profiler.rb
CHANGED
|
@@ -94,6 +94,7 @@ require_relative "profiler/collectors/log_collector"
|
|
|
94
94
|
require_relative "profiler/collectors/exception_collector"
|
|
95
95
|
require_relative "profiler/collectors/routes_collector"
|
|
96
96
|
require_relative "profiler/collectors/i18n_collector"
|
|
97
|
+
require_relative "profiler/collectors/env_collector"
|
|
97
98
|
|
|
98
99
|
require_relative "profiler/railtie" if defined?(Rails::Railtie)
|
|
99
100
|
require_relative "profiler/engine" if defined?(Rails::Engine)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-profiler
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sébastien Duplessy
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -107,6 +107,7 @@ files:
|
|
|
107
107
|
- app/assets/builds/profiler.css
|
|
108
108
|
- app/assets/builds/profiler.js
|
|
109
109
|
- app/controllers/profiler/api/ajax_controller.rb
|
|
110
|
+
- app/controllers/profiler/api/env_vars_controller.rb
|
|
110
111
|
- app/controllers/profiler/api/explain_controller.rb
|
|
111
112
|
- app/controllers/profiler/api/function_profiling_controller.rb
|
|
112
113
|
- app/controllers/profiler/api/jobs_controller.rb
|
|
@@ -128,6 +129,7 @@ files:
|
|
|
128
129
|
- lib/profiler/collectors/cache_collector.rb
|
|
129
130
|
- lib/profiler/collectors/database_collector.rb
|
|
130
131
|
- lib/profiler/collectors/dump_collector.rb
|
|
132
|
+
- lib/profiler/collectors/env_collector.rb
|
|
131
133
|
- lib/profiler/collectors/exception_collector.rb
|
|
132
134
|
- lib/profiler/collectors/flamegraph_collector.rb
|
|
133
135
|
- lib/profiler/collectors/function_profiler_collector.rb
|
|
@@ -139,6 +141,7 @@ files:
|
|
|
139
141
|
- lib/profiler/collectors/routes_collector.rb
|
|
140
142
|
- lib/profiler/collectors/view_collector.rb
|
|
141
143
|
- lib/profiler/configuration.rb
|
|
144
|
+
- lib/profiler/current_context.rb
|
|
142
145
|
- lib/profiler/engine.rb
|
|
143
146
|
- lib/profiler/explain_runner.rb
|
|
144
147
|
- lib/profiler/instrumentation/active_job_instrumentation.rb
|