Módulo 6: Postmortems And Iterating

Proyecto: escribe el postmortem de `recommendations` y planea su iteración

Descripción

Las siete lecciones de este módulo construyeron, pieza por pieza, todo lo que hace falta para cerrar el ciclo completo de aprendizaje de un incidente: cómo investigar el sistema sin culpar a nadie (L2), cómo estructurar esa investigación en timeline, factores contribuyentes y action items (L3), por qué el lenguaje decide si esa estructura cumple su propósito (L4), el marco del loop ship → measure → learn que le da sentido a todo esto (L5), cómo iterar en concreto sobre el ganador una vez arreglada la causa (L6), y cómo confirmar si ese ganador aguanta en el tiempo o era efecto novedad (L7). Este mini-proyecto te pone en la posición exacta del equipo de Mercado, semanas después del incidente: escribir el postmortem completo de la regresión de latencia de recommendations, y planear —con datos, no con esperanza— su iteración completa hasta la confirmación final.

Conexión con el módulo. Este proyecto no introduce ningún concepto nuevo: reutiliza buildPostmortem() tal como quedó en la lección 3, rolloutPlan() tal como quedó en el módulo 3 y se reusó en la lección 6, y noveltyCheck() tal como quedó en la lección 7 — las tres, sin cambiarles una línea. Es, literalmente, el ensayo del trabajo real que este módulo entero preparó: no memorizar que "los postmortems deben ser blameless" en abstracto, sino producir el documento completo y la evidencia de iteración sobre el caso concreto de Mercado.

Una analogía: el informe de investigación completo, de la caja negra a la próxima ruta aprobada

Un informe de investigación de aviación no termina en el análisis de la caja negra. Después de reconstruir la timeline y los factores sistémicos, el organismo de investigación produce recomendaciones de seguridad concretas —cambios de diseño, de procedimiento, de entrenamiento—, y solo cuando esas recomendaciones se implementan, la aeronave (o el modelo completo) vuelve a certificarse para vuelos con pasajeros. La certificación no es un trámite aparte del informe: es la consecuencia directa y verificable de que las recomendaciones se cumplieron y de que, en pruebas posteriores, el problema original ya no aparece.

Este proyecto sigue exactamente esa misma cadena completa: el postmortem de recommendations (la caja negra reconstruida), los action items que salen de él (las recomendaciones de seguridad), y la verificación de que, con esos cambios puestos, tanto el riesgo original (la latencia) como el beneficio que se buscaba (el lift de conversión) se sostienen en producción real — la "recertificación" completa antes de declarar el caso cerrado.

Parte 1 — El postmortem blameless completo

Reusando buildPostmortem() sin cambios, con el incidente completo de latencia:

// buildPostmortem: SIN cambios desde la leccion 3.
function buildPostmortem(incident) {
  const timeline = [...incident.events].sort((a, b) => a.time.localeCompare(b.time));
  const namedPeople = ['Ana', 'Bruno', 'Carla', 'Diego', 'Elena'];
  const factors = incident.contributingFactors.map((description) => {
    const redFlag = namedPeople.some((name) => description.includes(name));
    return { description, redFlag };
  });
  const blameless = factors.every((f) => !f.redFlag);
  return { incidentName: incident.name, timeline, contributingFactors: factors, actionItems: incident.actionItems, blameless };
}

const latencyIncident = {
  name: 'recommendations: p95Latency rompe el guardrail en la etapa de 10%',
  events: [
    { time: '14:20', what: 'Se declara el incidente (severidad Sev-2, siguiendo el runbook del modulo 5).' },
    { time: '13:58', what: 'El rollout avanza de canary (1%, limpio) a la etapa de 10%.' },
    { time: '16:30', what: 'Se identifica la causa tecnica: la llamada al motor de recomendaciones es sincrona/bloqueante y no escala al volumen de la etapa de 10%.' },
    { time: '14:12', what: 'guardrailWatch() marca HALT en la etapa de 10%: p95Latency=910ms, techo 800ms.' },
    { time: '16:42', what: 'Se cierra el incidente: rollbackDecision() confirma exposicion en 0% y el guardrail deja de estar en riesgo.' },
    { time: '14:15', what: 'El equipo activa el kill switch: recommendationsFlag.enabled pasa a false.' },
    { time: '14:45', what: 'Se confirma exposicion en 0%: ningun usuario nuevo ve variant desde el kill switch.' },
  ],
  contributingFactors: [
    'La llamada al motor de recomendaciones es sincrona y bloqueante; no estaba disenada para el volumen de trafico de la etapa de 10% del rollout.',
    'El criterio de avance de la rampa (guardrailWatch) no incluia una prueba de carga equivalente al volumen de la siguiente etapa antes de avanzar.',
    'No existia cache para las recomendaciones mas solicitadas, asi que cada request recalculaba el resultado completo del motor.',
  ],
  actionItems: [
    { owner: 'recommendations-team', action: 'Rediseñar la llamada al motor de recomendaciones en modo asincrono / no bloqueante.', due: '2026-08-08' },
    { owner: 'platform-sre', action: 'Agregar una prueba de carga equivalente al volumen de la siguiente etapa como parte del criterio de avance de guardrailWatch().', due: '2026-08-08' },
    { owner: 'recommendations-team', action: 'Implementar cache para las recomendaciones mas solicitadas.', due: '2026-08-15' },
  ],
};

const postmortem = buildPostmortem(latencyIncident);

console.log('=== PARTE 1: Postmortem -- ' + postmortem.incidentName + ' ===\n');
postmortem.timeline.forEach((e) => console.log(e.time + '  ' + e.what));
console.log('\nContributing factors:');
postmortem.contributingFactors.forEach((f, i) => console.log((i + 1) + '. [' + (f.redFlag ? 'RED FLAG' : 'blameless') + '] ' + f.description));
console.log('\nAction items:');
postmortem.actionItems.forEach((a, i) => console.log((i + 1) + '. (' + a.owner + ', vence ' + a.due + ') ' + a.action));
console.log('\nPostmortem blameless: ' + postmortem.blameless);

Parte 2 — Iterar: relanzar por la misma rampa

Con los tres action items ya cerrados, reusando rolloutPlan() sin cambios desde la lección 6:

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;
}

const ceiling = 800;
const relaunchPlan = [
  { percent: 0.01, label: 'canary 1%', measured: { p95Latency: 674 }, advanceIf: (m) => m.p95Latency <= ceiling },
  { percent: 0.10, label: 'rollout 10%', measured: { p95Latency: 738 }, advanceIf: (m) => m.p95Latency <= ceiling },
  { percent: 0.50, label: 'rollout 50%', measured: { p95Latency: 762 }, advanceIf: (m) => m.p95Latency <= ceiling },
  { percent: 1.00, label: 'rollout 100%', measured: { p95Latency: 781 }, advanceIf: (m) => m.p95Latency <= ceiling },
];

console.log('\n=== PARTE 2: re-lanzamiento por la rampa, despues de los 3 action items ===\n');
const relaunchResults = rolloutPlan(relaunchPlan);
relaunchResults.forEach((s) => {
  console.log(s.label.padEnd(14) + 'p95=' + String(s.measured.p95Latency).padStart(4) + 'ms  -> ' + s.decision);
});
console.log('\nLa rampa completa avanzo sin ningun HOLD: ' + relaunchResults.every((s) => s.decision === 'ADVANCE'));

Parte 3 — Medir de nuevo: ¿el lift aguanta o era novedad?

Reusando noveltyCheck() sin cambios desde la lección 7, sobre seis semanas de checkoutConversion medidas después del relanzamiento completo:

function noveltyCheck(weeklyLift) {
  const first = weeklyLift[0];
  const last = weeklyLift[weeklyLift.length - 1];
  const decayPct = Math.round(((first - last) / first) * 1000) / 10;
  const verdict = decayPct > 50 ? 'NOVELTY (se desvanece)' : 'HOLDS (se sostiene)';
  return { weeklyLift, first, last, decayPct, verdict };
}

// checkoutConversion lift medido semana a semana, YA con recommendations al
// 100% de la base (seis semanas de datos post re-lanzamiento).
const recommendationsPostRelaunchLift = [0.191, 0.183, 0.179, 0.185, 0.180, 0.182];

console.log('\n=== PARTE 3: noveltyCheck sobre el lift de checkoutConversion, 6 semanas post-relanzamiento ===\n');
console.log(noveltyCheck(recommendationsPostRelaunchLift));

Qué esperar. Al correr el archivo completo (las tres partes en secuencia) con Node, la salida es exactamente esta:

=== PARTE 1: Postmortem -- recommendations: p95Latency rompe el guardrail en la etapa de 10% ===

13:58  El rollout avanza de canary (1%, limpio) a la etapa de 10%.
14:12  guardrailWatch() marca HALT en la etapa de 10%: p95Latency=910ms, techo 800ms.
14:15  El equipo activa el kill switch: recommendationsFlag.enabled pasa a false.
14:20  Se declara el incidente (severidad Sev-2, siguiendo el runbook del modulo 5).
14:45  Se confirma exposicion en 0%: ningun usuario nuevo ve variant desde el kill switch.
16:30  Se identifica la causa tecnica: la llamada al motor de recomendaciones es sincrona/bloqueante y no escala al volumen de la etapa de 10%.
16:42  Se cierra el incidente: rollbackDecision() confirma exposicion en 0% y el guardrail deja de estar en riesgo.

Contributing factors:
1. [blameless] La llamada al motor de recomendaciones es sincrona y bloqueante; no estaba disenada para el volumen de trafico de la etapa de 10% del rollout.
2. [blameless] El criterio de avance de la rampa (guardrailWatch) no incluia una prueba de carga equivalente al volumen de la siguiente etapa antes de avanzar.
3. [blameless] No existia cache para las recomendaciones mas solicitadas, asi que cada request recalculaba el resultado completo del motor.

Action items:
1. (recommendations-team, vence 2026-08-08) Rediseñar la llamada al motor de recomendaciones en modo asincrono / no bloqueante.
2. (platform-sre, vence 2026-08-08) Agregar una prueba de carga equivalente al volumen de la siguiente etapa como parte del criterio de avance de guardrailWatch().
3. (recommendations-team, vence 2026-08-15) Implementar cache para las recomendaciones mas solicitadas.

Postmortem blameless: true

=== PARTE 2: re-lanzamiento por la rampa, despues de los 3 action items ===

canary 1%     p95= 674ms  -> ADVANCE
rollout 10%   p95= 738ms  -> ADVANCE
rollout 50%   p95= 762ms  -> ADVANCE
rollout 100%  p95= 781ms  -> ADVANCE

La rampa completa avanzo sin ningun HOLD: true

=== PARTE 3: noveltyCheck sobre el lift de checkoutConversion, 6 semanas post-relanzamiento ===

{
  weeklyLift: [ 0.191, 0.183, 0.179, 0.185, 0.18, 0.182 ],
  first: 0.191,
  last: 0.182,
  decayPct: 4.7,
  verdict: 'HOLDS (se sostiene)'
}

El resultado, en limpio

PartePreguntaResultado
1. Postmortem¿Qué pasó, por qué, y qué se hace al respecto?Timeline de 7 eventos (13:58-16:42), 3 factores contribuyentes —todos sistémicos—, 3 action items con dueño y fecha. blameless: true.
2. Relanzamiento¿El arreglo resolvió el guardrail?Las 4 etapas avanzan sin HOLD. p95Latency entre 674ms y 781ms, siempre bajo el techo de 800ms.
3. Medición¿El lift de conversión se sostiene?Lift semanal entre 17.9% y 19.1% durante 6 semanas, decayPct: 4.7. verdict: HOLDS (se sostiene).

Las tres partes juntas cuentan una historia completa y coherente: el incidente se investigó sin culpar a nadie, la causa identificada era real y sistémica —confirmado porque arreglarla resolvió el problema en producción, no solo en teoría—, y el beneficio que originalmente justificó lanzar recommendations —el +18.75% de conversión que product-metrics-and-experimentation-guide había medido con rigor sobre seis semanas— se reproduce, de forma estable, también después del relanzamiento al 100% de la base completa. Ninguna de las tres partes por sí sola cierra el caso: un postmortem sin verificación de que el fix funcionó es solo una teoría; una rampa que avanza sin verificar el lift solo confirma que el riesgo se contuvo, no que el beneficio se mantiene; y un lift medido una sola vez, sin las seis semanas, no distinguiría un ganador real de uno inflado por curiosidad inicial.

Errores comunes

Cerrar el postmortem sin conectar sus action items con ningún dato posterior que confirme que funcionaron. Qué pasa: el equipo escribe un postmortem impecable, con factores sistémicos y action items concretos, pero nunca vuelve a verificar —con datos, como en la Parte 2 de este proyecto— si esos action items de verdad resolvieron el problema. Por qué pasa: escribir el postmortem se siente como el trabajo "duro" ya hecho, y verificar su efectividad semanas después, cuando la atención del equipo ya se movió a otra prioridad, es fácil de posponer indefinidamente. Cómo detectarlo: si nadie puede mostrar un dato de latencia posterior al cierre de los action items —solo la promesa de que "ya se arregló"—, el postmortem generó un plan, pero no una confirmación. Cómo corregirlo: como en este proyecto, la Parte 2 no es opcional — un postmortem sin verificación posterior es, en el mejor de los casos, una hipótesis bien documentada, no un problema resuelto.

Confundir "la rampa avanzó limpia" con "el proyecto de iteración está completo". Qué pasa: al ver que las cuatro etapas de la Parte 2 pasan sin HOLD, el equipo reporta el caso como cerrado, sin llegar a medir el lift de conversión de la Parte 3. Por qué pasa: el guardrail (la latencia) es lo que causó el incidente original, así que resolverlo se siente, emocionalmente, como resolver "el" problema — aunque la razón de negocio para lanzar recommendations en primer lugar nunca fue la latencia, fue la conversión. Cómo detectarlo: si el reporte final de este proyecto no incluye ningún dato de checkoutConversion medido después del relanzamiento, la pregunta que dio origen a toda esta guía —¿vale la pena el ganador?— sigue sin contestar. Cómo corregirlo: las tres partes de este proyecto son secuenciales por una razón — el guardrail (Parte 2) protege contra el riesgo, pero solo la medición del lift (Parte 3) confirma el beneficio.

Reportar el resultado de noveltyCheck() sin mencionar que ya está alineado con el resultado original de la guía de métricas. Qué pasa: al presentar el decayPct: 4.7 y el veredicto HOLDS, alguien lo describe como un descubrimiento nuevo y sorprendente, sin conectar que ese resultado —lift estable alrededor de 18-19%— es exactamente lo que product-metrics-and-experimentation-guide ya había predicho al medir sobre seis semanas completas desde el principio. Por qué pasa: cada pieza de esta guía se vive, en el momento, como su propio descubrimiento, y es fácil no notar cuando un resultado nuevo simplemente confirma uno anterior en vez de revelar algo distinto. Cómo detectarlo: si el reporte final no menciona el +18.75% original como punto de comparación, se pierde la oportunidad de mostrar la coherencia completa del caso —desde el experimento hasta el relanzamiento—. Cómo corregirlo: como muestra la tabla de "El resultado, en limpio" de este proyecto, el lift post-relanzamiento (17.9%-19.1%) cae, semana a semana, dentro del rango del número original —una confirmación, no una sorpresa, y esa coherencia es, en sí misma, parte de la evidencia de que el ganador es real.

Ejercicios de transferencia

A diferencia de los ejercicios de las lecciones anteriores, este proyecto te pide aplicar el método completo a un caso que este módulo nunca vio — la prueba real de si aprendiste a cerrar el ciclo de postmortem-e-iteración, o solo memoraste el resultado de recommendations.

Ejercicio 1 — Escribe el postmortem de un incidente distinto. El equipo de logística de Mercado (del ejercicio 2 del proyecto del módulo 3) tiene un incidente propio: su algoritmo de tiempos de entrega estimados rompió el guardrail de errorRate (4%) en la etapa de rollout 10%, con errorRate: 5.2%. La causa identificada: el modelo de tiempos estimados no había sido probado con pedidos multi-paquete a ese volumen, y el criterio de avance de su rampa —copiado del de recommendations— no incluía una prueba específica para ese tipo de pedido. Escribe, en el formato de buildPostmortem() (events, contributingFactors, actionItems), el postmortem de este incidente, con al menos 2 factores contribuyentes y 2 action items, sin nombrar a ninguna persona.

Ver solución

Una versión razonable:

const logisticsIncident = {
  name: 'delivery-estimates: errorRate rompe el guardrail en rollout 10%',
  events: [
    { time: '10:00', what: 'El rollout del modelo de tiempos estimados avanza a la etapa de 10%.' },
    { time: '10:22', what: 'guardrailWatch() marca HALT: errorRate=5.2%, techo 4%.' },
    { time: '10:25', what: 'Se activa el kill switch del flag delivery-estimates.' },
    { time: '13:00', what: 'Se identifica la causa: el modelo no fue entrenado ni probado con pedidos multi-paquete a este volumen.' },
  ],
  contributingFactors: [
    'El modelo de tiempos estimados no incluia suficientes ejemplos de pedidos multi-paquete en sus datos de entrenamiento.',
    'El criterio de avance de la rampa, copiado del de recommendations, no incluia una prueba especifica para pedidos multi-paquete antes de avanzar de etapa.',
  ],
  actionItems: [
    { owner: 'logistics-team', action: 'Reentrenar el modelo con una muestra representativa de pedidos multi-paquete.', due: '2026-08-20' },
    { owner: 'platform-sre', action: 'Agregar una prueba de segmento (multi-paquete) al criterio de avance de la rampa de delivery-estimates.', due: '2026-08-20' },
  ],
};

Ningún factor nombra a una persona —ambos apuntan a los datos de entrenamiento y al criterio de avance—, así que buildPostmortem() lo marcaría blameless: true, exactamente como el de recommendations.

Ejercicio 2 — Simula la iteración del equipo de logística. Con el postmortem del ejercicio 1 ya resuelto, el equipo de logística relanza su rampa. Los datos medidos: canary 1% → errorRate 0.021, rollout 10% → errorRate 0.033, rollout 50% → errorRate 0.038, rollout 100% → errorRate 0.041. Con ceiling: 0.04 (4%), simula rolloutPlan() mentalmente (o en Node) y reporta en qué etapa, si acaso, se detiene.

Ver solución

canary 1% (0.021 ≤ 0.04) → ADVANCE. rollout 10% (0.033 ≤ 0.04) → ADVANCE. rollout 50% (0.038 ≤ 0.04) → ADVANCE, aunque con un margen más ajustado (0.002 de distancia al techo). rollout 100% (0.041 ≤ 0.04) → HOLD, porque 0.041 supera el techo de 0.04. A diferencia de recommendations, cuyo relanzamiento pasó limpio las cuatro etapas, este relanzamiento de logística se detiene justo en la última etapa — una señal de que, aunque el reentrenamiento del modelo mejoró mucho el errorRate respecto al incidente original (de 5.2% a 4.1% en el peor caso), el arreglo todavía no es suficiente para el volumen completo de 100%, y el equipo necesitaría investigar más antes de declarar la iteración completa.

Ejercicio 3 — Redacta el reporte ejecutivo final. Escribe el mensaje (120-180 palabras) que el equipo de recommendations enviaría al equipo ejecutivo de Mercado, cerrando el caso completo: el incidente original, el postmortem, el relanzamiento, y la confirmación de que el lift se sostiene. Incluye los números clave de las tres partes de este proyecto.

Ver solución

Un mensaje posible: "Cerramos el caso completo de recommendations. El incidente de latencia del [fecha] (p95 a 910ms, techo 800ms) se documentó en un postmortem blameless: tres causas sistémicas —una llamada bloqueante al motor de recomendaciones, falta de cache, y un criterio de avance sin prueba de carga—, cada una con su action item, dueño y fecha. Con los tres arreglos implementados, relanzamos por la misma rampa del rollout original: las cuatro etapas avanzaron limpias, con p95Latency entre 674ms y 781ms, siempre bajo el techo. Más importante todavía: medimos checkoutConversion durante seis semanas después del relanzamiento al 100% de la base, y el lift se mantuvo estable entre 17.9% y 19.1% —una caída de apenas 4.7% desde la primera hasta la última semana—, confirmando que el resultado no era efecto novedad, sino el mismo +18.75% real que el experimento original ya había medido. recommendations está en producción completa, con su beneficio de negocio confirmado dos veces." El mensaje conecta las tres partes del proyecto y cierra explícitamente la pregunta de novedad, sin dejar ambigüedad sobre el estado final del caso.

Resumen y siguiente paso

En este mini-proyecto cerraste el ciclo completo de aprendizaje sobre el incidente de recommendations: escribiste el postmortem blameless con buildPostmortem() —timeline de 7 eventos, 3 factores sistémicos, 3 action items, todo verificado como blameless—, confirmaste con rolloutPlan() que el relanzamiento por la misma rampa pasa las cuatro etapas sin un solo HOLD, y confirmaste con noveltyCheck() que el lift de conversión se sostiene, estable, durante seis semanas después del relanzamiento —un decayPct de apenas 4.7%, muy lejos del umbral de novedad—.

Con esto cierras el módulo 6: tienes la disciplina completa del postmortem blameless (qué es, cómo se estructura, por qué el lenguaje importa) y del loop ship → measure → learn (por qué lanzar es el comienzo, cómo iterar sobre un ganador, cómo confirmar que se sostiene), aplicada de principio a fin sobre el caso completo de Mercado.

Hacia dónde sigues. El módulo 7 aplica este mismo marco de disciplina —medir antes de confiar, iterar con evidencia— a la capa moderna de enviar modelos de IA: migrar un modelo con shadow mode, midiendo su tasa de acuerdo con el modelo anterior antes de cambiarlo, y el postmortem específico de un incidente de IA. El módulo 8, el capstone de toda la guía, te va a pedir ejecutar las siete piezas completas —flag, rollout, monitoreo, rollback, postmortem, iteración, y migración de modelo— sobre este mismo lanzamiento de recommendations, cerrando el ecosistema completo de Product Engineering.

Recursos

  • Google SRE Book, Capítulo 15, "Postmortem Culture: Learning from Failure" — sre.google/sre-book/postmortem-culture. La referencia formal completa de la práctica que la Parte 1 de este proyecto ejecutó de principio a fin. En inglés.
  • Eric Ries, The Lean Startuptheleanstartup.com/principles. El loop build-measure-learn original, la base conceptual de las Partes 2 y 3 de este proyecto: iterar y volver a medir, no lanzar y olvidar. En inglés.
  • Ron Kohavi y Stefan Thomke, "The Surprising Power of Online Experiments" (Harvard Business Review) — hbr.org/2017/09/the-surprising-power-of-online-experiments. Contexto adicional sobre por qué confirmar un resultado en el tiempo —como hace la Parte 3 de este proyecto— es una práctica estándar de la industria, no un paso opcional. En inglés.