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') {
}function getParam(name) {
const params = new URLSearchParams(window.location.search);
return params.get(name);
}
if (getParam('source') === 'email') {
}function getParam(name) {
const params = new URLSearchParams(window.location.search);
return params.get(name);
}
if (getParam('source') === 'email') {
}function getParam(name) {
const params = new URLSearchParams(window.location.search);
return params.get(name);
}
if (getParam('source') === 'email') {
}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.
button.outerHTML = '<button class="cta">Buy Now</button>';
button.textContent = 'Buy Now';
button.classList.add('cta-variant');
button.outerHTML = '<button class="cta">Buy Now</button>';
button.textContent = 'Buy Now';
button.classList.add('cta-variant');
button.outerHTML = '<button class="cta">Buy Now</button>';
button.textContent = 'Buy Now';
button.classList.add('cta-variant');
button.outerHTML = '<button class="cta">Buy Now</button>';
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.