Módulo 4: Monitoring The Launch
Cuándo frenar: el mismo mecanismo, dos resultados distintos
Descripción
guardrailWatch() ya te dio un veredicto sobre el rollout real de recommendations: HALT en la etapa de 10%. Esta lección se queda ahí y pregunta qué significa esa palabra en la práctica —y, para que quede imposible de confundir, corre exactamente el mismo mecanismo sobre un segundo escenario, hipotético, donde el equipo de ingeniería ya resolvió el problema de latencia antes de reiniciar el rollout. Comparar los dos resultados —uno que se detiene, uno que llega al 100%— es la forma más clara de ver que guardrailWatch() no tiene opinión sobre si recommendations es una buena idea: solo reporta, con disciplina, si los datos de cada etapa respetan los límites que el equipo fijó de antemano.
Conexión con el módulo. Esta lección no cambia una sola línea de guardrailWatch() ni de guardrailCheck() —las reutiliza exactamente como quedaron en la lección 3—; el único cambio es el conjunto de datos que se le pasa. Eso es, a propósito, el punto central de la lección: la disciplina de frenar no depende de inventar una función distinta para cada situación, depende de correr la misma función con honestidad sobre los datos que de verdad se midieron.
Una analogía: la aguja de temperatura no negocia con la velocidad
Volviendo al tablero del auto de la lección 1: imagina que la aguja de temperatura entra en zona roja mientras vas a buena velocidad en una carretera despejada. Un conductor que razona "voy rápido, el camino está libre, sigo un rato más y reviso la temperatura después" está tomando una decisión peligrosa disfrazada de paciencia. La aguja de temperatura no le pregunta a la velocidad si puede esperar — cuando entra en zona roja, la acción correcta es la misma sin importar qué tan bien vayan los demás números del tablero: reducir la velocidad, ahora, no "en un rato".
checkoutConversionRate subiendo con fuerza es la carretera despejada. checkoutLatencyP95Ms en 910ms, sobre un techo de 800, es la aguja en zona roja. El error que esta lección nombra con su nombre completo es razonar "el primario va bien, sigamos un poco más antes de frenar" — exactamente el mismo error que el conductor de la analogía.
Ejemplo trabajado: el mismo guardrailWatch(), dos rampas distintas
// guardrailCheck y guardrailWatch: EXACTAMENTE las mismas de la leccion 3,
// sin ningun cambio. Lo unico que cambia en esta leccion es el conjunto de
// datos que se les pasa.
function guardrailCheck(before, after, guardrails) {
const results = guardrails.map((g) => {
const beforeVal = before[g.metric];
const afterVal = after[g.metric];
let broken = false;
let detail = '';
if (g.type === 'ceiling') {
broken = afterVal > g.limit;
detail = afterVal + ' vs techo ' + g.limit;
} else if (g.type === 'floor') {
broken = afterVal < g.limit;
detail = afterVal + ' vs piso ' + g.limit;
} else if (g.type === 'maxIncrease') {
const delta = afterVal - beforeVal;
broken = delta > g.limit;
detail = 'delta +' + delta.toFixed(4) + ' vs maximo permitido +' + g.limit;
}
return { metric: g.metric, before: beforeVal, after: afterVal, broken, detail };
});
const brokenGuardrails = results.filter((r) => r.broken).map((r) => r.metric);
return { results, anyBroken: brokenGuardrails.length > 0, brokenGuardrails };
}
function guardrailWatch(stages, guardrails, baseline) {
const log = [];
let halted = false;
for (const stage of stages) {
if (halted) {
log.push({ stage: stage.name, percent: stage.percent, status: 'not reached (ramp halted earlier)' });
continue;
}
if (!stage.metrics) {
log.push({ stage: stage.name, percent: stage.percent, status: 'not measured yet' });
continue;
}
const check = guardrailCheck(baseline, stage.metrics, guardrails);
if (check.anyBroken) {
log.push({ stage: stage.name, percent: stage.percent, status: 'HALT', broken: check.brokenGuardrails });
halted = true;
} else {
log.push({ stage: stage.name, percent: stage.percent, status: 'continue' });
}
}
return { log, halted };
}
const baseline = { checkoutLatencyP95Ms: 650, complaintRate: 0.012, churnRate: 0.045, grossMargin: 0.220 };
const guardrails = [
{ metric: 'checkoutLatencyP95Ms', type: 'ceiling', limit: 800 },
{ metric: 'complaintRate', type: 'maxIncrease', limit: 0.005 },
{ metric: 'churnRate', type: 'maxIncrease', limit: 0.010 },
{ metric: 'grossMargin', type: 'floor', limit: 0.180 },
];
// Escenario hipotetico: antes de reiniciar el rollout, ingenieria cacheo la
// llamada al motor de recomendaciones (el COMO de ese arreglo tecnico es
// trabajo del modulo 5; aqui solo se usa el resultado hipotetico despues
// del arreglo, para contrastar contra el escenario real de la leccion 3).
const fixedStages = [
{ name: 'canary', percent: 1, metrics: { checkoutLatencyP95Ms: 670, complaintRate: 0.012, churnRate: 0.045, grossMargin: 0.220 } },
{ name: 'ramp-10', percent: 10, metrics: { checkoutLatencyP95Ms: 705, complaintRate: 0.012, churnRate: 0.045, grossMargin: 0.220 } },
{ name: 'ramp-50', percent: 50, metrics: { checkoutLatencyP95Ms: 740, complaintRate: 0.013, churnRate: 0.046, grossMargin: 0.219 } },
{ name: 'full', percent: 100, metrics: { checkoutLatencyP95Ms: 765, complaintRate: 0.013, churnRate: 0.046, grossMargin: 0.219 } },
];
console.log('=== guardrailWatch: mismo mecanismo, escenario hipotetico ya arreglado ===\n');
const watch = guardrailWatch(fixedStages, guardrails, baseline);
watch.log.forEach((e) => console.log(e.stage + ' (' + e.percent + '%): ' + e.status + (e.broken ? ' -- ' + e.broken.join(', ') : '')));
console.log('\nRampa detenida: ' + watch.halted);
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== guardrailWatch: mismo mecanismo, escenario hipotetico ya arreglado ===
canary (1%): continue
ramp-10 (10%): continue
ramp-50 (50%): continue
full (100%): continue
Rampa detenida: false
Compara esta salida con la de la lección 3, línea por línea. Ahí, la latencia subía de 680ms (canary) a 910ms (10%), rompiendo el techo. Aquí, con el motor de recomendaciones ya cacheado (hipotéticamente), la latencia sube mucho más despacio a lo largo de las cuatro etapas —670, 705, 740, 765— y nunca cruza los 800ms, ni siquiera al llegar al 100%. El resultado: Rampa detenida: false. Ninguna línea de guardrailWatch() ni de guardrailCheck() cambió entre los dos escenarios — lo único distinto es el dato real que se midió en producción. Esa es, exactamente, la propiedad que hace confiable a este mecanismo: no decide de antemano si recommendations "merece" llegar a 100%, decide, en cada etapa, si los datos reales respetan los límites — y dos rollouts distintos del mismo feature pueden tener veredictos completamente distintos, según qué tan bien se haya resuelto el problema técnico antes de reintentar.
Profundización: frenar no es lo mismo que cancelar
El error más caro de esta lección no es "seguir subiendo quando no se debe" — ya lo cubriste en la lección 3. Es el error contrario, casi tan costoso: leer un HALT como si fuera "cancelar recommendations para siempre". No lo es. HALT en la etapa de 10% dice, con precisión, tres cosas y solo tres: la rampa no avanza a 50% todavía; los usuarios que ya están en la etapa de 10% siguen expuestos al problema mientras se decide qué hacer con ellos —esa decisión, revertir o arreglar hacia adelante, es del módulo 5—; y el diagnóstico técnico (¿por qué la latencia subió tanto entre canary y 10%, no solo un poco?) tiene que resolverse antes de intentar la rampa de nuevo. El escenario hipotético de esta lección —latencia cacheada, rampa completa sin HALT— es exactamente esa segunda intentona, después de que el problema técnico se resolvió: mismo feature, mismo mecanismo de vigilancia, resultado distinto porque la causa raíz ya no está.
Errores comunes
Mirar solo la métrica primaria y no los guardrails. Qué pasa: al ver que checkoutConversionRate sigue subiendo con fuerza a lo largo del rollout, alguien argumenta que ese número, por sí solo, ya justifica seguir subiendo el porcentaje —sin haber corrido guardrailWatch() en absoluto, o habiéndolo corrido y decidiendo ignorar su resultado—. Por qué pasa: la métrica primaria es la que todos entienden y la que se reporta hacia arriba en la organización; los guardrails se sienten como un chequeo secundario, casi burocrático, comparado con el número que "de verdad importa". Cómo detectarlo: si una decisión de avanzar la rampa se toma sin que nadie mencione el estado actual de checkoutLatencyP95Ms, complaintRate, churnRate o grossMargin, la decisión está incompleta. Cómo corregirlo: guardrailWatch() evalúa los guardrails de forma independiente del valor de la métrica primaria — por diseño, no toma checkoutConversionRate como input siquiera. Esa independencia es la que hace posible que un feature "gane" y, al mismo tiempo, no esté listo para el 100%.
Seguir avanzando la rampa aunque un guardrail ya se rompió, "porque el primario gana". Qué pasa: el equipo ve el HALT en la etapa de 10%, reconoce que la latencia se rompió, y decide subir a 50% de todos modos, con el argumento de que "18.75% de lift en conversión compensa 110ms de latencia extra". Por qué pasa: convertir dos métricas distintas —conversión y latencia— a una sola "cuenta neta" se siente como una forma de razonar cuantitativamente, aunque en realidad esté comparando cosas que el equipo, antes de lanzar, ya había decidido que no se debían comparar entre sí —por eso existe un guardrail con un techo fijo, en vez de dejar la decisión abierta a discusión en el momento—. Cómo detectarlo: la frase "el primario compensa" o "vale la pena el trade-off" apareciendo después de un HALT, sin ningún plan de diagnóstico de por medio. Cómo corregirlo: un guardrail con un umbral fijo, definido antes de lanzar, existe exactamente para que esta negociación no ocurra "en caliente" — el momento de decidir si 110ms de latencia extra vale la pena fue la reunión de planeación, no la mitad del rollout con el HALT ya activado.
No definir el umbral de halt de antemano, y racionalizarlo en vivo. Qué pasa: el equipo no fijó, antes de lanzar, qué significa exactamente "frenar" —¿un solo guardrail roto? ¿dos? ¿solo si el roto es de latencia?— y termina discutiendo esa definición en medio del rollout, con la presión de una decisión real encima. Por qué pasa: definir el criterio de halt de antemano exige pensar en escenarios que todavía no pasaron, lo cual se siente menos urgente que resolver el problema que ya está pasando. Cómo detectarlo: si la pregunta "¿esto cuenta como motivo para frenar?" se contesta distinto según quién esté en la sala ese día, el criterio nunca se fijó con claridad. Cómo corregirlo: guardrailWatch() usa la misma regla que guardrailCheck() —anyBroken, un solo guardrail roto es suficiente— definida en el código, no en una conversación del momento; esa regla se decide antes de que el primer usuario vea recommendations, exactamente como advirtió la guía de métricas sobre los umbrales mismos.
Ejercicios
Ejercicio 1 — Corrige el escenario hipotético. Si en el escenario "ya arreglado" de esta lección, la etapa de 50% hubiera medido checkoutLatencyP95Ms: 810 en vez de 740, ¿cambiaría el resultado final? ¿En qué etapa se detendría la rampa?
Ver solución
Sí cambiaría. Con 810 > 800, la condición de guardrailCheck() marcaría ese guardrail como roto en la etapa de ramp-50, y guardrailWatch() se detendría ahí con HALT — a pesar de que las dos etapas anteriores (canary en 670, ramp-10 en 705) habían pasado limpias. La etapa de full (100%) pasaría a not reached. Esto muestra que un arreglo técnico puede resolver el problema en las primeras etapas y, aun así, no ser suficiente para sostener el guardrail hasta el final — el arreglo cachea la llamada, pero si el efecto se degrada con más carga concurrente en 50%, el guardrail puede romperse ahí en vez de en 10%.
Ejercicio 2 — Argumenta contra el "el primario compensa". Un compañero de equipo te dice: "Ya sé que la latencia se rompió, pero el lift de conversión es +18.75%, así que vale la pena aguantar la latencia un poco más mientras seguimos subiendo". Usando el vocabulario de esta lección, ¿qué le contestarías en 2-3 frases?
Ver solución
Una respuesta razonable: "El techo de 800ms no lo pusimos al azar — lo definimos antes de lanzar, precisamente para no tener que negociar en medio del rollout si vale la pena aguantar más latencia a cambio de más conversión. Si ahora decidimos que sí vale la pena, lo que estamos haciendo es cambiar la regla después de romperla, no aplicarla. Sigamos el plan: frenamos aquí, diagnosticamos por qué la latencia subió tanto, y si con el arreglo técnico el guardrail se sostiene, retomamos la rampa — eso no cancela el lift de conversión, solo lo pospone hasta que podamos capturarlo sin el costo que ya sabemos que existe."
Ejercicio 3 — Define el criterio de halt para un caso nuevo. El equipo de logística de Mercado va a lanzar deliveryEtaV2 (el algoritmo mejorado de tiempos de entrega, ya mencionado en módulos anteriores) con su propia rampa y sus propios guardrails. Antes de que arranque el rollout, escribe, en una frase, el criterio de halt que deberían fijar de antemano —usando la misma regla anyBroken de guardrailWatch(), no una nueva—.
Ver solución
Un criterio razonable: "La rampa de deliveryEtaV2 se detiene, sin excepción, en la primera etapa donde guardrailWatch() reporte HALT para cualquiera de sus guardrails definidos —no hace falta que todos se rompan, ni que el equipo esté de acuerdo en el momento en que uno solo ya rompió su umbral—, y no se retoma hasta que la causa raíz de ese guardrail roto tenga un diagnóstico y, si aplica, un arreglo verificado en una corrida nueva." La clave del ejercicio es notar que el criterio no depende de qué feature es —recommendations o deliveryEtaV2—, porque guardrailWatch() y su regla anyBroken son genéricas; lo único que cambia entre features son los guardrails específicos y sus umbrales.
Resumen y siguiente paso
En esta lección corriste el mismo guardrailWatch(), sin cambiarle una línea, sobre dos escenarios: el real, donde la latencia rompe el guardrail en 10% y la rampa se detiene; y uno hipotético, donde un arreglo técnico previo mantiene la latencia bajo control en las cuatro etapas y la rampa llega completa a 100%. La comparación deja el argumento central sin ambigüedad: el mecanismo no tiene opinión sobre si recommendations es bueno o malo — solo aplica, con disciplina, el criterio que el equipo fijó antes de lanzar. Viste también por qué HALT no es lo mismo que cancelar, y por qué "el primario compensa" es, precisamente, el razonamiento que un guardrail con umbral fijo existe para prevenir.
Antes de avanzar deberías poder: explicar la diferencia entre frenar una rampa y cancelar un feature; argumentar, con el vocabulario de esta lección, contra la idea de que una métrica primaria fuerte "compra" el permiso de ignorar un guardrail roto; y anticipar por qué fijar el criterio de halt antes de lanzar evita una negociación difícil en medio del rollout.
La lección 5 toma este mismo resultado —HALT en 10%, con su detalle completo de qué guardrail rompió y por qué— y diseña cómo debería verse en un dashboard: qué información necesita estar visible de un vistazo para que nadie tenga que correr código a mano para saber en qué etapa está la rampa, ahora mismo.
Recursos
- Google SRE Book, Capítulo 4, "Service Level Objectives" — sre.google/sre-book/service-level-objectives. El capítulo que argumenta por qué los umbrales se fijan antes, como compromiso explícito, y no se renegocian según el resultado que ya se observó — la base de esta lección. En inglés.
- LaunchDarkly, "Introducing Guardrail Metrics: best-practice metrics for every release" — launchdarkly.com/blog/introducing-guardrail-metrics. Cómo un guarded rollout real pausa o revierte automáticamente ante una regresión detectada, sin esperar a que alguien decida "en caliente" si vale la pena seguir. En inglés.
- Ronny Kohavi, "Guardrail Metrics for A/B Tests" — linkedin.com/pulse/guardrail-metrics-ab-tests-ronny-kohavi. Kohavi insiste en que un guardrail roto no se "compensa" con una métrica primaria positiva — son preguntas distintas, la misma distinción que argumenta esta lección. En inglés.