mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-12 08:04:41 +00:00
83 lines
2.7 KiB
TypeScript
83 lines
2.7 KiB
TypeScript
// Funnel beacons: view on load, start on first interaction, page as the
|
|
// visitor advances. Fire-and-forget; analytics must never slow the form or
|
|
// surface an error. Submits are recorded server-side by the submit pipeline.
|
|
|
|
import { apiHeaders, personalToken } from "./api";
|
|
|
|
const VISITOR_KEY_STORAGE = "wf_vk";
|
|
|
|
let memoryKey: string | null = null;
|
|
|
|
// visitorKey is a random, non-identifying id that lets analytics tell "one
|
|
// visitor saw four pages" from "four visitors". Storage can be blocked in
|
|
// third-party iframes; fall back gracefully to a per-load key.
|
|
export function visitorKey(): string {
|
|
if (memoryKey) return memoryKey;
|
|
const mint = () => crypto.randomUUID();
|
|
try {
|
|
let k = localStorage.getItem(VISITOR_KEY_STORAGE);
|
|
if (!k) {
|
|
k = mint();
|
|
localStorage.setItem(VISITOR_KEY_STORAGE, k);
|
|
}
|
|
memoryKey = k;
|
|
} catch {
|
|
memoryKey = mint();
|
|
}
|
|
return memoryKey;
|
|
}
|
|
|
|
interface EventPayload {
|
|
type: "view" | "start" | "page";
|
|
page_index: number;
|
|
pages_total: number;
|
|
}
|
|
|
|
function sendEvent(publicId: string, e: EventPayload) {
|
|
// keepalive lets a beacon survive navigation; sendBeacon cannot carry the
|
|
// render-token header, so plain fetch it is.
|
|
void fetch(`/api/forms/${encodeURIComponent(publicId)}/events`, {
|
|
method: "POST",
|
|
keepalive: true,
|
|
headers: { "Content-Type": "application/json", ...apiHeaders() },
|
|
body: JSON.stringify({
|
|
...e,
|
|
visitor_key: visitorKey(),
|
|
source_url: document.referrer || window.location.href,
|
|
link_token: personalToken() ?? undefined,
|
|
}),
|
|
}).catch(() => undefined);
|
|
}
|
|
|
|
export interface Tracker {
|
|
view(): void;
|
|
start(): void;
|
|
page(index: number): void;
|
|
}
|
|
|
|
// makeTracker enforces the once-guards client-side (the server dedupes views
|
|
// again by visitor key): one view per mount, one start, and page events only
|
|
// when the visitor reaches a page they have not seen this session.
|
|
export function makeTracker(publicId: string, pagesTotal: number): Tracker {
|
|
let viewed = false;
|
|
let started = false;
|
|
let maxPage = -1;
|
|
return {
|
|
view() {
|
|
if (viewed) return;
|
|
viewed = true;
|
|
sendEvent(publicId, { type: "view", page_index: 0, pages_total: pagesTotal });
|
|
},
|
|
start() {
|
|
if (started) return;
|
|
started = true;
|
|
sendEvent(publicId, { type: "start", page_index: Math.max(maxPage, 0), pages_total: pagesTotal });
|
|
},
|
|
page(index: number) {
|
|
if (index <= maxPage) return;
|
|
maxPage = index;
|
|
sendEvent(publicId, { type: "page", page_index: index, pages_total: pagesTotal });
|
|
},
|
|
};
|
|
}
|