Módulo 4: Monitoring The Launch

Proyecto: vigila el rollout de recommendations y decide frenar

Descripción

Las siete lecciones de este módulo construyeron, pieza por pieza, la disciplina completa de vigilar un lanzamiento: qué señales avisan rápido y cuáles tardan (L2), guardrailWatch() corriendo la rampa etapa por etapa (L3), la diferencia entre frenar y cancelar —y por qué "el primario gana" no compra el permiso de ignorar un guardrail roto (L4)—, la cabecera de un dashboard que hace visible el estado de un vistazo (L5), el criterio para que una alerta valga la pena sonar (L6), y el error budget que mide, en milisegundos, cuánto margen quedaba (L7). Este mini-proyecto te pide reunir la pieza central —guardrailWatch(), sin cambiarle una sola línea— sobre el caso completo, y producir el reporte formal que el equipo de Mercado necesita antes de tocar el rollout de nuevo.

Conexión con el módulo. Este proyecto no introduce ninguna función nueva: reutiliza guardrailCheck() y guardrailWatch() exactamente como quedaron en la lección 3, sin ningún cambio, exactamente como el proyecto del módulo 2 reutilizó isEnabled() sin tocarla. Lo que agrega es la disciplina de un reporte completo —vigilar cada etapa alcanzada, confirmar dónde se detuvo la rampa, y traducir el resultado a una decisión formal de frenar— antes de que el equipo considere que el trabajo de monitoreo de este módulo está cerrado sobre el caso real que arrastra toda la guía.

Una analogía: la bitácora del vuelo, cerrada al final del tramo

Un piloto no solo reacciona a las alarmas mientras vuela — al final de cada tramo, cierra una bitácora formal: qué altitud se alcanzó, qué sistemas se revisaron, si algo se salió de rango y en qué punto exacto. Esa bitácora no es un trámite burocrático: es el registro que le permite a la torre de control, y al siguiente turno de la tripulación, entender exactamente qué pasó sin tener que repetir el vuelo para averiguarlo. Este proyecto es esa bitácora, cerrada sobre el tramo real que voló recommendations: desde canary hasta el punto exacto donde el guardrail rompió, con el detalle completo de qué se midió en cada etapa alcanzada.

El reporte completo, paso a paso

Parte 1 — Vigilar la rampa completa con guardrailWatch()

Reutilizamos guardrailCheck() y guardrailWatch() exactamente como quedaron en la lección 3, corriéndolas sobre las cuatro etapas de la rampa del módulo 3, con los datos reales que se midieron en canary y en 10%.

Parte 2 — Confirmar en qué etapa se detuvo, y por qué guardrail

A partir del resultado de la Parte 1, extraemos la etapa exacta donde ocurrió el HALT y el guardrail responsable, con su detalle completo (antes, después, umbral).

Parte 3 — Calcular el error budget consumido en esa etapa

Reutilizamos errorBudgetTracker() de la lección 7 sobre la misma etapa, para expresar en milisegundos concretos cuánto se excedió el presupuesto de latencia.

Parte 4 — La decisión formal: frenar en 10%, no seguir a 50%

Juntamos las tres partes anteriores en una decisión explícita, con la evidencia que la sostiene.

// PROYECTO: vigilar el rollout de recommendations de Mercado etapa por etapa
// y decidir formalmente frenar en 10%, en vez de seguir a 50%. Reutiliza
// guardrailCheck(), guardrailWatch() y errorBudgetTracker() EXACTAMENTE como
// quedaron en las lecciones 3 y 7, sin ningun cambio.

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, results: check.results });
      halted = true;
    } else {
      log.push({ stage: stage.name, percent: stage.percent, status: 'continue', results: check.results });
    }
  }
  return { log, halted };
}

function errorBudgetTracker(baselineValue, ceiling, stages) {
  const totalBudgetMs = ceiling - baselineValue;
  return stages.map((s) => {
    const consumedMs = s.value - baselineValue;
    const remainingMs = totalBudgetMs - consumedMs;
    const pctConsumed = (consumedMs / totalBudgetMs) * 100;
    return {
      stage: s.name, percent: s.percent, value: s.value, consumedMs, remainingMs, pctConsumed,
      status: remainingMs < 0 ? 'EXHAUSTED' : 'within budget',
    };
  });
}

// Parte 1: vigilar la rampa completa
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 },
];
const rampStages = [
  { name: 'canary', percent: 1, metrics: { checkoutLatencyP95Ms: 680, complaintRate: 0.012, churnRate: 0.045, grossMargin: 0.220 } },
  { name: 'ramp-10', percent: 10, metrics: { checkoutLatencyP95Ms: 910, complaintRate: 0.013, churnRate: 0.045, grossMargin: 0.219 } },
  { name: 'ramp-50', percent: 50, metrics: null },
  { name: 'full', percent: 100, metrics: null },
];

console.log('=== Parte 1: guardrailWatch sobre la rampa completa ===\n');
const watch = guardrailWatch(rampStages, guardrails, baseline);
watch.log.forEach((entry) => {
  console.log(entry.stage + ' (' + entry.percent + '%): ' + entry.status);
});

// Parte 2: la etapa y el guardrail responsable del HALT
console.log('\n=== Parte 2: donde se detuvo la rampa, y por que ===\n');
const haltEntry = watch.log.find((e) => e.status === 'HALT');
const brokenDetail = haltEntry.results.find((r) => r.broken);
console.log('Etapa del HALT: ' + haltEntry.stage + ' (' + haltEntry.percent + '%)');
console.log('Guardrail roto: ' + brokenDetail.metric + '  ' + brokenDetail.before + ' -> ' + brokenDetail.after + '  (' + brokenDetail.detail + ')');
console.log('Etapas nunca alcanzadas: ' + watch.log.filter((e) => e.status.startsWith('not reached')).map((e) => e.stage).join(', '));

// Parte 3: el error budget consumido en esa etapa
console.log('\n=== Parte 3: error budget consumido en la etapa del HALT ===\n');
const budget = errorBudgetTracker(650, 800, [{ name: haltEntry.stage, percent: haltEntry.percent, value: brokenDetail.after }]);
const b = budget[0];
console.log(b.stage + ' (' + b.percent + '%): consumido=' + b.consumedMs + 'ms (' + b.pctConsumed.toFixed(1) +
  '% del presupuesto de 150ms) | restante=' + b.remainingMs + 'ms | ' + b.status);

// Parte 4: la decision formal
console.log('\n=== Parte 4: decision formal ===\n');
console.log('Rampa detenida: ' + watch.halted);
console.log('Decision: FRENAR en ' + haltEntry.percent + '%. NO avanzar a 50% ni a 100% hasta diagnosticar y resolver ' + brokenDetail.metric + '.');

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

=== Parte 1: guardrailWatch sobre la rampa completa ===

canary (1%): continue
ramp-10 (10%): HALT
ramp-50 (50%): not reached (ramp halted earlier)
full (100%): not reached (ramp halted earlier)

=== Parte 2: donde se detuvo la rampa, y por que ===

Etapa del HALT: ramp-10 (10%)
Guardrail roto: checkoutLatencyP95Ms  650 -> 910  (910 vs techo 800)
Etapas nunca alcanzadas: ramp-50, full

=== Parte 3: error budget consumido en la etapa del HALT ===

ramp-10 (10%): consumido=260ms (173.3% del presupuesto de 150ms) | restante=-110ms | EXHAUSTED

=== Parte 4: decision formal ===

Rampa detenida: true
Decision: FRENAR en 10%. NO avanzar a 50% ni a 100% hasta diagnosticar y resolver checkoutLatencyP95Ms.

Repasa las cuatro partes con lo que cada una confirma. La Parte 1 muestra la rampa completa vigilada en orden: canary pasa limpio, ramp-10 rompe, y las dos etapas siguientes ni siquiera se alcanzan — el rollout, en la práctica, nunca llegó a exponer a la mitad ni a la totalidad de la base de compradores de Mercado. La Parte 2 aísla el detalle exacto: el guardrail responsable es checkoutLatencyP95Ms, con la latencia saltando de 650ms a 910ms contra un techo de 800 — el mismo número, sin ninguna diferencia, que la guía de métricas ya había reportado como el resultado final del experimento. La Parte 3 traduce ese mismo dato a un saldo: 260ms consumidos contra un presupuesto de 150, un 173.3% de sobregasto, con -110ms de saldo negativo — la magnitud que justifica tratar esto como un problema de rediseño, no de ajuste fino. La Parte 4 cierra con la decisión que las tres partes anteriores sostienen: frenar en 10%, sin avanzar a 50% ni a 100%, hasta que la causa raíz de la latencia tenga un diagnóstico y un arreglo verificado.

La decisión, en limpio

VerificaciónResultado
Etapa donde se detuvo el rolloutramp-10 (10% de la base)
Guardrail que activó el HALTcheckoutLatencyP95Ms: 650ms → 910ms (techo 800ms)
Otros tres guardrails en esa misma etapacomplaintRate, churnRate, grossMargin — los tres OK
Presupuesto de error consumido173.3% de 150ms (260ms gastados, -110ms de saldo)
Etapas nunca alcanzadasramp-50 (50%), full (100%)
Decisión formalFrenar en 10%. No avanzar hasta resolver el guardrail de latencia.

Fíjate en que la tabla no dice "cancelar recommendations" en ningún lado — dice, con precisión, "frenar en 10%", que es exactamente lo que las siete lecciones de este módulo enseñaron a distinguir de una cancelación. La ganancia en checkoutConversionRate (+18.75%, p=0.0114) sigue siendo real y significativa; lo que este proyecto confirma es que el camino hacia el 100% de esa ganancia tiene que pasar, primero, por resolver un problema técnico ya identificado con precisión —no por ignorarlo mientras la rampa sigue subiendo.

Errores comunes

Reportar solo "el guardrail se rompió", sin la etapa ni el margen exacto. Qué pasa: alguien resume el resultado del proyecto como "la latencia falló", sin especificar que ocurrió en la etapa de 10% (no en canary, no al 100%), ni cuánto se excedió el presupuesto. Por qué pasa: "falló" se siente como toda la información necesaria, y el detalle de la etapa y la magnitud parece un añadido opcional. Cómo detectarlo: si el reporte no puede contestar "¿cuántos compradores estuvieron expuestos cuando se detuvo?" ni "¿por cuánto se excedió el límite?", falta la mitad de la evidencia que sostiene la decisión. Cómo corregirlo: como en la Parte 2 y la Parte 3 de este proyecto, el reporte completo necesita la etapa exacta, el guardrail responsable con sus tres números (antes, después, umbral), y el error budget consumido — no solo la palabra "ROTO".

Recalcular el error budget para las etapas que nunca se alcanzaron. Qué pasa: alguien, queriendo "completar" el reporte, proyecta cuánto se habría consumido en ramp-50 y full extrapolando la tendencia de las dos etapas medidas. Por qué pasa: dejar esas dos filas vacías se siente incompleto, y proyectar una tendencia parece más informativo que un simple "no medido". Cómo detectarlo: si el reporte incluye números de error budget para etapas marcadas como not reached, esos números son una proyección disfrazada de dato real. Cómo corregirlo: como confirmó la lección 3, not reached es información valiosa por sí sola — el reporte de este proyecto calcula el error budget únicamente sobre la etapa donde de verdad se midió algo, y no inventa datos para las etapas que la disciplina de guardrailWatch() correctamente nunca alcanzó.

Dar por cerrado el monitoreo de este módulo sin haber conectado el resultado con el módulo 5. Qué pasa: el equipo confirma la decisión de frenar en 10% y considera que el trabajo terminó ahí, sin haber articulado todavía qué sigue —¿se revierte el 10% ya expuesto? ¿se deja mientras se arregla?—. Por qué pasa: la decisión de frenar se siente como el final de la historia, porque es la pregunta que este módulo se propuso contestar. Cómo detectarlo: si nadie puede decir "y ahora, ¿qué hacemos con los compradores que ya están en variant?", el trabajo de este módulo terminó bien, pero el ciclo completo del lanzamiento todavía no. Cómo corregirlo: este proyecto entrega, con precisión, la decisión de frenar y la evidencia que la sostiene — la decisión de revertir ese 10% o arreglar hacia adelante mientras se queda expuesto, con su runbook y su proceso completo, es exactamente el trabajo del módulo 5, que empieza justo donde este proyecto se detiene.

Ejercicios de transferencia

A diferencia de los ejercicios de las lecciones anteriores, este proyecto te pide aplicar el mecanismo completo a un caso que este módulo nunca vio — la prueba real de si aprendiste a vigilar y decidir, o solo memoraste el resultado de recommendations.

Ejercicio 1 — Corre el proyecto sobre deliveryEtaV2. El equipo de logística de Mercado lanza deliveryEtaV2 con su propia rampa y sus propios guardrails: línea base deliveryLatencyP95Ms: 400, techo 600; los otros tres guardrails de negocio (complaintRate, churnRate, grossMargin) se mantienen sin cambios en cada etapa. En canary, deliveryLatencyP95Ms mide 420ms; en 10%, mide 580ms. Usando el mismo patrón de este proyecto (Partes 1 a 4), ¿la rampa se detiene, y en qué etapa?

Ver solución

No se detiene en ninguna de las dos etapas medidas: en canary, 420 > 600 es false (continue); en 10%, 580 > 600 también es false (continue). Con solo estas dos etapas con datos, guardrailWatch() reportaría continue en ambas, y halted: false — la rampa seguiría avanzando, a diferencia del caso de recommendations. El error budget en 10% sería consumedMs: 580 - 400 = 180ms contra un presupuesto total de 600 - 400 = 200ms, es decir, 90% consumido —dentro del presupuesto, pero con un margen ya estrecho (remainingMs: 20)—, una señal de que, aunque el guardrail no se rompió todavía, vale la pena vigilar de cerca la siguiente etapa antes de asumir que el margen se sostiene.

Ejercicio 2 — Diseña la Parte 5: el mensaje al equipo ejecutivo. Escribe el mensaje (100-150 palabras) que le enviarías al equipo ejecutivo de Mercado, reportando la decisión de frenar el rollout de recommendations en 10%. Incluye: dónde se detuvo, qué guardrail se rompió y por cuánto, que la métrica primaria sigue siendo positiva, y qué sigue (sin explicar todavía el proceso completo de rollback, solo nombrando que existe).

Ver solución

Un mensaje posible: "El rollout de recommendations se detuvo, como estaba planeado, en la etapa de 10% de nuestra base de compradores. La razón: la latencia de checkout (p95) subió de 650ms a 910ms, superando el techo de 800ms que definimos antes de lanzar — un exceso de 260ms contra un presupuesto de 150, más del triple de lo que teníamos disponible. Los otros tres guardrails que vigilamos —quejas, retención, márgenes— se mantienen sanos. Es importante notar que esto no invalida el resultado del experimento: la conversión de checkout sigue subiendo un 18.75%, con significancia estadística confirmada. Lo que hicimos fue frenar antes de exponer a más gente a un problema de latencia ya identificado, mientras el equipo de ingeniería diagnostica la causa técnica. En los próximos días vamos a decidir formalmente si revertimos el 10% ya expuesto o lo mantenemos mientras se resuelve — esa decisión, con su proceso completo, es el siguiente paso." El mensaje separa con claridad la buena noticia (conversión) de la mala (latencia), da los números exactos de ambas, y deja explícito que la decisión de revertir o no es un paso siguiente, todavía sin resolver.

Ejercicio 3 — Predicción sobre el módulo 5. Sin haber leído todavía el módulo 5, y basándote en todo lo que construiste en este proyecto, ¿qué información de este reporte crees que el módulo 5 va a necesitar como punto de partida para decidir entre revertir y arreglar hacia adelante?

Ver solución

Probablemente el módulo 5 va a necesitar, como mínimo, tres cosas de este reporte: la etapa exacta donde se detuvo (10%, no canary ni 50%, porque eso determina cuánta gente sigue expuesta ahora mismo mientras se decide), el guardrail responsable con su magnitud (checkoutLatencyP95Ms, 260ms de sobregasto, no un exceso menor), y la confirmación de que el resultado primario sigue siendo positivo (para descartar la opción más drástica de cancelar el feature por completo). Con esos tres datos, el módulo 5 puede evaluar si el problema es lo bastante grave y lo bastante lento de resolver como para justificar un rollback completo del 10% ya expuesto, o si conviene dejarlo expuesto mientras ingeniería aplica un arreglo —como el escenario hipotético de caché que ya viste en la lección 4— y reintenta la rampa desde ahí.

Resumen y siguiente paso

En este mini-proyecto vigilaste el rollout completo de recommendations con guardrailWatch(), sin cambiarle una línea respecto a la lección 3: la rampa avanza limpia en canary (680ms, dentro del techo de 800), se detiene con HALT en la etapa de 10% (910ms, 260ms de sobregasto contra un presupuesto de 150), y nunca alcanza las etapas de 50% ni 100%. Confirmaste, con errorBudgetTracker() reutilizado de la lección 7, que el sobregasto fue de 173.3% del presupuesto —no un exceso marginal—, y cerraste con una decisión formal: frenar en 10%, sin cancelar el feature, mientras se diagnostica la causa técnica. Con esto cierras el módulo 4: tienes, en código ejecutado y verificado, la disciplina completa de vigilar una rampa en vivo y saber, con evidencia y no con intuición, exactamente dónde detenerse.

Hacia dónde sigues. El módulo 5 toma esta misma decisión de frenar —el mismo HALT en 10%, el mismo guardrail roto— y contesta la pregunta que este proyecto dejó abierta a propósito: ¿se revierte el 10% de compradores que ya está expuesto a recommendations, o se arregla hacia adelante mientras siguen ahí? Vas a construir rollbackDecision(), el runbook, y el proceso completo de incident response —severidad, mitigación, resolución—. El módulo 6 cierra el ciclo con el postmortem blameless de esta misma regresión de latencia, y el loop ship → measure → learn. El módulo 7 aplica todo este marco —guardrails, monitoreo en vivo, decisión de frenar— a la capa moderna de migrar modelos de IA con shadow mode. El módulo 8, el capstone, junta las siete piezas de la guía completa sobre este mismo caso de recommendations, de principio a fin.

Recursos

  • Google SRE Book, Capítulo 6, "Monitoring Distributed Systems" — sre.google/sre-book/monitoring-distributed-systems. El capítulo de referencia sobre vigilancia sistemática con los cuatro golden signals, aplicado en este proyecto al reporte final de la rampa completa. En inglés.
  • Google SRE Book, Capítulo 3, "Embracing Risk" — sre.google/sre-book/embracing-risk. El capítulo sobre error budgets que sostiene la Parte 3 de este proyecto: cuánto margen quedaba, y cuánto se excedió. En inglés.
  • LaunchDarkly, "Introducing Guardrail Metrics: best-practice metrics for every release" — launchdarkly.com/blog/introducing-guardrail-metrics. Cierra el módulo donde lo abrió la lección 1: cómo la industria automatiza, en plataformas reales, exactamente la disciplina de vigilar y frenar que este proyecto ejecutó a mano. En inglés.