From 5a5976c9a3fc87c3197bd3e339c9448392ef532c Mon Sep 17 00:00:00 2001 From: Samuel Berthe Date: Wed, 15 Apr 2026 16:28:25 +0200 Subject: [PATCH] feat: track sponsor clicks with blocking event before navigation - Add recordCopy() for copy events (bumps session/lifetime counters) - Add recordAndWait() for blocking events (1500ms timeout, errors swallowed) - Extract shared sponsor click handler into site/src/scripts/sponsor.ts - Plain left-click blocks navigation until HTTP response; modifier/middle clicks track fire-and-forget and let the browser navigate natively - Distinguish header vs footer placement via data-sponsor-slot attribute --- site/src/components/Footer.astro | 7 ++++- site/src/components/Header.astro | 7 ++++- site/src/layouts/BaseLayout.astro | 4 +-- site/src/scripts/pipe.ts | 51 +++++++++++++++++++++++-------- site/src/scripts/sponsor.ts | 33 ++++++++++++++++++++ 5 files changed, 86 insertions(+), 16 deletions(-) create mode 100644 site/src/scripts/sponsor.ts diff --git a/site/src/components/Footer.astro b/site/src/components/Footer.astro index 1da41ea..34c207c 100644 --- a/site/src/components/Footer.astro +++ b/site/src/components/Footer.astro @@ -76,7 +76,7 @@ const featuredGroups = data.groups.filter((g) => featuredGroupSlugs.includes(get

Sponsors

{sponsors.map((s) => ( - + {`${s.name} ))} @@ -98,3 +98,8 @@ const featuredGroups = data.groups.filter((g) => featuredGroupSlugs.includes(get
+ + diff --git a/site/src/components/Header.astro b/site/src/components/Header.astro index b9992f5..2f193a1 100644 --- a/site/src/components/Header.astro +++ b/site/src/components/Header.astro @@ -126,7 +126,7 @@ const starsLabel = stars > 0 ? (stars >= 1000 ? `${(stars / 1000).toFixed(1)}k`
Sponsored by {sponsors.map((s) => ( - + {`${s.name} ))} @@ -149,6 +149,11 @@ const starsLabel = stars > 0 ? (stars >= 1000 ? `${(stars / 1000).toFixed(1)}k`
+ + diff --git a/site/src/scripts/pipe.ts b/site/src/scripts/pipe.ts index 00f56bc..33fbbb5 100644 --- a/site/src/scripts/pipe.ts +++ b/site/src/scripts/pipe.ts @@ -68,15 +68,13 @@ function getPageContext() { getUserId(); getSessionId(); -export function record(name: string, data: Record): void { - const payload = { +function buildPayload(name: string, data: Record): object { + return { timestamp: new Date().toISOString(), transaction_id: uid(), - name: "awesome_prometheus_alerts_"+name, + name: "awesome_prometheus_alerts_" + name, user_id: getUserId(), session_id: getSessionId(), - session_copy_count: bumpSessionCount(), - lifetime_copy_count: bumpLifetimeCount(), ...data, ...getPageContext(), language: navigator.language, @@ -87,11 +85,40 @@ export function record(name: string, data: Record): void { user_agent: navigator.userAgent, is_bot: /bot|crawl|spider/i.test(navigator.userAgent), }; - - fetch(PIPE_URL+"awesome_prometheus_alerts_"+name, { - method: 'POST', - body: JSON.stringify(payload), - headers: { Authorization: `Bearer ${PIPE_KEY}` }, - keepalive: true, - }).catch(console.error); +} + +export function record(name: string, data: Record): void { + const payload = buildPayload(name, data); + fetch(PIPE_URL + "awesome_prometheus_alerts_" + name, { + method: 'POST', + body: JSON.stringify(payload), + headers: { Authorization: `Bearer ${PIPE_KEY}` }, + keepalive: true, + }).catch(console.error); +} + +export function recordCopy(name: string, data: Record): void { + record(name, { + session_copy_count: bumpSessionCount(), + lifetime_copy_count: bumpLifetimeCount(), + ...data, + }); +} + +export async function recordAndWait(name: string, data: Record): Promise { + const payload = buildPayload(name, data); + const ctrl = new AbortController(); + const timer = setTimeout(() => ctrl.abort(), 1500); + try { + await fetch(PIPE_URL + "awesome_prometheus_alerts_" + name, { + method: 'POST', + body: JSON.stringify(payload), + headers: { Authorization: `Bearer ${PIPE_KEY}` }, + signal: ctrl.signal, + }); + } catch { + // swallow — a failed event must never break sponsor navigation + } finally { + clearTimeout(timer); + } } diff --git a/site/src/scripts/sponsor.ts b/site/src/scripts/sponsor.ts new file mode 100644 index 0000000..297307e --- /dev/null +++ b/site/src/scripts/sponsor.ts @@ -0,0 +1,33 @@ +import { record, recordAndWait } from './pipe'; + +export function initSponsorClickTracking(): void { + document.querySelectorAll('a[data-sponsor-name]').forEach((a) => { + a.addEventListener('click', async (e) => { + const me = e as MouseEvent; + const href = a.href; + const sponsorName = a.dataset.sponsorName!; + const sponsorSlot = a.dataset.sponsorSlot!; + const eventData = { sponsor_name: sponsorName, sponsor_url: href, sponsor_slot: sponsorSlot }; + + // Modifier / non-primary clicks: track fire-and-forget, let browser handle navigation + if (me.button !== 0 || me.metaKey || me.ctrlKey || me.shiftKey) { + record('sponsor_click', eventData); + return; + } + + // Plain left-click: block navigation until event is recorded + e.preventDefault(); + // Open blank tab now (inside user gesture) to avoid popup-blocker after await + const w = a.target === '_blank' ? window.open('', '_blank') : null; + try { + await recordAndWait('sponsor_click', eventData); + } finally { + if (w) { + w.location.href = href; + } else { + window.open(href, '_blank') ?? (window.location.href = href); + } + } + }); + }); +}