Testes A/B: implementando experimentos no front-end

1. Fundamentos dos Testes A/B no Front-End

Testes A/B são experimentos controlados onde duas versões de um elemento de interface (A = controle, B = variante) são apresentadas aleatoriamente a diferentes grupos de usuários para determinar qual performa melhor em relação a uma métrica pré-definida. No front-end, essa técnica é essencial para otimização de UX porque permite decisões baseadas em dados, não em opiniões.

A diferença entre testes A/B, testes multivariados e testes de funcionalidade é sutil mas crucial:
- Teste A/B: compara duas versões de uma única variável (ex.: cor do botão)
- Teste multivariado: testa múltiplas combinações de variáveis simultaneamente
- Teste de funcionalidade: verifica se um recurso funciona corretamente, não qual versão é melhor

Métricas-chave incluem taxa de conversão (objetivo principal), tempo de interação (engajamento) e taxa de rejeição (frustração).

2. Arquitetura de um Sistema de Experimentos Client-Side

A arquitetura ideal separa a lógica do experimento da lógica de negócio. Isso é feito através de feature flags e SDKs especializados como Split.io ou LaunchDarkly.

// Estrutura de separação de responsabilidades
// experiment-config.js
const experiments = {
  'signup-button-color': {
    variants: ['green', 'blue'],
    traffic: 0.5, // 50% dos usuários veem a variante
    key: 'signup-button-color'
  }
};

// experiment-engine.js
class ExperimentEngine {
  constructor(config) {
    this.config = config;
    this.assignments = this.loadAssignments();
  }

  getVariant(experimentKey) {
    const experiment = this.config[experimentKey];
    if (!experiment) return 'control';

    const stored = this.assignments[experimentKey];
    if (stored) return stored;

    const variant = Math.random() < experiment.traffic 
      ? experiment.variants[Math.floor(Math.random() * experiment.variants.length)]
      : 'control';

    this.assignments[experimentKey] = variant;
    this.saveAssignments();
    return variant;
  }

  loadAssignments() {
    try {
      return JSON.parse(localStorage.getItem('experiment_assignments')) || {};
    } catch {
      return {};
    }
  }

  saveAssignments() {
    localStorage.setItem('experiment_assignments', JSON.stringify(this.assignments));
  }
}

O armazenamento de variantes em localStorage garante consistência na sessão do usuário, enquanto cookies permitem persistência entre visitas.

3. Planejamento e Definição de Hipóteses

Uma hipótese mensurável segue o formato: "Se [mudança], então [resultado esperado] para [público-alvo]".

// Exemplo de hipótese bem formulada
// Hipótese: "Se alterarmos o botão de CTA para verde, 
// então a taxa de cliques aumentará em 10% para usuários mobile no Brasil"

// Cálculo de tamanho amostral (simplificado)
// n = (Zα/2 + Zβ)² * (p1(1-p1) + p2(1-p2)) / (p2 - p1)²
// Onde:
// Zα/2 = 1.96 (para 95% de confiança)
// Zβ = 0.84 (para 80% de poder)
// p1 = 0.05 (taxa de conversão atual)
// p2 = 0.055 (aumento esperado de 10%)

const sampleSize = Math.ceil(
  (Math.pow(1.96 + 0.84, 2) * (0.05 * 0.95 + 0.055 * 0.945)) / 
  Math.pow(0.005, 2)
);
// Resultado: aproximadamente 71.000 usuários por variante

A segmentação por atributos como geografia, dispositivo e perfil evita ruídos nos dados.

4. Implementação Prática com JavaScript e Frameworks

Estrutura básica com JavaScript puro:

// experiment-utils.js
function getVariation(experimentKey) {
  const engine = new ExperimentEngine(experimentConfig);
  return engine.getVariant(experimentKey);
}

// Aplicação no DOM
document.addEventListener('DOMContentLoaded', () => {
  const variant = getVariation('signup-button-color');
  const button = document.getElementById('cta-button');

  if (variant === 'green') {
    button.classList.add('btn-green');
  } else {
    button.classList.add('btn-blue');
  }

  // Rastreamento
  trackEvent('experiment_impression', {
    experiment: 'signup-button-color',
    variant: variant
  });
});

Exemplo com React:

// useExperiment.js
import { useState, useEffect } from 'react';

function useExperiment(experimentKey) {
  const [variant, setVariant] = useState(null);

  useEffect(() => {
    const engine = new ExperimentEngine(experimentConfig);
    setVariant(engine.getVariant(experimentKey));
  }, [experimentKey]);

  return variant;
}

// Componente
function SignupButton() {
  const variant = useExperiment('signup-button-color');

  if (!variant) return null; // Loading state

  return (
    <button 
      className={variant === 'green' ? 'btn-green' : 'btn-blue'}
      onClick={() => trackConversion(variant)}
    >
      Cadastre-se
    </button>
  );
}

Exemplo com Vue 3:

<!-- ExperimentDirective.vue -->
<template>
  <button 
    v-if="variant === 'green'"
    class="btn-green"
    @click="trackConversion"
  >
    Cadastre-se
  </button>
  <button 
    v-else
    class="btn-blue"
    @click="trackConversion"
  >
    Cadastre-se
  </button>
</template>

<script setup>
import { ref, onMounted } from 'vue';

const variant = ref(null);

onMounted(() => {
  const engine = new ExperimentEngine(experimentConfig);
  variant.value = engine.getVariant('signup-button-color');
});

function trackConversion() {
  // Rastreamento
}
</script>

5. Rastreamento e Coleta de Dados

A integração com analytics deve ser idêntica entre variantes para evitar viés.

// analytics-integration.js
function trackEvent(eventName, properties) {
  // Google Analytics 4
  if (window.gtag) {
    gtag('event', eventName, properties);
  }

  // Mixpanel
  if (window.mixpanel) {
    mixpanel.track(eventName, properties);
  }

  // Amplitude
  if (window.amplitude) {
    amplitude.getInstance().logEvent(eventName, properties);
  }
}

// Eventos customizados
function trackExperimentImpression(experimentKey, variant) {
  trackEvent('experiment_impression', {
    experiment_key: experimentKey,
    variant: variant,
    timestamp: Date.now(),
    page: window.location.pathname
  });
}

function trackExperimentConversion(experimentKey, variant, conversionType) {
  trackEvent('experiment_conversion', {
    experiment_key: experimentKey,
    variant: variant,
    conversion_type: conversionType,
    value: 1
  });
}

6. Validação Estatística e Tomada de Decisão

O teste de significância mais comum é o qui-quadrado para taxas de conversão.

// significance-test.js
function chiSquareTest(controlClicks, controlTotal, variantClicks, variantTotal) {
  const controlRate = controlClicks / controlTotal;
  const variantRate = variantClicks / variantTotal;
  const totalRate = (controlClicks + variantClicks) / (controlTotal + variantTotal);

  const expectedControl = controlTotal * totalRate;
  const expectedVariant = variantTotal * totalRate;

  const chiSquare = 
    Math.pow(controlClicks - expectedControl, 2) / expectedControl +
    Math.pow((controlTotal - controlClicks) - (controlTotal - expectedControl), 2) / (controlTotal - expectedControl) +
    Math.pow(variantClicks - expectedVariant, 2) / expectedVariant +
    Math.pow((variantTotal - variantClicks) - (variantTotal - expectedVariant), 2) / (variantTotal - expectedVariant);

  // Aproximação do p-valor (distribuição qui-quadrado com 1 grau de liberdade)
  const pValue = 1 - chiSquareCDF(chiSquare, 1);

  return {
    chiSquare,
    pValue,
    significant: pValue < 0.05,
    lift: ((variantRate - controlRate) / controlRate * 100).toFixed(2) + '%',
    confidenceInterval: calculateConfidenceInterval(variantRate, variantTotal)
  };
}

function calculateConfidenceInterval(rate, total) {
  const z = 1.96; // 95% de confiança
  const se = Math.sqrt(rate * (1 - rate) / total);
  return {
    lower: rate - z * se,
    upper: rate + z * se
  };
}

Evite o peeking problem definindo uma duração mínima antes de analisar resultados.

7. Boas Práticas e Armadilhas Comuns

  • Efeito Hawthorne: usuários podem mudar comportamento por saberem que estão sendo observados
  • Viés de novidade: variantes novas podem ter desempenho artificialmente alto inicialmente
  • Múltiplos testes: ajuste o nível de significância (correção de Bonferroni) quando testar várias hipóteses
// Documentação de experimento
const experimentDocumentation = {
  id: 'exp-001',
  name: 'Cor do botão de cadastro',
  hypothesis: 'Botão verde aumenta conversão em 10%',
  startDate: '2024-01-15',
  endDate: '2024-02-15',
  sampleSize: 142000,
  variants: ['control (azul)', 'green'],
  metrics: ['click_rate', 'conversion_rate'],
  results: {
    winner: 'green',
    lift: '12.3%',
    pValue: 0.003,
    confidence: '99.7%'
  },
  decisions: 'Implementar verde permanentemente'
};

8. Estudo de Caso: Otimização de um Formulário de Cadastro

Cenário: Formulário de cadastro em 3 etapas com 45% de abandono.

Hipótese: Formulário em etapa única reduz abandono para 30%.

// Implementação
// control: formulário em 3 etapas
// variant: formulário em etapa única

function renderForm() {
  const variant = getVariation('signup-form-layout');

  if (variant === 'single-step') {
    renderSingleStepForm();
  } else {
    renderMultiStepForm();
  }
}

// Coleta de dados (após 30 dias)
const results = {
  control: {
    impressions: 50000,
    completions: 27500,
    abandonmentRate: '45%'
  },
  variant: {
    impressions: 50000,
    completions: 36500,
    abandonmentRate: '27%'
  }
};

// Análise estatística
const analysis = chiSquareTest(
  27500, 50000, // control
  36500, 50000  // variant
);
// Resultado: p-value = 0.0001, significativo
// Lift: (0.73 - 0.55) / 0.55 = 32.7%
// Decisão: Implementar formulário de etapa única

O experimento mostrou que o formulário de etapa única reduziu o abandono de 45% para 27%, um ganho de 18 pontos percentuais, validando a hipótese com 99.99% de confiança estatística.

Referências