
Pyxis: análisis de uso sin cookies, sin IP y sin datos personales (y por qué hice el mío)
Mientras lees este texto, esta página se está midiendo. Cuando se abrió, salió un page_view con la ruta de la página (y, si fue la primera de tu visita, el dominio desde el que llegaste) y un article_viewed con el post y el idioma. Cuando pases un cuarto, la mitad, tres cuartos y el final, sale un article_read_25, _50, _75 y _100, cada uno con cuántos segundos estuvo visible la pestaña hasta ese momento. Si haces clic en un elemento del índice o copias un bloque de código, sale uno más. Eso es todo. La medición no guardó ninguna cookie en tu navegador (si no iniciaste sesión para comentar, la única que vas a encontrar es NEXT_LOCALE, que guarda el idioma del sitio), tu dirección IP no se guardó en ningún lugar, y cuando cierres esta pestaña nada va a unir esta visita con la siguiente.
Se puede comprobar. Abre las herramientas del navegador en la pestaña Red y filtra por batch: los eventos salen en lotes, cada cinco segundos o cuando la pestaña queda oculta, en un POST a api.pyxis-analytics.dev/v1/batch con un cuerpo en texto que puedes leer entero. En el pie del blog hay un interruptor que desactiva la medición en este navegador. Y si tu navegador envía la señal Global Privacy Control o Do Not Track, nada de esto sale.
La herramienta que hace esto es Pyxis, que escribí yo: tres repositorios públicos, en producción desde el 6 de octubre de 2026, midiendo BolsoVerde y, desde el 10 de octubre, este blog, que cambió Google Analytics por él. Este post cuenta por qué lo construí en lugar de instalar algo, qué guarda, qué nunca guarda y cómo cada promesa de privacidad se convirtió en una función con test. Las versiones citadas son las de hoy, 13 de octubre de 2026: el rastreador pyxis-analytics 0.2.1, la API 0.13.0 y el panel 0.15.0.
El problema: visitas de anuncios y ninguna idea de lo que hacían
BolsoVerde es una app para que pequeños vendedores controlen ventas, comisiones y liquidaciones. Cuando empezó a recibir visitas de anuncios, no tenía forma de saber qué hacían esas personas: si usaban la calculadora de la página de inicio, si abrían el registro, si se detenían en los Términos, si estaban en el móvil. Dentro de la app, la misma ceguera esperaba a los primeros usuarios: qué pantallas se usan, dónde falla una escritura, en qué punto alguien se rinde.
Eran siete preguntas, todas de producto:
- ¿Quién visitó cada página y de dónde vino (anuncio, buscador, enlace directo)?
- ¿Cuántos llegaron a ver un resultado de la calculadora?
- ¿Cuántos abrieron el registro, cuántos lo enviaron, cuántos lo terminaron?
- ¿Qué funciones de la app se usan de verdad?
- ¿Qué peticiones fallan, cuántas veces y en qué pantalla?
- ¿Qué hizo una persona, en orden?
- ¿Móvil u ordenador, qué navegador, qué sistema?
Por qué construir en lugar de instalar
La respuesta habitual es instalar Google Analytics, o una herramienta de análisis de producto como PostHog. Las dos responden a casi todo eso. El coste viene incluido: por defecto, guardan en el navegador un identificador que reconoce a la persona durante meses, y los datos van a otra empresa. Para una app que maneja el dinero de pequeños vendedores, eso significaba una empresa más en la Política de Privacidad y una medición que, en la lectura más común de la ley, dependería del consentimiento: solo contaría a quien aceptara el banner, y yo estaría mirando una muestra que se eligió sola.
También existen herramientas de análisis sin cookies, algunas de código abierto, para alojar en tu propio servidor. Podría haber usado una de ellas. Construí Pyxis por tres motivos:
- Preguntas de producto, no solo de tráfico. Quería ver una escritura fallida junto con la pantalla de la que vino, el embudo desde el anuncio hasta la primera venta, y la línea de tiempo de una persona empezando en la visita anónima de antes del registro. Y quería que todo eso se borrara solo cuando se elimina la cuenta.
- Ninguna empresa nueva recibiendo datos. Pyxis corre en la misma AWS, el mismo Neon y la misma Vercel que BolsoVerde ya usaba y que su Política ya mencionaba.
- Quería decidir cada regla. Qué sale del navegador, cuánto tiempo se guarda, qué nunca se lee. En una herramienta lista para usar, esas reglas son de otra persona. Aquí son funciones que escribí, probé y publiqué, y cualquiera puede auditarlas.
Y hay un cuarto, el más honesto: quería saber construir esto, y quería que el resultado fuera público.
Qué guarda Pyxis, y qué nunca sale del navegador
El punto de partida fue una lista de lo que no puede existir en la base de datos. El resto del diseño salió de ella.
| Qué | Qué pasa |
|---|---|
| Cookie | Ninguna. La visita es un id aleatorio en sessionStorage: muere cuando se cierra la pestaña y se cambia después de 30 minutos sin actividad. Dos pestañas son dos visitas |
| Dirección de la página | Reducida a una plantilla en el navegador: /vendas/9f1c… pasa a ser /vendas/:id, y los correos y las secuencias largas de dígitos nunca salen. La API vuelve a aplicar la plantilla de su lado |
| Query string | Descartada, salvo utm_source, utm_medium y utm_campaign. El código de clic de un anuncio queda solo como from_ad_click: true |
| Dirección IP | Leída en el momento para limitar abusos, y descartada. No se guarda nada, ni siquiera un hash. País y estado vienen de cabeceras que CloudFront deduce de la IP; la ciudad nunca se lee |
| User agent | Clasificado en tipo de dispositivo, familia del navegador y sistema, y descartado. Los robots que se anuncian como tales se rechazan |
| Propiedades de los eventos | Una barrera en el servidor descarta cualquier valor con aspecto de correo, teléfono o identificación fiscal brasileña (CPF, CNPJ) |
| "No rastrear" | Global Privacy Control y Do Not Track apagan el rastreador antes de que algo entre en la cola |
| Opt-out | optOut() guarda un único marcador en localStorage, lo único que el rastreador conserva más allá de la pestaña |
| Plazo | 13 meses. Un job diario borra lo que pasó de ahí |
| Persona identificada | Solo por el id interno del sitio, nunca por el correo. Una llamada borra sus eventos, incluidos los de la visita anónima en la que inició sesión, y otra los exporta |
Este conjunto tiene un precio, y prefiero decir cuál: Pyxis no sabe que la persona que usó la calculadora ayer es la misma que creó la cuenta hoy. Sin un identificador que dure, dos visitas anónimas nunca se unen. Una persona solo pasa a tener historial cuando inicia sesión, y desde ahí por el id interno del sitio. Fue una elección: la alternativa era un id que dura meses en el navegador, que es justo lo que yo no quería guardar.
Las reglas son código, no texto de una política
Una política de privacidad promete. Quien cumple es el código, y se puede demostrar que cumple cuando cada regla vive en una función pequeña, pura y probada. La que decide, en el rastreador, si un tramo de la ruta es un identificador es esta:
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const ALL_DIGITS = /^\d+$/;
const DIGIT = /\d/g;
const ENCODED_AT_SIGN = /%40/i;
const MIN_ID_DIGITS = 10;
function isIdentifier(segment: string): boolean {
return (
UUID_PATTERN.test(segment) ||
ALL_DIGITS.test(segment) ||
(segment.match(DIGIT)?.length ?? 0) >= MIN_ID_DIGITS ||
segment.includes('@') ||
ENCODED_AT_SIGN.test(segment)
);
}
templatePath la aplica a cada tramo y cambia lo que sea identificador por :id, después de probar las reglas que declaró el sitio (pathRules: ['/blog/:slug']). Los tests dicen la regla en voz alta: turns /vendas/9f1c… into /vendas/:id, turns /u/joao@x.com into /u/:id, handles a path made of thousands of slashes in linear time. El último existe porque la ruta es una entrada de quien visita: una regla de privacidad que se cuelga con una URL maliciosa también es un problema.
El "no rastrear" es lo primero que consulta el rastreador:
export function trackingStatusOf(signals: PrivacySignals): TrackingStatus {
if (signals.globalPrivacyControl || signals.doNotTrack) {
return 'blocked-by-browser';
}
return signals.optedOut ? 'opted-out' : 'on';
}
La señal del navegador gana a la elección hecha en la página, a propósito: un interruptor en el pie de un sitio no puede volver a encender un "no rastrear" que la persona activó en todo el navegador.
El rastreador es solo la primera capa. La clave pública de un proyecto está en el HTML del sitio, como toda clave pública, y cualquiera puede enviar un lote directo a la API sin pasar por el rastreador. Por eso la API vuelve a aplicar la plantilla a cada ruta con las mismas reglas, descarta propiedades con aspecto de dato personal y rechaza el evento entero cuando el id de usuario parece un correo o un documento. El navegador es educado; el servidor desconfía.
Los tres repositorios están al 100% de cobertura de sentencias, ramas, funciones y líneas, y el CI falla por debajo de eso. La cobertura no prueba que un test sea correcto, pero garantiza que ninguna regla se quedó sin que nadie la mirara.
Cómo es por dentro
navegador AWS, São Paulo panel
pyxis-analytics ── lote ──▶ CloudFront ◀───── consultas ──────── Next.js en Vercel
ruta como plantilla │ login con código
sin cookie ▼ sesión opaca
Lambda (NestJS)
valida cada evento,
descarta IP y user agent
│
▼
Postgres (Neon)
El rastreador (pyxis-analytics en npm) pesa cerca de 4,8 KB con gzip en la versión 0.2.1, dentro de un presupuesto de 5 KB que el CI mide en cada push, y no tiene dependencias. Las reglas viven en un núcleo puro, sin window ni document; el navegador entra por adaptadores pequeños (transporte, almacenamiento, History API, reloj) que los tests cambian por versiones falsas. El lote sale como text/plain y con credentials: "omit": así el navegador no hace la petición de preflight de CORS, sendBeacon funciona cuando la pestaña se oculta, y ninguna cookie de ningún dominio viaja con él. Cada versión se publica desde GitHub Actions con procedencia (provenance) en npm, así que se puede comprobar de qué commit salió el paquete que instalaste.
La API es NestJS sobre Fastify, en un AWS Lambda detrás de CloudFront, en la región de São Paulo, con Postgres en Neon y la infraestructura en Terraform. Tres de sus decisiones:
- Cada evento se valida por separado. Un evento inválido no tumba el lote: la respuesta es
202con cuántos se aceptaron, cuántos eran repetidos y cuántos se rechazaron. Un evento reenviado se reconoce y nunca se guarda dos veces. - Límite de tasa sin guardar la IP. Son 120 lotes por minuto por dirección y 3.000 por proyecto, contados en memoria en cada entorno de ejecución; ningún contador va a la base de datos. La dirección viene de una cabecera de CloudFront, que solo es fiable porque CloudFront envía también un secreto que la función exige antes de hacer cualquier otra cosa.
X-Forwarded-Forse ignora. - El panel lee los eventos en bruto, sin tablas preagregadas. En un Postgres local con un millón de eventos de un proyecto, la vista general de 30 días responde en cerca de medio segundo. Para el tamaño de los proyectos que mide Pyxis, un job de agregación sería complejidad sin retorno.
En una prueba de carga a través de CloudFront, 10.000 eventos en diez minutos se aceptaron sin ningún error, con un p50 de 104 ms y un p95 de 140 ms medidos en el cliente, y un p95 de 29 ms dentro del Lambda.
El panel es Next.js 16 en Vercel. El inicio de sesión es con un código de seis dígitos enviado por correo, que se cambia por una sesión opaca que el servidor puede revocar en cualquier momento, incluso en todos los dispositivos a la vez. El navegador solo ve una cookie HttpOnly del propio panel, escrita por una Server Action; quien habla con la API es el servidor de Next.js. Es el diseño que defendí en un post de julio sobre JWT y sesiones, ahora en un sistema en producción. Las pantallas son vista general, visitas, embudo, funcionalidades, peticiones, dispositivos, adquisición y la línea de tiempo de una persona, en inglés y portugués, y cada lunes llega por correo un resumen de la semana. Los gráficos son SVG dibujados en el servidor: ninguna librería de gráficos llega al navegador.
Qué se puede abrir ahora
- Demo del panel: las mismas pantallas que en producción, sin login, con los datos inventados de una tienda de ejemplo.
- Playground del rastreador: el rastreador real en modo seco, mostrando cada lote que enviaría, sin que ninguna petición salga de la página.
- Storybook del panel, con los componentes en sus estados, y cada historia probada también en accesibilidad.
- Referencia de la API, generada a partir del contrato OpenAPI en cada push.
- El código, con licencia MIT y las decisiones de arquitectura registradas en ADRs: pyxis-sdk, pyxis-api y pyxis-web. La integración de este blog también es pública, en vertex-web.
Qué mide BolsoVerde
Integrar un sitio es poca cosa: una clave pública, un init() en el layout y un track() en cada momento que importa.
import { init, track, identify } from 'pyxis-analytics';
init({
key: 'pyxis_pk_…',
endpoint: 'https://api.pyxis-analytics.dev',
pathRules: ['/vendas/:id'],
});
track('calculator_result_shown', { calculator: 'ifood' });
identify('id-interno-de-la-cuenta');
BolsoVerde envía las páginas vistas, por la plantilla de la ruta; acciones con nombre, como "vio un resultado de la calculadora", "hizo clic en Crear cuenta", "creó la cuenta" y "registró una venta"; el resultado de las escrituras (éxito o error, el código del error y cuánto tardó) y solo las lecturas que fallaron; y, después del login, el id interno de la cuenta. Nunca el correo, nunca un valor escrito.
Cuando alguien elimina su cuenta, el backend de BolsoVerde le pide a Pyxis que borre los eventos de esa persona, incluidos los de la visita anónima en la que inició sesión, y repite la petición hasta que sale bien. La copia que la persona descarga en "Descargar Mis Datos" incluye esos eventos. La Política de Privacidad nombra a Pyxis y describe exactamente eso, y el orden fue a propósito: la política se publicó antes de configurar la clave en producción. Una política nunca describe lo que no funciona, y el código nunca hace lo que la política no dice.
¿Sin banner? Mi lectura
La medición de Pyxis no pide consentimiento. En BolsoVerde, el banner de cookies que existe es el de los anuncios de Google, y Pyxis no pasa por él; en este blog no hay banner. La base legal que declara la Política de BolsoVerde es el interés legítimo de la LGPD, la ley brasileña de protección de datos, y el argumento es el diseño entero de este post: ninguna cookie, ningún identificador que reconozca a alguien otro día, IP y user agent nunca guardados, ninguna empresa de análisis externa recibiendo los datos, y, del otro lado, transparencia en la política, "no rastrear" respetado y una forma de desactivar la medición.
Esta es mi lectura, no un dictamen jurídico. Quien vaya a adoptar algo parecido debe consultarlo con quien sepa de protección de datos, y recordar que la conclusión vale para el conjunto, no para una pieza aislada.
Los límites
- Dos visitas anónimas nunca se unen, ni dos pestañas de la misma persona. Es el precio de la fila más importante de la tabla.
- Los conteos son un mínimo. Un bloqueador puede rechazar la petición, y quien activa el "no rastrear" no entra en la cuenta, como debe ser.
- Los robots que parecen navegadores pasan. Pyxis rechaza los que se anuncian como robots, pero uno que se presenta como un Chrome normal cuenta como visita.
- Está hecho para proyectos pequeños. Las consultas leen los eventos en bruto; un proyecto con millones de eventos al mes va a necesitar las tablas agregadas que todavía no escribí, y el archivo que define cada métrica ya dice qué tendría que contar cada una.
Algunas decisiones de Pyxis van a convertirse en posts propios: el bug que solo existía en Lambda, en el que Postgres devolvía una lista como texto y el panel mostraba una letra por fila; cómo el rastreador cabe en 5 KB; y por qué comparar "hoy" con "ayer" a las diez de la mañana mostraba una caída del 58% todos los días.
Si llegaste hasta aquí, esta página acaba de enviar un article_read_100, a menos que tu navegador haya pedido no ser rastreado.
Comentarios
Cargando comentarios...