Een gedeelde functie in de root layout van de site veranderde een exposure-teller voor A/B-tests in een page-view-teller, waardoor de steekproefomvang werd opgeblazen en de conversieratio's betekenisloos werden. De test, een 50/50-verdeling van de homepage, rapporteerde 178 hits voor de ene variant en 57 voor de andere — ver verwijderd van de verwachte gelijke verdeling.

Hoe de fout door de randomizer heen glipte

De ontwikkelaar controleerde eerst de randomizer die bezoekers aan een variant toewijst. Hij las de middleware, inspecteerde de cookie-logica en draaide een script dat de toewijzingsfunctie 10.000 keer aanriep; dit leverde een perfecte 50/50-verdeling op. De randomizer zelf werkte; het probleem zat in de manier waarop de exposure werd geregistreerd.

Eén enkele functie voerde twee taken uit:

  1. De variant instellen – wordt bij elke paginalading uitgevoerd om de ervaring van de bezoeker consistent te houden.
  2. Het exposure-event registreren – moet slechts één keer per bezoeker worden uitgevoerd, op het moment dat de variant voor het eerst verschijnt.

De functie bevond zich in de root layout, een component die bij elke navigatie wordt gerenderd. Omdat de code voor het registreren van de exposure elke keer werd uitgevoerd dat de layout werd gerenderd, telde elke page view als een nieuwe exposure. De twee homepage-varianten gebruikten licht verschillende layout-trees, waardoor hun page-view-ratio's uiteenliepen, wat de illusie wekte van een defecte randomizer.

Waarom de foutieve telling ertoe deed

Conversiecijfers — click-throughs, aanmeldingen, aankopen — werden correct gelogd. Maar de noemer (het aantal exposures) was onjuist. De berekende conversieratio's leken veel lager dan de werkelijkheid, en alle beslissingen gebaseerd op die ratio's waren onbetrouwbaar.

De test draaide twee weken voordat de discrepantie aan het licht kwam, waardoor het team de volledige dataset moest weggooien.

De oplossing

De oplossing was eenvoudig: splits de verantwoordelijkheden op in aparte functies. De code voor het registreren van de exposure controleert nu of de bezoeker al is geteld en wordt slechts één keer per gebruiker uitgevoerd. De code voor het instellen van de variant blijft waar hij is en wordt bij elke navigatie uitgevoerd.

Drie lessen voor iedereen die experimenten uitvoert

  • Scheid het instellen van de status van eenmalige events. Een functie die zowel een variant toewijst als een exposure logt, zal conflicteren omdat de eerste herhaald wordt terwijl de laatste dat juist niet mag.
  • Vermijd 'one-shot' logica in een root layout. Alles wat in een component wordt geplaatst dat bij elke paginalading wordt gerenderd, zal herhaaldelijk worden uitgevoerd, waardoor "één keer per bezoeker" verandert in "één keer per page view".
  • Wanneer de geobserveerde verdeling de randomizer tegenspreekt, controleer dan eerst de teller. Ontwikkelaars testen vaak de eerlijkheid van de randomizer, maar verifiëren zelden of het telmechanisme nauwkeurig is.

Waar je voortaan op moet letten

Elk experiment dat vertrouwt op een enkele teller voor exposure moet worden gecontroleerd op de plek waar die teller zich in de component-tree bevindt. Als de teller in een globale layout staat, voeg dan een controle toe die het event koppelt aan een persistente identifier — zoals een cookie of een local-storage flag. Teams zouden ook een sanity check in hun dashboards moeten inbouwen: als de geobserveerde variantverdeling afwijkt buiten een kleine statistische marge, markeer de test dan voor een controle van de teller voordat je ervan uitgaat dat de randomizer defect is.

Kortom, een goed functionerende randomizer is nutteloos zonder een betrouwbare exposure-telling. Door verantwoordelijkheden te splitsen en eenmalige events buiten componenten te plaatsen die altijd worden gerenderd, blijven A/B-tests betrouwbaar en voorkom je weken aan verspilde analyses.