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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a755b6d8e9f74901cac774282d218553d40fe1c59efe7e9a7973e2a6c7cf38ca
4
- data.tar.gz: 5f157e934571a297d1aa6484312cbc001478b71b776c940179d91f4b04f31d85
3
+ metadata.gz: a4e5e84c4946f549552450c0d2d2d2053b273d3bed8dba66fefa17d9c4e5f7f9
4
+ data.tar.gz: 6b4be4f2b3116dbdc4270df5a6f860eabbbf709c3fc296ea34f400aaeb26ce83
5
5
  SHA512:
6
- metadata.gz: 02b2e42cb2409c7db8a54277323d7c13483b10884ebd041c60aef6b8f874cde66d90cf3d02e0b60d65b526ad8e5d8d58bc52e19c7a60943ddfe65f97b1e655e3
7
- data.tar.gz: 7c13611e50dc1ada83e5e61f0cbee9f5cf1bf256a16d776012ff59c6a07abe6fff7b3a636f702e1445f44f6750207bbcf0fce25fcc3234d2f5008ad0c7be8f02
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 EnvTab({ envData }) {
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(false);
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("button", { onClick: refresh, disabled: refreshing, style: headerBtn, children: refreshing ? "\u2026" : "\u21BA Refresh" }),
1209
- /* @__PURE__ */ u3("button", { onClick: () => setShowImport((v3) => !v3), style: `${headerBtn}${showImport ? "border-color:var(--profiler-accent);color:var(--profiler-accent);" : ""}`, children: "\u2B06 Import" }),
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-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." }),
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 diffStyle = wasModified ? "border-left:3px solid #f59e0b;" : wasAdded ? "border-left:3px solid var(--profiler-success,#22c55e);" : "";
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
- wasModified && /* @__PURE__ */ u3("div", { style: "font-size:10px;color:#f59e0b;margin-top:2px;font-family:monospace;", children: [
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
- wasModified && /* @__PURE__ */ u3("div", { style: "font-size:10px;color:#f59e0b;margin-top:2px;font-family:monospace;", children: [
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 === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs })
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
- render json: { variables: variables, total: variables.size }
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
@@ -38,6 +38,10 @@ module Profiler
38
38
  end
39
39
  end
40
40
 
41
+ def capture(ex)
42
+ capture_exception(ex) if ex && @exception_data.nil?
43
+ end
44
+
41
45
  def collect
42
46
  ActiveSupport::Notifications.unsubscribe(@subscriber) if @subscriber
43
47
 
@@ -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
@@ -11,6 +11,7 @@ module Profiler
11
11
 
12
12
  class SidekiqMiddleware
13
13
  def call(worker, job, queue, &block)
14
+ Profiler.env_override_store.apply!
14
15
  Profiler::JobProfiler.profile(
15
16
  job_class: job["class"],
16
17
  job_id: job["jid"],
@@ -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
@@ -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|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.18.0"
4
+ VERSION = "0.19.0"
5
5
  end
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.18.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-14 00:00:00.000000000 Z
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