JavaScript que escribo cada semana en un rol de pruebas A/B

AB Testing

``` Manos de un desarrollador tecleando en un teclado en un espacio de trabajo oscuro, con código visible en la pantalla

La mayoría de tutoriales sobre A/B testing te muestran un toggle. Variante A o variante B, cambia un elemento, listo. Eso no se parece nada al trabajo real.

En un rol de CRO o experimentación, escribes JavaScript constantemente. No código complejo con frameworks, sino scripts prácticos, a veces un poco feos, muchas veces ingeniosos, que tienen que correr rápido, no romper nada y pasar desapercibidos para el usuario. Esto es lo que aparece de verdad en mi semana.

Esperar a elementos que todavía no existen

Es probablemente lo que más escribo. Necesitas modificar un elemento, pero la página lo renderiza de forma asíncrona. Si intentas capturarlo de inmediato, no existe aún.

function waitForElement(selector, callback, timeout = 5000) {
  const start = Date.now();

  const interval = setInterval(() => {
    const el = document.querySelector(selector);
    if (el) {
      clearInterval(interval);
      callback(el);
    }
    if (Date.now() - start > timeout) {
      clearInterval(interval);
    }
  }, 100);
}

waitForElement('.product-price', (el) => {
  el.textContent = 'From $49';
});
function waitForElement(selector, callback, timeout = 5000) {
  const start = Date.now();

  const interval = setInterval(() => {
    const el = document.querySelector(selector);
    if (el) {
      clearInterval(interval);
      callback(el);
    }
    if (Date.now() - start > timeout) {
      clearInterval(interval);
    }
  }, 100);
}

waitForElement('.product-price', (el) => {
  el.textContent = 'From $49';
});
function waitForElement(selector, callback, timeout = 5000) {
  const start = Date.now();

  const interval = setInterval(() => {
    const el = document.querySelector(selector);
    if (el) {
      clearInterval(interval);
      callback(el);
    }
    if (Date.now() - start > timeout) {
      clearInterval(interval);
    }
  }, 100);
}

waitForElement('.product-price', (el) => {
  el.textContent = 'From $49';
});
function waitForElement(selector, callback, timeout = 5000) {
  const start = Date.now();

  const interval = setInterval(() => {
    const el = document.querySelector(selector);
    if (el) {
      clearInterval(interval);
      callback(el);
    }
    if (Date.now() - start > timeout) {
      clearInterval(interval);
    }
  }, 100);
}

waitForElement('.product-price', (el) => {
  el.textContent = 'From $49';
});

Uso setInterval más que MutationObserver para esto. MutationObserver es más elegante en teoría, pero el enfoque de polling es más fácil de depurar cuando algo falla, y siempre falla algo.

Empujar eventos al dataLayer

Cada prueba necesita medición. La mayoría de las veces eso significa enviar algo a GA4 a través del dataLayer.

window.dataLayer = window.dataLayer || [];

window.dataLayer.push({
  event: 'experiment_interaction',
  experiment_id: 'checkout-cta-v2',
  variant: 'b',
  action: 'clicked'
});
window.dataLayer = window.dataLayer || [];

window.dataLayer.push({
  event: 'experiment_interaction',
  experiment_id: 'checkout-cta-v2',
  variant: 'b',
  action: 'clicked'
});
window.dataLayer = window.dataLayer || [];

window.dataLayer.push({
  event: 'experiment_interaction',
  experiment_id: 'checkout-cta-v2',
  variant: 'b',
  action: 'clicked'
});
window.dataLayer = window.dataLayer || [];

window.dataLayer.push({
  event: 'experiment_interaction',
  experiment_id: 'checkout-cta-v2',
  variant: 'b',
  action: 'clicked'
});

Parece sencillo, pero la parte de la que nadie habla es el timing. Si empujas el evento antes de que el contenedor de GTM esté listo, se encola correctamente. Si lo empujas después de una acción del usuario, tienes que asegurarte de que el listener del elemento se adjuntó en el momento correcto. He perdido más datos de prueba por problemas de timing que por hipótesis malas.

Leer parámetros de URL para el targeting

A veces la lógica de targeting vive en la herramienta, Monetate o Adobe Target. A veces hay que hacerlo tú mismo, dentro del script.

function getParam(name) {
  const params = new URLSearchParams(window.location.search);
  return params.get(name);
}

if (getParam('source') === 'email') {
  // show different hero variant
}
function getParam(name) {
  const params = new URLSearchParams(window.location.search);
  return params.get(name);
}

if (getParam('source') === 'email') {
  // show different hero variant
}
function getParam(name) {
  const params = new URLSearchParams(window.location.search);
  return params.get(name);
}

if (getParam('source') === 'email') {
  // show different hero variant
}
function getParam(name) {
  const params = new URLSearchParams(window.location.search);
  return params.get(name);
}

if (getParam('source') === 'email') {
  // show different hero variant
}

También uso esto para forzar variantes durante el QA. Compartir una URL con ?variant=b es más rápido que explicarle a un stakeholder cómo acceder al modo preview de la herramienta.

Modificar elementos sin romper los event listeners

Quieres cambiar el texto de un botón. Fácil. Pero si el código de otra persona está escuchando ese botón con un evento delegado, y reemplazas el elemento entero en lugar de simplemente actualizarlo, romperás cosas en silencio.

// Risky
button.outerHTML = '<button class="cta">Buy Now</button>';

// Safer
button.textContent = 'Buy Now';
button.classList.add('cta-variant');
// Risky
button.outerHTML = '<button class="cta">Buy Now</button>';

// Safer
button.textContent = 'Buy Now';
button.classList.add('cta-variant');
// Risky
button.outerHTML = '<button class="cta">Buy Now</button>';

// Safer
button.textContent = 'Buy Now';
button.classList.add('cta-variant');
// Risky
button.outerHTML = '<button class="cta">Buy Now</button>';

// Safer
button.textContent = 'Buy Now';
button.classList.add('cta-variant');

La diferencia entre textContent e innerHTML también importa. Si el botón original tiene un span hijo para un icono, textContent lo eliminará. innerHTML puede abrir un problema de XSS si el contenido es dinámico. Ninguna opción es siempre la correcta.

Persistir la asignación de variante entre páginas

Las pruebas que abarcan varias páginas necesitan alguna forma de recordar qué variante vio el usuario. Las cookies son el enfoque estándar.

function setCookie(name, value, days) {
  const expires = new Date(Date.now() + days * 864e5).toUTCString();
  document.cookie = `${name}=${value}; expires=${expires}; path=/`;
}

function getCookie(name) {
  return document.cookie.split('; ').reduce((acc, part) => {
    const [key, val] = part.split('=');
    return key === name ? val : acc;
  }, null);
}

if (!getCookie('exp-checkout')) {
  const variant = Math.random() < 0.5 ? 'control' : 'treatment';
  setCookie('exp-checkout', variant, 30);
}

const assignedVariant = getCookie('exp-checkout');
function setCookie(name, value, days) {
  const expires = new Date(Date.now() + days * 864e5).toUTCString();
  document.cookie = `${name}=${value}; expires=${expires}; path=/`;
}

function getCookie(name) {
  return document.cookie.split('; ').reduce((acc, part) => {
    const [key, val] = part.split('=');
    return key === name ? val : acc;
  }, null);
}

if (!getCookie('exp-checkout')) {
  const variant = Math.random() < 0.5 ? 'control' : 'treatment';
  setCookie('exp-checkout', variant, 30);
}

const assignedVariant = getCookie('exp-checkout');
function setCookie(name, value, days) {
  const expires = new Date(Date.now() + days * 864e5).toUTCString();
  document.cookie = `${name}=${value}; expires=${expires}; path=/`;
}

function getCookie(name) {
  return document.cookie.split('; ').reduce((acc, part) => {
    const [key, val] = part.split('=');
    return key === name ? val : acc;
  }, null);
}

if (!getCookie('exp-checkout')) {
  const variant = Math.random() < 0.5 ? 'control' : 'treatment';
  setCookie('exp-checkout', variant, 30);
}

const assignedVariant = getCookie('exp-checkout');
function setCookie(name, value, days) {
  const expires = new Date(Date.now() + days * 864e5).toUTCString();
  document.cookie = `${name}=${value}; expires=${expires}; path=/`;
}

function getCookie(name) {
  return document.cookie.split('; ').reduce((acc, part) => {
    const [key, val] = part.split('=');
    return key === name ? val : acc;
  }, null);
}

if (!getCookie('exp-checkout')) {
  const variant = Math.random() < 0.5 ? 'control' : 'treatment';
  setCookie('exp-checkout', variant, 30);
}

const assignedVariant = getCookie('exp-checkout');

La herramienta suele gestionar esto, pero a veces estás ejecutando algo ligero fuera de ella, o necesitas que la asignación persista más tiempo que la sesión.

Una cosa que he dejado de hacer

Antes escribía la lógica de la variante directamente dentro del script de la prueba, con mucho anidamiento. Varios if, uno por elemento, todos en secuencia. Funciona, pero se vuelve ilegible al cabo de una semana.

Ahora separo la lógica en funciones pequeñas con nombres claros, aunque el script completo tenga 40 líneas. Hace el QA más rápido y hace que pasarle el código a otra persona sea mucho menos doloroso.

Nada de esto es JavaScript avanzado. No tiene por qué serlo. La disciplina en este rol no está en que el código sea brillante, sino en que sea fiable en el momento exacto en que un usuario real lo activa, en una página que tú no construiste, cargada en un navegador que no controlas.

Esa restricción hace que incluso los scripts más simples sean más interesantes de escribir de lo que parecen.

¿Buscas Alguien Que Pueda Hacer Esto En Tu Equipo?

Escribo estos análisis porque es lo que hago: encontrar los cuellos de botella reales (no los obvios) y solucionarlos con datos.

Si tu equipo necesita alguien que:

  • Diagnostique problemas de conversión con data, no opiniones

  • Implemente fixes con impacto medible en 30-60 días

  • Se mueva entre estrategia, análisis y ejecución

Hablemos.

Josue Somarribas

Product Designer especializado en conversión y crecimiento

Contacto

Copy Email

JOSUÉ SB

Crear soluciones digitales que realmente tienen sentido

2025 - Todos los derechos reservados

JOSUÉ SB

Crear soluciones digitales que realmente tienen sentido

2025 - Todos los derechos reservados

JOSUÉ SB

Crear soluciones digitales que realmente tienen sentido

2025 - Todos los derechos reservados