rails-profiler 0.18.0 → 0.19.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 +131 -19
- data/app/controllers/profiler/api/env_vars_controller.rb +21 -1
- data/config/routes.rb +2 -0
- data/lib/profiler/collectors/exception_collector.rb +4 -0
- data/lib/profiler/env_override_store.rb +138 -0
- data/lib/profiler/instrumentation/sidekiq_middleware.rb +1 -0
- data/lib/profiler/job_profiler.rb +14 -1
- data/lib/profiler/railtie.rb +4 -0
- data/lib/profiler/version.rb +1 -1
- data/lib/profiler.rb +5 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a4e5e84c4946f549552450c0d2d2d2053b273d3bed8dba66fefa17d9c4e5f7f9
|
|
4
|
+
data.tar.gz: 6b4be4f2b3116dbdc4270df5a6f860eabbbf709c3fc296ea34f400aaeb26ce83
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 66136505b84f792418deb7a29d7d77f36a3cc62cf3bdab9d7defc842f9797b39667c92e18984925a14276880a129ef5b42faa644112ab1c917b61e9f11f8b359
|
|
7
|
+
data.tar.gz: b4ac6c0b37496721a3960732ab0839224e896aa4390757b36d84595200e6441034649f8829573a1fee30cf9c8216c5275e5faa9b0534bc48fe49a20e738d2bf3
|
|
@@ -990,9 +990,24 @@
|
|
|
990
990
|
throw new Error(body.error ?? `Request failed (${res.status})`);
|
|
991
991
|
}
|
|
992
992
|
}
|
|
993
|
-
function
|
|
993
|
+
async function resetEnvVar(key) {
|
|
994
|
+
const res = await fetch(`/_profiler/api/env_vars/reset?key=${encodeURIComponent(key)}`, {
|
|
995
|
+
method: "DELETE"
|
|
996
|
+
});
|
|
997
|
+
if (!res.ok) {
|
|
998
|
+
const body = await res.json().catch(() => ({}));
|
|
999
|
+
throw new Error(body.error ?? `Request failed (${res.status})`);
|
|
1000
|
+
}
|
|
1001
|
+
return res.json();
|
|
1002
|
+
}
|
|
1003
|
+
async function resetAllEnvVars() {
|
|
1004
|
+
const res = await fetch("/_profiler/api/env_vars/reset_all", { method: "DELETE" });
|
|
1005
|
+
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
|
1006
|
+
}
|
|
1007
|
+
function EnvTab({ envData, readOnly: forceReadOnly = false }) {
|
|
994
1008
|
const initial = T2(() => ({ ...envData?.variables ?? {} }), []);
|
|
995
1009
|
const [variables, setVariables] = d2(initial);
|
|
1010
|
+
const [overrides, setOverrides] = d2(envData?.overrides ?? {});
|
|
996
1011
|
const [total, setTotal] = d2(envData?.total ?? 0);
|
|
997
1012
|
const [search, setSearch] = d2("");
|
|
998
1013
|
const [collapsedGroups, setCollapsedGroups] = d2(/* @__PURE__ */ new Set());
|
|
@@ -1005,7 +1020,7 @@
|
|
|
1005
1020
|
const [refreshing, setRefreshing] = d2(false);
|
|
1006
1021
|
const [unmaskedKeys, setUnmaskedKeys] = d2(loadUnmasked);
|
|
1007
1022
|
const [copiedId, setCopiedId] = d2(null);
|
|
1008
|
-
const [readOnly, setReadOnly] = d2(
|
|
1023
|
+
const [readOnly, setReadOnly] = d2(forceReadOnly);
|
|
1009
1024
|
const [showImport, setShowImport] = d2(false);
|
|
1010
1025
|
const [importContent, setImportContent] = d2("");
|
|
1011
1026
|
const editInputRef = A2(null);
|
|
@@ -1013,7 +1028,7 @@
|
|
|
1013
1028
|
if (editingKey !== null) editInputRef.current?.focus();
|
|
1014
1029
|
}, [editingKey]);
|
|
1015
1030
|
y2(() => {
|
|
1016
|
-
refresh();
|
|
1031
|
+
if (!forceReadOnly) refresh();
|
|
1017
1032
|
}, []);
|
|
1018
1033
|
const showFlash = (type, message) => {
|
|
1019
1034
|
setFlash({ type, message });
|
|
@@ -1053,6 +1068,7 @@
|
|
|
1053
1068
|
const data = await res.json();
|
|
1054
1069
|
setVariables(data.variables);
|
|
1055
1070
|
setTotal(data.total);
|
|
1071
|
+
setOverrides(data.overrides ?? {});
|
|
1056
1072
|
showFlash("success", `Refreshed \u2014 ${data.total} variables`);
|
|
1057
1073
|
} catch (e3) {
|
|
1058
1074
|
showFlash("error", e3.message ?? "Failed to refresh");
|
|
@@ -1166,6 +1182,45 @@
|
|
|
1166
1182
|
if (e3.key === "Enter") saveEdit();
|
|
1167
1183
|
if (e3.key === "Escape") cancelEdit();
|
|
1168
1184
|
};
|
|
1185
|
+
const resetVar = async (key) => {
|
|
1186
|
+
setSaving(true);
|
|
1187
|
+
try {
|
|
1188
|
+
const data = await resetEnvVar(key);
|
|
1189
|
+
setOverrides((prev) => {
|
|
1190
|
+
const n2 = { ...prev };
|
|
1191
|
+
delete n2[key];
|
|
1192
|
+
return n2;
|
|
1193
|
+
});
|
|
1194
|
+
setVariables((prev) => {
|
|
1195
|
+
const n2 = { ...prev };
|
|
1196
|
+
if (data.value === null) {
|
|
1197
|
+
delete n2[key];
|
|
1198
|
+
} else {
|
|
1199
|
+
n2[key] = data.value;
|
|
1200
|
+
}
|
|
1201
|
+
return n2;
|
|
1202
|
+
});
|
|
1203
|
+
showFlash("success", `${key} reset to original`);
|
|
1204
|
+
} catch (e3) {
|
|
1205
|
+
showFlash("error", e3.message ?? "Failed to reset");
|
|
1206
|
+
} finally {
|
|
1207
|
+
setSaving(false);
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
const doResetAll = async () => {
|
|
1211
|
+
setSaving(true);
|
|
1212
|
+
try {
|
|
1213
|
+
await resetAllEnvVars();
|
|
1214
|
+
setOverrides({});
|
|
1215
|
+
await refresh();
|
|
1216
|
+
showFlash("success", "All overrides reset");
|
|
1217
|
+
} catch (e3) {
|
|
1218
|
+
showFlash("error", e3.message ?? "Failed to reset all");
|
|
1219
|
+
} finally {
|
|
1220
|
+
setSaving(false);
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
const overrideCount = Object.keys(overrides).length;
|
|
1169
1224
|
const allEntries = Object.entries(variables);
|
|
1170
1225
|
const filteredEntries = T2(() => {
|
|
1171
1226
|
const q2 = search.toLowerCase().trim();
|
|
@@ -1205,11 +1260,27 @@
|
|
|
1205
1260
|
/* @__PURE__ */ u3("div", { class: "profiler-flex profiler-mb-4", style: "align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;", children: [
|
|
1206
1261
|
/* @__PURE__ */ u3("h2", { class: "profiler-section__header", style: "margin:0;", children: "Environment Variables" }),
|
|
1207
1262
|
/* @__PURE__ */ u3("div", { class: "profiler-flex profiler-flex--gap-2", children: [
|
|
1208
|
-
/* @__PURE__ */ u3(
|
|
1209
|
-
|
|
1263
|
+
!forceReadOnly && /* @__PURE__ */ u3(k, { children: [
|
|
1264
|
+
/* @__PURE__ */ u3("button", { onClick: refresh, disabled: refreshing, style: headerBtn, children: refreshing ? "\u2026" : "\u21BA Refresh" }),
|
|
1265
|
+
/* @__PURE__ */ u3("button", { onClick: () => setShowImport((v3) => !v3), style: `${headerBtn}${showImport ? "border-color:var(--profiler-accent);color:var(--profiler-accent);" : ""}`, children: "\u2B06 Import" })
|
|
1266
|
+
] }),
|
|
1210
1267
|
/* @__PURE__ */ u3("button", { onClick: exportEnv, style: headerBtn, children: "\u2B07 Export" }),
|
|
1211
1268
|
/* @__PURE__ */ u3("button", { onClick: toggleAllMasks, style: headerBtn, children: allRevealed ? "\u{1F648} Mask all" : "\u{1F441} Reveal all" }),
|
|
1212
|
-
/* @__PURE__ */ u3(
|
|
1269
|
+
!forceReadOnly && overrideCount > 0 && /* @__PURE__ */ u3(
|
|
1270
|
+
"button",
|
|
1271
|
+
{
|
|
1272
|
+
onClick: doResetAll,
|
|
1273
|
+
disabled: saving,
|
|
1274
|
+
title: "Remove all Sidekiq overrides and restore original values",
|
|
1275
|
+
style: `${headerBtn}border-color:var(--profiler-warning,#f59e0b);color:var(--profiler-warning,#f59e0b);`,
|
|
1276
|
+
children: [
|
|
1277
|
+
"\u21A9 Reset all (",
|
|
1278
|
+
overrideCount,
|
|
1279
|
+
")"
|
|
1280
|
+
]
|
|
1281
|
+
}
|
|
1282
|
+
),
|
|
1283
|
+
!forceReadOnly && /* @__PURE__ */ u3(
|
|
1213
1284
|
"button",
|
|
1214
1285
|
{
|
|
1215
1286
|
onClick: () => setReadOnly((v3) => !v3),
|
|
@@ -1220,7 +1291,11 @@
|
|
|
1220
1291
|
)
|
|
1221
1292
|
] })
|
|
1222
1293
|
] }),
|
|
1223
|
-
/* @__PURE__ */ u3("div", { style: "background:var(--profiler-
|
|
1294
|
+
forceReadOnly ? /* @__PURE__ */ u3("div", { style: "background:var(--profiler-bg-subtle,rgba(0,0,0,0.03));border:1px solid var(--profiler-border);border-radius:6px;padding:8px 12px;margin-bottom:16px;font-size:12px;color:var(--profiler-text-muted);", children: [
|
|
1295
|
+
"Snapshot taken at request time \u2014 modify env vars in ",
|
|
1296
|
+
/* @__PURE__ */ u3("a", { href: "/_profiler?section=env", style: "color:var(--profiler-accent);", children: "/_profiler?section=env" }),
|
|
1297
|
+
"."
|
|
1298
|
+
] }) : /* @__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." }),
|
|
1224
1299
|
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: [
|
|
1225
1300
|
flash.type === "success" ? "\u2713" : "\u2717",
|
|
1226
1301
|
" ",
|
|
@@ -1315,7 +1390,8 @@
|
|
|
1315
1390
|
const isCopiedKey = copiedId === `__key__${key}`;
|
|
1316
1391
|
const wasModified = initial[key] !== void 0 && initial[key] !== value;
|
|
1317
1392
|
const wasAdded = initial[key] === void 0;
|
|
1318
|
-
const
|
|
1393
|
+
const override = overrides[key];
|
|
1394
|
+
const diffStyle = override ? "border-left:3px solid var(--profiler-accent);" : wasModified ? "border-left:3px solid #f59e0b;" : wasAdded ? "border-left:3px solid var(--profiler-success,#22c55e);" : "";
|
|
1319
1395
|
return /* @__PURE__ */ u3("tr", { style: diffStyle, children: [
|
|
1320
1396
|
/* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("div", { class: "profiler-flex", style: "align-items:center;gap:4px;", children: [
|
|
1321
1397
|
/* @__PURE__ */ u3("code", { class: "profiler-text--xs profiler-text--mono", children: key }),
|
|
@@ -1368,7 +1444,13 @@
|
|
|
1368
1444
|
}
|
|
1369
1445
|
)
|
|
1370
1446
|
] }),
|
|
1371
|
-
|
|
1447
|
+
override && /* @__PURE__ */ u3("div", { style: "font-size:10px;color:var(--profiler-accent);margin-top:2px;font-family:monospace;", children: [
|
|
1448
|
+
"workers \u2190 ",
|
|
1449
|
+
override.value === "__profiler_deleted__" ? "(deleted)" : override.value,
|
|
1450
|
+
" \xB7 original: ",
|
|
1451
|
+
override.original ?? "(unset)"
|
|
1452
|
+
] }),
|
|
1453
|
+
!override && wasModified && /* @__PURE__ */ u3("div", { style: "font-size:10px;color:#f59e0b;margin-top:2px;font-family:monospace;", children: [
|
|
1372
1454
|
"was: ",
|
|
1373
1455
|
initial[key]
|
|
1374
1456
|
] })
|
|
@@ -1406,7 +1488,13 @@
|
|
|
1406
1488
|
}
|
|
1407
1489
|
)
|
|
1408
1490
|
] }),
|
|
1409
|
-
|
|
1491
|
+
override && /* @__PURE__ */ u3("div", { style: "font-size:10px;color:var(--profiler-accent);margin-top:2px;font-family:monospace;", children: [
|
|
1492
|
+
"workers \u2190 ",
|
|
1493
|
+
override.value === "__profiler_deleted__" ? "(deleted)" : unmasked ? override.value : "\u2022".repeat(Math.min((override.value ?? "").length, 20)),
|
|
1494
|
+
" \xB7 original: ",
|
|
1495
|
+
override.original == null ? "(unset)" : unmasked ? override.original : "\u2022".repeat(Math.min(override.original.length, 20))
|
|
1496
|
+
] }),
|
|
1497
|
+
!override && wasModified && /* @__PURE__ */ u3("div", { style: "font-size:10px;color:#f59e0b;margin-top:2px;font-family:monospace;", children: [
|
|
1410
1498
|
"was: ",
|
|
1411
1499
|
unmasked ? isQuoted(initial[key]) ? `"${initial[key]}"` : initial[key] : "\u2022".repeat(Math.min(initial[key].length, 20))
|
|
1412
1500
|
] })
|
|
@@ -1414,11 +1502,18 @@
|
|
|
1414
1502
|
!readOnly && /* @__PURE__ */ u3("td", { style: "text-align:right;white-space:nowrap;", children: isEditing ? /* @__PURE__ */ u3(k, { children: [
|
|
1415
1503
|
/* @__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" }),
|
|
1416
1504
|
/* @__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" })
|
|
1417
|
-
] }) : isBool(value) ? /* @__PURE__ */ u3(k, { children: [
|
|
1418
|
-
/* @__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" }),
|
|
1419
|
-
/* @__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" })
|
|
1420
1505
|
] }) : /* @__PURE__ */ u3(k, { children: [
|
|
1421
1506
|
/* @__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" }),
|
|
1507
|
+
override && /* @__PURE__ */ u3(
|
|
1508
|
+
"button",
|
|
1509
|
+
{
|
|
1510
|
+
onClick: () => resetVar(key),
|
|
1511
|
+
disabled: saving,
|
|
1512
|
+
title: `Reset to original: ${override.original ?? "(unset)"}`,
|
|
1513
|
+
style: "background:none;border:none;cursor:pointer;color:var(--profiler-warning,#f59e0b);font-size:11px;padding:0 4px;",
|
|
1514
|
+
children: "\u21A9 Reset"
|
|
1515
|
+
}
|
|
1516
|
+
),
|
|
1422
1517
|
/* @__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" })
|
|
1423
1518
|
] }) })
|
|
1424
1519
|
] }, key);
|
|
@@ -4385,7 +4480,7 @@
|
|
|
4385
4480
|
activeTab === "routes" && /* @__PURE__ */ u3(RoutesTab, { routesData: cd["routes"] }),
|
|
4386
4481
|
activeTab === "i18n" && /* @__PURE__ */ u3(I18nTab, { i18nData: cd["i18n"] }),
|
|
4387
4482
|
activeTab === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs }),
|
|
4388
|
-
activeTab === "env" && /* @__PURE__ */ u3(EnvTab, { envData: cd["env"] })
|
|
4483
|
+
activeTab === "env" && /* @__PURE__ */ u3(EnvTab, { envData: cd["env"], readOnly: true })
|
|
4389
4484
|
] })
|
|
4390
4485
|
] }),
|
|
4391
4486
|
!embedded && /* @__PURE__ */ u3("div", { class: "profiler-mt-6", children: /* @__PURE__ */ u3("a", { href: "/_profiler", style: "color: var(--profiler-accent);", children: "\u2190 Back to profiles" }) })
|
|
@@ -4448,12 +4543,15 @@
|
|
|
4448
4543
|
|
|
4449
4544
|
// app/assets/typescript/profiler/components/dashboard/JobProfileDashboard.tsx
|
|
4450
4545
|
function JobProfileDashboard({ profile, initialTab, embedded }) {
|
|
4451
|
-
const validTabs = ["job", "database", "cache", "http", "jobs"];
|
|
4452
|
-
const defaultTab = validTabs.includes(initialTab) ? initialTab : "job";
|
|
4453
|
-
const [activeTab, setActiveTab] = d2(defaultTab);
|
|
4454
4546
|
const cd = profile.collectors_data || {};
|
|
4455
4547
|
const hasHttp = cd["http"]?.total_requests > 0;
|
|
4456
4548
|
const hasJobs = (profile.child_jobs?.length ?? 0) > 0;
|
|
4549
|
+
const hasDumps = (cd["dump"]?.count ?? 0) > 0;
|
|
4550
|
+
const hasLogs = (cd["logs"]?.total ?? 0) > 0;
|
|
4551
|
+
const hasException = !!cd["exception"]?.exception_class;
|
|
4552
|
+
const validTabs = ["job", "database", "cache", "http", "jobs", "dump", "logs", "exception", "env", "timeline"];
|
|
4553
|
+
const defaultTab = validTabs.includes(initialTab) ? initialTab : "job";
|
|
4554
|
+
const [activeTab, setActiveTab] = d2(hasException ? "exception" : defaultTab);
|
|
4457
4555
|
const jobData = cd["job"];
|
|
4458
4556
|
const isFailed = jobData?.status === "failed";
|
|
4459
4557
|
const parent = profile.parent_profile;
|
|
@@ -4515,22 +4613,36 @@
|
|
|
4515
4613
|
] }),
|
|
4516
4614
|
/* @__PURE__ */ u3("div", { class: "profiler-panel profiler-mb-6", children: [
|
|
4517
4615
|
/* @__PURE__ */ u3("div", { class: "tabs", children: [
|
|
4616
|
+
hasException && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("exception"), onClick: handleTabClick("exception"), style: "color:var(--profiler-error,#ef4444);", children: "\u{1F4A5} Exception" }),
|
|
4518
4617
|
/* @__PURE__ */ u3("a", { href: "#", class: tabClass("job"), onClick: handleTabClick("job"), children: "Job" }),
|
|
4519
4618
|
/* @__PURE__ */ u3("a", { href: "#", class: tabClass("database"), onClick: handleTabClick("database"), children: "Database" }),
|
|
4520
4619
|
/* @__PURE__ */ u3("a", { href: "#", class: tabClass("cache"), onClick: handleTabClick("cache"), children: "Cache" }),
|
|
4521
4620
|
hasHttp && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("http"), onClick: handleTabClick("http"), children: "Outbound HTTP" }),
|
|
4621
|
+
/* @__PURE__ */ u3("a", { href: "#", class: tabClass("timeline"), onClick: handleTabClick("timeline"), children: "Timeline" }),
|
|
4522
4622
|
hasJobs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("jobs"), onClick: handleTabClick("jobs"), children: [
|
|
4523
4623
|
"Jobs (",
|
|
4524
4624
|
profile.child_jobs.length,
|
|
4525
4625
|
")"
|
|
4526
|
-
] })
|
|
4626
|
+
] }),
|
|
4627
|
+
hasDumps && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("dump"), onClick: handleTabClick("dump"), children: [
|
|
4628
|
+
"Dumps (",
|
|
4629
|
+
cd["dump"].count,
|
|
4630
|
+
")"
|
|
4631
|
+
] }),
|
|
4632
|
+
hasLogs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("logs"), onClick: handleTabClick("logs"), children: "Logs" }),
|
|
4633
|
+
/* @__PURE__ */ u3("a", { href: "#", class: tabClass("env"), onClick: handleTabClick("env"), children: "Env" })
|
|
4527
4634
|
] }),
|
|
4528
4635
|
/* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
|
|
4636
|
+
activeTab === "exception" && /* @__PURE__ */ u3(ExceptionTab, { exceptionData: cd["exception"] }),
|
|
4529
4637
|
activeTab === "job" && /* @__PURE__ */ u3(JobTab, { jobData: cd["job"] }),
|
|
4530
4638
|
activeTab === "database" && /* @__PURE__ */ u3(DatabaseTab, { dbData: cd["database"], token: profile.token }),
|
|
4531
4639
|
activeTab === "cache" && /* @__PURE__ */ u3(CacheTab, { cacheData: cd["cache"] }),
|
|
4532
4640
|
activeTab === "http" && /* @__PURE__ */ u3(HttpTab, { httpData: cd["http"] }),
|
|
4533
|
-
activeTab === "
|
|
4641
|
+
activeTab === "timeline" && /* @__PURE__ */ u3(FlameGraphTab, { flamegraphData: cd["flamegraph"], perfData: cd["performance"], functionProfileData: cd["function_profile"] }),
|
|
4642
|
+
activeTab === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs }),
|
|
4643
|
+
activeTab === "dump" && /* @__PURE__ */ u3(DumpsTab, { dumpData: cd["dump"] }),
|
|
4644
|
+
activeTab === "logs" && /* @__PURE__ */ u3(LogsTab, { logData: cd["logs"] }),
|
|
4645
|
+
activeTab === "env" && /* @__PURE__ */ u3(EnvTab, { envData: cd["env"], readOnly: true })
|
|
4534
4646
|
] })
|
|
4535
4647
|
] }),
|
|
4536
4648
|
!embedded && /* @__PURE__ */ u3("div", { class: "profiler-mt-6", children: /* @__PURE__ */ u3("a", { href: "/_profiler", style: "color: var(--profiler-accent);", children: "\u2190 Back to profiles" }) })
|
|
@@ -7,7 +7,8 @@ module Profiler
|
|
|
7
7
|
|
|
8
8
|
def show
|
|
9
9
|
variables = ENV.to_h.sort.to_h
|
|
10
|
-
|
|
10
|
+
overrides = Profiler.env_override_store.all_overrides
|
|
11
|
+
render json: { variables: variables, total: variables.size, overrides: overrides }
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def update
|
|
@@ -22,12 +23,31 @@ module Profiler
|
|
|
22
23
|
|
|
23
24
|
if value.nil? || value.to_s.empty?
|
|
24
25
|
ENV.delete(key)
|
|
26
|
+
Profiler.env_override_store.delete(key)
|
|
25
27
|
render json: { key: key, value: nil, deleted: true }
|
|
26
28
|
else
|
|
27
29
|
ENV[key] = value.to_s
|
|
30
|
+
Profiler.env_override_store.set(key, value.to_s)
|
|
28
31
|
render json: { key: key, value: ENV[key] }
|
|
29
32
|
end
|
|
30
33
|
end
|
|
34
|
+
|
|
35
|
+
def reset_override
|
|
36
|
+
key = params[:key].to_s.strip
|
|
37
|
+
|
|
38
|
+
if key.blank?
|
|
39
|
+
render json: { error: "Key cannot be blank" }, status: :unprocessable_entity
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
Profiler.env_override_store.reset(key)
|
|
44
|
+
render json: { key: key, value: ENV[key], reset: true }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def reset_all
|
|
48
|
+
Profiler.env_override_store.reset_all
|
|
49
|
+
render json: { reset: true }
|
|
50
|
+
end
|
|
31
51
|
end
|
|
32
52
|
end
|
|
33
53
|
end
|
data/config/routes.rb
CHANGED
|
@@ -35,5 +35,7 @@ Profiler::Engine.routes.draw do
|
|
|
35
35
|
post "explain", to: "explain#create"
|
|
36
36
|
resource :function_profiling, only: [:show, :update], controller: "function_profiling"
|
|
37
37
|
resource :env_vars, only: [:show, :update], controller: "env_vars"
|
|
38
|
+
delete "env_vars/reset", to: "env_vars#reset_override"
|
|
39
|
+
delete "env_vars/reset_all", to: "env_vars#reset_all"
|
|
38
40
|
end
|
|
39
41
|
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Profiler
|
|
7
|
+
class EnvOverrideStore
|
|
8
|
+
DELETED_SENTINEL = "__profiler_deleted__"
|
|
9
|
+
RESTORE_SENTINEL = "__profiler_restore__"
|
|
10
|
+
|
|
11
|
+
# File format: { "KEY" => { "value" => "...", "original" => "..." } }
|
|
12
|
+
# Sentinels for "value":
|
|
13
|
+
# DELETED_SENTINEL → ENV.delete(key)
|
|
14
|
+
# RESTORE_SENTINEL → restore ENV[key] to "original" (cleaned up after apply!)
|
|
15
|
+
|
|
16
|
+
def set(key, value)
|
|
17
|
+
overrides = load_overrides
|
|
18
|
+
original = original_for(overrides, key)
|
|
19
|
+
overrides[key] = { "value" => value.to_s, "original" => original }
|
|
20
|
+
save_overrides(overrides)
|
|
21
|
+
rescue => e
|
|
22
|
+
warn "[Profiler] EnvOverrideStore: failed to set #{key}: #{e.message}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def delete(key)
|
|
26
|
+
overrides = load_overrides
|
|
27
|
+
original = original_for(overrides, key)
|
|
28
|
+
overrides[key] = { "value" => DELETED_SENTINEL, "original" => original }
|
|
29
|
+
save_overrides(overrides)
|
|
30
|
+
rescue => e
|
|
31
|
+
warn "[Profiler] EnvOverrideStore: failed to delete #{key}: #{e.message}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def reset(key)
|
|
35
|
+
overrides = load_overrides
|
|
36
|
+
entry = overrides[key]
|
|
37
|
+
return unless entry
|
|
38
|
+
|
|
39
|
+
original = entry["original"]
|
|
40
|
+
# Keep a RESTORE entry so Sidekiq workers pick it up on next job
|
|
41
|
+
overrides[key] = { "value" => RESTORE_SENTINEL, "original" => original }
|
|
42
|
+
save_overrides(overrides)
|
|
43
|
+
|
|
44
|
+
# Apply immediately to the current (web) process
|
|
45
|
+
original.nil? ? ENV.delete(key) : ENV[key] = original
|
|
46
|
+
rescue => e
|
|
47
|
+
warn "[Profiler] EnvOverrideStore: failed to reset #{key}: #{e.message}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def reset_all
|
|
51
|
+
overrides = load_overrides
|
|
52
|
+
restore_overrides = {}
|
|
53
|
+
|
|
54
|
+
overrides.each do |key, entry|
|
|
55
|
+
original = entry.is_a?(Hash) ? entry["original"] : nil
|
|
56
|
+
# Apply immediately to the current (web) process
|
|
57
|
+
original.nil? ? ENV.delete(key) : ENV[key] = original
|
|
58
|
+
# Leave a RESTORE sentinel for Sidekiq workers to pick up
|
|
59
|
+
restore_overrides[key] = { "value" => RESTORE_SENTINEL, "original" => original }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
restore_overrides.empty? ? clear : save_overrides(restore_overrides)
|
|
63
|
+
rescue => e
|
|
64
|
+
warn "[Profiler] EnvOverrideStore: failed to reset all: #{e.message}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def apply!
|
|
68
|
+
overrides = load_overrides
|
|
69
|
+
restore_keys = []
|
|
70
|
+
|
|
71
|
+
overrides.each do |key, entry|
|
|
72
|
+
value = entry.is_a?(Hash) ? entry["value"] : entry
|
|
73
|
+
original = entry.is_a?(Hash) ? entry["original"] : nil
|
|
74
|
+
|
|
75
|
+
case value
|
|
76
|
+
when DELETED_SENTINEL
|
|
77
|
+
ENV.delete(key)
|
|
78
|
+
when RESTORE_SENTINEL
|
|
79
|
+
original.nil? ? ENV.delete(key) : ENV[key] = original
|
|
80
|
+
restore_keys << key
|
|
81
|
+
else
|
|
82
|
+
ENV[key] = value
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
if restore_keys.any?
|
|
87
|
+
restore_keys.each { |k| overrides.delete(k) }
|
|
88
|
+
save_overrides(overrides)
|
|
89
|
+
end
|
|
90
|
+
rescue => e
|
|
91
|
+
warn "[Profiler] EnvOverrideStore: failed to apply overrides: #{e.message}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns active overrides (excludes RESTORE entries — already being reverted)
|
|
95
|
+
def all_overrides
|
|
96
|
+
load_overrides.reject do |_, entry|
|
|
97
|
+
value = entry.is_a?(Hash) ? entry["value"] : entry
|
|
98
|
+
value == RESTORE_SENTINEL
|
|
99
|
+
end.transform_values do |entry|
|
|
100
|
+
entry.is_a?(Hash) ? entry : { "value" => entry, "original" => nil }
|
|
101
|
+
end
|
|
102
|
+
rescue => e
|
|
103
|
+
warn "[Profiler] EnvOverrideStore: failed to load overrides: #{e.message}"
|
|
104
|
+
{}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def clear
|
|
108
|
+
FileUtils.rm_f(override_file_path)
|
|
109
|
+
rescue => e
|
|
110
|
+
warn "[Profiler] EnvOverrideStore: failed to clear: #{e.message}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def original_for(overrides, key)
|
|
116
|
+
overrides.key?(key) ? overrides[key]["original"] : ENV[key]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def override_file_path
|
|
120
|
+
Profiler.configuration.tmp_path.join("env_overrides.json")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def load_overrides
|
|
124
|
+
path = override_file_path
|
|
125
|
+
return {} unless File.exist?(path)
|
|
126
|
+
|
|
127
|
+
JSON.parse(File.read(path))
|
|
128
|
+
rescue JSON::ParserError
|
|
129
|
+
{}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def save_overrides(overrides)
|
|
133
|
+
path = override_file_path
|
|
134
|
+
FileUtils.mkdir_p(path.dirname)
|
|
135
|
+
File.write(path, JSON.generate(overrides))
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -6,13 +6,23 @@ require_relative "collectors/job_collector"
|
|
|
6
6
|
require_relative "collectors/database_collector"
|
|
7
7
|
require_relative "collectors/cache_collector"
|
|
8
8
|
require_relative "collectors/http_collector"
|
|
9
|
+
require_relative "collectors/dump_collector"
|
|
10
|
+
require_relative "collectors/log_collector"
|
|
11
|
+
require_relative "collectors/exception_collector"
|
|
12
|
+
require_relative "collectors/env_collector"
|
|
13
|
+
require_relative "collectors/flamegraph_collector"
|
|
9
14
|
|
|
10
15
|
module Profiler
|
|
11
16
|
class JobProfiler
|
|
12
17
|
JOB_COLLECTOR_CLASSES = [
|
|
13
18
|
Collectors::DatabaseCollector,
|
|
14
19
|
Collectors::CacheCollector,
|
|
15
|
-
Collectors::HttpCollector
|
|
20
|
+
Collectors::HttpCollector,
|
|
21
|
+
Collectors::DumpCollector,
|
|
22
|
+
Collectors::LogCollector,
|
|
23
|
+
Collectors::ExceptionCollector,
|
|
24
|
+
Collectors::EnvCollector,
|
|
25
|
+
Collectors::FlameGraphCollector
|
|
16
26
|
].freeze
|
|
17
27
|
|
|
18
28
|
def self.profile(job_class:, job_id:, queue:, arguments:, executions:, parent_token: nil, &block)
|
|
@@ -55,6 +65,8 @@ module Profiler
|
|
|
55
65
|
collectors = [job_collector] + JOB_COLLECTOR_CLASSES.map { |klass| klass.new(profile) }
|
|
56
66
|
collectors.each { |c| c.subscribe if c.respond_to?(:subscribe) }
|
|
57
67
|
|
|
68
|
+
exception_collector = collectors.find { |c| c.is_a?(Collectors::ExceptionCollector) }
|
|
69
|
+
|
|
58
70
|
memory_before = current_memory if Profiler.configuration.track_memory
|
|
59
71
|
|
|
60
72
|
job_status = "completed"
|
|
@@ -68,6 +80,7 @@ module Profiler
|
|
|
68
80
|
rescue => e
|
|
69
81
|
job_status = "failed"
|
|
70
82
|
error_message = "#{e.class}: #{e.message}"
|
|
83
|
+
exception_collector&.capture(e)
|
|
71
84
|
raise
|
|
72
85
|
ensure
|
|
73
86
|
Profiler::CurrentContext.token = previous_token
|
data/lib/profiler/railtie.rb
CHANGED
|
@@ -6,6 +6,10 @@ module Profiler
|
|
|
6
6
|
class Railtie < Rails::Railtie
|
|
7
7
|
config.profiler = ActiveSupport::OrderedOptions.new
|
|
8
8
|
|
|
9
|
+
initializer "profiler.apply_env_overrides" do
|
|
10
|
+
Profiler.env_override_store.apply!
|
|
11
|
+
end
|
|
12
|
+
|
|
9
13
|
initializer "profiler.set_configs" do |app|
|
|
10
14
|
# Set default configuration for Rails environment
|
|
11
15
|
Profiler.configure do |config|
|
data/lib/profiler/version.rb
CHANGED
data/lib/profiler.rb
CHANGED
|
@@ -25,6 +25,10 @@ module Profiler
|
|
|
25
25
|
@storage ||= configuration.storage_backend
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
def env_override_store
|
|
29
|
+
@env_override_store ||= EnvOverrideStore.new
|
|
30
|
+
end
|
|
31
|
+
|
|
28
32
|
def enabled?
|
|
29
33
|
configuration.enabled
|
|
30
34
|
end
|
|
@@ -100,5 +104,6 @@ require_relative "profiler/collectors/routes_collector"
|
|
|
100
104
|
require_relative "profiler/collectors/i18n_collector"
|
|
101
105
|
require_relative "profiler/collectors/env_collector"
|
|
102
106
|
|
|
107
|
+
require_relative "profiler/env_override_store"
|
|
103
108
|
require_relative "profiler/railtie" if defined?(Rails::Railtie)
|
|
104
109
|
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.19.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-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -143,6 +143,7 @@ files:
|
|
|
143
143
|
- lib/profiler/configuration.rb
|
|
144
144
|
- lib/profiler/current_context.rb
|
|
145
145
|
- lib/profiler/engine.rb
|
|
146
|
+
- lib/profiler/env_override_store.rb
|
|
146
147
|
- lib/profiler/explain_runner.rb
|
|
147
148
|
- lib/profiler/instrumentation/active_job_instrumentation.rb
|
|
148
149
|
- lib/profiler/instrumentation/net_http_instrumentation.rb
|