Saltar al contenido
Development

Analítica que pregunta primero: construir una biblioteca de rastreo solo por consentimiento

Por Victor Da Luz
astroprivacyanalyticsdev-logastro-tools

Quiero saber si alguien lee mis sitios. También creo que el rastreo debería ser por consentimiento explícito, incluso cuando la ley diga que no hace falta. Umami autoalojado no usa cookies y está exento de aviso de banner, así que legalmente podría simplemente agregar el snippet y seguir adelante. No quise hacerlo. Si el visitante nunca dice que sí, mi script de analítica no tiene nada que hacer en su página.

Así que construí una pequeña biblioteca que convierte el valor por defecto ético en el valor por defecto fácil: astro-opt-in-analytics. El script de rastreo no existe hasta que das tu consentimiento. No está desactivado. No está en pausa. No existe.

El rastreador que nunca se instaló

La fase de investigación reveló algo curioso. Yo había asumido que vdaluz.com ya tenía rastreo activo, porque desplegué un servidor de Umami exactamente para eso hace meses. El repo del sitio no tenía ni una sola referencia a Umami. El HTML en producción lo confirmó: cuatro etiquetas script, una de ellas un esquema JSON-LD, ninguna de ellas un rastreador. Había desplegado el servidor de analítica y después nunca agregué el snippet al sitio. El ticket para hacerlo ni siquiera existía.

Ese accidente se volvió una ventaja. Nada que migrar, ningún usuario que hubiera rechazado el rastreo para convertir. Ambos sitios reciben rastreo por consentimiento desde su primer visitante rastreado.

Por qué no usé una plataforma de consentimiento

Klaro y vanilla-cookieconsent son buenos gestores de consentimiento de código abierto, y consideré ambos en serio. Resuelven un problema distinto: cumplimiento de cookies multi-proveedor con granularidad por servicio, un marco legal propio, y su propia interfaz modal. Mi problema es un rastreador sin cookies y una pregunta binaria. Envolver un CMP habría costado más código de integración que escribir toda la puerta de consentimiento entera. La biblioteca terminó en unas 470 líneas contando los dos componentes.

El diseño

Dos componentes de Astro y un runtime pequeño. ConsentGate lee un registro versionado de localStorage: una concesión inyecta la etiqueta script del rastreador, cualquier otra cosa significa que la etiqueta nunca existe. ConsentPrompt es un toast fijo que solo aparece cuando no hay una decisión tomada. La respuesta se guarda como { v, decision, at }; subir la versión en la configuración cuando cambia el alcance del rastreo hace que las decisiones anteriores vuelvan a preguntarse.

Hay dos decisiones sobre las que tengo una postura firme.

Global Privacy Control es una respuesta. Si navigator.globalPrivacyControl es verdadero, el visitante ya dijo que no a través de su navegador. El aviso nunca se muestra. Pedirle a alguien que reconsidere una preferencia que ya configuró de forma global es en sí mismo un patrón oscuro.

Las reglas contra patrones oscuros están escritas directamente en el código. Botones de igual peso visual gracias a una sola regla de CSS. “Rechazar” primero en el orden del DOM. Esc significa no. Cerrar el aviso significa no. Sin overlay, sin bloqueo de scroll. Nada de esto es configurable. Para un aviso de consentimiento donde “aceptar” brille y “rechazar” se esconda en un submenú de configuración, hace falta un fork, y el README va a juzgar esa decisión.

Probar flujos de consentimiento con nada más que Chrome headless

No hay Playwright en esta máquina, y los flujos de consentimiento son justo el tipo de cosa que necesita verificación de varios pasos: decidir, reiniciar el navegador, comprobar que la decisión se mantuvo. El truco que lo hizo funcionar: --user-data-dir mantiene localStorage entre invocaciones headless. Cada “usuario” de prueba es un directorio de perfil. La página de prueba se maneja sola, lee un parámetro de consulta ?e2e=accept, hace clic en sus propios botones, y estampa el estado observable en el DOM como JSON para que --dump-dom lo capture.

Simular la señal GPC también resultó fácil. Un script inline en el head corre antes que cualquier módulo empaquetado, así que Object.defineProperty(navigator, 'globalPrivacyControl', ...) detrás de un parámetro de consulta lo logra sin necesidad de extensión alguna.

Siete flujos, todos en verde: un visitante nuevo ve el aviso y cero ejecución de rastreador; aceptar inyecta y corre el script de inmediato sin recargar; la concesión sobrevive un reinicio del navegador; rechazar no carga nada, para siempre; GPC significa sin aviso y sin rastreador; el enlace del pie de página reabre el aviso después de un rechazo; Esc cuenta como no. El fixture del rastreador falso pone una bandera en window para que las pruebas verifiquen que el script realmente se ejecutó, no solo que la etiqueta existía.

Una trampa para anotar: Chrome headless imprime el DOM y después se queda dando vueltas, reteniendo el bloqueo del perfil y haciendo fallar la siguiente corrida con SingletonLock: File exists. La solución es --timeout más matar los procesos sobrantes entre corridas, y si se van a matar por patrón desde un comando de shell compuesto, pkill -f "profile[-]a" para que el patrón no coincida con su propia línea de comando.

Publicado

La v0.1.0 está etiquetada y es pública, MIT con un archivo LICENSE real desde el primer día (el paquete del blog se lanzó sin uno durante meses; lección aprendida). Ambos sitios la consumen ahora como dependencias de tarball fijadas por versión. La idea completa cabe en una frase: cero solicitudes hasta que se dé el sí.

Lecturas relacionadas