150 lines
4.8 KiB
JavaScript
150 lines
4.8 KiB
JavaScript
const accountSelect = document.getElementById("account-select");
|
|||
|
|
const txForm = document.getElementById("tx-form");
|
||
|
|
const resultJson = document.getElementById("result-json");
|
||
|
|
const resultRtt = document.getElementById("result-rtt");
|
||
|
|
const resultRequestId = document.getElementById("result-request-id");
|
||
|
|
const burstButton = document.getElementById("burst-button");
|
||
|
|
const burstN = document.getElementById("burst-n");
|
||
|
|
const burstTableBody = document.querySelector("#burst-table tbody");
|
||
|
|
const chaosFlagsDiv = document.getElementById("chaos-flags");
|
||
|
|
const healthStrip = document.getElementById("health-strip");
|
||
|
|
|
||
|
|
async function loadAccounts() {
|
||
|
|
const resp = await fetch("/api/accounts");
|
||
|
|
const accounts = await resp.json();
|
||
|
|
accountSelect.innerHTML = accounts
|
||
|
|
.slice(0, 200)
|
||
|
|
.map((a) => `<option value="${a.id}">${a.holder_name} (${a.currency})</option>`)
|
||
|
|
.join("");
|
||
|
|
}
|
||
|
|
|
||
|
|
txForm.addEventListener("submit", async (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
const payload = {
|
||
|
|
account_id: accountSelect.value,
|
||
|
|
kind: document.querySelector('input[name="kind"]:checked').value,
|
||
|
|
amount_minor: Number(document.getElementById("amount-input").value),
|
||
|
|
currency: "EUR",
|
||
|
|
merchant_name: document.getElementById("merchant-input").value,
|
||
|
|
};
|
||
|
|
const start = performance.now();
|
||
|
|
const resp = await fetch("/api/transactions", {
|
||
|
|
method: "POST",
|
||
|
|
headers: { "Content-Type": "application/json" },
|
||
|
|
body: JSON.stringify(payload),
|
||
|
|
});
|
||
|
|
const rtt = performance.now() - start;
|
||
|
|
const body = await resp.json();
|
||
|
|
resultRtt.textContent = rtt.toFixed(1);
|
||
|
|
resultRequestId.textContent = body.request_id || "--";
|
||
|
|
resultJson.textContent = JSON.stringify(body, null, 2);
|
||
|
|
});
|
||
|
|
|
||
|
|
function percentile(sorted, p) {
|
||
|
|
if (sorted.length === 0) return 0;
|
||
|
|
const rank = Math.max(1, Math.min(sorted.length, Math.ceil(p * sorted.length)));
|
||
|
|
return sorted[rank - 1];
|
||
|
|
}
|
||
|
|
|
||
|
|
burstButton.addEventListener("click", async () => {
|
||
|
|
const n = Number(burstN.value);
|
||
|
|
const accounts = Array.from(accountSelect.options).map((o) => o.value);
|
||
|
|
burstTableBody.innerHTML = `<tr><td colspan="8">running ${n} requests...</td></tr>`;
|
||
|
|
|
||
|
|
const latencies = [];
|
||
|
|
let success = 0;
|
||
|
|
let errors = 0;
|
||
|
|
|
||
|
|
await Promise.all(
|
||
|
|
Array.from({ length: n }, async () => {
|
||
|
|
const accountId = accounts[Math.floor(Math.random() * accounts.length)];
|
||
|
|
const start = performance.now();
|
||
|
|
try {
|
||
|
|
const resp = await fetch("/api/transactions", {
|
||
|
|
method: "POST",
|
||
|
|
headers: { "Content-Type": "application/json" },
|
||
|
|
body: JSON.stringify({
|
||
|
|
account_id: accountId,
|
||
|
|
kind: "purchase",
|
||
|
|
amount_minor: 500,
|
||
|
|
currency: "EUR",
|
||
|
|
merchant_name: "Load Burst",
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
latencies.push(performance.now() - start);
|
||
|
|
if (resp.ok) success += 1;
|
||
|
|
else errors += 1;
|
||
|
|
} catch (e) {
|
||
|
|
latencies.push(performance.now() - start);
|
||
|
|
errors += 1;
|
||
|
|
}
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
latencies.sort((a, b) => a - b);
|
||
|
|
const mean = latencies.reduce((a, b) => a + b, 0) / latencies.length;
|
||
|
|
const row = [
|
||
|
|
latencies.length,
|
||
|
|
success,
|
||
|
|
errors,
|
||
|
|
latencies[0].toFixed(1),
|
||
|
|
mean.toFixed(1),
|
||
|
|
percentile(latencies, 0.5).toFixed(1),
|
||
|
|
percentile(latencies, 0.95).toFixed(1),
|
||
|
|
latencies[latencies.length - 1].toFixed(1),
|
||
|
|
];
|
||
|
|
burstTableBody.innerHTML = `<tr>${row.map((v) => `<td>${v}</td>`).join("")}</tr>`;
|
||
|
|
});
|
||
|
|
|
||
|
|
async function loadChaos() {
|
||
|
|
const resp = await fetch("/api/admin/chaos");
|
||
|
|
const { flags } = await resp.json();
|
||
|
|
chaosFlagsDiv.innerHTML = Object.entries(flags)
|
||
|
|
.map(
|
||
|
|
([flag, enabled]) => `
|
||
|
|
<label>
|
||
|
|
<input type="checkbox" data-flag="${flag}" ${enabled ? "checked" : ""}>
|
||
|
|
${flag}
|
||
|
|
</label>`
|
||
|
|
)
|
||
|
|
.join("");
|
||
|
|
|
||
|
|
chaosFlagsDiv.querySelectorAll("input[type=checkbox]").forEach((el) => {
|
||
|
|
el.addEventListener("change", async () => {
|
||
|
|
await fetch("/api/admin/chaos", {
|
||
|
|
method: "POST",
|
||
|
|
headers: { "Content-Type": "application/json" },
|
||
|
|
body: JSON.stringify({ flag: el.dataset.flag, enabled: el.checked }),
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
document.querySelectorAll("#chaos-profiles button").forEach((btn) => {
|
||
|
|
btn.addEventListener("click", async () => {
|
||
|
|
await fetch("/api/admin/chaos/profile", {
|
||
|
|
method: "POST",
|
||
|
|
headers: { "Content-Type": "application/json" },
|
||
|
|
body: JSON.stringify({ profile: btn.dataset.profile }),
|
||
|
|
});
|
||
|
|
await loadChaos();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
async function pollHealth() {
|
||
|
|
try {
|
||
|
|
const resp = await fetch("/api/health/all");
|
||
|
|
const { services } = await resp.json();
|
||
|
|
healthStrip.innerHTML = Object.entries(services)
|
||
|
|
.map(([name, status]) => `<span class="dot ${status === "ok" ? "green" : "red"}">${name}</span>`)
|
||
|
|
.join("");
|
||
|
|
} catch (e) {
|
||
|
|
// ignore transient polling failures
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
loadAccounts();
|
||
|
|
loadChaos();
|
||
|
|
pollHealth();
|
||
|
|
setInterval(pollHealth, 5000);
|