Schema eventi & attribuzione
Riferimento tecnico per integrazioni, analisti e team dati. Descrive shape degli eventi ab_events, regole di dedup, gruppi holdout e finestre configurabili per esperimento.
Tipi di evento
| Tipo | Regola |
|---|---|
| impression | Primo pageview dopo l'apply. Dedup per visitor+experiment (indice unico parziale). |
| click | Emesso da elementi con data-ab-goal o link decorati. Dedup finestrata. |
| engaged | Auto: sessione >= 10s con interazione (scroll/click/keydown). |
| bounce | Auto: uscita senza engagement, TTL sessione. |
| conversion | Goal esplicito. Idempotente se dedup_key presente (es. order:XYZ). Server accetta finestre configurabili. |
Holdout
holdout_*(es. holdout_conversion). Il conteggio impression rimane unico per non rompere il confronto delle varianti.Schema riga
-- ab_events (append-only)
-- id uuid pk
-- experiment_id uuid -- FK esperimento
-- variant_id uuid|null -- assegnata al visitor per l'esperimento
-- visitor_id text -- identità first-party (cookie + localStorage)
-- event_type text -- impression | click | engaged | bounce | conversion
-- -- Prefisso holdout_* per baseline (non contamina varianti)
-- goal_id uuid|null -- goal esplicito quando disponibile
-- value numeric|null -- valore economico (per revenue goals)
-- currency text|null -- ISO 4217
-- dedup_key text|null -- se presente: garanzia unicità server-side
-- metadata jsonb|null -- payload arbitrario (utm, url, session_id, …)
-- occurred_at timestamptz -- server timeRegole di attribuzione
- Ogni esperimento assegna al più un
variant_idper visitor (upsert suab_assignments). - Se la conversione non specifica
experiment_ide il visitor ha più assegnazioni attive, viene creditata la più recente in bucket variant (fallback: holdout). - Cross-domain: il visitor è tracciato via
?lvbl_vid=firmato HMAC; i webhook Stripe/WooCommerce ricostruiscono l'assegnazione lato server. - UTM/click-id sono persistiti per 30 giorni in
localStoragee propagati durante i redirect.
Parametri configurabili per esperimento
goal_lock_ttl_sec— TTL client-side per non riemettere lo stesso goal. Default 1800.dedup_window_ms— Finestra dedup server per eventi senzadedup_key. Default 5000.dedup_key_template— Template per generarededup_keylato snippet. Placeholder:{exp} {variant} {visitor} {goal} {day} {hour}.
Identity resolution & conversioni orfane
Il server prova a ricostruire l'identità del visitor in questo ordine:
visitor_idnel body (localStorage lato snippet)- cookie first-party
lvbl_vid(server-set, sopravvive a Safari ITP e checkout esterno) — richiede il flag persistence_cookie attivo - fingerprint server (IP troncato /24 + UA + lang, salt per-esperimento) — richiede il flag persistence_fingerprint attivo
- webhook Stripe/WooCommerce con
client_reference_id=lvbl_vidometadata.lvbl_vid
Conversioni orfane
ab_events con orphan=true e variant_id=NULL. Le aggregazioni per variante le ignorano (nessuna contaminazione), ma appaiono in Advanced → Tracking come avviso e vengono contate in ab_tracking_health_daily.orphan_conversions.Fix ricorrenti: attiva Cookie first-party e Fingerprintnel pannello Persistenza dell'esperimento; configura il webhook Stripe/WooCommerce (Advanced → Cross-domain) puntandolo a /api/public/ab/in/stripe/<experimentId>e assicurati che il checkout passi client_reference_id con il valore diwindow.lvbl.visitorId.
Quando attivare Cross-domain
Il toggle Advanced → Cross-domain serve solo se il visitor cambia hostname durante il funnel di conversione. Tre scenari tipici:
1. Thank-you page sullo stesso dominio → Cross-domain OFF
L'utente paga su Stripe/Kajabi ma viene poi rediretto su tuosito.it/grazie. Il cookie first-party lvbl_vid è ancora valido: lo snippet Lovable caricato sulla TY page rilegge il visitor e la conversione si aggancia senza configurazione extra.
<!-- tuosito.it/grazie -->
<script src="https://cdn.lovable.dev/lvbl.js" data-project="…"></script>
<script>
// opzionale: conversione client-side (il webhook resta come backup)
window.lvbl?.track('purchase', { value: 49, currency: 'EUR' });
</script>2. Thank-you page su dominio diverso → Cross-domain ON
La TY page vive su checkout.kajabi.com o simili e vuoi trackare anche lì. Attiva Cross-domain, aggiungi l'host nella whitelist, installa lo snippet Lovable sulla TY page. I link verso quei domini vengono decorati con ?lvbl_vid=… firmato HMAC per propagare l'identità.
Advanced → Cross-domain
Enabled: ✓
Hosts: checkout.kajabi.com
pay.example.com3. Nessuna TY page, solo webhook → Cross-domain opzionale
Non c'è redirect al tuo dominio: la conversione arriva solo via webhook Stripe/Kajabi. Non serve Cross-domain, ma devi passare il visitor_id al provider di pagamento come client_reference_id (Stripe) o metadata.lvbl_vid(altri), altrimenti il webhook non riesce ad associare l'ordine e la conversione finisce fra le orfane.
// creazione della Stripe Session lato server
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [...],
success_url: 'https://tuosito.it/grazie?sid={CHECKOUT_SESSION_ID}',
cancel_url: 'https://tuosito.it/checkout',
// 👇 fondamentale: aggancia l'ordine al visitor A/B
client_reference_id: visitorIdFromClient, // window.lvbl.visitorId
metadata: { lvbl_vid: visitorIdFromClient },
});Endpoint webhook: /api/public/ab/in/stripe/<experimentId>
Il webhook è comunque consigliato
Query utili
-- Impression per visitor uniche (indice unico parziale già in DB)
select experiment_id, variant_id, count(*) as unique_impressions
from ab_events
where event_type = 'impression'
and occurred_at >= now() - interval '30 days'
group by 1,2;
-- Conversioni idempotenti per ordine
select experiment_id, count(distinct dedup_key) as orders
from ab_events
where event_type = 'conversion' and dedup_key like 'order:%'
group by 1;-- Gap di attribuzione: conversioni salvate ma senza assegnazione
select experiment_id,
count(*) filter (where orphan) as orphan_conversions,
count(*) filter (where not orphan) as attributed_conversions,
round(100.0 * count(*) filter (where orphan) / greatest(count(*), 1), 2) as orphan_pct
from ab_events
where event_type = 'conversion'
and occurred_at >= now() - interval '30 days'
group by 1
order by orphan_pct desc;