rails-profiler 0.28.0 → 0.30.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-toolbar.js +60 -1
- data/app/assets/builds/profiler.js +120 -31
- data/app/controllers/profiler/api/cluster_controller.rb +35 -0
- data/app/controllers/profiler/api/events_controller.rb +35 -0
- data/app/controllers/profiler/api/profiles_controller.rb +8 -2
- data/app/controllers/profiler/api/slave_proxy_controller.rb +49 -0
- data/app/views/layouts/profiler/application.html.erb +3 -0
- data/config/routes.rb +11 -0
- data/lib/profiler/cluster/master_client.rb +79 -0
- data/lib/profiler/cluster/slave_proxy.rb +106 -0
- data/lib/profiler/cluster/slave_registry.rb +50 -0
- data/lib/profiler/configuration.rb +20 -1
- data/lib/profiler/mcp/server.rb +49 -20
- data/lib/profiler/mcp/slave_support.rb +23 -0
- data/lib/profiler/mcp/tools/analyze_queries.rb +5 -2
- data/lib/profiler/mcp/tools/clear_profiles.rb +11 -1
- data/lib/profiler/mcp/tools/delete_env_var.rb +7 -0
- data/lib/profiler/mcp/tools/explain_query.rb +8 -0
- data/lib/profiler/mcp/tools/get_profile_ajax.rb +5 -2
- data/lib/profiler/mcp/tools/get_profile_detail.rb +5 -2
- data/lib/profiler/mcp/tools/get_profile_dumps.rb +5 -2
- data/lib/profiler/mcp/tools/get_profile_http.rb +5 -2
- data/lib/profiler/mcp/tools/get_profile_mailers.rb +5 -2
- data/lib/profiler/mcp/tools/get_test_profile_detail.rb +5 -2
- data/lib/profiler/mcp/tools/list_env_vars.rb +38 -0
- data/lib/profiler/mcp/tools/list_slaves.rb +27 -0
- data/lib/profiler/mcp/tools/query_console_profiles.rb +4 -1
- data/lib/profiler/mcp/tools/query_jobs.rb +4 -1
- data/lib/profiler/mcp/tools/query_mailers.rb +4 -1
- data/lib/profiler/mcp/tools/query_profiles.rb +4 -1
- data/lib/profiler/mcp/tools/query_test_profiles.rb +4 -1
- data/lib/profiler/mcp/tools/reset_all_env_vars.rb +8 -1
- data/lib/profiler/mcp/tools/reset_env_var.rb +9 -0
- data/lib/profiler/mcp/tools/run_tests.rb +41 -0
- data/lib/profiler/mcp/tools/set_env_var.rb +7 -0
- data/lib/profiler/railtie.rb +11 -0
- data/lib/profiler/sse/bus.rb +13 -0
- data/lib/profiler/sse/event_bus.rb +47 -0
- data/lib/profiler/sse/redis_event_bus.rb +79 -0
- data/lib/profiler/storage/base_store.rb +18 -1
- data/lib/profiler/storage/file_store.rb +1 -1
- data/lib/profiler/storage/memory_store.rb +1 -1
- data/lib/profiler/storage/redis_store.rb +3 -1
- data/lib/profiler/storage/sqlite_store.rb +1 -1
- data/lib/profiler/version.rb +1 -1
- data/lib/profiler.rb +10 -0
- metadata +13 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fcc4a5ad4cdedde3dd6427df52aeb75a54c4c8e973e0816f5f3d71e958042a89
|
|
4
|
+
data.tar.gz: b4f2c42db2a3a4fd0d1afd3648dcde087284dd3cf35aef0bca86049a832f3f71
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3e0083dfee931dcc4e1d13eff5e873cd18eb24a8853ce503b57a38e4ec7bd3d32862e4298cf8a4cc100ee4b47863eb715a0317488e41b0b9e228127d7fcd62fd
|
|
7
|
+
data.tar.gz: 8eabcaefdfeaf5fd5fbc0149aacb179dd139b05c92157fa48708484e140c868c36869c3d03023ffcfc7603f318fde2e156ef292ca1265406d176de591c832d64
|
|
@@ -4146,9 +4146,26 @@
|
|
|
4146
4146
|
] }) });
|
|
4147
4147
|
}
|
|
4148
4148
|
|
|
4149
|
+
// app/assets/typescript/profiler/cluster-context.ts
|
|
4150
|
+
var STORAGE_KEY = "profiler-active-slave";
|
|
4151
|
+
var activeSlavePrefix = (() => {
|
|
4152
|
+
try {
|
|
4153
|
+
const stored = sessionStorage.getItem(STORAGE_KEY);
|
|
4154
|
+
return stored ? `/slaves/${stored}` : "";
|
|
4155
|
+
} catch {
|
|
4156
|
+
return "";
|
|
4157
|
+
}
|
|
4158
|
+
})();
|
|
4159
|
+
function getActiveSlavePrefix() {
|
|
4160
|
+
return activeSlavePrefix;
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4149
4163
|
// app/assets/typescript/profiler/api-fetcher.ts
|
|
4150
4164
|
async function apiFetch(config) {
|
|
4151
|
-
const
|
|
4165
|
+
const prefix = getActiveSlavePrefix();
|
|
4166
|
+
const resolvedUrl = prefix ? config.url.replace("/_profiler/api", `/_profiler/api${prefix}`) : config.url;
|
|
4167
|
+
const { method, params, data, headers = {}, signal } = config;
|
|
4168
|
+
const url = resolvedUrl;
|
|
4152
4169
|
const qs = params ? "?" + new URLSearchParams(Object.entries(params).map(([k3, v3]) => [k3, String(v3)])).toString() : "";
|
|
4153
4170
|
const res = await fetch(url + qs, {
|
|
4154
4171
|
method,
|
|
@@ -4192,6 +4209,45 @@
|
|
|
4192
4209
|
return query;
|
|
4193
4210
|
}
|
|
4194
4211
|
|
|
4212
|
+
// app/assets/typescript/profiler/hooks/useProfileEvents.ts
|
|
4213
|
+
var BASE = "/_profiler";
|
|
4214
|
+
function useProfileEvents(token, collectors, onUpdate) {
|
|
4215
|
+
const [connected, setConnected] = d2(false);
|
|
4216
|
+
const fallbackRef = A2(null);
|
|
4217
|
+
y2(() => {
|
|
4218
|
+
if (!token) return;
|
|
4219
|
+
const clearFallback = () => {
|
|
4220
|
+
if (fallbackRef.current !== null) {
|
|
4221
|
+
clearInterval(fallbackRef.current);
|
|
4222
|
+
fallbackRef.current = null;
|
|
4223
|
+
}
|
|
4224
|
+
};
|
|
4225
|
+
const params = new URLSearchParams();
|
|
4226
|
+
collectors.forEach((c3) => params.append("collectors[]", c3));
|
|
4227
|
+
const url = `${BASE}/api/events/${token}?${params.toString()}`;
|
|
4228
|
+
const es = new EventSource(url);
|
|
4229
|
+
es.addEventListener("profile_update", () => {
|
|
4230
|
+
onUpdate();
|
|
4231
|
+
});
|
|
4232
|
+
es.onopen = () => {
|
|
4233
|
+
setConnected(true);
|
|
4234
|
+
clearFallback();
|
|
4235
|
+
};
|
|
4236
|
+
es.onerror = () => {
|
|
4237
|
+
es.close();
|
|
4238
|
+
setConnected(false);
|
|
4239
|
+
if (fallbackRef.current === null) {
|
|
4240
|
+
fallbackRef.current = setInterval(() => onUpdate(), 1e4);
|
|
4241
|
+
}
|
|
4242
|
+
};
|
|
4243
|
+
return () => {
|
|
4244
|
+
es.close();
|
|
4245
|
+
clearFallback();
|
|
4246
|
+
};
|
|
4247
|
+
}, [token]);
|
|
4248
|
+
return { connected };
|
|
4249
|
+
}
|
|
4250
|
+
|
|
4195
4251
|
// app/assets/typescript/profiler/toolbar-bundle.tsx
|
|
4196
4252
|
var ANIM_MS = 280;
|
|
4197
4253
|
function applyTheme(elements) {
|
|
@@ -4209,6 +4265,9 @@
|
|
|
4209
4265
|
refetch();
|
|
4210
4266
|
};
|
|
4211
4267
|
}, [refetch]);
|
|
4268
|
+
useProfileEvents(token, [], () => {
|
|
4269
|
+
refetch();
|
|
4270
|
+
});
|
|
4212
4271
|
const profile = data?.profile ?? null;
|
|
4213
4272
|
if (!profile) return null;
|
|
4214
4273
|
return /* @__PURE__ */ u3(ToolbarApp, { profile, token });
|
|
@@ -3419,9 +3419,42 @@
|
|
|
3419
3419
|
);
|
|
3420
3420
|
}
|
|
3421
3421
|
|
|
3422
|
+
// app/assets/typescript/profiler/cluster-context.ts
|
|
3423
|
+
var STORAGE_KEY = "profiler-active-slave";
|
|
3424
|
+
var activeSlavePrefix = (() => {
|
|
3425
|
+
try {
|
|
3426
|
+
const stored = sessionStorage.getItem(STORAGE_KEY);
|
|
3427
|
+
return stored ? `/slaves/${stored}` : "";
|
|
3428
|
+
} catch {
|
|
3429
|
+
return "";
|
|
3430
|
+
}
|
|
3431
|
+
})();
|
|
3432
|
+
function getActiveSlavePrefix() {
|
|
3433
|
+
return activeSlavePrefix;
|
|
3434
|
+
}
|
|
3435
|
+
function getActiveSlaveName() {
|
|
3436
|
+
if (!activeSlavePrefix) return null;
|
|
3437
|
+
return activeSlavePrefix.replace("/slaves/", "");
|
|
3438
|
+
}
|
|
3439
|
+
function setActiveSlave(name) {
|
|
3440
|
+
try {
|
|
3441
|
+
if (name) {
|
|
3442
|
+
sessionStorage.setItem(STORAGE_KEY, name);
|
|
3443
|
+
} else {
|
|
3444
|
+
sessionStorage.removeItem(STORAGE_KEY);
|
|
3445
|
+
}
|
|
3446
|
+
} catch {
|
|
3447
|
+
}
|
|
3448
|
+
activeSlavePrefix = name ? `/slaves/${name}` : "";
|
|
3449
|
+
window.location.reload();
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3422
3452
|
// app/assets/typescript/profiler/api-fetcher.ts
|
|
3423
3453
|
async function apiFetch(config) {
|
|
3424
|
-
const
|
|
3454
|
+
const prefix = getActiveSlavePrefix();
|
|
3455
|
+
const resolvedUrl = prefix ? config.url.replace("/_profiler/api", `/_profiler/api${prefix}`) : config.url;
|
|
3456
|
+
const { method, params, data, headers = {}, signal } = config;
|
|
3457
|
+
const url = resolvedUrl;
|
|
3425
3458
|
const qs = params ? "?" + new URLSearchParams(Object.entries(params).map(([k3, v3]) => [k3, String(v3)])).toString() : "";
|
|
3426
3459
|
const res = await fetch(url + qs, {
|
|
3427
3460
|
method,
|
|
@@ -4390,8 +4423,8 @@
|
|
|
4390
4423
|
open && /* @__PURE__ */ u3(
|
|
4391
4424
|
HttpReqRespDetail,
|
|
4392
4425
|
{
|
|
4393
|
-
request: { headers: req.request_headers || {}, body: req.request_body, body_encoding: req.request_body_encoding },
|
|
4394
|
-
response: { headers: req.response_headers || {}, body: req.response_body, body_encoding: req.response_body_encoding },
|
|
4426
|
+
request: { headers: req.request_headers || {}, body: req.request_body ?? void 0, body_encoding: req.request_body_encoding },
|
|
4427
|
+
response: { headers: req.response_headers || {}, body: req.response_body ?? void 0, body_encoding: req.response_body_encoding },
|
|
4395
4428
|
backtrace: req.backtrace
|
|
4396
4429
|
}
|
|
4397
4430
|
)
|
|
@@ -4626,7 +4659,7 @@
|
|
|
4626
4659
|
if (!entries.length) return;
|
|
4627
4660
|
setSaving(true);
|
|
4628
4661
|
try {
|
|
4629
|
-
await Promise.all(entries.map(([k3, v3]) => patchEnvVar({ key: k3, value: v3 })));
|
|
4662
|
+
await Promise.all(entries.map(([k3, v3]) => patchEnvVar({ data: { key: k3, value: v3 } })));
|
|
4630
4663
|
setVariables((prev) => ({ ...prev, ...importPreview }));
|
|
4631
4664
|
setImportContent("");
|
|
4632
4665
|
setShowImport(false);
|
|
@@ -4654,7 +4687,7 @@
|
|
|
4654
4687
|
}
|
|
4655
4688
|
setSaving(true);
|
|
4656
4689
|
try {
|
|
4657
|
-
const data = await patchEnvVar({ key, value: editValue });
|
|
4690
|
+
const data = await patchEnvVar({ data: { key, value: editValue } });
|
|
4658
4691
|
setVariables((prev) => ({ ...prev, [key]: editValue }));
|
|
4659
4692
|
setOverrides((prev) => {
|
|
4660
4693
|
const n2 = { ...prev };
|
|
@@ -4676,7 +4709,7 @@
|
|
|
4676
4709
|
const deleteVar = async (key) => {
|
|
4677
4710
|
setSaving(true);
|
|
4678
4711
|
try {
|
|
4679
|
-
const data = await patchEnvVar({ key, value: null });
|
|
4712
|
+
const data = await patchEnvVar({ data: { key, value: null } });
|
|
4680
4713
|
setVariables((prev) => {
|
|
4681
4714
|
const n2 = { ...prev };
|
|
4682
4715
|
delete n2[key];
|
|
@@ -4708,7 +4741,7 @@
|
|
|
4708
4741
|
const next = /^(true|yes)$/i.test(current) ? "false" : "true";
|
|
4709
4742
|
setSaving(true);
|
|
4710
4743
|
try {
|
|
4711
|
-
const data = await patchEnvVar({ key, value: next });
|
|
4744
|
+
const data = await patchEnvVar({ data: { key, value: next } });
|
|
4712
4745
|
setVariables((prev) => ({ ...prev, [key]: next }));
|
|
4713
4746
|
setOverrides((prev) => {
|
|
4714
4747
|
const n2 = { ...prev };
|
|
@@ -4730,7 +4763,7 @@
|
|
|
4730
4763
|
if (!key) return;
|
|
4731
4764
|
setSaving(true);
|
|
4732
4765
|
try {
|
|
4733
|
-
const data = await patchEnvVar({ key, value: newValue });
|
|
4766
|
+
const data = await patchEnvVar({ data: { key, value: newValue } });
|
|
4734
4767
|
setVariables((prev) => ({ ...prev, [key]: newValue }));
|
|
4735
4768
|
setOverrides((prev) => {
|
|
4736
4769
|
const n2 = { ...prev };
|
|
@@ -4757,7 +4790,7 @@
|
|
|
4757
4790
|
const resetVar = async (key) => {
|
|
4758
4791
|
setSaving(true);
|
|
4759
4792
|
try {
|
|
4760
|
-
const data = await resetEnvVar2({ key });
|
|
4793
|
+
const data = await resetEnvVar2({ params: { key } });
|
|
4761
4794
|
setOverrides((prev) => {
|
|
4762
4795
|
const n2 = { ...prev };
|
|
4763
4796
|
delete n2[key];
|
|
@@ -4765,7 +4798,7 @@
|
|
|
4765
4798
|
});
|
|
4766
4799
|
const updater = (prev) => {
|
|
4767
4800
|
const n2 = { ...prev };
|
|
4768
|
-
if (data.value === null) {
|
|
4801
|
+
if (data.value === null || data.value === void 0) {
|
|
4769
4802
|
delete n2[key];
|
|
4770
4803
|
} else {
|
|
4771
4804
|
n2[key] = data.value;
|
|
@@ -4784,7 +4817,7 @@
|
|
|
4784
4817
|
const doResetAll = async () => {
|
|
4785
4818
|
setSaving(true);
|
|
4786
4819
|
try {
|
|
4787
|
-
await resetAllEnvVars2(
|
|
4820
|
+
await resetAllEnvVars2();
|
|
4788
4821
|
setOverrides({});
|
|
4789
4822
|
const data = await refresh();
|
|
4790
4823
|
if (data) setInitial(data.variables);
|
|
@@ -5397,7 +5430,7 @@
|
|
|
5397
5430
|
if (selected.size === 0 || isRunning) return;
|
|
5398
5431
|
setError(null);
|
|
5399
5432
|
try {
|
|
5400
|
-
const data = await startTestRun({ files: Array.from(selected), framework });
|
|
5433
|
+
const data = await startTestRun({ data: { files: Array.from(selected), framework } });
|
|
5401
5434
|
setCurrentRun(data);
|
|
5402
5435
|
setIsRunning(true);
|
|
5403
5436
|
} catch {
|
|
@@ -6004,7 +6037,7 @@
|
|
|
6004
6037
|
" ms"
|
|
6005
6038
|
] }) }),
|
|
6006
6039
|
/* @__PURE__ */ u3("td", { children: p3.collectors_data?.database?.total_queries ?? "\u2014" }),
|
|
6007
|
-
/* @__PURE__ */ u3("td", { children: formatMemory(p3.memory) }),
|
|
6040
|
+
/* @__PURE__ */ u3("td", { children: formatMemory(p3.memory ?? void 0) }),
|
|
6008
6041
|
/* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("span", { class: statusClass(p3.status), children: p3.status }) }),
|
|
6009
6042
|
/* @__PURE__ */ u3("td", { class: "profiler-text--xs profiler-text--mono profiler-text--muted", children: /* @__PURE__ */ u3("button", { class: "token-copy", onClick: () => copyToken(p3.token), title: "Copy full token", children: copiedToken === p3.token ? "\u2713" : p3.token.substring(0, 8) + "\u2026" }) }),
|
|
6010
6043
|
/* @__PURE__ */ u3("td", { children: /* @__PURE__ */ u3("button", { class: "btn-row-delete", onClick: () => deleteProfile2(p3.token), title: "Delete", children: "\xD7" }) })
|
|
@@ -6321,6 +6354,53 @@
|
|
|
6321
6354
|
] });
|
|
6322
6355
|
}
|
|
6323
6356
|
|
|
6357
|
+
// app/assets/typescript/profiler/components/ProfilerSelector.tsx
|
|
6358
|
+
function ProfilerSelector() {
|
|
6359
|
+
const [slaves, setSlaves] = d2([]);
|
|
6360
|
+
const [loading, setLoading] = d2(true);
|
|
6361
|
+
const activeSlave = getActiveSlaveName();
|
|
6362
|
+
y2(() => {
|
|
6363
|
+
fetch("/_profiler/api/cluster/slaves").then((r3) => r3.json()).then((data) => {
|
|
6364
|
+
setSlaves(data.slaves ?? []);
|
|
6365
|
+
setLoading(false);
|
|
6366
|
+
}).catch(() => setLoading(false));
|
|
6367
|
+
}, []);
|
|
6368
|
+
if (loading || slaves.length === 0) return null;
|
|
6369
|
+
const handleChange = (e3) => {
|
|
6370
|
+
const value = e3.target.value;
|
|
6371
|
+
setActiveSlave(value === "__local__" ? null : value);
|
|
6372
|
+
};
|
|
6373
|
+
const currentValue = activeSlave ?? "__local__";
|
|
6374
|
+
return /* @__PURE__ */ u3("div", { class: "profiler-selector", style: { display: "flex", alignItems: "center", gap: "8px", padding: "4px 12px", background: "var(--profiler-bg-secondary, #f5f5f5)", borderBottom: "1px solid var(--profiler-border, #e0e0e0)" }, children: [
|
|
6375
|
+
/* @__PURE__ */ u3("span", { style: { fontSize: "12px", fontWeight: 600, color: "var(--profiler-text-secondary, #666)", textTransform: "uppercase", letterSpacing: "0.05em" }, children: "Profiler" }),
|
|
6376
|
+
/* @__PURE__ */ u3(
|
|
6377
|
+
"select",
|
|
6378
|
+
{
|
|
6379
|
+
value: currentValue,
|
|
6380
|
+
onChange: handleChange,
|
|
6381
|
+
style: { fontSize: "13px", padding: "2px 6px", borderRadius: "4px", border: "1px solid var(--profiler-border, #ccc)", background: "var(--profiler-bg, #fff)", color: "var(--profiler-text, #333)", cursor: "pointer" },
|
|
6382
|
+
children: [
|
|
6383
|
+
/* @__PURE__ */ u3("option", { value: "__local__", children: "Local (master)" }),
|
|
6384
|
+
slaves.map((slave) => /* @__PURE__ */ u3(
|
|
6385
|
+
"option",
|
|
6386
|
+
{
|
|
6387
|
+
value: slave.name,
|
|
6388
|
+
disabled: slave.status === "offline",
|
|
6389
|
+
children: [
|
|
6390
|
+
slave.name,
|
|
6391
|
+
" ",
|
|
6392
|
+
slave.status === "offline" ? "(offline)" : ""
|
|
6393
|
+
]
|
|
6394
|
+
},
|
|
6395
|
+
slave.name
|
|
6396
|
+
))
|
|
6397
|
+
]
|
|
6398
|
+
}
|
|
6399
|
+
),
|
|
6400
|
+
activeSlave && /* @__PURE__ */ u3("span", { style: { fontSize: "11px", color: "var(--profiler-text-secondary, #888)", fontStyle: "italic" }, children: "Viewing slave data via proxy" })
|
|
6401
|
+
] });
|
|
6402
|
+
}
|
|
6403
|
+
|
|
6324
6404
|
// app/assets/typescript/profiler/components/dashboard/tabs/RequestTab.tsx
|
|
6325
6405
|
function buildCurl2(profile) {
|
|
6326
6406
|
const headers = profile.headers ?? {};
|
|
@@ -6376,13 +6456,13 @@
|
|
|
6376
6456
|
{
|
|
6377
6457
|
request: {
|
|
6378
6458
|
headers: profile.headers ?? {},
|
|
6379
|
-
body: profile.request_body,
|
|
6459
|
+
body: profile.request_body ?? void 0,
|
|
6380
6460
|
body_encoding: profile.request_body_encoding,
|
|
6381
6461
|
params: hasParams ? profile.params : void 0
|
|
6382
6462
|
},
|
|
6383
6463
|
response: {
|
|
6384
6464
|
headers: profile.response_headers ?? {},
|
|
6385
|
-
body: profile.response_body,
|
|
6465
|
+
body: profile.response_body ?? void 0,
|
|
6386
6466
|
body_encoding: profile.response_body_encoding
|
|
6387
6467
|
}
|
|
6388
6468
|
}
|
|
@@ -6525,7 +6605,7 @@
|
|
|
6525
6605
|
const runExplain = async (queryIndex) => {
|
|
6526
6606
|
setExplainState({ open: true, loading: true, result: null, format: "text", adapter: "", error: null });
|
|
6527
6607
|
try {
|
|
6528
|
-
const data = await runExplainMutation({ token, query_index: queryIndex });
|
|
6608
|
+
const data = await runExplainMutation({ data: { token, query_index: queryIndex } });
|
|
6529
6609
|
setExplainState((s3) => ({
|
|
6530
6610
|
...s3,
|
|
6531
6611
|
loading: false,
|
|
@@ -7155,7 +7235,8 @@
|
|
|
7155
7235
|
if (node.payload && category !== "method") {
|
|
7156
7236
|
let payloadText = null;
|
|
7157
7237
|
if (category === "sql" && node.payload.sql) {
|
|
7158
|
-
|
|
7238
|
+
const sql = node.payload.sql;
|
|
7239
|
+
payloadText = sql.length > 200 ? sql.slice(0, 200) + "..." : sql;
|
|
7159
7240
|
} else if (category === "cache" && node.payload.key) {
|
|
7160
7241
|
payloadText = `Key: ${node.payload.key}`;
|
|
7161
7242
|
} else if (category === "http" && node.payload.url) {
|
|
@@ -7283,7 +7364,7 @@
|
|
|
7283
7364
|
const toggleFunctionProfiling = async () => {
|
|
7284
7365
|
setFnToggling(true);
|
|
7285
7366
|
try {
|
|
7286
|
-
const json = await patchFunctionProfiling({ enabled: !fnEnabled });
|
|
7367
|
+
const json = await patchFunctionProfiling({ data: { enabled: !fnEnabled } });
|
|
7287
7368
|
setFnEnabled(json.enabled);
|
|
7288
7369
|
} finally {
|
|
7289
7370
|
setFnToggling(false);
|
|
@@ -7292,7 +7373,7 @@
|
|
|
7292
7373
|
const updateMaxFrames = async (value) => {
|
|
7293
7374
|
setFnMaxFramesUpdating(true);
|
|
7294
7375
|
try {
|
|
7295
|
-
const json = await patchFunctionProfiling({ max_frames: value });
|
|
7376
|
+
const json = await patchFunctionProfiling({ data: { max_frames: value } });
|
|
7296
7377
|
setFnMaxFrames(json.max_frames);
|
|
7297
7378
|
} finally {
|
|
7298
7379
|
setFnMaxFramesUpdating(false);
|
|
@@ -7302,7 +7383,7 @@
|
|
|
7302
7383
|
setFnMode(value);
|
|
7303
7384
|
setFnModeUpdating(true);
|
|
7304
7385
|
try {
|
|
7305
|
-
const json = await patchFunctionProfiling({ mode: value });
|
|
7386
|
+
const json = await patchFunctionProfiling({ data: { mode: value } });
|
|
7306
7387
|
setFnMode(json.mode ?? value);
|
|
7307
7388
|
} finally {
|
|
7308
7389
|
setFnModeUpdating(false);
|
|
@@ -7311,7 +7392,7 @@
|
|
|
7311
7392
|
const updateClock = async (value) => {
|
|
7312
7393
|
setFnClockUpdating(true);
|
|
7313
7394
|
try {
|
|
7314
|
-
const json = await patchFunctionProfiling({ clock: value });
|
|
7395
|
+
const json = await patchFunctionProfiling({ data: { clock: value } });
|
|
7315
7396
|
setFnClock(json.clock ?? value);
|
|
7316
7397
|
} finally {
|
|
7317
7398
|
setFnClockUpdating(false);
|
|
@@ -8518,7 +8599,7 @@
|
|
|
8518
8599
|
mode === "preview" && hasHtml && /* @__PURE__ */ u3(
|
|
8519
8600
|
"iframe",
|
|
8520
8601
|
{
|
|
8521
|
-
srcdoc: email.body_html,
|
|
8602
|
+
srcdoc: email.body_html ?? void 0,
|
|
8522
8603
|
sandbox: "allow-same-origin",
|
|
8523
8604
|
style: { width: "100%", height: "300px", border: "1px solid var(--profiler-border)", borderRadius: "var(--profiler-radius-md)", background: "#fff", display: "block" }
|
|
8524
8605
|
}
|
|
@@ -9228,11 +9309,11 @@
|
|
|
9228
9309
|
/* @__PURE__ */ u3("div", { class: "profiler-p-4 tab-content active", children: [
|
|
9229
9310
|
activeTab === "test" && testData && /* @__PURE__ */ u3(TestTab, { testData }),
|
|
9230
9311
|
activeTab === "database" && /* @__PURE__ */ u3(DatabaseTab, { dbData: cd["database"], token: profile.token }),
|
|
9231
|
-
activeTab === "cache" && /* @__PURE__ */ u3(CacheTab, {
|
|
9232
|
-
activeTab === "timeline" && /* @__PURE__ */ u3(FlameGraphTab, {
|
|
9233
|
-
activeTab === "dump" && hasDumps && /* @__PURE__ */ u3(DumpsTab, {
|
|
9234
|
-
activeTab === "logs" && hasLogs && /* @__PURE__ */ u3(LogsTab, {
|
|
9235
|
-
activeTab === "exception" && hasException && /* @__PURE__ */ u3(ExceptionTab, {
|
|
9312
|
+
activeTab === "cache" && /* @__PURE__ */ u3(CacheTab, { cacheData: cd["cache"] }),
|
|
9313
|
+
activeTab === "timeline" && /* @__PURE__ */ u3(FlameGraphTab, { flamegraphData: cd["flamegraph"], perfData: cd["performance"], functionProfileData: cd["function_profile"] }),
|
|
9314
|
+
activeTab === "dump" && hasDumps && /* @__PURE__ */ u3(DumpsTab, { dumpData: cd["dump"] }),
|
|
9315
|
+
activeTab === "logs" && hasLogs && /* @__PURE__ */ u3(LogsTab, { logData: cd["logs"] }),
|
|
9316
|
+
activeTab === "exception" && hasException && /* @__PURE__ */ u3(ExceptionTab, { exceptionData: cd["exception"] }),
|
|
9236
9317
|
activeTab === "env" && /* @__PURE__ */ u3(EnvTab, { envData: cd["env"] })
|
|
9237
9318
|
] })
|
|
9238
9319
|
] })
|
|
@@ -9382,7 +9463,7 @@ Duration: ${event.duration.toFixed(2)}ms`;
|
|
|
9382
9463
|
}
|
|
9383
9464
|
|
|
9384
9465
|
// app/assets/typescript/profiler/theme.ts
|
|
9385
|
-
var
|
|
9466
|
+
var STORAGE_KEY2 = "profiler-theme";
|
|
9386
9467
|
var THEME_ATTRIBUTE = "data-theme";
|
|
9387
9468
|
var ThemeManager = class {
|
|
9388
9469
|
constructor() {
|
|
@@ -9398,18 +9479,18 @@ Duration: ${event.duration.toFixed(2)}ms`;
|
|
|
9398
9479
|
}
|
|
9399
9480
|
});
|
|
9400
9481
|
window.addEventListener("storage", (e3) => {
|
|
9401
|
-
if (e3.key ===
|
|
9482
|
+
if (e3.key === STORAGE_KEY2 && e3.newValue) {
|
|
9402
9483
|
this.currentTheme = e3.newValue;
|
|
9403
9484
|
this.applyTheme(this.currentTheme);
|
|
9404
9485
|
}
|
|
9405
9486
|
});
|
|
9406
9487
|
}
|
|
9407
9488
|
loadTheme() {
|
|
9408
|
-
const stored = localStorage.getItem(
|
|
9489
|
+
const stored = localStorage.getItem(STORAGE_KEY2);
|
|
9409
9490
|
return stored || "auto";
|
|
9410
9491
|
}
|
|
9411
9492
|
saveTheme(theme) {
|
|
9412
|
-
localStorage.setItem(
|
|
9493
|
+
localStorage.setItem(STORAGE_KEY2, theme);
|
|
9413
9494
|
}
|
|
9414
9495
|
applyTheme(theme) {
|
|
9415
9496
|
const resolvedTheme = this.resolveTheme(theme);
|
|
@@ -9507,6 +9588,14 @@ Duration: ${event.duration.toFixed(2)}ms`;
|
|
|
9507
9588
|
defaultOptions: { queries: { staleTime: 3e4, retry: 1 } }
|
|
9508
9589
|
});
|
|
9509
9590
|
document.addEventListener("DOMContentLoaded", () => {
|
|
9591
|
+
const selectorEl = document.getElementById("profiler-cluster-selector");
|
|
9592
|
+
const isMaster = document.querySelector('meta[name="profiler-is-master"]')?.getAttribute("content") === "true";
|
|
9593
|
+
if (selectorEl && isMaster) {
|
|
9594
|
+
J(
|
|
9595
|
+
/* @__PURE__ */ u3(QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ u3(ProfilerSelector, {}) }),
|
|
9596
|
+
selectorEl
|
|
9597
|
+
);
|
|
9598
|
+
}
|
|
9510
9599
|
const indexEl = document.getElementById("profiler-index");
|
|
9511
9600
|
if (indexEl) {
|
|
9512
9601
|
J(
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Profiler
|
|
4
|
+
module Api
|
|
5
|
+
class ClusterController < Profiler::ApplicationController
|
|
6
|
+
skip_before_action :verify_authenticity_token
|
|
7
|
+
|
|
8
|
+
def register
|
|
9
|
+
name = params[:name].to_s
|
|
10
|
+
url = params[:url].to_s
|
|
11
|
+
|
|
12
|
+
if name.empty? || url.empty?
|
|
13
|
+
return render json: { error: "name and url are required" }, status: :unprocessable_entity
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
Profiler.slave_registry.register(name: name, url: url)
|
|
17
|
+
render json: { ok: true, name: name }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def heartbeat
|
|
21
|
+
name = params[:name].to_s
|
|
22
|
+
entry = Profiler.slave_registry.heartbeat(name)
|
|
23
|
+
if entry.nil?
|
|
24
|
+
render json: { error: "Unknown slave — please re-register" }, status: :not_found
|
|
25
|
+
else
|
|
26
|
+
render json: { ok: true }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def slaves
|
|
31
|
+
render json: { slaves: Profiler.slave_registry.all }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Profiler
|
|
4
|
+
module Api
|
|
5
|
+
class EventsController < Profiler::ApplicationController
|
|
6
|
+
include ActionController::Live
|
|
7
|
+
skip_before_action :verify_authenticity_token
|
|
8
|
+
|
|
9
|
+
def subscribe
|
|
10
|
+
response.headers["Content-Type"] = "text/event-stream"
|
|
11
|
+
response.headers["Cache-Control"] = "no-cache"
|
|
12
|
+
response.headers["X-Accel-Buffering"] = "no"
|
|
13
|
+
|
|
14
|
+
sse = ActionController::Live::SSE.new(response.stream, retry: 3000, event: "profile_update")
|
|
15
|
+
id = Profiler::SSE.current.subscribe(params[:token], Array(params[:collectors]))
|
|
16
|
+
|
|
17
|
+
begin
|
|
18
|
+
loop do
|
|
19
|
+
event = Profiler::SSE.current.wait_for_event(id, timeout: 30)
|
|
20
|
+
if event
|
|
21
|
+
sse.write(event)
|
|
22
|
+
else
|
|
23
|
+
sse.write({}, event: "heartbeat")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
rescue ActionController::Live::ClientDisconnected, IOError
|
|
27
|
+
# Client disconnected — normal exit
|
|
28
|
+
ensure
|
|
29
|
+
Profiler::SSE.current.unsubscribe(id)
|
|
30
|
+
sse.close
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -9,8 +9,10 @@ module Profiler
|
|
|
9
9
|
limit = (params[:limit] || 50).to_i
|
|
10
10
|
offset = (params[:offset] || 0).to_i
|
|
11
11
|
all = Profiler.storage.list(limit: 1000, offset: 0)
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
# all_types is an opt-in used by the cluster proxy so it can mirror the full
|
|
13
|
+
# storage.list contract; the dashboard relies on the default http-only filter.
|
|
14
|
+
scope = all_types? ? all : all.select { |p| p.profile_type == "http" }
|
|
15
|
+
page = scope.drop(offset).first(limit + 1)
|
|
14
16
|
render json: {
|
|
15
17
|
profiles: page.first(limit).map(&:to_h),
|
|
16
18
|
limit: limit,
|
|
@@ -50,6 +52,10 @@ module Profiler
|
|
|
50
52
|
|
|
51
53
|
private
|
|
52
54
|
|
|
55
|
+
def all_types?
|
|
56
|
+
%w[1 true].include?(params[:all_types].to_s)
|
|
57
|
+
end
|
|
58
|
+
|
|
53
59
|
def recalculate_ajax_data(profile)
|
|
54
60
|
# Find AJAX collector in the configured collectors
|
|
55
61
|
ajax_collector_class = Profiler::Collectors::AjaxCollector
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../lib/profiler/cluster/slave_proxy"
|
|
4
|
+
|
|
5
|
+
module Profiler
|
|
6
|
+
module Api
|
|
7
|
+
class SlaveProxyController < Profiler::ApplicationController
|
|
8
|
+
skip_before_action :verify_authenticity_token
|
|
9
|
+
|
|
10
|
+
def forward
|
|
11
|
+
proxy = Cluster::SlaveProxy.new(params[:slave_name])
|
|
12
|
+
sub_path = params[:path].to_s
|
|
13
|
+
full_path = "/_profiler/api/#{sub_path}"
|
|
14
|
+
|
|
15
|
+
result = case request.method
|
|
16
|
+
when "GET"
|
|
17
|
+
proxy.get_json(full_path, request.query_parameters.to_h)
|
|
18
|
+
when "POST"
|
|
19
|
+
proxy.post_json(full_path, parsed_body)
|
|
20
|
+
when "PATCH"
|
|
21
|
+
proxy.patch_json(full_path, parsed_body)
|
|
22
|
+
when "DELETE"
|
|
23
|
+
proxy.delete_json(full_path)
|
|
24
|
+
else
|
|
25
|
+
return head :method_not_allowed
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
render json: result
|
|
29
|
+
rescue Profiler::Error => e
|
|
30
|
+
render json: { error: e.message }, status: :bad_gateway
|
|
31
|
+
rescue => e
|
|
32
|
+
render json: { error: "Proxy error: #{e.message}" }, status: :bad_gateway
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def parsed_body
|
|
38
|
+
return {} if request.body.nil?
|
|
39
|
+
|
|
40
|
+
raw = request.body.read
|
|
41
|
+
return {} if raw.empty?
|
|
42
|
+
|
|
43
|
+
JSON.parse(raw)
|
|
44
|
+
rescue JSON::ParserError
|
|
45
|
+
{}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -6,12 +6,15 @@
|
|
|
6
6
|
<%= csrf_meta_tags %>
|
|
7
7
|
<%= csp_meta_tag %>
|
|
8
8
|
<meta name="profiler-version" content="<%= Profiler::VERSION %>">
|
|
9
|
+
<meta name="profiler-name" content="<%= Profiler.configuration.resolved_name %>">
|
|
10
|
+
<meta name="profiler-is-master" content="<%= Profiler.configuration.master? %>">
|
|
9
11
|
|
|
10
12
|
<link rel="stylesheet" href="/_profiler/assets/profiler.css">
|
|
11
13
|
<script src="/_profiler/assets/profiler.js" defer></script>
|
|
12
14
|
</head>
|
|
13
15
|
|
|
14
16
|
<body>
|
|
17
|
+
<div id="profiler-cluster-selector"></div>
|
|
15
18
|
<%= yield %>
|
|
16
19
|
</body>
|
|
17
20
|
</html>
|
data/config/routes.rb
CHANGED
|
@@ -50,5 +50,16 @@ Profiler::Engine.routes.draw do
|
|
|
50
50
|
get "test_runner/runs/:id", to: "test_runner#show", as: :test_runner_run
|
|
51
51
|
get "test_runner/runs/:id/stream", to: "test_runner#stream", as: :test_runner_run_stream
|
|
52
52
|
delete "test_runner/runs/:id", to: "test_runner#destroy"
|
|
53
|
+
get "events/:token", to: "events#subscribe", as: :profile_events
|
|
54
|
+
|
|
55
|
+
# Cluster endpoints (master-side)
|
|
56
|
+
post "cluster/register", to: "cluster#register"
|
|
57
|
+
post "cluster/heartbeat", to: "cluster#heartbeat"
|
|
58
|
+
get "cluster/slaves", to: "cluster#slaves"
|
|
59
|
+
|
|
60
|
+
# Slave proxy — must be last to avoid shadowing other api routes
|
|
61
|
+
scope "/slaves/:slave_name" do
|
|
62
|
+
match "*path", to: "slave_proxy#forward", via: :all
|
|
63
|
+
end
|
|
53
64
|
end
|
|
54
65
|
end
|