Módulo 7: Shipping Ai Safely
Privacidad y datos al lanzar: qué se registra, y qué no hacía falta
Descripción
Cada lección de este módulo, hasta ahora, generó registros: las salidas de recs-v1 y recs-v2 en shadow (lección 3 y 4), las llamadas repetidas de un incidente (lección 5). En ningún momento se preguntó, todavía, qué tan detallados deberían ser esos registros. Esta lección contesta esa pregunta: cuando shadow logging, canary o un postmortem necesitan guardar datos del comprador para funcionar, la disciplina correcta no es "guarda todo, por si acaso hace falta después" — es data minimization: registra únicamente lo necesario para el propósito específico, nada más. Esta lección construye redactPII(), la función que aplica esa disciplina a un registro de log real.
Conexión con el módulo. El shadow mode de la lección 3 y el shadowCompare() de la lección 4 necesitan registrar las salidas de los dos modelos para poder compararlas — eso ya implica guardar algo sobre cada request. Esta lección no cambia esa necesidad; le pone un límite. redactPII() es la función que el proyecto de la lección 8 reutiliza, sin cambios, para definir exactamente qué campos de un registro de shadow logging llegan a persistirse y cuáles no.
Una analogía: la copia de la credencial, sobre el mostrador
Imagina un empleado en un mostrador de atención al cliente que, para verificar la identidad de cada persona que atiende, le pide su credencial y le saca una fotocopia — "por si acaso la necesitamos después". Con el tiempo, ese mostrador acumula una pila de fotocopias con nombres completos, direcciones, números de identificación, de cientos de clientes que nunca dieron un consentimiento explícito para que su credencial se guardara más allá del momento de la verificación. Ninguna de esas fotocopias era necesaria para resolver la consulta original de cada cliente — verificar que la persona frente al mostrador es quien dice ser no requiere conservar una copia permanente de su documento de identidad.
Un sistema que registra "todo el request" en un log de shadow logging, sin pensar qué campos hacen falta de verdad, es ese mostrador. El nombre completo del comprador, su dirección de envío, su número de teléfono —campos que probablemente llegaron en el request original por alguna razón operativa— terminan copiados en un log que existe para comparar recomendaciones de dos modelos, un propósito que no necesita saber quién es la persona, solo qué decidieron los modelos sobre su caso. Data minimization es la disciplina de no sacar esa fotocopia — de preguntarte, antes de guardar cualquier dato, "¿este campo específico hace falta para el propósito específico de este registro?", y de guardar solo lo que la respuesta confirma.
Ejemplo trabajado: redactPII() sobre un registro de shadow logging
Un ingeniero, configurando el logging de shadow mode para recs-v2, arma un registro con todo lo que tiene disponible en el request — sin pensar todavía qué hace falta de verdad para comparar recomendaciones. Vamos a construir redactPII(), la función que limpia ese registro antes de que se persista:
// pseudonymizeId: convierte un userId real en un identificador estable pero no
// reversible a simple vista -- el mismo estilo de hash deterministico que ya
// viste en isEnabled() (modulo 2), aplicado aca a minimizar datos de log.
function pseudonymizeId(id) {
let hash = 0;
for (let i = 0; i < id.length; i++) hash = (hash * 31 + id.charCodeAt(i)) % 100000;
return 'usr-' + String(hash).padStart(5, '0');
}
// redactPII: aplica data minimization a un registro de log.
// - FULLY_REMOVE: campos que el proposito de este log NO necesita, se eliminan.
// - MASK: campos que a veces hacen falta parcialmente (ej. soporte), se enmascaran.
// - userId: se pseudonimiza -- sigue siendo util para depurar (mismo id siempre
// produce el mismo pseudonimo), pero ya no identifica directamente a la persona.
function redactPII(record) {
const FULLY_REMOVE = ['fullName', 'shippingAddress', 'phoneNumber'];
const MASK = ['email'];
const clean = { ...record };
FULLY_REMOVE.forEach((field) => { delete clean[field]; });
MASK.forEach((field) => {
if (clean[field]) {
const [localPart, domain] = clean[field].split('@');
clean[field] = localPart.slice(0, 2) + '***@' + domain;
}
});
if (clean.userId) clean.userId = pseudonymizeId(clean.userId);
return clean;
}
const rawShadowLogRecord = {
requestId: 'req-cold-07',
userId: 'buyer-84213',
fullName: 'Marta Gonzalez Ibarra',
email: 'marta.gonzalez84@example.com',
shippingAddress: 'Av. Insurgentes Sur 1457, CDMX',
phoneNumber: '+52-55-1234-5678',
region: 'south',
oldTopRec: 'trending-general',
newTopRec: 'home',
modelVersions: { old: 'recs-v1', new: 'recs-v2' },
timestamp: '2026-07-22T14:03:11Z',
};
console.log('=== redactPII sobre un registro de log crudo ===\n');
console.log('--- ANTES (lo que alguien queria loggear) ---');
console.log(JSON.stringify(rawShadowLogRecord, null, 2));
console.log('\n--- DESPUES (lo que redactPII deja pasar) ---');
console.log(JSON.stringify(redactPII(rawShadowLogRecord), null, 2));
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== redactPII sobre un registro de log crudo ===
--- ANTES (lo que alguien queria loggear) ---
{
"requestId": "req-cold-07",
"userId": "buyer-84213",
"fullName": "Marta Gonzalez Ibarra",
"email": "marta.gonzalez84@example.com",
"shippingAddress": "Av. Insurgentes Sur 1457, CDMX",
"phoneNumber": "+52-55-1234-5678",
"region": "south",
"oldTopRec": "trending-general",
"newTopRec": "home",
"modelVersions": {
"old": "recs-v1",
"new": "recs-v2"
},
"timestamp": "2026-07-22T14:03:11Z"
}
--- DESPUES (lo que redactPII deja pasar) ---
{
"requestId": "req-cold-07",
"userId": "usr-19934",
"email": "ma***@example.com",
"region": "south",
"oldTopRec": "trending-general",
"newTopRec": "home",
"modelVersions": {
"old": "recs-v1",
"new": "recs-v2"
},
"timestamp": "2026-07-22T14:03:11Z"
}
Compara los dos registros con cuidado. fullName, shippingAddress y phoneNumber desaparecieron por completo — ninguno de los tres hace falta para comparar qué recomendó recs-v1 contra qué recomendó recs-v2, así que redactPII() no los enmascara ni los abrevia: los elimina. email se conserva, pero enmascarado (ma***@example.com) — un compromiso razonable si el equipo de soporte necesita, alguna vez, correlacionar un registro con un ticket de un comprador que escribió desde esa dirección, sin exponer el correo completo en cada log. Y userId —el campo que sí hace falta para poder decir "estos dos registros son del mismo comprador" al analizar patrones— no desaparece, pero tampoco queda como buyer-84213: se convierte en usr-19934, un pseudónimo estable (el mismo userId real siempre produce el mismo pseudónimo, así que sigue sirviendo para el análisis) pero que no revela, a quien solo ve el log, quién es la persona detrás de ese registro.
Los tres movimientos de redactPII(), y por qué son distintos
Vale la pena nombrar con precisión los tres movimientos que hace esta función, porque no son intercambiables — cada uno responde a una pregunta distinta sobre el dato.
Eliminar por completo (fullName, shippingAddress, phoneNumber) es la respuesta correcta cuando el propósito del log —comparar recomendaciones entre dos modelos— no necesita el campo bajo ninguna circunstancia razonable. Este es el corazón de data minimization: no es "ocultar datos sensibles", es no recolectarlos para este propósito en primer lugar. La pregunta que decide esto no es "¿es sensible este campo?" —lo es, casi cualquier dato personal lo es en algún grado— sino "¿el propósito específico de este registro lo necesita?".
Enmascarar parcialmente (email) es la respuesta cuando existe una razón operativa legítima para conservar parte del dato —correlacionar con soporte, en este ejemplo—, pero no la totalidad. Enmascarar no es lo mismo que minimizar del todo: sigue siendo una decisión consciente sobre exactamente cuánta información conservar, no un permiso general para guardar el campo completo "por si sirve para algo más adelante".
Pseudonimizar (userId) es la respuesta cuando el propósito del log sí necesita poder distinguir "estos registros pertenecen a la misma persona" sin necesitar saber quién es esa persona. Es la diferencia entre identificación y correlación: usr-19934 te permite agrupar todos los registros de un mismo comprador para un análisis, pero no te dice su nombre ni cómo contactarlo sin acceso a una tabla de mapeo separada, guardada aparte y con sus propios controles de acceso.
Estos tres movimientos —eliminar, enmascarar, pseudonimizar— no son intercambiables entre sí: aplicar pseudonimización a fullName en vez de eliminarlo dejaría un dato que nadie necesita, solo un poco más disfrazado. La disciplina de data minimization empieza, siempre, con la pregunta de si el campo hace falta en absoluto — y solo para los que sí, decide entre conservarlo completo, enmascarado, o pseudonimizado.
Errores comunes
Loggear el request completo "por si acaso" hace falta después. Qué pasa: un ingeniero, sin pensar demasiado, serializa el objeto completo del request original —con todos sus campos, incluidos los que llegaron ahí por razones completamente ajenas al shadow logging— directamente al log de comparación de modelos. Por qué pasa: guardar todo es más simple de programar que decidir campo por campo qué hace falta, y "por si acaso lo necesitamos" se siente como una precaución razonable, no como un riesgo. Cómo detectarlo: si nadie puede justificar, campo por campo, para qué sirve cada dato guardado en un log específico, probablemente hay más de lo necesario. Cómo corregirlo: como en el ejemplo de esta lección, define explícitamente qué campos hacen falta para el propósito del log —comparar recomendaciones, en este caso— y elimina el resto, en vez de guardar todo por default y decidir después.
Asumir que pseudonimizar el userId resuelve todo el problema de privacidad, sin revisar los demás campos. Qué pasa: el equipo aplica una técnica de pseudonimización cuidadosa al identificador principal, se siente satisfecho con esa mejora, y no revisa si el resto del registro —nombre, dirección, teléfono— sigue viajando en texto plano junto a ese mismo pseudónimo. Por qué pasa: pseudonimizar el identificador principal se siente como "resolver la privacidad" en un solo movimiento, y es fácil no notar que otros campos del mismo registro pueden re-identificar a la persona de todos modos. Cómo detectarlo: si un registro tiene un userId pseudonimizado pero junto a él sigue viajando un nombre completo o una dirección, la pseudonimización del identificador no sirvió de mucho — cualquiera con acceso al log puede identificar a la persona por esos otros campos. Cómo corregirlo: revisa cada campo del registro por separado, como hace redactPII() en el ejemplo — pseudonimizar un campo no exime de aplicar la disciplina correcta a los demás.
Tratar el consentimiento del comprador para el servicio como consentimiento implícito para cualquier uso de sus datos. Qué pasa: el equipo asume que, como el comprador aceptó los términos generales de Mercado al registrarse, eso cubre automáticamente cualquier nuevo uso de sus datos —como guardarlos en un log de comparación de modelos que ni existía cuando se registró—. Por qué pasa: un consentimiento general, dado una sola vez al principio de la relación con el comprador, se siente como una autorización amplia que cubre lo que venga después, aunque los principios de privacidad que sostienen regulaciones como el GDPR distingan entre el propósito original de la recolección y usos nuevos no contemplados en ese momento. Cómo detectarlo: si un uso nuevo de datos del comprador —como este log de shadow— no se puede explicar en términos del propósito que el comprador aceptó originalmente, ese consentimiento probablemente no lo cubre. Cómo corregirlo: cualquier uso nuevo y no evidente de datos personales merece su propia revisión de si el consentimiento existente lo cubre, en vez de asumir que un consentimiento genérico ya resolvió la pregunta — un tema que, a nivel legal y de política, típicamente involucra al equipo legal o de privacidad de la empresa, no solo a ingeniería.
Ejercicios
Ejercicio 1 — Clasifica un campo nuevo. El registro de shadow logging de Mercado también incluye, a veces, lastFourCardDigits (los últimos cuatro dígitos de la tarjeta usada en la compra más reciente del comprador). Usando el criterio de esta lección, ¿debería redactPII() eliminarlo, enmascararlo, o dejarlo pasar sin cambios, dado que el propósito del log es comparar recomendaciones entre modelos?
Ver solución
Debería eliminarse por completo — pertenece a FULLY_REMOVE, junto a fullName, shippingAddress y phoneNumber. El propósito del log es comparar qué recomendó recs-v1 contra qué recomendó recs-v2; ningún aspecto de esa comparación necesita saber nada sobre el método de pago del comprador, ni siquiera de forma parcial. Este es un buen ejemplo del principio central de la lección: la sensibilidad del dato no es lo que decide la respuesta —los últimos cuatro dígitos de una tarjeta parecen, a primera vista, "ya bastante enmascarados"—; lo que decide es si el propósito específico del log lo necesita, y en este caso, no lo necesita en absoluto.
Ejercicio 2 — Encuentra el problema de diseño. Un compañero propone una versión de redactPII() que pseudonimiza fullName de la misma forma que userId (con un hash determinista), en vez de eliminarlo. ¿Por qué esto no resuelve el problema de la forma correcta, aunque técnicamente "oculte" el nombre?
Ver solución
Pseudonimizar fullName seguiría guardando un dato que el propósito del log —comparar recomendaciones— no necesita en absoluto; solo lo disfrazaría con un valor distinto, sin eliminar la pregunta de fondo: ¿para qué guardamos esto? A diferencia de userId, que sí cumple un propósito legítimo en el log (agrupar registros del mismo comprador para análisis), fullName no tiene ningún uso dentro del propósito de este log específico, pseudonimizado o no. La pseudonimización es la herramienta correcta para datos que hacen falta pero no necesitan identificar directamente a la persona — no es un sustituto genérico de la pregunta más básica de data minimization: ¿hace falta este campo, para empezar?
Ejercicio 3 — El caso del equipo de soporte. El equipo de soporte de Mercado argumenta que necesita el email completo, sin enmascarar, en los logs de shadow para poder ayudar más rápido a un comprador que escribe un ticket. ¿Cómo reconciliarías esa necesidad real con el principio de data minimization de esta lección, sin simplemente negarles el acceso?
Ver solución
Una reconciliación razonable no es "todos ven el email completo en todos los logs" ni "nadie lo ve nunca" — es separar el acceso según el propósito: el log de shadow logging que usa el equipo de ingeniería para comparar modelos puede seguir enmascarado (como en el ejemplo de esta lección, ma***@example.com es suficiente para confirmar de qué dirección se trata sin exponerla completa), mientras que el equipo de soporte, cuando de verdad necesita resolver un ticket específico, accede al email completo a través de un sistema separado y con permisos propios —el sistema de tickets, no el log de comparación de modelos—, donde ese acceso completo sí tiene un propósito claro y auditable. La solución de data minimization no es negar el dato a quien lo necesita de verdad; es no exponerlo, sin necesidad, a quien no lo necesita para su propósito específico.
Resumen y siguiente paso
En esta lección construiste redactPII() y la corriste sobre un registro real de shadow logging: eliminó por completo tres campos que el propósito del log no necesitaba (fullName, shippingAddress, phoneNumber), enmascaró parcialmente uno que sí tenía un uso operativo legítimo (email), y pseudonimizó el identificador que hacía falta conservar para el análisis (userId, de buyer-84213 a usr-19934). Confirmaste la idea central de data minimization: la pregunta correcta no es "¿es sensible este dato?" sino "¿el propósito específico de este registro lo necesita?".
Antes de avanzar deberías poder: distinguir entre eliminar, enmascarar y pseudonimizar, y explicar cuándo aplica cada uno; aplicar el criterio de "¿el propósito lo necesita?" a un campo nuevo que no viste en el ejemplo; y explicar por qué un consentimiento general no cubre automáticamente cualquier uso nuevo de datos personales.
La lección 7 junta todo lo que este módulo construyó —shadow mode, tasa de acuerdo, postmortems probabilísticos, minimización de datos— en un proceso completo de migración, de principio a fin, con una función de decisión que cruza el resultado de shadowCompare() contra umbrales explícitos antes de autorizar un canary.
Recursos
- Reglamento General de Protección de Datos (GDPR), Artículo 5 — gdpr-info.eu/art-5-gdpr. El texto legal donde se define el principio de minimización de datos: los datos personales deben ser "adecuados, pertinentes y limitados a lo necesario en relación con los fines para los que se tratan" — la definición formal del criterio que
redactPII()aplica en código. En inglés. - OWASP, "Logging Cheat Sheet" — cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html. Guía práctica de ingeniería sobre qué evitar loggear y cómo tratar datos sensibles en logs de aplicación — el nivel de detalle de implementación que complementa el principio legal del GDPR. En inglés.
- Oficina del Fiscal General de California, "California Consumer Privacy Act (CCPA)" — oag.ca.gov/privacy/ccpa. El equivalente estadounidense al GDPR en cuanto a derechos del consumidor sobre sus datos personales — útil para contrastar cómo dos marcos regulatorios distintos llegan a principios prácticos similares. En inglés.