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
This commit is contained in:
Samuel Berthe 2026-04-15 16:28:25 +02:00
parent 1c5f626046
commit 5a5976c9a3
No known key found for this signature in database
GPG key ID: 64863511FFBD0E3C
5 changed files with 86 additions and 16 deletions

View file

@ -76,7 +76,7 @@ const featuredGroups = data.groups.filter((g) => featuredGroupSlugs.includes(get
<h3 class="text-sm font-semibold text-slate-900 dark:text-white mb-3">Sponsors</h3>
<div class="space-y-4">
{sponsors.map((s) => (
<a href={s.url} target="_blank" rel="noopener noreferrer" class="block hover:opacity-80 transition-opacity">
<a href={s.url} target="_blank" rel="noopener noreferrer" class="block hover:opacity-80 transition-opacity" data-sponsor-name={s.name} data-sponsor-slot="footer">
<img src={`${base}${s.logo}`} alt={`${s.name} — ${s.description}`} class="h-6" />
</a>
))}
@ -98,3 +98,8 @@ const featuredGroups = data.groups.filter((g) => featuredGroupSlugs.includes(get
</div>
</div>
</footer>
<script>
import { initSponsorClickTracking } from '../scripts/sponsor';
initSponsorClickTracking();
</script>

View file

@ -126,7 +126,7 @@ const starsLabel = stars > 0 ? (stars >= 1000 ? `${(stars / 1000).toFixed(1)}k`
<div class="max-w-7xl mx-auto flex items-center justify-center gap-3">
<span class="text-xs font-medium tracking-wider uppercase text-slate-400 dark:text-slate-500">Sponsored by</span>
{sponsors.map((s) => (
<a href={s.url} target="_blank" rel="noopener noreferrer" class="hover:opacity-75 transition-opacity" title={s.name}>
<a href={s.url} target="_blank" rel="noopener noreferrer" class="hover:opacity-75 transition-opacity" title={s.name} data-sponsor-name={s.name} data-sponsor-slot="header">
<img src={`${base}${s.logo}`} alt={`${s.name} — ${s.description}`} class="h-10 w-auto" />
</a>
))}
@ -149,6 +149,11 @@ const starsLabel = stars > 0 ? (stars >= 1000 ? `${(stars / 1000).toFixed(1)}k`
</div>
</header>
<script>
import { initSponsorClickTracking } from '../scripts/sponsor';
initSponsorClickTracking();
</script>
<script>
const btn = document.getElementById('mobile-menu-btn');
const menu = document.getElementById('mobile-menu');

View file

@ -99,10 +99,10 @@ const canonical = canonicalUrl ?? `${SITE_ORIGIN}${base}${Astro.url.pathname.rep
<Footer base={base} />
<StarToast />
<script>
import { record } from '../scripts/pipe';
import { recordCopy } from '../scripts/pipe';
window.addEventListener('apa-copy', (e: Event) => {
const { name, ...data } = (e as CustomEvent<Record<string, unknown>>).detail;
record(name as string, data);
recordCopy(name as string, data);
});
</script>
</body>

View file

@ -68,15 +68,13 @@ function getPageContext() {
getUserId();
getSessionId();
export function record(name: string, data: Record<string, unknown>): void {
const payload = {
function buildPayload(name: string, data: Record<string, unknown>): 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<string, unknown>): 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<string, unknown>): 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<string, unknown>): void {
record(name, {
session_copy_count: bumpSessionCount(),
lifetime_copy_count: bumpLifetimeCount(),
...data,
});
}
export async function recordAndWait(name: string, data: Record<string, unknown>): Promise<void> {
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);
}
}

View file

@ -0,0 +1,33 @@
import { record, recordAndWait } from './pipe';
export function initSponsorClickTracking(): void {
document.querySelectorAll<HTMLAnchorElement>('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);
}
}
});
});
}