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

TipoRegola
impressionPrimo pageview dopo l'apply. Dedup per visitor+experiment (indice unico parziale).
clickEmesso da elementi con data-ab-goal o link decorati. Dedup finestrata.
engagedAuto: sessione >= 10s con interazione (scroll/click/keydown).
bounceAuto: uscita senza engagement, TTL sessione.
conversionGoal esplicito. Idempotente se dedup_key presente (es. order:XYZ). Server accetta finestre configurabili.

Holdout

I visitor nel gruppo holdout emettono eventi con prefisso holdout_*(es. holdout_conversion). Il conteggio impression rimane unico per non rompere il confronto delle varianti.

Schema riga

sql
-- 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 time

Regole di attribuzione

  • Ogni esperimento assegna al più un variant_id per visitor (upsert su ab_assignments).
  • Se la conversione non specifica experiment_id e 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 localStorage e 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 senza dedup_key. Default 5000.
  • dedup_key_template — Template per generare dedup_key lato snippet. Placeholder: {exp} {variant} {visitor} {goal} {day} {hour}.

Identity resolution & conversioni orfane

Il server prova a ricostruire l'identità del visitor in questo ordine:

  1. visitor_id nel body (localStorage lato snippet)
  2. cookie first-party lvbl_vid (server-set, sopravvive a Safari ITP e checkout esterno) — richiede il flag persistence_cookie attivo
  3. fingerprint server (IP troncato /24 + UA + lang, salt per-esperimento) — richiede il flag persistence_fingerprint attivo
  4. webhook Stripe/WooCommerce con client_reference_id = lvbl_vid o metadata.lvbl_vid

Conversioni orfane

Se dopo tutti i fallback non trova un'assegnazione, la conversione viene comunque salvata in 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.

html
<!-- 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à.

text
Advanced → Cross-domain
  Enabled: ✓
  Hosts:   checkout.kajabi.com
           pay.example.com

3. 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.

js
// 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

Anche con TY same-origin funzionante, configura il webhook come rete di sicurezza: cattura gli utenti che chiudono il browser o perdono la connessione prima del redirect alla TY page.

Query utili

sql
-- 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;
sql
-- 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;