Reglas avanzadas de lanzamiento con JavaScript: Los patrones que utilizo para mantenerlas sostenibles

Blog

Vista angular de un portátil que muestra un editor de código en blanco y negro en la pantalla, con un fondo desenfocado.

Escucha este artículo

0:00/1:34

Si has trabajado seriamente en Adobe Launch (Adobe Experience Platform Data Collection), lo habrás sentido: las reglas de código personalizado comienzan como "un pequeño fragmento rápido" y lentamente se convierten... en una casa embrujada. Las cosas se disparan dos veces, solo se rompen en producción, y nadie recuerda por qué la Regla 38 depende de una clase CSS de 2019.

Este post es básicamente mi intento de mantener el código de Launch aburrido. Predecible. Mantenible. El tipo de aburrido que no te despierta a las 2am.

Y sí, gran parte de esto también se aplica a los experimentos de Monetate y acciones JS. Diferente plataforma, mismos problemas.

Lo que realmente significa "reglas avanzadas" en Launch

Para mí, "avanzado" no es astuto. Es cuando tu código tiene que sobrevivir a la realidad:

  • Las reglas pueden ejecutarse más de una vez (navegaciones SPA, componentes re-renderizados, observadores "útiles").

  • El DOM no está listo cuando lo deseas.

  • Otros tags compiten contigo, o mutan la página después de que hayas ejecutado.

  • Necesitas depurar, pero no quieres saturar la consola para cada usuario.

  • Necesitas disciplina de rendimiento porque Launch a menudo se ejecuta en páginas ya pesadas.

Entonces, el objetivo no es "más JavaScript". Es menos sorpresas.

Patrón 1: Una pequeña capa de utilidad compartida (a propósito)

Si copias y pegas asistentes en cada acción de código personalizado, te desviarás. Una regla utiliza un tiempo de espera de 200ms, otra usa 800ms, una maneja errores, otra no.

En cambio, me gusta un único objeto de utilidad compartido que se carga una vez, luego se reutiliza.

El mejor caso: construyes una pequeña extensión de Launch, la versionas y la importas limpiamente.

El caso más común: creas una regla "Biblioteca" que se ejecuta temprano y define window.__launchUtils (con espacio de nombres), luego cada otra regla lo usa.

Esa capa compartida se convierte en tu "biblioteca estándar". Aburrida, consistente, prácticamente testeable.

Patrón 2: Idempotencia, o "no hacer lo mismo dos veces"

Idempotencia solo significa: ejecutar la misma regla dos veces no inyecta UI doble, ni enlaza eventos dobles, ni hace seguimiento doble.

En Launch y Monetate, esta es la diferencia entre "funciona en QA" y "¿por qué estamos rastreando 14 clics por clic?"

Cosas que hago constantemente:

  • Marcar nodos del DOM que ya he tocado (a través de data-*).

  • Almacenar una bandera de una sola vez en memoria (window.__state) claveada por nombre de regla.

  • Para los oyentes de eventos, prefiero la delegación de eventos para así enlazar una vez a un padre estable en lugar de enlazar a elementos que se siguen re-renderizando.

Patrón 3: Manejo de errores que no oculta problemas

Las reglas de código personalizado aman fallar silenciosamente. Eso se siente bien hasta que necesitas saber por qué las conversiones bajaron.

Mi preferencia:

  • Envolver los puntos de entrada de la regla en un try/catch.

  • Registrar un error con contexto (nombre de la regla, variante, tipo de página).

  • Informarlo opcionalmente en algún lugar liviano (incluso solo Adobe Analytics como un evento, si ese es tu configuración), pero solo si tienes una forma limpia de hacerlo.

La idea clave: los errores deben ser visibles durante QA y descubribles en producción, sin convertir el sitio en un árbol de Navidad de registros en consola.

Patrón 4: Observabilidad, pero manténlo razonable

No necesitas una plataforma completa de telemetría para obtener valor. Necesitas un interruptor.

Normalmente agrego una bandera de depuración como:

  • localStorage.setItem('launch_debug', '1')

Entonces el registro solo ocurre cuando esa bandera está encendida. Eso significa:

  • QA puede depurar rápidamente.

  • Los usuarios de producción no reciben ruido.

  • Puedes pedirle a alguien, "enciende la bandera, reproduce, envíame los registros."

Bono: prefijo los registros con algo consistente, así que filtrar es fácil.

Patrón 5: Reglas de rendimiento, porque Launch se ejecuta en sitios reales

Algunos hábitos prácticos:

  • Almacenar selectores. No consultar el mismo elemento 10 veces.

  • Evitar configuraciones agresivas de MutationObserver a menos que realices una desconexión y espera.

  • Preferir requestAnimationFrame / requestIdleCallback para trabajo no crítico.

  • Mantener el sondeo (setInterval) como último recurso, y siempre establecer un límite de tiempo.

En otras palabras, actuar como si estuvieras pagando por milisegundos. Porque tus usuarios lo están.

Patrón 6: Evitar dependencias que no puedes controlar

Es tentador pegar una biblioteca de ayuda o depender de cualquier versión de jQuery que tenga el sitio. Así es como heredas caos.

Mi regla:

  • No libs externas en el código de la regla a menos que sean de primera parte, versionadas y propias.

  • No "el sitio ya tiene X" a menos que sea contractualmente cierto.

Si necesitas capacidad compartida, inclúyela en tu capa de utilidades.

Mini kit de inicio: utilidades que realmente reutilizo en reglas de Launch

Aquí tienes un pequeño "kit de inicio" que puedes incluir en una única regla de Launch que se ejecute temprano (o una extensión). Luego otras reglas llaman a window.__launchUtils.*.

(function initLaunchUtils(w) {
  const NS = "__launchUtils";
  if (w[NS]) return; // idempotent init

  const now = () => (w.performance && performance.now ? performance.now() : Date.now());

  const debugEnabled = () => {
    try { return w.localStorage && localStorage.getItem("launch_debug") === "1"; }
    catch (e) { return false; }
  };

  const log = (level, ...args) => {
    if (!debugEnabled()) return;
    const prefix = "[Launch]";
    (console[level] || console.log).call(console, prefix, ...args);
  };

  const safe = (fn, context = {}) => {
    return function wrapped(...args) {
      try { return fn.apply(this, args); }
      catch (err) {
        log("error", "Rule error", { err, context });
      }
    };
  };

  const once = (key, fn) => {
    w.__launchState = w.__launchState || {};
    if (w.__launchState[key]) return false;
    w.__launchState[key] = true;
    fn();
    return true;
  };

  const waitFor = (predicate, { interval = 50, timeout = 4000 } = {}) => {
    const start = now();
    return new Promise((resolve, reject) => {
      const tick = () => {
        let ok = false;
        try { ok = !!predicate(); } catch (e) {}
        if (ok) return resolve(true);
        if (now() - start > timeout) return reject(new Error("waitFor timeout"));
        setTimeout(tick, interval);
      };
      tick();
    });
  };

  const onReady = (fn) => {
    if (document.readyState === "complete" || document.readyState === "interactive") fn();
    else document.addEventListener("DOMContentLoaded", fn, { once: true });
  };

  const delegate = (root, eventName, selector, handler) => {
    root.addEventListener(eventName, (e) => {
      const el = e.target && e.target.closest ? e.target.closest(selector) : null;
      if (!el || !root.contains(el)) return;
      handler(e, el);
    });
  };

  const markOnce = (el, mark) => {
    if (!el) return false;
    const attr = `data-launch-${mark}`;
    if (el.hasAttribute(attr)) return false;
    el.setAttribute(attr, "1");
    return true;
  };

  w[NS] = {
    log,
    safe,
    once,
    waitFor,
    onReady,
    delegate,
    markOnce
  };

  log("log", "Utilities ready");
})(window);

(function initLaunchUtils(w) {
  const NS = "__launchUtils";
  if (w[NS]) return; // idempotent init

  const now = () => (w.performance && performance.now ? performance.now() : Date.now());

  const debugEnabled = () => {
    try { return w.localStorage && localStorage.getItem("launch_debug") === "1"; }
    catch (e) { return false; }
  };

  const log = (level, ...args) => {
    if (!debugEnabled()) return;
    const prefix = "[Launch]";
    (console[level] || console.log).call(console, prefix, ...args);
  };

  const safe = (fn, context = {}) => {
    return function wrapped(...args) {
      try { return fn.apply(this, args); }
      catch (err) {
        log("error", "Rule error", { err, context });
      }
    };
  };

  const once = (key, fn) => {
    w.__launchState = w.__launchState || {};
    if (w.__launchState[key]) return false;
    w.__launchState[key] = true;
    fn();
    return true;
  };

  const waitFor = (predicate, { interval = 50, timeout = 4000 } = {}) => {
    const start = now();
    return new Promise((resolve, reject) => {
      const tick = () => {
        let ok = false;
        try { ok = !!predicate(); } catch (e) {}
        if (ok) return resolve(true);
        if (now() - start > timeout) return reject(new Error("waitFor timeout"));
        setTimeout(tick, interval);
      };
      tick();
    });
  };

  const onReady = (fn) => {
    if (document.readyState === "complete" || document.readyState === "interactive") fn();
    else document.addEventListener("DOMContentLoaded", fn, { once: true });
  };

  const delegate = (root, eventName, selector, handler) => {
    root.addEventListener(eventName, (e) => {
      const el = e.target && e.target.closest ? e.target.closest(selector) : null;
      if (!el || !root.contains(el)) return;
      handler(e, el);
    });
  };

  const markOnce = (el, mark) => {
    if (!el) return false;
    const attr = `data-launch-${mark}`;
    if (el.hasAttribute(attr)) return false;
    el.setAttribute(attr, "1");
    return true;
  };

  w[NS] = {
    log,
    safe,
    once,
    waitFor,
    onReady,
    delegate,
    markOnce
  };

  log("log", "Utilities ready");
})(window);

(function initLaunchUtils(w) {
  const NS = "__launchUtils";
  if (w[NS]) return; // idempotent init

  const now = () => (w.performance && performance.now ? performance.now() : Date.now());

  const debugEnabled = () => {
    try { return w.localStorage && localStorage.getItem("launch_debug") === "1"; }
    catch (e) { return false; }
  };

  const log = (level, ...args) => {
    if (!debugEnabled()) return;
    const prefix = "[Launch]";
    (console[level] || console.log).call(console, prefix, ...args);
  };

  const safe = (fn, context = {}) => {
    return function wrapped(...args) {
      try { return fn.apply(this, args); }
      catch (err) {
        log("error", "Rule error", { err, context });
      }
    };
  };

  const once = (key, fn) => {
    w.__launchState = w.__launchState || {};
    if (w.__launchState[key]) return false;
    w.__launchState[key] = true;
    fn();
    return true;
  };

  const waitFor = (predicate, { interval = 50, timeout = 4000 } = {}) => {
    const start = now();
    return new Promise((resolve, reject) => {
      const tick = () => {
        let ok = false;
        try { ok = !!predicate(); } catch (e) {}
        if (ok) return resolve(true);
        if (now() - start > timeout) return reject(new Error("waitFor timeout"));
        setTimeout(tick, interval);
      };
      tick();
    });
  };

  const onReady = (fn) => {
    if (document.readyState === "complete" || document.readyState === "interactive") fn();
    else document.addEventListener("DOMContentLoaded", fn, { once: true });
  };

  const delegate = (root, eventName, selector, handler) => {
    root.addEventListener(eventName, (e) => {
      const el = e.target && e.target.closest ? e.target.closest(selector) : null;
      if (!el || !root.contains(el)) return;
      handler(e, el);
    });
  };

  const markOnce = (el, mark) => {
    if (!el) return false;
    const attr = `data-launch-${mark}`;
    if (el.hasAttribute(attr)) return false;
    el.setAttribute(attr, "1");
    return true;
  };

  w[NS] = {
    log,
    safe,
    once,
    waitFor,
    onReady,
    delegate,
    markOnce
  };

  log("log", "Utilities ready");
})(window);

(function initLaunchUtils(w) {
  const NS = "__launchUtils";
  if (w[NS]) return; // idempotent init

  const now = () => (w.performance && performance.now ? performance.now() : Date.now());

  const debugEnabled = () => {
    try { return w.localStorage && localStorage.getItem("launch_debug") === "1"; }
    catch (e) { return false; }
  };

  const log = (level, ...args) => {
    if (!debugEnabled()) return;
    const prefix = "[Launch]";
    (console[level] || console.log).call(console, prefix, ...args);
  };

  const safe = (fn, context = {}) => {
    return function wrapped(...args) {
      try { return fn.apply(this, args); }
      catch (err) {
        log("error", "Rule error", { err, context });
      }
    };
  };

  const once = (key, fn) => {
    w.__launchState = w.__launchState || {};
    if (w.__launchState[key]) return false;
    w.__launchState[key] = true;
    fn();
    return true;
  };

  const waitFor = (predicate, { interval = 50, timeout = 4000 } = {}) => {
    const start = now();
    return new Promise((resolve, reject) => {
      const tick = () => {
        let ok = false;
        try { ok = !!predicate(); } catch (e) {}
        if (ok) return resolve(true);
        if (now() - start > timeout) return reject(new Error("waitFor timeout"));
        setTimeout(tick, interval);
      };
      tick();
    });
  };

  const onReady = (fn) => {
    if (document.readyState === "complete" || document.readyState === "interactive") fn();
    else document.addEventListener("DOMContentLoaded", fn, { once: true });
  };

  const delegate = (root, eventName, selector, handler) => {
    root.addEventListener(eventName, (e) => {
      const el = e.target && e.target.closest ? e.target.closest(selector) : null;
      if (!el || !root.contains(el)) return;
      handler(e, el);
    });
  };

  const markOnce = (el, mark) => {
    if (!el) return false;
    const attr = `data-launch-${mark}`;
    if (el.hasAttribute(attr)) return false;
    el.setAttribute(attr, "1");
    return true;
  };

  w[NS] = {
    log,
    safe,
    once,
    waitFor,
    onReady,
    delegate,
    markOnce
  };

  log("log", "Utilities ready");
})(window);

No es elegante, pero es consistente. Y la consistencia es lo que evita que "código personalizado de Adobe Launch" se convierta en un montón de incógnitas.

Checklist rápido que uso antes de enviar una regla

  • ¿Puede esto ejecutarse dos veces sin romper nada?

  • ¿Están los oyentes de eventos enlazados una vez, idealmente a través de delegación?

  • Si falla, ¿lo notaré? (al menos en QA)

  • ¿Puedo depurarlo sin editar código? (bandera de depuración)

  • ¿Evitó el sondeo de DOM pesado y observadores caros?

  • ¿Evitó dependencias dudosas?

  • ¿Es el nombre de la regla y la clave consistente en flujos de trabajo de Launch y Monetate?

Pensamiento final

La mantenibilidad en las reglas de Launch es principalmente sobre humildad. No estás escribiendo "un fragmento". Estás escribiendo software que vivirá dentro de un gestor de etiquetas, en un sitio que no controlas completamente, junto a otros scripts que también creen que son el personaje principal.

Así que optimizo para lo aburrido: pequeñas utilidades, idempotencia, código defensivo y la suficiente observabilidad para explicar lo que pasó cuando inevitablemente algo ocurre.

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