Módulo 6: Postmortems And Iterating

Timeline, contributing factors y action items: la estructura de un postmortem

Descripción

La lección anterior instaló el principio —investiga el sistema, no a la persona— pero un principio, por sí solo, no produce un documento útil. Esta lección construye la pieza central del módulo: buildPostmortem(), la función que toma la información cruda de un incidente —eventos con su timestamp, factores contribuyentes en texto libre, action items propuestos— y la organiza en la estructura canónica de un postmortem: timeline ordenado cronológicamente, contributing factors (con el chequeo blameless de la lección 2 ya incorporado), y action items con dueño y fecha. La misma función, además, verifica el resultado completo: si algún factor contribuyente nombra a una persona, el postmortem entero se marca como no blameless.

Conexión con el módulo. Esta es la función que la lección 4 va a reutilizar sin cambiarle una línea —el mismo patrón que ya viste en módulos anteriores de esta guía, donde isEnabled(), guardrailCheck() o rolloutPlan() se construyen una vez y se reusan en las lecciones siguientes—. El proyecto de la lección 8 va a correr esta misma función sobre el incidente completo de recommendations, sin ninguna modificación.

Una analogía: la caja negra, ahora con las tres cintas juntas

La analogía de la lección anterior —la caja negra del avión— tenía dos grabadoras: la de datos de vuelo (qué hizo el sistema, segundo a segundo) y la de voces de cabina (qué dijo y decidió la tripulación). Un informe de investigación de aviación no reporta esas dos cintas por separado, ni las reporta sin orden: las sincroniza en una sola línea de tiempo, agrega por qué —los factores que el sistema y el entrenamiento permitieron— y termina con qué cambia —las recomendaciones de seguridad, con un organismo responsable de implementarlas y un plazo—. Sin esas tres piezas juntas, la caja negra es solo un montón de datos; con ellas, es un documento que otro piloto, en otra cabina, puede usar para no repetir el mismo incidente.

buildPostmortem() hace exactamente esa síntesis con el incidente de recommendations: toma los eventos sueltos (la caja negra), los ordena en una timeline legible, agrega los factores contribuyentes con su chequeo blameless, y cierra con los action items —las "recomendaciones de seguridad" del equipo de Mercado, cada una con su dueño y su fecha.

Ejemplo trabajado: buildPostmortem() sobre el incidente real de recommendations

// buildPostmortem: dada la timeline cruda de un incidente (eventos con
// timestamp, sin ordenar) y sus factores contribuyentes, arma un postmortem
// estructurado -- timeline ordenado + contributing factors + action items --
// y marca CUALQUIER factor que nombre a una persona especifica (en vez de un
// sistema o proceso) como red flag: la senal de que el postmortem dejo de
// ser blameless. Modelo pedagogico, no un template completo de postmortem.
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,
  };
}

// El incidente real: la regresion de latencia de recommendations, tal como
// quedo en el modulo 5. Los eventos NO estan en orden a proposito -- eso es
// justo lo que buildPostmortem() tiene que resolver.
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('=== Postmortem: ' + postmortem.incidentName + ' ===\n');
console.log('--- Timeline (ordenada) ---');
postmortem.timeline.forEach((e) => console.log(e.time + '  ' + e.what));

console.log('\n--- Contributing factors ---');
postmortem.contributingFactors.forEach((f, i) => console.log((i + 1) + '. [' + (f.redFlag ? 'RED FLAG' : 'blameless') + '] ' + f.description));

console.log('\n--- Action items ---');
postmortem.actionItems.forEach((a, i) => console.log((i + 1) + '. (' + a.owner + ', vence ' + a.due + ') ' + a.action));

console.log('\nPostmortem blameless: ' + postmortem.blameless);

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

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

--- Timeline (ordenada) ---
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

Lee las tres secciones en el orden en que buildPostmortem() las produce, porque ese orden es la lógica del postmortem. La timeline reconstruye, cronológicamente, exactamente lo que pasó —desde el avance a 10% a las 13:58 hasta el cierre del incidente a las 16:42—, sin interpretación todavía, solo hechos ordenados. Los contributing factors dan un paso más: explican por qué la timeline se desarrolló así —tres causas técnicas y de proceso, ninguna de las cuales nombra a una persona, así que el chequeo automático de la lección 2 les da blameless a las tres—. Los action items cierran el ciclo: cada factor contribuyente tiene, al menos, una acción concreta que lo ataca, con un dueño (recommendations-team, platform-sre) y una fecha (2026-08-08, 2026-08-15), no una intención vaga de "tener más cuidado".

Profundización: por qué el orden timeline → factores → action items no es arbitrario

Vale la pena notar que estas tres secciones no podrían escribirse en cualquier otro orden sin perder rigor. Si un equipo intentara escribir los action items primero —"vamos a rediseñar la llamada al motor de recomendaciones"— sin haber reconstruido antes la timeline completa y los factores contribuyentes, correría el riesgo de arreglar un síntoma sin haber confirmado la causa real: quizás el problema no era la llamada bloqueante, sino la falta de cache, o el criterio de avance sin prueba de carga —o los tres a la vez, como efectivamente pasó aquí—. La timeline y los factores contribuyentes son la evidencia que justifica cada action item; sin ellos, un action item es una corazonada disfrazada de plan.

Fíjate también en algo que la salida de este ejemplo muestra con claridad: tres factores contribuyentes producen tres action items, cada uno atacando directamente uno de los factores. Esto no es casualidad ni un requisito rígido de buildPostmortem() —la función acepta cualquier cantidad de cada uno—, pero sí es una buena práctica que vale la pena imitar: un postmortem que identifica cinco factores contribuyentes y produce un solo action item vago probablemente no atacó las otras cuatro causas, aunque las haya nombrado.

Errores comunes

Escribir los contributing factors sin conectarlos con ningún evento específico de la timeline. Qué pasa: el postmortem lista causas generales —"falta de pruebas de carga", "arquitectura no escalable"— sin que ninguna se pueda rastrear a un momento concreto de la timeline que la confirme. Por qué pasa: es más rápido escribir causas en abstracto que revisar la timeline completa buscando qué evento específico revela cada causa. Cómo detectarlo: si preguntas "¿en qué momento de la timeline se ve esto?" sobre un factor contribuyente y nadie puede señalar un evento concreto, el factor podría ser una suposición, no una causa confirmada por la evidencia. Cómo corregirlo: como en el ejemplo de hoy, cada factor contribuyente debería poder conectarse con al menos un evento de la timeline —el factor de la llamada bloqueante se confirma directamente en el evento de las 16:30, no es una teoría suelta.

Dejar un action item sin dueño o sin fecha, "para definir después". Qué pasa: el postmortem termina con una lista de buenas intenciones —"deberíamos agregar cache", "convendría revisar el criterio de avance"— sin que ninguna tenga asignado quién la hace ni cuándo. Por qué pasa: en el momento de cerrar el postmortem, después de una investigación larga, es tentador dejar los detalles de ejecución para "una reunión de seguimiento" que muchas veces nunca ocurre. Cómo detectarlo: si un action item no tiene un dueño con nombre de equipo (no de persona individual, para mantenerlo blameless) y una fecha concreta, es, en la práctica, indistinguible de no haberlo escrito. Cómo corregirlo: como en buildPostmortem(), cada actionItem tiene owner y due como campos obligatorios de la estructura — un postmortem que no puede llenar esos dos campos para un action item probablemente todavía no terminó de investigar la causa lo suficiente como para saber qué hacer al respecto.

Confundir "muchos factores contribuyentes" con "el incidente fue culpa de nadie en particular, así que no hace falta profundizar en ninguno". Qué pasa: al ver que hay tres causas distintas —la llamada bloqueante, el criterio de avance, la falta de cache—, alguien concluye que como "hay muchas causas", ninguna es lo suficientemente importante como para atacarla a fondo, y los action items terminan siendo superficiales en los tres frentes. Por qué pasa: la responsabilidad distribuida entre varias causas se siente, erróneamente, como responsabilidad diluida — como si tres causas "menores" sumaran menos urgencia que una causa "grande". Cómo detectarlo: si los tres action items proponen arreglos parciales o de bajo esfuerzo para las tres causas, en vez de un arreglo real para cada una, la investigación se quedó corta. Cómo corregirlo: un sistema que falla por la combinación de tres factores —ninguno suficiente por sí solo, pero los tres juntos sí— necesita que los tres se arreglen, no una versión diluida de cada uno. Los tres action items de este ejemplo atacan, cada uno, una causa completa, no un parche superficial.

Ejercicios

Ejercicio 1 — Agrega un evento a la timeline. El equipo de Mercado descubre que, a las 15:30, alguien del equipo de soporte al cliente reportó "algunos compradores mencionan que la página tarda en cargar" — un reporte que llegó antes de que guardrailWatch() confirmara el HALT a las 14:12... espera, ¿eso tiene sentido? Revisa las horas con cuidado y explica qué error de continuidad tendría agregar este evento tal como está descrito.

Ver solución

El evento tiene un error de continuidad: dice que llegó "antes de que guardrailWatch() confirmara el HALT a las 14:12", pero el timestamp que le dieron es 15:30, que es después de las 14:12, no antes. Si el evento realmente ocurrió a las 15:30, buildPostmortem() lo ordenaría correctamente entre el evento de las 14:45 y el de las 16:30 —no antes del HALT—, sin importar lo que diga la descripción en texto. Este ejercicio es un recordatorio importante: buildPostmortem() ordena por el campo time, no por el orden en que alguien escribe o cuenta los eventos — si la descripción en prosa de un evento no coincide con su propio timestamp, hay que corregir el dato, no la función.

Ejercicio 2 — Detecta el red flag. Un compañero de equipo propone agregar este contributing factor al postmortem: "Carla desactivo temporalmente la alerta de latencia el dia anterior para probar un cambio no relacionado, y se le olvido reactivarla." Corre mentalmente el chequeo de buildPostmortem() sobre esta frase. ¿Qué devolvería, y cómo la reescribirías para que pase el chequeo sin perder el hecho real?

Ver solución

buildPostmortem() marcaría este factor con redFlag: true, porque la frase contiene el nombre "Carla" —uno de los namedPeople—, y el postmortem completo pasaría a blameless: false. Una reescritura sistémica razonable: "El proceso no exigia una confirmacion automatica de que las alertas de latencia estuvieran activas antes de avanzar el rollout a una nueva etapa, lo que permitio que una desactivacion temporal para pruebas pasara desapercibida." El hecho de fondo —una alerta estuvo desactivada cuando hacía falta— se conserva completo; lo que cambia es que la causa apunta a la falta de una verificación automática, no a que una persona "se olvidó" de algo, que le podría pasar a cualquiera bajo el mismo sistema.

Ejercicio 3 — Diseña un action item completo. El equipo identifica un cuarto factor contribuyente que buildPostmortem() todavía no tiene en este ejemplo: "El dashboard de monitoreo del modulo 4 no mostraba una proyeccion de que pasaria con la latencia si el rollout avanzaba a la siguiente etapa, solo el estado actual." Escribe un action item completo —owner, action, due— que ataque directamente esta causa.

Ver solución

Un action item razonable: { owner: 'platform-sre', action: 'Agregar al dashboard de monitoreo (modulo 4) una proyeccion simple del guardrail de latencia extrapolada a la siguiente etapa del rollout, antes de habilitar el boton de avance.', due: '2026-08-22' }. Fíjate en la estructura: el owner es un equipo (no una persona), la action es específica y describe exactamente qué se construye (una proyección en el dashboard, no "mejorar el monitoreo" en general), y el due es una fecha concreta —no "pronto" ni "en el próximo sprint" sin más—.

Resumen y siguiente paso

En esta lección construiste y ejecutaste buildPostmortem(), la función central del módulo: convierte los eventos crudos, factores y action items de un incidente en la estructura canónica —timeline ordenado, contributing factors con su chequeo blameless, action items con dueño y fecha— y verifica el resultado completo. Sobre el incidente real de recommendations, viste los siete eventos ordenados desde las 13:58 hasta las 16:42, los tres factores contribuyentes —todos técnicos o de proceso, ninguno señalando a una persona—, y los tres action items que los atacan directamente, cerrando con blameless: true.

Antes de avanzar deberías poder: explicar por qué el orden timeline → factores → action items no es arbitrario; identificar cuándo un action item está incompleto por falta de dueño o fecha; y detectar, en una frase nueva, si un factor contribuyente nombra a una persona en vez de señalar al sistema.

La lección 4 profundiza en la pregunta que este ejemplo dejó abierta: buildPostmortem() puede detectar un nombre propio en un factor, pero, ¿por qué el lenguaje —más allá de si contiene o no un nombre literal— es lo que de verdad determina si la cultura de un equipo es blameless?

Recursos

  • Google SRE Book, Capítulo 15, "Postmortem Culture: Learning from Failure" — sre.google/sre-book/postmortem-culture. La sección sobre qué incluye un buen postmortem —resumen, impacto, causa raíz, acciones de seguimiento— es la referencia formal de la estructura que buildPostmortem() implementa en código. En inglés.
  • Atlassian, "A Guide to the Incident Postmortem Process" — atlassian.com/incident-management/postmortem/templates. Plantillas reales de postmortem con las mismas tres secciones —timeline, causas, acciones— usadas en la práctica de la industria. En inglés.
  • GitHub, colección "postmortem-templates" — github.com/dastergon/postmortem-templates. Una recopilación de plantillas de postmortem públicas de empresas reales (incluyendo la de Etsy y otras), útil para comparar cómo distintos equipos estructuran las mismas tres piezas. En inglés.