Extends the existing /api/v2/event beacon into a small, typed, durable event schema so the interactive views can answer product questions the request log cannot: whether a visitor ever gets a location, whether search finds anything, which controls and views earn their maintenance, and which dead ends people actually hit. Seven events with three enum dimension slots, stored as hourly aggregates in a TimescaleDB hypertable plus one JSONL line per event for the 30-day Loki view. No row per interaction and no column an identifier could go in: no cookie, no session id, no user id, no IP, no coordinates, no free text, no URLs. The IP is a per-minute rate-limit key and nothing else. Abuse resistance for a public endpoint on a 60%-crawler site: closed allowlist (unknown names collapse to one bucket), 4 KB streaming body cap, per-IP ceiling raised to 120/min (30 was sized for four coarse events and would have silently truncated a batched stream), soft Sec-Fetch-Site first-party check, uniform 204. Ships inert -- THERMOGRAPH_EVENTS gates both the recorder and the flag stamp on <html> that makes the client send anything, and is unset everywhere. See UI-EVENTS.md for the schema, the rejected transport/storage alternatives, and the privacy decisions that must be settled before the flag is turned on.
51 lines
1.9 KiB
JavaScript
51 lines
1.9 KiB
JavaScript
// Digest signup form. Loaded on every page (base.html.j2).
|
|
//
|
|
// It degrades: the form is a real <form method="post"> that works with this
|
|
// file blocked.
|
|
//
|
|
// The product-event beacon used to live here; it now lives in track.js, which
|
|
// also owns the [data-event] click delegation this file used to do. `track` is
|
|
// re-exported for the pages that import it from here.
|
|
|
|
export { track, trackLegacy } from "./track.js";
|
|
import { trackLegacy } from "./track.js";
|
|
|
|
// --- digest form -------------------------------------------------------------
|
|
for (const form of document.querySelectorAll("form[data-digest]")) {
|
|
const status = form.querySelector(".digest-status");
|
|
const button = form.querySelector("button[type=submit]");
|
|
|
|
form.addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
const email = form.querySelector("input[name=email]")?.value?.trim();
|
|
if (!email) return;
|
|
|
|
button.disabled = true;
|
|
try {
|
|
const res = await fetch(form.action, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email, place: form.dataset.place || null }),
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (status) {
|
|
status.textContent = data.message || "Something went wrong. Try again.";
|
|
status.hidden = false;
|
|
}
|
|
if (res.ok) {
|
|
// Collapse the inputs on success so the confirmation is the only thing
|
|
// left to read; the form is done either way.
|
|
form.querySelector(".digest-row")?.setAttribute("hidden", "");
|
|
form.querySelector(".digest-note")?.setAttribute("hidden", "");
|
|
trackLegacy("home.digest_signup");
|
|
}
|
|
} catch {
|
|
if (status) {
|
|
status.textContent = "Couldn't reach the server. Try again.";
|
|
status.hidden = false;
|
|
}
|
|
} finally {
|
|
button.disabled = false;
|
|
}
|
|
});
|
|
}
|