Módulo 8: Project Ship Mercados Recommendations

Proyecto final: lanza (y aterriza a salvo) las recomendaciones de Mercado

Descripción

Este es el proyecto que cierra la guía completa, y con ella, el ecosistema Product Engineering entero. No introduce ningún mecanismo nuevo — reutiliza, sin cambios, las siete piezas que las lecciones de esta guía construyeron y las dos que este módulo integró: blastRadius(), isEnabled(), rolloutPlan(), guardrailWatch(), rollbackDecision(), mttr(), buildPostmortem(), shadowCompare() y noveltyCheck(). Tu tarea es la que un equipo de producto real haría el día completo del lanzamiento de recommendations: escribir el plan de lanzamiento completo, correr la cadena end-to-end que encadena las siete capas en un solo script de Node, y entregar el resultado final — no un "lanzamos y ya", sino la evidencia completa de un proceso que funcionó, con turbulencia real incluida.

El encargo. El equipo de liderazgo de Mercado necesita, antes de declarar terminado el ciclo de recommendations, un documento único que un ingeniero nuevo en el equipo pueda leer y entender exactamente qué pasó, en qué orden, y por qué cada decisión se tomó como se tomó — desde la primera línea de exposición hasta la confirmación, semanas después, de que el resultado se sostiene. Tu entrega: el plan, la corrida verificada, y el cierre.

El caso, recapitulado en una frase

recommendations, el carrusel de productos relacionados en la página de producto de Mercado, ganó su experimento con significancia estadística real (+18.75% en conversión de checkout, p=0.0114) pero rompió un guardrail de latencia conocido (p95 de 650ms a 910ms, techo 800ms). Esta guía completa —ocho módulos— tomó ese resultado exacto y lo lanzó con seguridad: flag, rampa gradual, monitoreo en vivo, decisión de rollback medida, postmortem blameless, migración de modelo validada en shadow, y re-lanzamiento con verificación de durabilidad.

El plan de lanzamiento completo

Así es como se ve, en un solo documento, el plan que un equipo de Mercado real habría escrito y ejecutado — cada sección corresponde a una lección de este módulo, y cada número es el mismo que ya viste ejecutado en Node.

1. El feature flag

CampoValor
namerecommendations
MecanismoisEnabled(userId, flag) — asignación determinista por hash, sin Math.random()
Kill switchrecommendationsFlag.enabled = false — apaga la exposición a 0% al instante, sin deploy
typerelease — se retira del código una vez alcance el 100% de forma estable

2. La rampa de rollout

EtapapercentminSampleSizeminDwellHoursCriterio de avance
canary1%1,0004hp95Latency <= 800ms
rollout10%5,00012hp95Latency <= 800ms
rollout50%20,00024hp95Latency <= 800ms
rollout100%50,00024hp95Latency <= 800ms

3. El plan de monitoreo

Cuatro guardrails vigilados en cada etapa con guardrailWatch(), no solo el que ya se sospechaba roto:

MétricaTipoLímite
checkoutLatencyP95Msceiling800ms
complaintRatemaxIncrease+0.005
churnRatemaxIncrease+0.010
grossMarginfloor0.180

4. El criterio de rollback

rollbackDecision() compara, exclusivamente, el estado de los guardrails críticos contra el costo de revertir versus el costo de arreglar hacia adelante — el resultado del experimento primario nunca entra en esa comparación. Para recommendations: revertCost = 2min (kill switch), fixForwardCost = 180min (diagnosticar + parchear + verificar). Cualquier guardrail crítico roto con revertCost <= fixForwardCost produce rollback.

5. El postmortem

Blameless, con tres factores del sistema (llamada síncrona al motor v1, canary sin suficiente concurrencia, ausencia de timeout) y tres action items con dueño — el documento completo está en la lección 5 de este módulo. El primero de esos action items es la causa directa de la sección siguiente.

6. El plan de re-lanzamiento

  1. Migrar el motor de recomendaciones de v1 a v2 (inferencia más rápida).
  2. Validar v2 en shadow mode antes de tocar producción: shadowCompare() y modelMigrationDecision(), reusados de M7 sin cambios, con doble umbral (global ≥70% Y segmento crítico cold-start ≥60%) — la misma pareja de funciones que, en la primera iteración de v2, detectó un cold-start roto (26.7%) y bloqueó la migración a tiempo.
  3. Agregar un timeout de 200ms con fallback (ocultar el carrusel si el motor no responde a tiempo).
  4. Re-correr la rampa completa desde el canary, con los mismos cuatro guardrails.
  5. Verificar, con noveltyCheck(), que el lift se sostiene varias semanas después del 100% — no declarar éxito antes de esa verificación.

La corrida end-to-end, en Node

Las siete capas del método, encadenadas en un solo script — cada función es exactamente la de su lección de origen, sin ningún cambio, corridas en el orden real sobre el lanzamiento completo de recommendations:

// ===== CORRIDA END-TO-END: lanzamiento de recommendations en Mercado =====
// M1-M5: funciones EXACTAMENTE como quedaron en sus modulos de origen, sin cambios.
// M6-M7: buildPostmortem(), noveltyCheck() y shadowCompare() son las piezas de
// integracion de este capstone, construidas sobre el mismo patron de "funcion pura +
// dato de entrada + salida estructurada" que usa toda la guia.

// ---------- M1: blast radius ----------
function blastRadius({ totalUsers, rolloutPercent, regressionRate }) {
  const exposedUsers = Math.round(totalUsers * rolloutPercent);
  const affectedUsers = Math.round(exposedUsers * regressionRate);
  return { rolloutPercent, exposedUsers, affectedUsers };
}

// ---------- M2: feature flag ----------
function hashUserId(userId) {
  let hash = 0;
  for (let i = 0; i < userId.length; i++) {
    hash = (hash * 31 + userId.charCodeAt(i)) % 100;
  }
  return hash;
}
function isEnabled(userId, flag) {
  if (!flag.enabled) return false;
  const bucket = hashUserId(userId + flag.name);
  return bucket < flag.rolloutPercent;
}

// ---------- M3: rollout plan ----------
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;
}

// ---------- M4: guardrail watch ----------
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 };
}

// ---------- M5: rollback decision + mttr ----------
function rollbackDecision({ guardrails, primary, revertCost, fixForwardCost }) {
  const brokenCritical = guardrails.filter((g) => g.critical && g.broken);
  if (brokenCritical.length === 0) return 'continue';
  return revertCost <= fixForwardCost ? 'rollback' : 'fix_forward';
}
function mttr(events) {
  const minutesBetween = (from, to) => Math.round((new Date(to) - new Date(from)) / 60000);
  return {
    timeToMitigate: minutesBetween(events.detected, events.mitigated),
    timeToResolve: minutesBetween(events.mitigated, events.resolved),
    totalMTTR: minutesBetween(events.detected, events.resolved),
  };
}

// ---------- M6: blameless postmortem ----------
function buildPostmortem({ title, severity, timeline, contributingFactors, actionItems }) {
  const blamesAPerson = contributingFactors.some((f) => f.blamesPerson);
  return { title, severity, timeline, contributingFactors, actionItems, blameless: !blamesAPerson };
}

// ---------- M6: novelty check ----------
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 };
}

// ---------- M7: shadow mode model migration ----------
function shadowCompare(oldOutputs, newOutputs) {
  const bySegment = {};
  const differences = [];
  let agreements = 0;
  oldOutputs.forEach((oldReq, i) => {
    const newReq = newOutputs[i];
    const agree = oldReq.topRec === newReq.topRec;
    const seg = oldReq.segment;
    if (!bySegment[seg]) bySegment[seg] = { total: 0, agreements: 0 };
    bySegment[seg].total++;
    if (agree) {
      bySegment[seg].agreements++;
      agreements++;
    } else {
      differences.push({ requestId: oldReq.requestId, segment: seg, oldTopRec: oldReq.topRec, newTopRec: newReq.topRec });
    }
  });
  const total = oldOutputs.length;
  const agreementRate = (agreements / total) * 100;
  const segmentBreakdown = Object.entries(bySegment).map(([segment, s]) => ({
    segment, total: s.total, agreements: s.agreements, agreementRate: (s.agreements / s.total) * 100,
  }));
  return { total, agreements, agreementRate, differences, segmentBreakdown };
}
function modelMigrationDecision({ agreementRate, minAgreement, criticalSegmentAgreementRate, minCriticalSegmentAgreement }) {
  if (agreementRate < minAgreement) {
    return 'NO promuevas a canary: la tasa de acuerdo global esta por debajo del minimo -- revisa las diferencias primero';
  }
  if (criticalSegmentAgreementRate < minCriticalSegmentAgreement) {
    return 'NO promuevas a canary todavia: un segmento critico difiere demasiado -- diagnostica esa estrategia antes de exponer trafico real';
  }
  return 'promueve recs-v2 a canary (1%), vigilando los guardrails de M4 en cada etapa';
}

console.log('########## CAPSTONE END-TO-END: recommendations en Mercado ##########\n');

// 1) blast radius: por que no se lanza a 100% de una
console.log('--- 1) blastRadius: por que arrancar chico (M1) ---');
const totalUsers = 250000;
const regressionRate = 0.05;
const full = blastRadius({ totalUsers, rolloutPercent: 1.0, regressionRate });
const canary = blastRadius({ totalUsers, rolloutPercent: 0.01, regressionRate });
console.log('100% de golpe: exposedUsers=' + full.exposedUsers + '  affectedUsers=' + full.affectedUsers);
console.log('canary 1%:     exposedUsers=' + canary.exposedUsers + '  affectedUsers=' + canary.affectedUsers);

// 2) feature flag: exposicion determinista al 10%
console.log('\n--- 2) isEnabled: flag al 10%, exposicion estable (M2) ---');
const recommendationsFlag = { name: 'recommendations', enabled: true, rolloutPercent: 10 };
const sampleN = 5000;
const buyerIds = [];
for (let i = 0; i < sampleN; i++) buyerIds.push('buyer-' + String(i).padStart(5, '0'));
const enabledBuyers = buyerIds.filter((id) => isEnabled(id, recommendationsFlag));
console.log(enabledBuyers.length + ' de ' + sampleN + ' compradores ven recommendations (' + (enabledBuyers.length / sampleN * 100).toFixed(2) + '%)');

// 3) rollout plan + guardrail watch: la rampa se frena en 10%
console.log('\n--- 3) rolloutPlan + guardrailWatch: la rampa se frena en 10% (M3+M4) ---');
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 },
];
const watch = guardrailWatch(rampStages, guardrails, baseline);
watch.log.forEach((e) => console.log(e.stage + ' (' + e.percent + '%): ' + e.status));
const haltEntry = watch.log.find((e) => e.status === 'HALT');
const brokenDetail = haltEntry.results.find((r) => r.broken);

// 4) rollback decision + mttr
console.log('\n--- 4) rollbackDecision + mttr: revertir en minutos, no en horas (M5) ---');
const decision = rollbackDecision({
  guardrails: [
    { name: 'p95Latency', critical: true, broken: true, value: 910, ceiling: 800 },
    { name: 'checkoutErrorRate', critical: true, broken: false, value: 0.006, ceiling: 0.02 },
  ],
  primary: { name: 'checkoutConversion', win: true, lift: 0.1875 },
  revertCost: 2,
  fixForwardCost: 180,
});
console.log('decision = ' + decision);
const timeline = { detected: '2026-03-04T14:12:00', mitigated: '2026-03-04T14:15:00', resolved: '2026-03-04T16:42:00' };
const mttrResult = mttr(timeline);
console.log('timeToMitigate=' + mttrResult.timeToMitigate + 'min  timeToResolve=' + mttrResult.timeToResolve + 'min  totalMTTR=' + mttrResult.totalMTTR + 'min');

// 5) postmortem blameless (M6)
console.log('\n--- 5) buildPostmortem: la investigacion, sin culpar personas (M6) ---');
const postmortem = buildPostmortem({
  title: 'Latency regression -- recommendations rollout stopped at 10%',
  severity: 'SEV2',
  timeline: [
    { time: '14:12', event: 'guardrail dashboard confirms checkoutLatencyP95Ms=910ms (ceiling 800ms) at 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('title: ' + postmortem.title + '  (' + postmortem.severity + ')');
console.log('blameless: ' + postmortem.blameless);
console.log(postmortem.contributingFactors.length + ' contributing factors, ' + postmortem.actionItems.length + ' action items');

// 6) shadow mode: migrar el modelo v1 -> v2, segunda iteracion con el cold-start
// corregido -- shadowCompare() y modelMigrationDecision() de M7, sin cambios (M7)
console.log('\n--- 6) shadowCompare + modelMigrationDecision: v2 (iteracion 2) en shadow antes de re-lanzar (M7) ---');
const shadowRequests = [];
for (let i = 0; i < 20; i++) {
  shadowRequests.push({ requestId: 'buyer-' + i, segment: i < 7 ? 'with-history' : 'cold-start' });
}
const oldTopRecs = ['sku-104','sku-221','sku-330','sku-104','sku-558','sku-221','sku-777','sku-104','sku-330','sku-909',
  'sku-221','sku-104','sku-558','sku-330','sku-777','sku-221','sku-104','sku-909','sku-330','sku-558'];
const newTopRecs = ['sku-104','sku-221','sku-330','sku-104','sku-558','sku-221','sku-777','sku-441','sku-330','sku-909',
  'sku-221','sku-104','sku-558','sku-330','sku-616','sku-221','sku-104','sku-909','sku-330','sku-558'];
const oldOutputsV1 = shadowRequests.map((r, i) => ({ requestId: r.requestId, segment: r.segment, topRec: oldTopRecs[i] }));
const newOutputsV2 = shadowRequests.map((r, i) => ({ requestId: r.requestId, segment: r.segment, topRec: newTopRecs[i] }));
const shadow = shadowCompare(oldOutputsV1, newOutputsV2);
console.log('agreementRate global=' + shadow.agreementRate.toFixed(1) + '% (' + shadow.agreements + '/' + shadow.total + ')');
shadow.segmentBreakdown.forEach((s) => {
  console.log('  ' + s.segment.padEnd(14) + 'tasa=' + s.agreementRate.toFixed(1) + '%  (' + s.agreements + '/' + s.total + ')');
});
const coldStart = shadow.segmentBreakdown.find((s) => s.segment === 'cold-start');
const migrationDecision = modelMigrationDecision({
  agreementRate: shadow.agreementRate,
  minAgreement: 70,
  criticalSegmentAgreementRate: coldStart.agreementRate,
  minCriticalSegmentAgreement: 60,
});
console.log('Decision: ' + migrationDecision);

// 7) re-lanzar: rolloutPlan corre limpio hasta 100% con la latencia arreglada
console.log('\n--- 7) rolloutPlan: re-lanzamiento con v2 + fix de latencia (M3) ---');
const relaunchStages = [
  { percent: 0.01, label: 'canary 1%', measured: { p95Latency: 705 }, advanceIf: (m) => m.p95Latency <= 800 },
  { percent: 0.10, label: 'rollout 10%', measured: { p95Latency: 740 }, advanceIf: (m) => m.p95Latency <= 800 },
  { percent: 0.50, label: 'rollout 50%', measured: { p95Latency: 765 }, advanceIf: (m) => m.p95Latency <= 800 },
  { percent: 1.00, label: 'rollout 100%', measured: { p95Latency: 778 }, advanceIf: (m) => m.p95Latency <= 800 },
];
const relaunchResult = rolloutPlan(relaunchStages);
relaunchResult.forEach((s) => console.log(s.label + ': p95=' + s.measured.p95Latency + 'ms -> ' + s.decision));

// 8) novelty check: el lift aguanta 4 semanas despues del relanzamiento
console.log('\n--- 8) noveltyCheck: el lift aguanta, o era novedad? (M6) ---');
const weeklyLift = [0.1875, 0.1810, 0.1755, 0.1740];
const novelty = noveltyCheck(weeklyLift);
console.log('semana 1: ' + (novelty.first * 100).toFixed(2) + '%  ->  semana 4: ' + (novelty.last * 100).toFixed(2) + '%  (decay ' + novelty.decayPct + '%)');
console.log(novelty.verdict);

console.log('\n########## FIN: HALT en ' + haltEntry.percent + '% -> rollback en ' + mttrResult.timeToMitigate + 'min -> postmortem blameless -> v2 en shadow (' + shadow.agreementRate.toFixed(1) + '% agreement) -> re-lanzamiento limpio a 100% -> lift durable ##########');

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

########## CAPSTONE END-TO-END: recommendations en Mercado ##########

--- 1) blastRadius: por que arrancar chico (M1) ---
100% de golpe: exposedUsers=250000  affectedUsers=12500
canary 1%:     exposedUsers=2500  affectedUsers=125

--- 2) isEnabled: flag al 10%, exposicion estable (M2) ---
497 de 5000 compradores ven recommendations (9.94%)

--- 3) rolloutPlan + guardrailWatch: la rampa se frena en 10% (M3+M4) ---
canary (1%): continue
ramp-10 (10%): HALT
ramp-50 (50%): not reached (ramp halted earlier)
full (100%): not reached (ramp halted earlier)

--- 4) rollbackDecision + mttr: revertir en minutos, no en horas (M5) ---
decision = rollback
timeToMitigate=3min  timeToResolve=147min  totalMTTR=150min

--- 5) buildPostmortem: la investigacion, sin culpar personas (M6) ---
title: Latency regression -- recommendations rollout stopped at 10%  (SEV2)
blameless: true
3 contributing factors, 3 action items

--- 6) shadowCompare + modelMigrationDecision: v2 (iteracion 2) en shadow antes de re-lanzar (M7) ---
agreementRate global=90.0% (18/20)
  with-history  tasa=100.0%  (7/7)
  cold-start    tasa=84.6%  (11/13)
Decision: promueve recs-v2 a canary (1%), vigilando los guardrails de M4 en cada etapa

--- 7) rolloutPlan: re-lanzamiento con v2 + fix de latencia (M3) ---
canary 1%: p95=705ms -> ADVANCE
rollout 10%: p95=740ms -> ADVANCE
rollout 50%: p95=765ms -> ADVANCE
rollout 100%: p95=778ms -> ADVANCE

--- 8) noveltyCheck: el lift aguanta, o era novedad? (M6) ---
semana 1: 18.75%  ->  semana 4: 17.40%  (decay 7.2%)
HOLDS (se sostiene)

########## FIN: HALT en 10% -> rollback en 3min -> postmortem blameless -> v2 en shadow (90.0% agreement) -> re-lanzamiento limpio a 100% -> lift durable ##########

Ocho pasos, un solo hilo. El paso 1 justifica por qué el lanzamiento empezó chico (12,500 afectados de golpe contra 125 en canary). El paso 2 confirma que el mecanismo de exposición es estable (9.94% sobre 5,000 compradores, prácticamente idéntico al 10% configurado). El paso 3 es donde la turbulencia real aparece: la rampa avanza limpio en canary y se detiene con HALT exactamente en ramp-10, el mismo guardrail y el mismo número (910ms) que la guía de métricas identificó desde el principio. El paso 4 convierte esa confirmación en una decisión medida: rollback, ejecutado en apenas 3 minutos de exposición real, dentro de un totalMTTR de 150. El paso 5 documenta, sin culpar a nadie, qué condiciones del sistema lo permitieron. El paso 6 valida, antes de tocar producción de nuevo, que el modelo que resuelve la causa raíz es seguro: 90.0% de acuerdo global, y —más importante todavía— 84.6% en el segmento cold-start, el mismo que la primera iteración de v2 había dejado en 26.7%, muy por debajo del umbral crítico. El paso 7 re-lanza limpio hasta el 100%, con la latencia siempre bajo el techo. Y el paso 8 confirma que el resultado —el motivo original por el que valía la pena todo este esfuerzo— se sostiene, no era solo curiosidad pasajera.

Rúbrica de evaluación

Un plan de lanzamiento completo de este proyecto se evalúa contra ocho criterios. Usa esta rúbrica para autoevaluar tu propia versión antes de darla por terminada:

#CriterioQué debe estar presenteNivel logrado si...
1Flag verificadoisEnabled() corrido sobre una muestra suficientemente grande; estabilidad confirmada (mismo conjunto en corridas repetidas); kill switch probado.El plan cita el porcentaje real medido (9.94% o similar) y confirma explícitamente que el kill switch lleva la exposición a 0.
2Rampa diseñada antes del lanzamientoCuatro etapas con minSampleSize, minDwellHours y criterio de avance, fijados antes de ver ningún dato real de la etapa siguiente.La tabla de la rampa existe con números crecientes por etapa, y ninguno de esos números se ajustó después de ver el resultado de guardrailWatch().
3Monitoreo integrado y HALT correctamente diagnosticadoguardrailWatch() corrido sobre los cuatro guardrails de negocio, no solo el que ya se sospechaba; la etapa y el guardrail exactos del HALT identificados con sus tres números (antes, después, límite).El plan cita ramp-10, checkoutLatencyP95Ms: 650 -> 910 (techo 800), y confirma que los otros tres guardrails se mantuvieron sanos.
4Decisión de rollback y MTTR con los dos tramosrollbackDecision() corrida sin que primary.win influya en el resultado; mttr() con timeToMitigate y timeToResolve reportados por separado.El plan cita decision: rollback, timeToMitigate=3min y totalMTTR=150min, sin confundir los dos tramos entre sí.
5Postmortem blameless completoTimeline, factores contribuyentes que describen el sistema (nunca a una persona), y action items con dueño explícito.El plan confirma blameless: true con evidencia —los tres factores citados no atribuyen culpa a nadie— y nombra al menos un action item con su owner.
6Migración de modelo validada en shadow, con privacidadshadowCompare() corrida con desglose por segmento; modelMigrationDecision() cruzando los dos umbrales (global y segmento crítico) antes de autorizar el canary; los logs de la comparación pasan por redactPII() antes de guardarse.El plan cita agreementRate=90.0% global y 84.6% en cold-start (contra los umbrales de 70% y 60%), y confirma que los campos personales (email, ip) se redactan en los logs.
7Re-lanzamiento verificado, no solo reintentadoLa rampa completa re-corrida desde el canary con datos reales del sistema corregido, no reactivada directamente al 100%.El plan muestra las cuatro etapas del re-lanzamiento con ADVANCE en cada una, y la latencia medida en cada etapa, no solo la etapa final.
8Durabilidad confirmada antes de declarar éxitonoveltyCheck() corrida con datos de varias semanas; el decayPct reportado y comparado contra el umbral de 50.El plan cita decay=7.2 y el veredicto HOLDS (se sostiene), explicando por qué esa cifra —y no solo "el lift bajó un poco"— es la que importa.

Un plan que cumple los ocho criterios está completo. Un plan que llega limpio hasta el criterio 7 —el re-lanzamiento— pero nunca completa el criterio 8 se queda exactamente en el error que la lección 7 de este módulo nombró: declarar éxito en el momento en que se alcanza el 100%, sin confirmar todavía si ese éxito se sostiene.

Ejercicios de transferencia

Los siguientes tres ejercicios no piden repetir la corrida de recommendations — piden aplicar el método completo, las siete capas, a un caso nuevo. Es la prueba real de si aprendiste el proceso o solo memorizaste los números de Mercado.

Ejercicio 1 — Transfiere las primeras cuatro capas a deliveryEtaV2. El equipo de logística de Mercado (el caso que acompañó los ejercicios de los módulos 1, 3, 4 y 5 de esta guía) lanza su algoritmo de tiempos de entrega estimados: base alcanzable 80,000 pedidos por semana, regressionRate: 0.03, guardrail errorRate (tipo ceiling, límite 0.04), revertCost: 3min, fixForwardCost: 25min. Corren un canary al 2%, y miden errorRate: 0.046 en la etapa de 10%. Con la línea de tiempo detected: 11:00:00, mitigated: 11:03:00, resolved: 11:35:00, aplica blastRadius(), guardrailWatch(), rollbackDecision() y mttr() al caso completo.

Ver solución

Corriendo el mismo código de este proyecto con los datos de logística: blastRadius() en canary (rolloutPercent: 0.02) da exposedUsers: 1,600, affectedUsers: 48. guardrailWatch() sobre dos etapas (canary con errorRate: 0.025, ramp-10 con errorRate: 0.046, contra un techo de 0.04) reporta: canary (2%): continue (0.025 no rompe el techo) y ramp-10 (10%): HALT (0.046 > 0.04). rollbackDecision(), con el guardrail crítico roto y revertCost: 3 <= fixForwardCost: 25, devuelve rollback — la misma decisión que recommendations, aunque con una brecha de costos mucho más chica (3 contra 25, no 2 contra 180). mttr() sobre la línea de tiempo dada da timeToMitigate: 3min, timeToResolve: 32min, totalMTTR: 35min — una respuesta bastante más rápida en total que la de recommendations (150min), aunque el timeToMitigate (la parte que más protege a los usuarios) es prácticamente igual en ambos casos. El método completo se transfiere sin cambiar una línea de las funciones — solo los datos de entrada cambian.

Ejercicio 2 — Aplica el método completo a un caso nuevo, de otra industria. Una fintech lanza un modelo de aprobación instantánea de crédito que reduce el tiempo de espera de los usuarios en un 35% (resultado significativo, ya validado por su propia guía de métricas), pero incrementa la tasa de falsos positivos de fraude (aprobar solicitudes que después resultan fraudulentas) por encima de un límite regulatorio interno. Tienen un flag para controlar la exposición, una rampa de cuatro etapas, un dashboard de guardrails que incluye fraudFalsePositiveRate, un runbook de rollback, y — porque el modelo aprueba crédito — una necesidad clara de shadow mode antes de cualquier futura actualización del modelo. Sin escribir el código completo, describe qué función de esta guía usarías para cada una de estas cinco preguntas: (a) ¿cuánta gente exponer primero, dado el riesgo de aprobar crédito fraudulento? (b) ¿quién ve el modelo nuevo, de forma estable? (c) ¿el guardrail de fraude se rompe en alguna etapa? (d) ¿revertir o arreglar hacia adelante, y qué tan rápido respondieron? (e) ¿es seguro migrar a una versión mejorada del modelo en el futuro, sin arriesgar a los usuarios?

Ver solución

(a) blastRadius(), con un regressionRate que representa la fracción de solicitudes aprobadas que resultan fraudulentas, comparado contra un maxAcceptableExposure que el equipo de riesgo fije explícitamente —probablemente mucho más conservador que el de recommendations, dado que aprobar crédito fraudulento tiene un costo financiero directo, no solo una molestia de latencia—. (b) isEnabled(), con el mismo mecanismo de hash determinista, asignando usuarios de forma estable al modelo nuevo de aprobación. (c) guardrailWatch(), con fraudFalsePositiveRate como uno de los guardrails de tipo ceiling (o maxIncrease, dependiendo de cómo el equipo lo defina) contra el límite regulatorio. (d) rollbackDecision() y mttr(), con el mismo principio de que el resultado positivo (menor tiempo de espera) nunca debe influir en si un guardrail de fraude crítico manda revertir. (e) shadowCompare() y modelMigrationDecision(), corriendo la versión mejorada del modelo en paralelo, sin afectar ninguna decisión de crédito real, comparando sus aprobaciones/rechazos contra el modelo actual antes de migrar — con umbrales (global y, sobre todo, el del segmento más sensible a fraude) probablemente mucho más estrictos que el 70% / 60% de recommendations, dado que el costo de un desacuerdo en este dominio es mucho más alto que recomendar el producto equivocado. El punto central: las siete capas del método no son específicas de e-commerce — se aplican, con la misma lógica, a cualquier lanzamiento donde un resultado positivo convive con un riesgo real que necesita contención.

Ejercicio 3 — Diseña una función que combine rollbackDecision() y noveltyCheck(). Imagina un escenario futuro: recommendations está en 100%, y cuatro semanas después, noveltyCheck() reporta 'NOVELTY (se desvanece)' en vez de 'HOLDS (se sostiene)' — el lift se desinfló más allá del umbral. Propón, en pseudocódigo o en prosa, cómo diseñarías una función postLaunchReview({ noveltyResult, guardrailsStillHealthy }) que decida entre tres resultados: keep_as_is (si el lift sigue siendo positivo, aunque menor), iterate_on_feature (si el decaimiento es significativo pero los guardrails siguen sanos), o revisit_with_metrics (si además algún guardrail empezó a fallar semanas después de un lanzamiento que originalmente pasó limpio).

Ver solución

Una propuesta razonable: function postLaunchReview({ noveltyResult, guardrailsStillHealthy }) { if (noveltyResult.verdict.startsWith('HOLDS')) return 'keep_as_is'; if (!guardrailsStillHealthy) return 'revisit_with_metrics'; return 'iterate_on_feature'; }. La lógica evalúa primero si el resultado sigue siendo durable —en cuyo caso no hace falta ninguna acción especial—, y si no lo es, distingue entre dos situaciones muy distintas: un decaimiento del lift con los guardrails todavía sanos (probablemente solo necesita iterar sobre la feature misma, ajustar el algoritmo de recomendación) contra un decaimiento del lift junto con un guardrail que empieza a fallar semanas después (una señal más seria, que amerita volver a la guía de métricas para re-medir con rigor, no solo ajustar la feature). Este ejercicio confirma la misma idea que sostiene toda esta guía: un lanzamiento no es un evento con un solo momento de verificación — es un proceso que necesita puntos de revisión definidos de antemano, incluso semanas después de que todo parecía haber salido bien.

Cierre de la guía: de "el A/B ganó" a un lanzamiento completo, con turbulencia real y todo

recommendations completó su recorrido de punta a punta en esta guía exactamente donde la guía de métricas lo dejó: un resultado significativo (+18.75%, p=0.0114) que convive con un guardrail roto (checkoutLatencyP95Ms, 650ms910ms, techo 800ms). Ocho módulos después, ese resultado está en producción al 100% de la base de Mercado, con el problema que lo detuvo a mitad de camino identificado, corregido, y validado — y con evidencia, no solo esperanza, de que el valor original se sostiene.

El arco completo de esta guía, en una frase: el radio de impacto (M1) justificó empezar chico; el flag (M2) hizo posible controlar la exposición sin un nuevo deploy; la rampa (M3) definió cómo subir con criterios explícitos; el monitoreo (M4) confirmó, en vivo, exactamente cuándo el guardrail se rompió; la decisión de rollback (M5) revirtió en minutos, no en horas, sin que el resultado positivo comprara ningún permiso especial; el postmortem (M6) investigó la causa raíz sin culpar a nadie; el shadow mode (M7) validó el fix antes de arriesgar producción de nuevo; y este módulo relanzó y verificó que todo el esfuerzo valió la pena, con el lift sosteniéndose semanas después.

Ninguna de esas ocho piezas, sola, habría sido suficiente. Sin el radio de impacto, el equipo habría lanzado a ciegas. Sin el flag, revertir hubiera tomado horas, no minutos. Sin la rampa y el monitoreo, el guardrail roto habría llegado al 100% de la base antes de que nadie lo notara. Sin el postmortem, el mismo error se habría repetido en el próximo lanzamiento con un motor externo. Sin el shadow mode, la migración al modelo v2 habría sido una apuesta, no una decisión informada. Y sin la verificación de durabilidad, el equipo nunca habría sabido si todo ese cuidado protegió algo que realmente valía la pena proteger.

Cierre del arco: Product Engineering, de punta a punta

Con este capstone se cierra también el arco completo del ecosistema Product Engineering, construido en cuatro guías que se pasan la posta, cada una entregando exactamente lo que la siguiente necesita como insumo:

product-thinking-for-      product-discovery-and-   product-metrics-and-      shipping-and-iterating-
engineers-guide        ->  prototyping-guide     ->  experimentation-guide->  products-guide
"¿QUE apostamos?"           "¿VALE la pena, barato?"  "¿FUNCIONO, de verdad?"   "¿COMO lo lanzamos SEGURO?"

RICE, dimensionamiento      Fake door, 6.5% interes,   Funnel, retencion,        Flag, rampa, guardrails
de oportunidades:           confianza 0.3 -> 0.79:     North Star + guardrails,  en vivo, rollback medido,
recommendations es la       vale la pena construirla   A/B test con              postmortem, shadow mode,
apuesta priorizada                                     significancia:            re-lanzamiento verificado
                                                        ship_after_fix
  • product-thinking-for-engineers-guide decidió qué apostar: entre el backlog completo de ideas de Mercado, recommendations se priorizó con RICE y un modelo de dimensionamiento de oportunidades.
  • product-discovery-and-prototyping-guide validó esa apuesta barato, antes de construir el motor completo: un fake door midió una señal cualitativa de interés (6.5%, confianza 0.3 → 0.79), sin todavía ningún compromiso de ingeniería real.
  • product-metrics-and-experimentation-guide midió el resultado real, con rigor cuantitativo: el funnel, la retención, la North Star, los guardrails, y el A/B test con significancia estadística, terminando en una decisión matizada — ship_after_fix, no un "enviar" ingenuo.
  • shipping-and-iterating-products-guide —esta guía— tomó ese ship_after_fix y lo convirtió en un lanzamiento real: flag, rampa gradual, monitoreo en vivo, rollback medido, postmortem blameless, migración de modelo validada en shadow, y re-lanzamiento con la durabilidad confirmada.

Ninguna de las cuatro guías reemplaza a las otras tres. Pensamiento de producto sin discovery arriesga construir caro algo que nadie quería. Discovery sin medición rigurosa se conforma con "parece que va bien" en vez de saberlo con certeza. Medición rigurosa sin un proceso de lanzamiento cuidadoso mide, con mucha precisión, un resultado que después se lanza sin ningún cuidado y termina rompiendo algo de todos modos. Y un proceso de lanzamiento impecable, sin las tres guías anteriores, sabe lanzar con seguridad cosas que quizás nunca debieron construirse. Las cuatro juntas son el ciclo completo de un ingeniero de producto maduro: decidir, validar, medir, y lanzar e iterar — con criterio en cada paso, no con suerte.

Hacia dónde sigues

Con el ciclo completo de Product Engineering cerrado, hay dos direcciones razonables desde aquí, según qué quieras profundizar.

Fullstack es la frontera que esta guía, de punta a punta, dejó del otro lado a propósito: asumió que el código de recommendations ya estaba construido, deployado, e instrumentado — listo para controlar su exposición con un flag. Construir esa pieza —el pipeline de CI/CD, el deploy atómico, la instrumentación de performance que produce los datos de latencia que esta guía vigiló en cada guardrail— es el territorio de fullstack-performance-and-deployment-guide. Un ingeniero que domina las dos piezas —Fullstack para construir e instrumentar, y esta guía para lanzar e iterar con seguridad— tiene el ciclo técnico completo, desde el código hasta el usuario final, con criterio en cada capa.

product-strategy-for-engineers-guide es la dirección opuesta: hacia arriba, no hacia el código. Esta guía completa —de hecho, el ecosistema completo de Product Engineering— trabajó siempre sobre una apuesta ya decidida: recommendations. Ninguna de las cuatro guías contestó la pregunta de nivel más alto: ¿por qué esta apuesta, y no otra, entre todas las que Mercado podría haber perseguido? ¿Cómo se conecta el resultado de recommendations con la estrategia de negocio completa de la empresa? Si terminaste esta guía preguntándote eso, product-strategy-for-engineers-guide es el siguiente paso natural.

Cualquiera de las dos direcciones que elijas, llevas contigo algo que ninguna guía por separado te habría dado: la experiencia completa de tomar un resultado real, con una tensión sin resolver en el centro —un ganador que rompe un guardrail—, y resolverla con proceso, no con suerte.

Recursos

  • Google SRE Workbook, Capítulo 16, "Canarying Releases" — sre.google/workbook/canarying-releases. La referencia formal completa del rollout gradual que esta guía entera ejecutó, dos veces, sobre el mismo caso. En inglés.
  • Google SRE Book, Capítulo 14, "Managing Incidents", y Capítulo 15, "Postmortem Culture" — sre.google/sre-book. Los dos capítulos que sostienen, formalmente, la disciplina de respuesta a incidentes y aprendizaje blameless que este capstone ejecutó de punta a punta. En inglés.
  • Chip Huyen, Designing Machine Learning Systems, Capítulo 9 — huyenchip.com/machine-learning-systems-design. La referencia formal sobre shadow deployment de modelos que sostiene la migración de v1 a v2 de este proyecto. En inglés.
  • Ron Kohavi, Diane Tang y Ya Xu, Trustworthy Online Controlled Experimentsexperimentguide.com. El libro de referencia de la guía de métricas, cuya discusión sobre el efecto de novedad sostiene la verificación de durabilidad de este capstone. En inglés.