rails-profiler 0.14.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 +559 -2
- data/app/controllers/profiler/api/env_vars_controller.rb +33 -0
- data/config/routes.rb +1 -0
- data/lib/profiler/collectors/env_collector.rb +38 -0
- data/lib/profiler/version.rb +1 -1
- data/lib/profiler.rb +1 -0
- metadata +4 -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
|
|
@@ -3618,6 +3618,561 @@
|
|
|
3618
3618
|
] });
|
|
3619
3619
|
}
|
|
3620
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
|
+
|
|
3621
4176
|
// app/assets/typescript/profiler/components/dashboard/ProfileDashboard.tsx
|
|
3622
4177
|
function ProfileDashboard({ profile, initialTab, embedded }) {
|
|
3623
4178
|
const cd = profile.collectors_data || {};
|
|
@@ -3688,7 +4243,8 @@
|
|
|
3688
4243
|
"Jobs (",
|
|
3689
4244
|
profile.child_jobs.length,
|
|
3690
4245
|
")"
|
|
3691
|
-
] })
|
|
4246
|
+
] }),
|
|
4247
|
+
/* @__PURE__ */ u3("a", { href: "#", class: tabClass("env"), onClick: handleTabClick("env"), children: "Env" })
|
|
3692
4248
|
] }),
|
|
3693
4249
|
/* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
|
|
3694
4250
|
activeTab === "exception" && /* @__PURE__ */ u3(ExceptionTab, { exceptionData: cd["exception"] }),
|
|
@@ -3703,7 +4259,8 @@
|
|
|
3703
4259
|
activeTab === "logs" && /* @__PURE__ */ u3(LogsTab, { logData: cd["logs"] }),
|
|
3704
4260
|
activeTab === "routes" && /* @__PURE__ */ u3(RoutesTab, { routesData: cd["routes"] }),
|
|
3705
4261
|
activeTab === "i18n" && /* @__PURE__ */ u3(I18nTab, { i18nData: cd["i18n"] }),
|
|
3706
|
-
activeTab === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs })
|
|
4262
|
+
activeTab === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs }),
|
|
4263
|
+
activeTab === "env" && /* @__PURE__ */ u3(EnvTab, { envData: cd["env"] })
|
|
3707
4264
|
] })
|
|
3708
4265
|
] }),
|
|
3709
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" }) })
|
|
@@ -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
|
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
|
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
|