Módulo 8: Project Ship Mercados Recommendations
El postmortem, sin culpar a nadie
Descripción
El incidente está resuelto: recommendations revertida en 3 minutos, causa raíz confirmada y verificada en 150 minutos totales. Pero "resuelto" solo describe que el daño se contuvo — no dice nada sobre si el equipo entendió por qué pasó, ni qué va a cambiar para que la próxima etapa del rollout no vuelva a romperse de la misma forma. Esta lección escribe ese análisis con buildPostmortem(): un timeline, una lista de factores contribuyentes que describen el sistema, nunca a una persona, y una lista de action items con dueño.
Conexión con el módulo. Esta lección presenta buildPostmortem(), la primera pieza de integración de este capstone construida específicamente para juntar, en un solo documento, todo lo que ya sabes sobre el incidente de recommendations desde las lecciones 3 y 4 de este mismo módulo — el mismo timeline, el mismo guardrail roto, la misma decisión de revertir. El resultado de esta lección incluye, como su primer action item, la pieza que abre la siguiente lección: migrar el motor de recomendaciones a una versión más rápida, validada en shadow mode antes de tocar producción de nuevo.
Una analogía: la caja negra del avión
Cuando un avión tiene un incidente, la investigación no empieza preguntando "¿quién cometió el error?" — empieza recuperando la caja negra: los datos exactos de cada instrumento, segundo a segundo, antes y durante el incidente. La pregunta que guía toda la investigación no es "¿quién falló?", sino "¿qué condiciones del sistema completo —el diseño, los procedimientos, las herramientas disponibles en ese momento— hicieron posible que esto pasara, y qué habría que cambiar para que no vuelva a pasar, sin importar quién esté en la cabina la próxima vez?"
Esa distinción no es un gesto de cortesía hacia el piloto — es, de hecho, más efectiva para prevenir el siguiente incidente. Culpar a una persona específica resuelve, como mucho, ese caso puntual; entender el sistema completo previene toda una categoría de incidentes futuros, sin importar quién esté de guardia. El postmortem de esta lección es esa misma caja negra, aplicada al incidente de recommendations: no busca quién escribió la llamada síncrona al motor de recomendaciones — busca por qué el sistema completo permitió que esa llamada llegara a producción sin un timeout, y qué cambia para que la próxima feature con un motor externo no repita el mismo patrón.
Ejemplo trabajado: buildPostmortem() sobre el incidente de recommendations
// M8 L05: construye el postmortem blameless del incidente de recommendations --
// timeline, factores contribuyentes (del SISTEMA, nunca de una persona), y action
// items con dueno. buildPostmortem() es la pieza de integracion de este modulo que
// junta el timeline y los datos de las lecciones 3 y 4 en un documento estructurado.
function buildPostmortem({ title, severity, timeline, contributingFactors, actionItems }) {
const blamesAPerson = contributingFactors.some((f) => f.blamesPerson);
return { title, severity, timeline, contributingFactors, actionItems, blameless: !blamesAPerson };
}
const postmortem = buildPostmortem({
title: 'Latency regression -- recommendations rollout stopped at 10%',
severity: 'SEV2',
timeline: [
{ time: '14:12', event: 'guardrail dashboard confirma checkoutLatencyP95Ms=910ms (techo 800ms) en ramp-10' },
{ time: '14:15', event: 'kill switch activado, recommendationsFlag.enabled=false' },
{ time: '16:42', event: 'causa raiz confirmada y verificada' },
],
contributingFactors: [
{ factor: 'La llamada al motor de recomendaciones v1 es sincrona y bloquea el render de la pagina de producto bajo trafico concurrente real.', blamesPerson: false },
{ factor: 'El canary (1%, 4h) no tuvo suficiente trafico concurrente para exponer el problema; solo aparecio con volumen real en ramp-10.', blamesPerson: false },
{ factor: 'No existia un timeout ni un fallback que ocultara el carrusel si el motor tardaba de mas.', blamesPerson: false },
],
actionItems: [
{ item: 'Migrar el motor de recomendaciones de v1 a v2 (arquitectura de inferencia mas rapida), validado en shadow mode antes de re-lanzar.', owner: 'team-recommendations' },
{ item: 'Agregar un timeout de 200ms con fallback (ocultar el carrusel) si el motor no responde a tiempo.', owner: 'team-recommendations' },
{ item: 'Ampliar el trafico del canary para que capture concurrencia real antes de pasar a 10%.', owner: 'team-platform' },
],
});
console.log('=== Postmortem: ' + postmortem.title + ' (' + postmortem.severity + ') ===\n');
console.log('-- Timeline --');
postmortem.timeline.forEach((t) => console.log(t.time + ' ' + t.event));
console.log('\n-- Contributing factors (blameless: ' + postmortem.blameless + ') --');
postmortem.contributingFactors.forEach((f, i) => console.log((i + 1) + '. ' + f.factor));
console.log('\n-- Action items --');
postmortem.actionItems.forEach((a, i) => console.log((i + 1) + '. [' + a.owner + '] ' + a.item));
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== Postmortem: Latency regression -- recommendations rollout stopped at 10% (SEV2) ===
-- Timeline --
14:12 guardrail dashboard confirma checkoutLatencyP95Ms=910ms (techo 800ms) en ramp-10
14:15 kill switch activado, recommendationsFlag.enabled=false
16:42 causa raiz confirmada y verificada
-- Contributing factors (blameless: true) --
1. La llamada al motor de recomendaciones v1 es sincrona y bloquea el render de la pagina de producto bajo trafico concurrente real.
2. El canary (1%, 4h) no tuvo suficiente trafico concurrente para exponer el problema; solo aparecio con volumen real en ramp-10.
3. No existia un timeout ni un fallback que ocultara el carrusel si el motor tardaba de mas.
-- Action items --
1. [team-recommendations] Migrar el motor de recomendaciones de v1 a v2 (arquitectura de inferencia mas rapida), validado en shadow mode antes de re-lanzar.
2. [team-recommendations] Agregar un timeout de 200ms con fallback (ocultar el carrusel) si el motor no responde a tiempo.
3. [team-platform] Ampliar el trafico del canary para que capture concurrencia real antes de pasar a 10%.
Fíjate en cómo buildPostmortem() verifica la propiedad blameless: no es una promesa en la introducción del documento — es un chequeo real, blamesAPerson = contributingFactors.some((f) => f.blamesPerson), sobre cada factor contribuyente. Si cualquiera de los tres factores tuviera blamesPerson: true —por ejemplo, "el ingeniero que escribió la llamada síncrona no revisó el timeout"—, el postmortem completo se marcaría blameless: false, y eso sería una señal explícita de que el documento necesita reescribirse antes de compartirse.
Repasa los tres factores contribuyentes: ninguno menciona una persona, un equipo específico "que se equivocó", ni una decisión individual — los tres describen condiciones del sistema: una llamada síncrona (una decisión de arquitectura, no de una persona en un momento dado), un canary sin suficiente concurrencia real (una limitación del diseño de la rampa), y la ausencia de un timeout (una pieza de infraestructura que faltaba). Los tres action items, en cambio, sí tienen dueño —team-recommendations, team-platform— porque asignar responsabilidad de arreglar hacia adelante es completamente distinto de asignar culpa por lo que ya pasó. Esa es, en una frase, la disciplina completa del postmortem blameless.
El primer action item es, literalmente, el tema de la próxima lección
Vale la pena notar algo estructural: el primer action item de este postmortem —"migrar el motor de recomendaciones de v1 a v2, validado en shadow mode antes de re-lanzar"— no es una idea suelta. Es exactamente el trabajo de la lección 6, que sigue después de esta. Esto no es casualidad: en un lanzamiento real, el postmortem no es el final de la historia — es el documento que decide qué sigue. Sin este action item, la lección 6 no tendría ninguna razón de ser dentro de la narrativa de este capstone; con él, la migración del modelo deja de ser "una buena práctica genérica de IA" y se convierte en la respuesta específica y justificada a un problema real, ya documentado.
Errores comunes
Escribir un factor contribuyente que, aunque no nombra a nadie explícitamente, sigue siendo un señalamiento disfrazado. Qué pasa: alguien escribe "el código de la llamada al motor no se revisó con suficiente cuidado antes del deploy" — sin nombrar a una persona, pero implicando claramente que alguien no hizo bien su trabajo de revisión. Por qué pasa: es fácil pensar que "blameless" significa solo "no usar nombres propios", cuando en realidad significa no atribuir la causa a una falla de juicio o de cuidado de ninguna persona o equipo, aunque sea de forma indirecta. Cómo detectarlo: pregúntate si el factor, tal como está escrito, respondería "sí" a la pregunta "¿esto implica que alguien debería haber actuado distinto?". Si la respuesta es sí, todavía no es blameless. Cómo corregirlo: como en los tres factores de esta lección, describe la condición técnica o de proceso que hizo posible el problema —una llamada síncrona, un canary sin suficiente volumen, la ausencia de un timeout— sin ninguna referencia, directa o indirecta, a que alguien "debería haber sabido mejor".
Dejar los action items sin dueño, o con un dueño demasiado vago ("el equipo", sin especificar cuál). Qué pasa: alguien escribe los tres action items del postmortem sin asignarles ningún owner, o con un owner: 'todos' que en la práctica significa que nadie se hace responsable. Por qué pasa: en el momento de escribir el postmortem, "vamos a arreglarlo" se siente como suficiente compromiso, y asignar un equipo específico puede sentirse como un paso burocrático adicional. Cómo detectarlo: si le preguntas "¿quién está trabajando en esto ahora mismo?" sobre cualquier action item y no hay una respuesta con un nombre de equipo o persona, ese action item probablemente no se va a completar. Cómo corregirlo: como en los tres action items de esta lección, cada uno lleva un owner explícito y específico —team-recommendations, team-platform— nunca "el equipo" en genérico.
Tratar el postmortem como el cierre de la conversación, sin conectar los action items con lo que sigue. Qué pasa: el equipo escribe el postmortem, lo archiva, y nadie vuelve a revisar si el primer action item —migrar a v2— realmente se ejecutó. Por qué pasa: escribir el documento se siente como "haber aprendido la lección", incluso cuando ningún action item se ha completado todavía. Cómo detectarlo: si pasan semanas y nadie puede decir en qué estado está cada action item del postmortem, el documento se archivó sin cerrar el ciclo. Cómo corregirlo: como muestra la estructura de este módulo, el primer action item de este postmortem se convierte, en la próxima lección, en trabajo real y ejecutado —no en una promesa que queda escrita y olvidada.
Ejercicios
Ejercicio 1 — Reescribe un factor con sesgo de culpa. Alguien propone este factor contribuyente para el postmortem: "El ingeniero que implementó la llamada al motor de recomendaciones no consideró el caso de tráfico alto." Reescríbelo siguiendo el patrón blameless de esta lección — sin nombrar ni implicar responsabilidad de ninguna persona.
Ver solución
Una reescritura posible: "El diseño original de la llamada al motor de recomendaciones no incluía un caso de prueba específico para tráfico concurrente alto, lo que dejó ese escenario sin verificar antes del lanzamiento." La diferencia clave: la versión original apunta a que una persona "no consideró" algo (una falla de juicio individual); la reescritura apunta a que el proceso de diseño y prueba no incluía ese escenario — una condición del sistema que cualquier equipo, con ese mismo proceso, podría haber pasado por alto, y que se puede corregir agregando ese caso de prueba al proceso, no reprendiendo a nadie.
Ejercicio 2 — Verifica la propiedad blameless con código. Si agregaras un cuarto factor contribuyente al arreglo contributingFactors de esta lección con { factor: 'El equipo no probo suficiente antes del deploy.', blamesPerson: true }, ¿qué valor tomaría postmortem.blameless al correr el código de nuevo? ¿Por qué el chequeo de buildPostmortem() detecta esto aunque el texto del factor no use ningún nombre propio?
Ver solución
postmortem.blameless pasaría a false. El chequeo contributingFactors.some((f) => f.blamesPerson) no analiza el texto del factor en absoluto — evalúa el campo explícito blamesPerson que cada factor declara al escribirlo. Esto significa que la responsabilidad de marcar correctamente un factor como blameless o no recae en quien escribe el postmortem, no en el código: buildPostmortem() puede confirmar que ningún factor fue marcado como culpa de una persona, pero no puede, por sí solo, detectar un señalamiento disfrazado como el del ejercicio 1 si alguien lo marca incorrectamente como blamesPerson: false. La disciplina blameless depende, en última instancia, de que el equipo la aplique con honestidad al escribir cada factor, no solo del código que la verifica.
Ejercicio 3 — Prioriza los tres action items. De los tres action items de este postmortem, ¿cuál crees que debería empezar primero, y por qué? Justifica en 2-3 frases, considerando cuál resuelve la causa raíz más directamente y cuál protege mejor contra un incidente similar mientras el primero se completa.
Ver solución
No hay una única respuesta correcta, pero un argumento razonable: el segundo action item —agregar un timeout con fallback— probablemente debería empezar primero, porque es la protección más rápida de implementar y no depende de completar la migración de modelo (que toma más tiempo y necesita validación en shadow mode). Mientras el timeout no exista, cualquier lanzamiento futuro con el motor de recomendaciones —incluso ya migrado a v2— seguiría corriendo el riesgo de bloquear el render si el motor tarda más de lo esperado. La migración a v2 (primer action item) resuelve la causa raíz de fondo, pero el timeout es la red de seguridad que protege mientras esa migración se completa — los dos son necesarios, pero en el orden correcto, el timeout no debería esperar a que termine la migración.
Resumen y siguiente paso
En esta lección escribiste el postmortem completo del incidente de recommendations con buildPostmortem(): un timeline de tres momentos, tres factores contribuyentes que describen el sistema —nunca a una persona—, y tres action items con dueño explícito. Confirmaste, con el chequeo real del código (blameless: true), que ningún factor atribuye la causa a una falla individual.
Antes de avanzar deberías poder: reescribir un factor contribuyente con sesgo de culpa en su versión blameless; y explicar por qué asignar un owner a un action item es distinto de asignar culpa por lo que ya pasó.
El primer action item de este postmortem —migrar el motor de recomendaciones a v2, validado en shadow mode— es exactamente el trabajo de la lección 6: no vas a migrar a ciegas, confiando en que el modelo nuevo es mejor porque "debería serlo" — vas a correrlo en paralelo, sin tocar a ningún comprador real, y comparar sus resultados contra el modelo viejo antes de decidir.
Recursos
- Google SRE Book, Capítulo 15, "Postmortem Culture: Learning from Failure" — sre.google/sre-book/postmortem-culture. La referencia formal completa de la cultura de postmortems blameless que esta lección ejecuta en versión simplificada: investigar el sistema, no a las personas. En inglés.
- Etsy Engineering, "Blameless PostMortems and a Just Culture" — codeascraft.com/2012/05/22/blameless-postmortems. Uno de los artículos fundacionales de la industria sobre por qué culpar a personas reduce, en la práctica, la calidad de la información que un equipo comparte durante una investigación. En inglés.