import init, { Tracker, generate_entropy } from "./lytics_core.js";
const script =
document.currentScript ||
document.querySelector('script[src*="loader.js"][data-endpoint]') ||
document.querySelector('script[type="module"][src*="/lytics/tracker/"]') ||
document.querySelector('script[type="module"][src*="loader.js"]');
const ENDPOINT = (script?.dataset?.endpoint || "/lytics").replace(/\/$/, "");
const DOMAIN =
script?.dataset?.domain || location.hostname.replace(/^www\./, "");
const HRP = script?.dataset?.hrp || "lytics";
const HEARTBEAT_MS = 15000; const IDLE_MS = 5000;
const KEY = `lytics:entropy:${DOMAIN}`;
let tracker, eventTarget, enrollTarget, ready;
const queue = [];
function loadOrCreateEntropy() {
let e = localStorage.getItem(KEY);
if (!e) {
e = generate_entropy(); localStorage.setItem(KEY, e);
}
return e;
}
window.lytics = window.lytics || {};
window.lytics.exportEntropy = () => localStorage.getItem(KEY);
window.lytics.exportPhrase = async () => {
const { entropyToPhrase } = await import("./words.js");
return entropyToPhrase(localStorage.getItem(KEY));
};
window.lytics.importPhrase = async (phrase) => {
const { phraseToEntropy } = await import("./words.js");
localStorage.setItem(KEY, phraseToEntropy(phrase));
location.reload();
};
window.lytics.forget = () => {
localStorage.removeItem(KEY);
};
async function difficulty() {
const neuron = tracker.neuron;
const r = await fetch(`${ENDPOINT}/api/difficulty?neuron=${neuron}`);
const d = await r.json();
eventTarget = BigInt(d.event_target);
enrollTarget = BigInt(d.enroll_target);
return d.enrolled;
}
let enrolled = false;
function currentTarget() {
return enrolled ? eventTarget : enrollTarget;
}
async function send(spec) {
const target = currentTarget();
const json = tracker.build_event(
spec.kind,
spec.pathname,
spec.navigation ?? undefined,
spec.referrer ?? undefined,
spec.attention_ms ?? undefined,
spec.scroll_depth ?? undefined,
spec.agent_name ?? undefined,
spec.agent_operator ?? undefined,
spec.timestamp,
target,
);
const r = await fetch(`${ENDPOINT}/api/event`, {
method: "POST",
headers: { "content-type": "application/json" },
body: json,
keepalive: true,
});
if (r.status === 202) enrolled = true; return r.status;
}
let attentionMs = 0,
scrollDepth = 0,
lastTick = 0,
counting = false,
lastInput = 0;
function now() {
return Date.now();
}
function startCounting() {
if (!counting) {
counting = true;
lastTick = now();
}
}
function stopCounting() {
accrue();
counting = false;
}
function accrue() {
if (!counting) return;
const t = now();
attentionMs += t - lastTick;
lastTick = t;
if (t - lastInput > IDLE_MS) counting = false; }
function markActive() {
lastInput = now();
if (document.visibilityState === "visible") startCounting();
}
function trackScroll() {
const h = document.documentElement;
const denom = h.scrollHeight - h.clientHeight || 1;
const pct = Math.min(100, Math.round((h.scrollTop / denom) * 100));
if (pct > scrollDepth) scrollDepth = pct;
}
async function flushAttention() {
accrue();
if (attentionMs < 1000) return;
const ms = attentionMs;
attentionMs = 0;
await afterReady(() =>
send({
kind: "attention",
pathname: location.pathname,
attention_ms: ms,
scroll_depth: scrollDepth,
timestamp: now(),
}),
);
}
let firstView = true;
function pageview() {
const nav = firstView
? document.referrer && !document.referrer.includes(DOMAIN)
? "external"
: "direct"
: "internal";
firstView = false;
scrollDepth = 0;
attentionMs = 0;
markActive();
const spec = {
kind: "pageview",
pathname: location.pathname,
navigation: nav,
referrer: document.referrer || null,
timestamp: now(),
};
afterReady(() => send(spec));
}
function afterReady(fn) {
if (ready) return fn();
queue.push(fn); }
function hookHistory() {
for (const m of ["pushState", "replaceState"]) {
const orig = history[m];
history[m] = function () {
const r = orig.apply(this, arguments);
onRoute();
return r;
};
}
addEventListener("popstate", onRoute);
}
let lastPath = location.pathname;
function flushAndView() {
flushAttention().then(pageview);
}
function onRoute() {
if (location.pathname === lastPath) return;
lastPath = location.pathname;
flushAndView();
}
addEventListener("scroll", trackScroll, { passive: true });
for (const e of ["mousemove", "keydown", "click", "scroll", "touchstart"]) {
addEventListener(e, markActive, { passive: true });
}
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
stopCounting();
flushAttention();
} else {
markActive();
}
});
addEventListener("pagehide", () => {
stopCounting();
flushAttention();
});
addEventListener("blur", accrue);
setInterval(flushAttention, HEARTBEAT_MS);
pageview();
hookHistory();
(async () => {
await init();
tracker = new Tracker(loadOrCreateEntropy(), DOMAIN, HRP);
enrolled = await difficulty();
ready = true;
window.lytics.neuron = tracker.neuron;
while (queue.length) {
const fn = queue.shift();
await fn();
}
})().catch((e) => console.error("lytics:", e));