Un cambio de token de una línea que encontró un botón roto y un vacío de GDPR
Parecía el ticket más pequeño posible: Cloudflare Web Analytics inyectaba automáticamente su beacon de rastreo en el edge, saltándose la barrera de consentimiento propia de este sitio. Desactivar la inyección automática en el dashboard, cambiar un token de marcador de posición por uno real en analytics.ts, listo. Un clic en el dashboard, un reemplazo de cadena.
En cambio, se convirtió en la cacería de bugs más productiva de este sitio en semanas, y ninguno de los dos errores encontrados era el que se estaba buscando.
El dashboard había cambiado de lugar
Empecé de memoria: Analytics & Logs, un interruptor, listo. El dashboard real no coincidía con nada de eso. Cloudflare había reorganizado la página de Web Analytics desde la última vez. En vez de volver a adivinar, fui a la documentación actual y encontré la ruta real: Manage RUM Settings → “Enable with JS Snippet installation.” Algo pequeño, pero es la diferencia entre estar seguro y equivocado, o simplemente confirmar qué es cierto antes de actuar.
El cambio de token en sí estaba bien. Verificarlo, no.
El cambio de código realmente fue de una línea: fuera la cadena de marcador de posición, adentro el token real, más un atributo type: "module" para igualar el formato actual del snippet de Cloudflare (en algún momento también habían cambiado de defer). astro check corrió limpio. Normalmente ahí se daría por terminado.
Pero este es un cambio relevante para la seguridad, un script se dispara sin consentimiento o no se dispara, así que corrí una pasada completa de browser-verify en vez de confiar en el chequeo de tipos. Primer intento, contra npm run dev: una violación de Content-Security-Policy, sin relación alguna con nada tocado. Segundo intento, contra un build real de producción (npm run build && npm run preview): una violación de CSP distinta. Misma categoría de error, dos causas diferentes, ninguna de las dos propia de este cambio.
La del servidor de desarrollo resultó ser una pista falsa: Astro empaqueta los scripts en línea de forma distinta en modo dev que en un build de producción, así que un hash de CSP calculado contra la salida de dev nunca va a coincidir con lo que realmente se publica. Lección anotada: nunca confiar en una verificación de CSP contra astro dev. Siempre hacer build y preview primero.
La que no era una pista falsa
La violación del build de producción no desaparecía. La rastreé hasta el manejador de clic en línea de GamesSection.astro, el script que revela el iframe del juego Planetary Pathways al presionar “Launch.” Su hash está fijado en public/_headers, definido una sola vez cuando se agregaron por primera vez los encabezados de seguridad. Un cambio de i18n sin relación en ese mismo componente, dos días antes, había alterado la salida compilada del script lo suficiente como para invalidar ese hash. Nada lo detectó, ni astro check, ni el build, ni ninguna prueba. El único síntoma era un error de CSP visible solo en la consola y un botón que no hacía nada al hacer clic.
Guardé el cambio de analítica con stash y reconstruí para confirmar que no era una conexión imaginada. Misma violación, main sin modificar. Confirmado: el botón de lanzar el juego había estado muerto en silencio en producción durante dos días, y la única forma de notarlo era abrir devtools en la página correcta en el momento correcto. Se registró como un bug aparte en vez de meter la corrección en un ticket sin relación, los dos problemas no tienen nada que ver entre sí, solo coincidieron en la misma pestaña del navegador.
Una pregunta que valía la pena hacerse dos veces
A mitad de camino, paré a hacerme una pregunta que venía postergando: ¿la librería de consentimiento maneja algo específico de la UE? El primer instinto fue buscar lógica de geodetección, no hay ninguna, por diseño. La respuesta de la librería a “algo específico de la UE” es “no hay nada específico de la UE, porque todos reciben opt-in por defecto,” lo cual es un estándar más exigente que el que cumplen la mayoría de los banners de consentimiento con detección geográfica.
Pero ese no era el vacío real. El estándar de consentimiento informado de GDPR espera que un visitante pueda ver qué se recopila y por qué, típicamente mediante un enlace a la política de privacidad ahí mismo en el aviso de consentimiento. La configuración del aviso de la librería tiene un mensaje, un botón de aceptar, un botón de rechazar, ningún campo para un enlace. Y imperfectsystems.com no tiene una política de privacidad de todo el sitio a la cual enlazar aunque lo tuviera. vdaluz.com sí tiene una, pero su propia configuración de consentimiento tampoco enlaza a ella, así que el vacío no es realmente de este sitio, está en la librería compartida de la que dependen ambos sitios. Se registró como su propio seguimiento en vez de intentar diseñar y publicar una corrección sobre la marcha.
Lo que seguiría haciendo
Nada de esto, la corrección del dashboard, el botón roto, el vacío de consentimiento, habría salido a la luz con un typecheck en verde. Cada uno necesitó un navegador real, solicitudes de red reales, y un build de producción real en vez del servidor de desarrollo. El ticket de una línea tomó la mayor parte de una sesión, pero la alternativa era publicar un “listo” que dejaba en silencio una función rota de dos días de antigüedad y nunca preguntaba si el aviso de privacidad era legalmente suficiente. Más lento, pero del tipo de lento que vale la pena.
Lecturas relacionadas
Convertir un hash de CSP en una salida del build en vez de mantenerlo a mano
Un botón de lanzamiento del juego se rompió en silencio dos días antes de que alguien lo notara. El arreglo no fue recalcular un hash, fue asegurarse de que nadie tuviera que volver a hacerlo.
El bug detrás del bug
Un widget de GitHub roto, rastreado hasta una demo del proyecto original en pausa, y, encontrado en el camino, una CSP fijada por hash invalidada en silencio por un arreglo de accesibilidad de una sola línea.
La suite de pruebas en verde que no probaba lo que yo pensaba que probaba
Un override de seguridad que pasó todas las verificaciones en local, y una llamada directa de cinco líneas que mostró que falla en tiempo de ejecución porque la forma de exportación del módulo cambió entre versiones mayores.