Módulo 8: Project Ship Mercados Recommendations

El flag encendido y la rampa diseñada

Descripción

El módulo 1 terminó con una decisión: el equipo de Mercado elige empezar chico, con un canary del 1%, en vez de lanzar recommendations al 100% de golpe. Esta lección convierte esa decisión en las dos primeras piezas concretas del lanzamiento real: el flag que hace posible controlar la exposición sin un nuevo deploy (módulo 2), y la rampa completa de cuatro etapas que define, de antemano, cómo se sube del canary al 100% (módulo 3). Ninguna herramienta nueva — isEnabled() y el patrón de diseño de rampa de sus módulos de origen, aplicados en el orden en que un equipo real los usaría el día del lanzamiento.

Conexión con el módulo. Este capstone no introduce ningún mecanismo nuevo en esta lección: reutiliza hashUserId() e isEnabled() exactamente como quedaron en las lecciones 4 y 5 del módulo 2, y el patrón de rolloutPlan() exactamente como quedó en las lecciones 2 y 4 del módulo 3. Lo que agrega es la secuencia correcta: primero se enciende el flag y se verifica en la etapa ya decidida (canary 1%), después se diseña — por escrito, con criterios explícitos, antes de tocar el flag de nuevo — la rampa completa hasta el 100%. Esta lección se detiene justo antes de subir a la etapa de 10%: vigilar esa subida en vivo es el trabajo de la lección 3, que sigue después de esta.

Una analogía: el interruptor ya cableado, y el itinerario ya escrito

Piensa en dos preparativos distintos, y necesarios los dos, antes de un viaje largo en auto de noche. El primero es el interruptor de las luces del auto: ya está cableado, funciona, y tú decides cuándo encenderlo — no hace falta desarmar el tablero cada vez que quieres luz. El segundo es el itinerario del viaje: en qué ciudad paras a dormir, cuánta gasolina necesitas entre una parada y la siguiente, y qué señal te haría desviarte del plan — decidido antes de arrancar el motor, no improvisado en cada cruce.

El flag de recommendations es ese interruptor: ya está cableado (el código vive en producción, apagado), y esta lección lo enciende para la población exacta que el módulo 1 decidió exponer primero. La rampa de cuatro etapas es el itinerario: las paradas (1% → 10% → 50% → 100%), cuánta gente hace falta en cada una para confiar en la medición, y cuánto tiempo esperar antes de la siguiente — todo escrito antes de que el auto salga del canary.

Ejemplo trabajado: enciende el flag, diseña la rampa

// M8 L02: enciende el flag de recommendations en la etapa canary (1%, decidida en el
// modulo 1) y disena la rampa completa de 4 etapas ANTES de subir mas alla del canary.
// hashUserId() e isEnabled() son EXACTAMENTE las del modulo 2 (L4, L5). rolloutPlan()
// sigue el mismo patron del modulo 3 (L2, L4).

function hashUserId(userId) {
  let hash = 0;
  for (let i = 0; i < userId.length; i++) {
    hash = (hash * 31 + userId.charCodeAt(i)) % 100;
  }
  return hash;
}
function isEnabled(userId, flag) {
  if (!flag.enabled) return false;
  const bucket = hashUserId(userId + flag.name);
  return bucket < flag.rolloutPercent;
}
function rolloutPlan(stages) {
  const results = [];
  let halted = false;
  for (const s of stages) {
    if (halted) { results.push({ ...s, decision: 'NOT_REACHED' }); continue; }
    const decision = s.advanceIf(s.measured) ? 'ADVANCE' : 'HOLD';
    results.push({ ...s, decision });
    if (decision === 'HOLD') halted = true;
  }
  return results;
}

console.log('=== Parte 1: flag de recommendations en canary (1%) ===');
const recommendationsFlag = { name: 'recommendations', enabled: true, rolloutPercent: 1 };
const sampleN = 5000;
const buyerIds = [];
for (let i = 0; i < sampleN; i++) buyerIds.push('buyer-' + String(i).padStart(5, '0'));
const enabledBuyers = buyerIds.filter((id) => isEnabled(id, recommendationsFlag));
console.log(enabledBuyers.length + ' de ' + sampleN + ' compradores ven recommendations (' + (enabledBuyers.length / sampleN * 100).toFixed(2) + '%)');

console.log('\n=== Parte 2: rampa disenada (4 etapas), antes de subir del canary ===');
const ramp = [
  { percent: 0.01, label: 'canary 1%', minSampleSize: 1000, minDwellHours: 4 },
  { percent: 0.10, label: 'rollout 10%', minSampleSize: 5000, minDwellHours: 12 },
  { percent: 0.50, label: 'rollout 50%', minSampleSize: 20000, minDwellHours: 24 },
  { percent: 1.00, label: 'rollout 100%', minSampleSize: 50000, minDwellHours: 24 },
];
ramp.forEach((s) => console.log(s.label.padEnd(14) + 'minSampleSize=' + String(s.minSampleSize).padStart(6) + '  minDwellHours=' + s.minDwellHours));

console.log('\n=== Parte 3: rolloutPlan sobre la etapa ya cumplida (canary) ===');
const ceiling = 800;
const canaryStage = [{ percent: 0.01, label: 'canary 1%', measured: { p95Latency: 720 }, advanceIf: (m) => m.p95Latency <= ceiling }];
const canaryResult = rolloutPlan(canaryStage);
canaryResult.forEach((s) => console.log(s.label + ': p95=' + s.measured.p95Latency + 'ms -> ' + s.decision));
console.log('\nCanary avanza limpio. La etapa 10% se vigila EN VIVO en la proxima leccion.');

Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:

=== Parte 1: flag de recommendations en canary (1%) ===
50 de 5000 compradores ven recommendations (1.00%)

=== Parte 2: rampa disenada (4 etapas), antes de subir del canary ===
canary 1%     minSampleSize=  1000  minDwellHours=4
rollout 10%   minSampleSize=  5000  minDwellHours=12
rollout 50%   minSampleSize= 20000  minDwellHours=24
rollout 100%  minSampleSize= 50000  minDwellHours=24

=== Parte 3: rolloutPlan sobre la etapa ya cumplida (canary) ===
canary 1%: p95=720ms -> ADVANCE

Canary avanza limpio. La etapa 10% se vigila EN VIVO en la proxima leccion.

Repasa las tres partes en orden. La Parte 1 confirma el mecanismo: con rolloutPercent: 1, exactamente 1.00% de una muestra de 5,000 compradores queda expuesto — el mismo hashUserId() determinista de siempre, sin Math.random(), así que cualquiera que corra este mismo código obtiene el mismo resultado. La Parte 2 es la pieza nueva de esta lección: la tabla completa de la rampa, con su porcentaje, su tamaño mínimo de muestra y su tiempo mínimo de espera por etapa — decidida antes de que el flag suba un solo punto porcentual más allá del canario. La Parte 3 confirma que la primera etapa de esa rampa, con el dato real medido en canary (720ms, dentro del techo de 800ms), avanza limpio.

Fíjate en algo importante sobre la Parte 3: rolloutPlan() corre únicamente sobre el arreglo canaryStage, que tiene una sola etapa. No es un descuido — es la disciplina correcta en este punto exacto del lanzamiento: todavía no hay datos reales medidos para 10%, 50% ni 100%, así que no tiene sentido evaluarlos todavía. Evaluar solo lo que de verdad se ha medido, y nada más, es exactamente el error que el ejercicio 1 del proyecto del módulo 3 nombró: dejar un campo en null y confiar en que el código "no avance por error" es un riesgo real, no una hipótesis.

Por qué el orden importa: primero el flag, después la rampa completa

Podrías preguntarte por qué esta lección no diseña la rampa completa primero y enciende el flag después — al final, ambas cosas ocurren "antes de lanzar de verdad". La razón tiene que ver con qué información necesitas para cada paso. Encender el flag en la etapa canary no requiere haber decidido todavía los criterios de las etapas 50% y 100% — solo requiere el mecanismo (isEnabled()) y el porcentaje ya decidido en el módulo 1. Diseñar la rampa completa, en cambio, sí se beneficia de tener el canary ya corriendo: los minSampleSize y minDwellHours de las etapas siguientes no son números arbitrarios — reflejan cuánta confianza necesita el equipo antes de exponer a más gente, y esa confianza se calibra mejor con datos reales del canary ya en marcha, no solo con la intuición previa al lanzamiento.

Esto no significa que la rampa se diseñe "sobre la marcha" — todo lo contrario: los cuatro criterios de la Parte 2 están fijados antes de que la etapa de 10% empiece, exactamente como advirtió el error común del proyecto del módulo 3 ("diseñar la Parte 1 después de correr la Parte 2, para justificar el resultado que ya se vio"). El orden correcto es: canario corriendo con datos reales → rampa completa diseñada con esos datos como contexto, pero antes de que la siguiente etapa se ejecute → cada etapa siguiente evaluada contra ese diseño ya fijo, sin negociarlo después de ver el resultado.

Errores comunes

Encender el flag directamente al porcentaje de la siguiente etapa (10%), sin pasar por el canary verificado. Qué pasa: alguien, con la rampa completa ya diseñada, decide "ahorrar tiempo" y sube rolloutPercent directo a 10 sin haber confirmado primero que el canary al 1% se comporta como se espera. Por qué pasa: la rampa completa ya está escrita, con las cuatro etapas visibles, y saltarse la primera se siente como una optimización razonable cuando "de todos modos vamos a llegar ahí". Cómo detectarlo: si en algún punto el rolloutPercent del flag salta de 1 a 10 sin que exista un registro de qué se midió en el 1% antes del salto, la etapa canary no se respetó. Cómo corregirlo: como en la Parte 3 de esta lección, cada etapa de la rampa se evalúa con rolloutPlan() sobre datos reales de esa etapa específica antes de avanzar a la siguiente — nunca saltando una etapa completa porque la siguiente "ya estaba planeada".

Diseñar minSampleSize y minDwellHours iguales para las cuatro etapas, en vez de crecientes. Qué pasa: alguien copia el mismo minSampleSize: 1000 y minDwellHours: 4 para las cuatro filas de la tabla de la Parte 2, sin ajustar los valores al tamaño real de cada etapa. Por qué pasa: escribir un solo valor y repetirlo es más rápido que calcular, etapa por etapa, cuánta gente y cuánto tiempo hace falta para una medición confiable a esa escala. Cómo detectarlo: si el minSampleSize de rollout 100% (que expone a 250,000 compradores) es el mismo que el de canary 1% (que expone a 2,500), la tabla no refleja que una muestra de 1,000 personas da mucha menos confianza sobre 250,000 compradores que sobre 2,500. Cómo corregirlo: como en la tabla de esta lección, minSampleSize y minDwellHours crecen con cada etapa —1,0005,00020,00050,000—, porque la confianza que necesitas antes de exponer a más gente escala con el riesgo de esa etapa, no se queda fija.

Confundir "el flag está verificado" con "la rampa completa ya está segura". Qué pasa: después de ver que el flag funciona correctamente en la Parte 1 (1.00% de exposición, estable), alguien concluye que el lanzamiento completo está listo, sin haber diseñado todavía los criterios de las etapas siguientes. Por qué pasa: la Parte 1 da una confirmación técnica clara y satisfactoria, y esa claridad puede sentirse, erróneamente, como el trabajo completo de esta lección. Cómo detectarlo: si alguien pregunta "¿y qué pasa si la latencia se rompe en la etapa de 10%?" y no hay una tabla de criterios ya escrita para responder, la rampa todavía no está diseñada, aunque el flag sí funcione. Cómo corregirlo: el flag verificado (Parte 1) es una condición necesaria, no suficiente — la rampa completa (Parte 2), con sus criterios explícitos por etapa, es la pieza que falta para que "el mecanismo funciona" se convierta en "el lanzamiento está planeado".

Ejercicios

Ejercicio 1 — Cambia el porcentaje del canary. El equipo de logística de Mercado quiere aplicar el mismo patrón de esta lección a su algoritmo de tiempos de entrega estimados (deliveryEtaV2), pero empezando con un canary de 0.5% en vez de 1%. Ejecuta mentalmente (o en Node) la Parte 1 con rolloutPercent: 0.5 sobre la misma muestra de 5,000 usuarios. ¿Qué porcentaje esperarías ver expuesto, aproximadamente?

Ver solución

Con rolloutPercent: 0.5, la condición bucket < flag.rolloutPercent de isEnabled() solo es verdadera para los usuarios cuyo bucket (un entero de 0 a 99) sea exactamente 0 — es decir, aproximadamente 0.5% de los 5,000 usuarios de la muestra, unos 25 compradores. A diferencia de un porcentaje entero como 1 o 10, un porcentaje fraccionario como 0.5 reduce el número de valores de bucket que califican, pero el mecanismo de isEnabled() no cambia en absoluto — sigue siendo la misma comparación exacta, solo con un umbral más bajo.

Ejercicio 2 — Diseña la rampa para un caso con menos margen. El equipo de logística fija, para deliveryEtaV2, un minSampleSize de 2,000 en la etapa de 10% (más alto que los 5,000... espera, más bajo que el de recommendations) porque su base total es más chica (80,000 pedidos por semana, contra los 250,000 compradores de Mercado). Si mantienes la misma proporción que usó recommendations entre minSampleSize de cada etapa y el tamaño de la base total, ¿qué minSampleSize le correspondería a la etapa de 50% de deliveryEtaV2?

Ver solución

En recommendations, la etapa de 50% tiene minSampleSize: 20,000 sobre una base de 250,000 — una proporción de 20000 / 250000 = 8% de la base total. Aplicando esa misma proporción a deliveryEtaV2 (base de 80,000): 80000 * 0.08 = 6,400. El punto de este ejercicio no es que 8% sea una regla universal —cada equipo puede calibrar su propia proporción—, sino que mantener la misma lógica proporcional entre casos distintos es una forma razonable de transferir un diseño de rampa a un contexto con una base de usuarios de otro tamaño, en vez de copiar los números absolutos sin ajustarlos.

Ejercicio 3 — Explica la secuencia por escrito. En 3-4 frases, explica a alguien que no conoce esta guía por qué el equipo de Mercado enciende el flag en la etapa canary primero, y diseña la rampa completa de las etapas siguientes después — en vez de diseñar todo de antemano, incluyendo el canary, en un solo paso.

Ver solución

Un ejemplo de respuesta: "Encendemos el flag en la etapa más chica y controlada primero (el canary, 1% de nuestros compradores) porque eso no requiere ninguna decisión adicional — el porcentaje ya estaba definido desde el análisis de radio de impacto. Diseñamos las etapas siguientes con más cuidado, usando los primeros datos reales del canary como contexto, porque los criterios de cuánta gente necesitamos medir y cuánto tiempo esperar en cada etapa dependen de qué tan confiables resultan las mediciones a esa escala — algo que se calibra mejor viendo datos reales que solo con la intuición previa al lanzamiento. Aun así, ningún criterio se ajusta después de ver los resultados de una etapa — se fija antes de que esa etapa específica ocurra."

Resumen y siguiente paso

En esta lección encendiste el flag de recommendations en la etapa exacta que el módulo 1 decidió (canary 1%, 1.00% de exposición confirmada sobre 5,000 compradores), diseñaste la rampa completa de cuatro etapas con sus criterios de tamaño mínimo y tiempo de espera, y confirmaste con rolloutPlan() que la primera etapa —la única con datos reales todavía— avanza limpio (720ms, dentro del techo de 800ms).

Antes de avanzar deberías poder: explicar por qué minSampleSize y minDwellHours crecen con cada etapa en vez de mantenerse fijos; y ejecutar isEnabled() a mano para calcular, aproximadamente, cuántos usuarios de una muestra dada quedarían expuestos a un porcentaje distinto de rollout.

La lección 3 toma esta misma rampa y la vigila en vivo, con los cuatro guardrails completos de la guía de métricas —no solo latencia— sobre las dos etapas que sí tienen datos reales: el canary que acabas de confirmar, y la etapa de 10% que todavía no se ha medido.

Recursos

  • Pete Hodgson (con Martin Fowler), "Feature Toggles (aka Feature Flags)" — martinfowler.com/articles/feature-toggles.html. La referencia completa sobre feature flags que sostiene el mecanismo de la Parte 1 de esta lección. En inglés.
  • Google SRE Workbook, Capítulo 16, "Canarying Releases" — sre.google/workbook/canarying-releases. La práctica formal de diseñar una rampa de etapas sucesivas, cada una con su propio criterio de avance — exactamente la tabla de la Parte 2 de esta lección. En inglés.