rails-profiler 0.18.0 → 0.19.1

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: 713e1b01bcd916191e3ae2b89124f4637eeb95d7b9837d350d2ee76a575a2f14
4
+ data.tar.gz: 9f1becf53b9b39145a4ebdf81981793aeb5f0494cce1e6872facdcf11c0a597a
5
5
  SHA512:
6
- metadata.gz: 02b2e42cb2409c7db8a54277323d7c13483b10884ebd041c60aef6b8f874cde66d90cf3d02e0b60d65b526ad8e5d8d58bc52e19c7a60943ddfe65f97b1e655e3
7
- data.tar.gz: 7c13611e50dc1ada83e5e61f0cbee9f5cf1bf256a16d776012ff59c6a07abe6fff7b3a636f702e1445f44f6750207bbcf0fce25fcc3234d2f5008ad0c7be8f02
6
+ metadata.gz: 298e772fd273b734126c095e57186e36d7f6fbaf26c7bb7db9899d02a38431231e4ecbde42f0b1a103deac26c29f48a596a065b5dd93f16a900b24d66e69a280
7
+ data.tar.gz: 78f23d51a52d0467e3acf9d912cbcbeebec03cf245632d88c7306d00d7c00645dda2be0a6208f3e58f4c815ea3a9efe0cb4e6783a3c6c4be44b784fd72e35f37
@@ -989,10 +989,26 @@
989
989
  const body = await res.json().catch(() => ({}));
990
990
  throw new Error(body.error ?? `Request failed (${res.status})`);
991
991
  }
992
+ return res.json();
992
993
  }
993
- function EnvTab({ envData }) {
994
- const initial = T2(() => ({ ...envData?.variables ?? {} }), []);
994
+ async function resetEnvVar(key) {
995
+ const res = await fetch(`/_profiler/api/env_vars/reset?key=${encodeURIComponent(key)}`, {
996
+ method: "DELETE"
997
+ });
998
+ if (!res.ok) {
999
+ const body = await res.json().catch(() => ({}));
1000
+ throw new Error(body.error ?? `Request failed (${res.status})`);
1001
+ }
1002
+ return res.json();
1003
+ }
1004
+ async function resetAllEnvVars() {
1005
+ const res = await fetch("/_profiler/api/env_vars/reset_all", { method: "DELETE" });
1006
+ if (!res.ok) throw new Error(`Request failed (${res.status})`);
1007
+ }
1008
+ function EnvTab({ envData, readOnly: forceReadOnly = false }) {
1009
+ const [initial, setInitial] = d2(() => ({ ...envData?.variables ?? {} }));
995
1010
  const [variables, setVariables] = d2(initial);
1011
+ const [overrides, setOverrides] = d2(envData?.overrides ?? {});
996
1012
  const [total, setTotal] = d2(envData?.total ?? 0);
997
1013
  const [search, setSearch] = d2("");
998
1014
  const [collapsedGroups, setCollapsedGroups] = d2(/* @__PURE__ */ new Set());
@@ -1005,7 +1021,7 @@
1005
1021
  const [refreshing, setRefreshing] = d2(false);
1006
1022
  const [unmaskedKeys, setUnmaskedKeys] = d2(loadUnmasked);
1007
1023
  const [copiedId, setCopiedId] = d2(null);
1008
- const [readOnly, setReadOnly] = d2(false);
1024
+ const [readOnly, setReadOnly] = d2(forceReadOnly);
1009
1025
  const [showImport, setShowImport] = d2(false);
1010
1026
  const [importContent, setImportContent] = d2("");
1011
1027
  const editInputRef = A2(null);
@@ -1013,7 +1029,7 @@
1013
1029
  if (editingKey !== null) editInputRef.current?.focus();
1014
1030
  }, [editingKey]);
1015
1031
  y2(() => {
1016
- refresh();
1032
+ if (!forceReadOnly) refresh();
1017
1033
  }, []);
1018
1034
  const showFlash = (type, message) => {
1019
1035
  setFlash({ type, message });
@@ -1053,7 +1069,9 @@
1053
1069
  const data = await res.json();
1054
1070
  setVariables(data.variables);
1055
1071
  setTotal(data.total);
1072
+ setOverrides(data.overrides ?? {});
1056
1073
  showFlash("success", `Refreshed \u2014 ${data.total} variables`);
1074
+ return data;
1057
1075
  } catch (e3) {
1058
1076
  showFlash("error", e3.message ?? "Failed to refresh");
1059
1077
  } finally {
@@ -1100,11 +1118,25 @@
1100
1118
  };
1101
1119
  const saveEdit = async () => {
1102
1120
  if (!editingKey) return;
1121
+ const key = editingKey;
1122
+ if (editValue === variables[key]) {
1123
+ setEditingKey(null);
1124
+ return;
1125
+ }
1103
1126
  setSaving(true);
1104
1127
  try {
1105
- await patchEnvVar(editingKey, editValue);
1106
- setVariables((prev) => ({ ...prev, [editingKey]: editValue }));
1107
- showFlash("success", `${editingKey} updated`);
1128
+ const data = await patchEnvVar(key, editValue);
1129
+ setVariables((prev) => ({ ...prev, [key]: editValue }));
1130
+ setOverrides((prev) => {
1131
+ const n2 = { ...prev };
1132
+ if (data.override) {
1133
+ n2[key] = data.override;
1134
+ } else {
1135
+ delete n2[key];
1136
+ }
1137
+ return n2;
1138
+ });
1139
+ showFlash("success", `${key} updated`);
1108
1140
  setEditingKey(null);
1109
1141
  } catch (e3) {
1110
1142
  showFlash("error", e3.message ?? "Failed to update");
@@ -1115,12 +1147,21 @@
1115
1147
  const deleteVar = async (key) => {
1116
1148
  setSaving(true);
1117
1149
  try {
1118
- await patchEnvVar(key, null);
1150
+ const data = await patchEnvVar(key, null);
1119
1151
  setVariables((prev) => {
1120
1152
  const n2 = { ...prev };
1121
1153
  delete n2[key];
1122
1154
  return n2;
1123
1155
  });
1156
+ setOverrides((prev) => {
1157
+ const n2 = { ...prev };
1158
+ if (data.override) {
1159
+ n2[key] = data.override;
1160
+ } else {
1161
+ delete n2[key];
1162
+ }
1163
+ return n2;
1164
+ });
1124
1165
  setUnmaskedKeys((prev) => {
1125
1166
  const n2 = new Set(prev);
1126
1167
  n2.delete(key);
@@ -1138,8 +1179,17 @@
1138
1179
  const next = /^(true|yes)$/i.test(current) ? "false" : "true";
1139
1180
  setSaving(true);
1140
1181
  try {
1141
- await patchEnvVar(key, next);
1182
+ const data = await patchEnvVar(key, next);
1142
1183
  setVariables((prev) => ({ ...prev, [key]: next }));
1184
+ setOverrides((prev) => {
1185
+ const n2 = { ...prev };
1186
+ if (data.override) {
1187
+ n2[key] = data.override;
1188
+ } else {
1189
+ delete n2[key];
1190
+ }
1191
+ return n2;
1192
+ });
1143
1193
  } catch (e3) {
1144
1194
  showFlash("error", e3.message ?? "Failed to update");
1145
1195
  } finally {
@@ -1151,8 +1201,17 @@
1151
1201
  if (!key) return;
1152
1202
  setSaving(true);
1153
1203
  try {
1154
- await patchEnvVar(key, newValue);
1204
+ const data = await patchEnvVar(key, newValue);
1155
1205
  setVariables((prev) => ({ ...prev, [key]: newValue }));
1206
+ setOverrides((prev) => {
1207
+ const n2 = { ...prev };
1208
+ if (data.override) {
1209
+ n2[key] = data.override;
1210
+ } else {
1211
+ delete n2[key];
1212
+ }
1213
+ return n2;
1214
+ });
1156
1215
  setNewKey("");
1157
1216
  setNewValue("");
1158
1217
  showFlash("success", `${key} added`);
@@ -1166,6 +1225,48 @@
1166
1225
  if (e3.key === "Enter") saveEdit();
1167
1226
  if (e3.key === "Escape") cancelEdit();
1168
1227
  };
1228
+ const resetVar = async (key) => {
1229
+ setSaving(true);
1230
+ try {
1231
+ const data = await resetEnvVar(key);
1232
+ setOverrides((prev) => {
1233
+ const n2 = { ...prev };
1234
+ delete n2[key];
1235
+ return n2;
1236
+ });
1237
+ const updater = (prev) => {
1238
+ const n2 = { ...prev };
1239
+ if (data.value === null) {
1240
+ delete n2[key];
1241
+ } else {
1242
+ n2[key] = data.value;
1243
+ }
1244
+ return n2;
1245
+ };
1246
+ setVariables(updater);
1247
+ setInitial(updater);
1248
+ showFlash("success", `${key} reset to original`);
1249
+ } catch (e3) {
1250
+ showFlash("error", e3.message ?? "Failed to reset");
1251
+ } finally {
1252
+ setSaving(false);
1253
+ }
1254
+ };
1255
+ const doResetAll = async () => {
1256
+ setSaving(true);
1257
+ try {
1258
+ await resetAllEnvVars();
1259
+ setOverrides({});
1260
+ const data = await refresh();
1261
+ if (data) setInitial(data.variables);
1262
+ showFlash("success", "All overrides reset");
1263
+ } catch (e3) {
1264
+ showFlash("error", e3.message ?? "Failed to reset all");
1265
+ } finally {
1266
+ setSaving(false);
1267
+ }
1268
+ };
1269
+ const overrideCount = Object.keys(overrides).length;
1169
1270
  const allEntries = Object.entries(variables);
1170
1271
  const filteredEntries = T2(() => {
1171
1272
  const q2 = search.toLowerCase().trim();
@@ -1205,11 +1306,27 @@
1205
1306
  /* @__PURE__ */ u3("div", { class: "profiler-flex profiler-mb-4", style: "align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;", children: [
1206
1307
  /* @__PURE__ */ u3("h2", { class: "profiler-section__header", style: "margin:0;", children: "Environment Variables" }),
1207
1308
  /* @__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" }),
1309
+ !forceReadOnly && /* @__PURE__ */ u3(k, { children: [
1310
+ /* @__PURE__ */ u3("button", { onClick: refresh, disabled: refreshing, style: headerBtn, children: refreshing ? "\u2026" : "\u21BA Refresh" }),
1311
+ /* @__PURE__ */ u3("button", { onClick: () => setShowImport((v3) => !v3), style: `${headerBtn}${showImport ? "border-color:var(--profiler-accent);color:var(--profiler-accent);" : ""}`, children: "\u2B06 Import" })
1312
+ ] }),
1210
1313
  /* @__PURE__ */ u3("button", { onClick: exportEnv, style: headerBtn, children: "\u2B07 Export" }),
1211
1314
  /* @__PURE__ */ u3("button", { onClick: toggleAllMasks, style: headerBtn, children: allRevealed ? "\u{1F648} Mask all" : "\u{1F441} Reveal all" }),
1212
- /* @__PURE__ */ u3(
1315
+ !forceReadOnly && overrideCount > 0 && /* @__PURE__ */ u3(
1316
+ "button",
1317
+ {
1318
+ onClick: doResetAll,
1319
+ disabled: saving,
1320
+ title: "Remove all Sidekiq overrides and restore original values",
1321
+ style: `${headerBtn}border-color:var(--profiler-warning,#f59e0b);color:var(--profiler-warning,#f59e0b);`,
1322
+ children: [
1323
+ "\u21A9 Reset all (",
1324
+ overrideCount,
1325
+ ")"
1326
+ ]
1327
+ }
1328
+ ),
1329
+ !forceReadOnly && /* @__PURE__ */ u3(
1213
1330
  "button",
1214
1331
  {
1215
1332
  onClick: () => setReadOnly((v3) => !v3),
@@ -1220,7 +1337,11 @@
1220
1337
  )
1221
1338
  ] })
1222
1339
  ] }),
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." }),
1340
+ 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: [
1341
+ "Snapshot taken at request time \u2014 modify env vars in ",
1342
+ /* @__PURE__ */ u3("a", { href: "/_profiler?section=env", style: "color:var(--profiler-accent);", children: "/_profiler?section=env" }),
1343
+ "."
1344
+ ] }) : /* @__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
1345
  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
1346
  flash.type === "success" ? "\u2713" : "\u2717",
1226
1347
  " ",
@@ -1315,7 +1436,8 @@
1315
1436
  const isCopiedKey = copiedId === `__key__${key}`;
1316
1437
  const wasModified = initial[key] !== void 0 && initial[key] !== value;
1317
1438
  const wasAdded = initial[key] === void 0;
1318
- const diffStyle = wasModified ? "border-left:3px solid #f59e0b;" : wasAdded ? "border-left:3px solid var(--profiler-success,#22c55e);" : "";
1439
+ const override = overrides[key];
1440
+ 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
1441
  return /* @__PURE__ */ u3("tr", { style: diffStyle, children: [
1320
1442
  /* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("div", { class: "profiler-flex", style: "align-items:center;gap:4px;", children: [
1321
1443
  /* @__PURE__ */ u3("code", { class: "profiler-text--xs profiler-text--mono", children: key }),
@@ -1368,9 +1490,11 @@
1368
1490
  }
1369
1491
  )
1370
1492
  ] }),
1371
- wasModified && /* @__PURE__ */ u3("div", { style: "font-size:10px;color:#f59e0b;margin-top:2px;font-family:monospace;", children: [
1372
- "was: ",
1373
- initial[key]
1493
+ override && /* @__PURE__ */ u3("div", { style: "font-size:10px;color:var(--profiler-accent);margin-top:2px;font-family:monospace;", children: [
1494
+ "workers \u2190 ",
1495
+ override.value === "__profiler_deleted__" ? "(deleted)" : override.value,
1496
+ " \xB7 original: ",
1497
+ override.original ?? "(unset)"
1374
1498
  ] })
1375
1499
  ] }) : /* @__PURE__ */ u3(k, { children: [
1376
1500
  /* @__PURE__ */ u3("div", { class: "profiler-flex", style: "align-items:center;gap:4px;", children: [
@@ -1379,7 +1503,9 @@
1379
1503
  "span",
1380
1504
  {
1381
1505
  class: "profiler-text--xs profiler-text--mono",
1382
- style: "word-break:break-all;flex:1;",
1506
+ onClick: () => !readOnly && !isEditing && startEdit(key),
1507
+ title: !readOnly ? "Click to edit" : void 0,
1508
+ style: `word-break:break-all;flex:1;${!readOnly ? "cursor:pointer;" : ""}`,
1383
1509
  children: unmasked ? isQuoted(value) ? /* @__PURE__ */ u3(k, { children: [
1384
1510
  /* @__PURE__ */ u3("span", { style: "color:var(--profiler-text-muted);opacity:0.5;", children: '"' }),
1385
1511
  value,
@@ -1406,20 +1532,29 @@
1406
1532
  }
1407
1533
  )
1408
1534
  ] }),
1409
- wasModified && /* @__PURE__ */ u3("div", { style: "font-size:10px;color:#f59e0b;margin-top:2px;font-family:monospace;", children: [
1410
- "was: ",
1411
- unmasked ? isQuoted(initial[key]) ? `"${initial[key]}"` : initial[key] : "\u2022".repeat(Math.min(initial[key].length, 20))
1535
+ override && /* @__PURE__ */ u3("div", { style: "font-size:10px;color:var(--profiler-accent);margin-top:2px;font-family:monospace;", children: [
1536
+ "workers \u2190 ",
1537
+ override.value === "__profiler_deleted__" ? "(deleted)" : unmasked ? override.value : "\u2022".repeat(Math.min((override.value ?? "").length, 20)),
1538
+ " \xB7 original: ",
1539
+ override.original == null ? "(unset)" : unmasked ? override.original : "\u2022".repeat(Math.min(override.original.length, 20))
1412
1540
  ] })
1413
1541
  ] }) }),
1414
1542
  !readOnly && /* @__PURE__ */ u3("td", { style: "text-align:right;white-space:nowrap;", children: isEditing ? /* @__PURE__ */ u3(k, { children: [
1415
1543
  /* @__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
1544
  /* @__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
1545
  ] }) : /* @__PURE__ */ u3(k, { children: [
1421
- /* @__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" }),
1422
- /* @__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" })
1546
+ /* @__PURE__ */ u3("button", { onClick: () => startEdit(key), disabled: saving, title: "Edit", style: "background:none;border:none;cursor:pointer;color:var(--profiler-accent);font-size:14px;padding:0 4px;", children: "\u270E" }),
1547
+ override && /* @__PURE__ */ u3(
1548
+ "button",
1549
+ {
1550
+ onClick: () => resetVar(key),
1551
+ disabled: saving,
1552
+ title: `Reset to original: ${override.original ?? "(unset)"}`,
1553
+ style: "background:none;border:none;cursor:pointer;color:var(--profiler-warning,#f59e0b);font-size:14px;padding:0 4px;",
1554
+ children: "\u21A9"
1555
+ }
1556
+ ),
1557
+ /* @__PURE__ */ u3("button", { onClick: () => deleteVar(key), disabled: saving, title: "Delete", style: "background:none;border:none;cursor:pointer;color:var(--profiler-error,#ef4444);font-size:14px;padding:0 4px;", children: "\u2715" })
1423
1558
  ] }) })
1424
1559
  ] }, key);
1425
1560
  })
@@ -4385,7 +4520,7 @@
4385
4520
  activeTab === "routes" && /* @__PURE__ */ u3(RoutesTab, { routesData: cd["routes"] }),
4386
4521
  activeTab === "i18n" && /* @__PURE__ */ u3(I18nTab, { i18nData: cd["i18n"] }),
4387
4522
  activeTab === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs }),
4388
- activeTab === "env" && /* @__PURE__ */ u3(EnvTab, { envData: cd["env"] })
4523
+ activeTab === "env" && /* @__PURE__ */ u3(EnvTab, { envData: cd["env"], readOnly: true })
4389
4524
  ] })
4390
4525
  ] }),
4391
4526
  !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 +4583,15 @@
4448
4583
 
4449
4584
  // app/assets/typescript/profiler/components/dashboard/JobProfileDashboard.tsx
4450
4585
  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
4586
  const cd = profile.collectors_data || {};
4455
4587
  const hasHttp = cd["http"]?.total_requests > 0;
4456
4588
  const hasJobs = (profile.child_jobs?.length ?? 0) > 0;
4589
+ const hasDumps = (cd["dump"]?.count ?? 0) > 0;
4590
+ const hasLogs = (cd["logs"]?.total ?? 0) > 0;
4591
+ const hasException = !!cd["exception"]?.exception_class;
4592
+ const validTabs = ["job", "database", "cache", "http", "jobs", "dump", "logs", "exception", "env", "timeline"];
4593
+ const defaultTab = validTabs.includes(initialTab) ? initialTab : "job";
4594
+ const [activeTab, setActiveTab] = d2(hasException ? "exception" : defaultTab);
4457
4595
  const jobData = cd["job"];
4458
4596
  const isFailed = jobData?.status === "failed";
4459
4597
  const parent = profile.parent_profile;
@@ -4515,22 +4653,36 @@
4515
4653
  ] }),
4516
4654
  /* @__PURE__ */ u3("div", { class: "profiler-panel profiler-mb-6", children: [
4517
4655
  /* @__PURE__ */ u3("div", { class: "tabs", children: [
4656
+ hasException && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("exception"), onClick: handleTabClick("exception"), style: "color:var(--profiler-error,#ef4444);", children: "\u{1F4A5} Exception" }),
4518
4657
  /* @__PURE__ */ u3("a", { href: "#", class: tabClass("job"), onClick: handleTabClick("job"), children: "Job" }),
4519
4658
  /* @__PURE__ */ u3("a", { href: "#", class: tabClass("database"), onClick: handleTabClick("database"), children: "Database" }),
4520
4659
  /* @__PURE__ */ u3("a", { href: "#", class: tabClass("cache"), onClick: handleTabClick("cache"), children: "Cache" }),
4521
4660
  hasHttp && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("http"), onClick: handleTabClick("http"), children: "Outbound HTTP" }),
4661
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("timeline"), onClick: handleTabClick("timeline"), children: "Timeline" }),
4522
4662
  hasJobs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("jobs"), onClick: handleTabClick("jobs"), children: [
4523
4663
  "Jobs (",
4524
4664
  profile.child_jobs.length,
4525
4665
  ")"
4526
- ] })
4666
+ ] }),
4667
+ hasDumps && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("dump"), onClick: handleTabClick("dump"), children: [
4668
+ "Dumps (",
4669
+ cd["dump"].count,
4670
+ ")"
4671
+ ] }),
4672
+ hasLogs && /* @__PURE__ */ u3("a", { href: "#", class: tabClass("logs"), onClick: handleTabClick("logs"), children: "Logs" }),
4673
+ /* @__PURE__ */ u3("a", { href: "#", class: tabClass("env"), onClick: handleTabClick("env"), children: "Env" })
4527
4674
  ] }),
4528
4675
  /* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
4676
+ activeTab === "exception" && /* @__PURE__ */ u3(ExceptionTab, { exceptionData: cd["exception"] }),
4529
4677
  activeTab === "job" && /* @__PURE__ */ u3(JobTab, { jobData: cd["job"] }),
4530
4678
  activeTab === "database" && /* @__PURE__ */ u3(DatabaseTab, { dbData: cd["database"], token: profile.token }),
4531
4679
  activeTab === "cache" && /* @__PURE__ */ u3(CacheTab, { cacheData: cd["cache"] }),
4532
4680
  activeTab === "http" && /* @__PURE__ */ u3(HttpTab, { httpData: cd["http"] }),
4533
- activeTab === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs })
4681
+ activeTab === "timeline" && /* @__PURE__ */ u3(FlameGraphTab, { flamegraphData: cd["flamegraph"], perfData: cd["performance"], functionProfileData: cd["function_profile"] }),
4682
+ activeTab === "jobs" && /* @__PURE__ */ u3(JobsTab, { jobs: profile.child_jobs }),
4683
+ activeTab === "dump" && /* @__PURE__ */ u3(DumpsTab, { dumpData: cd["dump"] }),
4684
+ activeTab === "logs" && /* @__PURE__ */ u3(LogsTab, { logData: cd["logs"] }),
4685
+ activeTab === "env" && /* @__PURE__ */ u3(EnvTab, { envData: cd["env"], readOnly: true })
4534
4686
  ] })
4535
4687
  ] }),
4536
4688
  !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
@@ -21,13 +22,37 @@ module Profiler
21
22
  value = params[:value]
22
23
 
23
24
  if value.nil? || value.to_s.empty?
25
+ Profiler.env_override_store.delete(key)
24
26
  ENV.delete(key)
25
- render json: { key: key, value: nil, deleted: true }
27
+ render json: { key: key, value: nil, deleted: true, override: Profiler.env_override_store.all_overrides[key] }
26
28
  else
29
+ current_original = Profiler.env_override_store.all_overrides.dig(key, "original")
30
+ if current_original == value.to_s
31
+ Profiler.env_override_store.reset(key)
32
+ else
33
+ Profiler.env_override_store.set(key, value.to_s)
34
+ end
27
35
  ENV[key] = value.to_s
28
- render json: { key: key, value: ENV[key] }
36
+ render json: { key: key, value: ENV[key], override: Profiler.env_override_store.all_overrides[key] }
29
37
  end
30
38
  end
39
+
40
+ def reset_override
41
+ key = params[:key].to_s.strip
42
+
43
+ if key.blank?
44
+ render json: { error: "Key cannot be blank" }, status: :unprocessable_entity
45
+ return
46
+ end
47
+
48
+ Profiler.env_override_store.reset(key)
49
+ render json: { key: key, value: ENV[key], reset: true }
50
+ end
51
+
52
+ def reset_all
53
+ Profiler.env_override_store.reset_all
54
+ render json: { reset: true }
55
+ end
31
56
  end
32
57
  end
33
58
  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
@@ -14,6 +14,11 @@ module Profiler
14
14
  def call(env)
15
15
  return @app.call(env) unless should_profile?(env)
16
16
 
17
+ status = nil
18
+ headers = nil
19
+ body = nil
20
+ collectors = nil
21
+
17
22
  profile = Models::Profile.new(build_request(env))
18
23
  Profiler::CurrentContext.token = profile.token
19
24
 
@@ -23,14 +28,12 @@ module Profiler
23
28
  # Store profile in env for collectors
24
29
  env["profiler.profile"] = profile
25
30
 
26
- # Create and subscribe collectors
27
31
  collectors = create_collectors(profile)
28
32
  env["profiler.collectors"] = collectors
29
33
 
30
34
  # Measure memory before
31
35
  memory_before = current_memory if Profiler.configuration.track_memory
32
36
 
33
- # Process request
34
37
  status, headers, body = @app.call(env)
35
38
 
36
39
  # Measure memory after
@@ -39,14 +42,11 @@ module Profiler
39
42
  profile.memory = memory_after - memory_before
40
43
  end
41
44
 
42
- # Collect and buffer response body (avoids double-reading by ToolbarInjector)
43
45
  body_content = collect_body(body)
44
46
  body = [body_content]
45
47
 
46
- # Finish profile
47
48
  profile.finish(status, headers)
48
49
 
49
- # Store request and response bodies
50
50
  profile.set_bodies(
51
51
  request_body: req_body_raw,
52
52
  response_body: body_content,
@@ -54,7 +54,6 @@ module Profiler
54
54
  resp_content_type: (headers["content-type"] || headers["Content-Type"]).to_s
55
55
  )
56
56
 
57
- # Collect data from all collectors
58
57
  collectors.each do |collector|
59
58
  begin
60
59
  collector.collect if collector.respond_to?(:collect)
@@ -64,14 +63,11 @@ module Profiler
64
63
  end
65
64
  end
66
65
 
67
- # Store profile
68
66
  Profiler.storage.save(profile.token, profile)
69
67
  Profiler::CurrentContext.clear
70
68
 
71
- # Add profiler token header
72
69
  headers["X-Profiler-Token"] = profile.token
73
70
 
74
- # Inject toolbar if HTML response
75
71
  if html_response?(headers)
76
72
  nonce = env['action_dispatch.content_security_policy_nonce']
77
73
  body = ToolbarInjector.new(body, profile.token, nonce).inject
@@ -80,7 +76,9 @@ module Profiler
80
76
  [status, headers, body]
81
77
  rescue => e
82
78
  warn "Profiler error: #{e.message}\n#{e.backtrace.join("\n")}"
83
- @app.call(env)
79
+ collectors&.each { |c| c.unsubscribe if c.respond_to?(:unsubscribe) }
80
+ Profiler::CurrentContext.clear
81
+ status ? [status, headers, body || []] : @app.call(env)
84
82
  end
85
83
 
86
84
  private
@@ -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|
@@ -19,6 +19,9 @@ module Profiler
19
19
 
20
20
  @db = SQLite3::Database.new(db_path.to_s)
21
21
  @db.results_as_hash = true
22
+ @db.busy_timeout = 5000
23
+ @db.execute("PRAGMA journal_mode=WAL")
24
+ @db.execute("PRAGMA synchronous=NORMAL")
22
25
 
23
26
  @blob_store = BlobStore.new(blob_path.to_s)
24
27
 
@@ -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.1"
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.1
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-17 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