Traccia
Un binario que sirve el script de tracking, la API, el dashboard y el panel de admin. Los plugins son JavaScript que corre dentro del binario: mutan o descartan eventos antes de guardarse y declaran paneles que el servidor renderiza. Ni una línea de ese JS llega al navegador.
Los plugins de Traccia se escriben en JavaScript. Ni una línea llega al navegador.
Analytics web self-hosted en un binario Go y un Postgres. Para extenderlo dejas un .js en una carpeta: corre dentro del binario, engancha cada evento antes de guardarse y declara paneles que el dashboard pinta en el servidor. Sin Node, sin fork, sin proceso aparte.
Un evento viaja de tu página al dashboard en un solo proceso
Pulsa los pasos o deja que avance. Cada paso ilumina la pieza de la que habla.
La instalación mínima: una etiqueta y, si quieres, un atributo en el botón que te interesa.
El sistema de plugins entero entró sin tocar un caso de uso
Hexagonal de verdad tiene una prueba sencilla: la pieza más invasiva del producto se enchufa desde fuera. Aquí es un decorador del port EventRepository.
ports.EventRepository y no saben que los plugins existen. Cambiar el storage es implementar la interfaz, cablearla en cmd/api y recompilar; hoy solo existe el adapter de Postgres, SQLite y ClickHouse están en el roadmap.Así se ve un plugin: un fichero, dos ganchos. Uno corre en cada evento, el otro una vez al arrancar.
metadata->>'from_currency'. Cifras de ejemplo.De una calculadora de divisas a un panel propio
Un caso inventado con números pequeños, para ver qué pasa en cada tramo antes de hablar de los retos.
/admin, copia el project_id a la etiqueta script y guarda la api_key que solo verá una vez. Los pageviews empiezan a contarse solos.traccia.track("calculator_used", { from_currency: "USD" }). El servidor guarda la IP como 203.0.113.0, nunca la real.calculadora.js en la carpeta de plugins y reinicia. registerPanel declara "Uso por moneda" agrupado por from_currency. El dashboard ya muestra la tabla: USD 312, VES 187, EUR 41 en la semana.traccia.trackError("rate_unavailable"). El onEvent del mismo plugin lo ve antes de guardarse y hace http.post al webhook de Slack. El evento se guarda igual; el aviso llega en segundos.Cinco veces en que la solución obvia estaba mal
Cada reto sigue el mismo patrón: lo que cualquiera haría primero, por qué no sirve, lo que se hizo y el número que lo demuestra. El detalle técnico queda plegado.
Esa frase decide el resto: dashboard renderizado en el servidor, paneles declarativos, assets vendoreados y embebidos, un sandbox de tres objetos. Es más simple que un plugin-server y más accesible que WASM, y es lo que separa a Traccia de un producto que se extiende con un fork.
Un solo filtro: ¿esto rompe la historia de un binario y un Postgres?
Si la respuesta era sí, se buscó otra forma. Si no la había, se escribió como límite en la documentación.
docker compose up. Traccia empezó como la primera pieza de un backend de utilidades compartidas y pivotó a producto: API, dashboard, admin y plugins.Profundizar
El script embebido y servido en /t.js es parte de la misma historia: un binario que trae su propio SDK, como GoatCounter. El mismo fichero es el paquete npm bajo sdk/js, sin build step en ninguno de los dos caminos. Cada tag publica binarios para linux y darwin, amd64 y arm64, y una imagen multi-arch en ghcr.io que corre como usuario sin privilegios.
onEvent y registerPanel, y el sandbox es cerrado por construcción: solo existe lo que el servidor le pone al runtime.Profundizar
WASM daría aislamiento más fuerte y varios lenguajes, a cambio de que escribir un plugin exija una toolchain. Un plugin-server como el de PostHog escala mejor, a cambio de otro proceso. Para un producto cuyo argumento es "un binario", un intérprete embebido es el punto medio.
kv de los plugins. Las migraciones son SQL plano que Postgres aplica en el primer arranque; después el proceso no ejecuta nada por su cuenta. La copia de seguridad es un pg_dump -Fc.Profundizar
make backup escribe un dump comprimido y portable entre versiones de Postgres; make restore es destructivo y lo dice. No hay calendario automático: quien quiera uno lo pone en su cron. El límite por IP es en memoria y de un solo nodo; está declarado en el README como tal.
internal/domain importa solo time. Todo lo que necesitan los 17 casos de uso es una interfaz en internal/ports con un adapter por defecto. MIT prioriza adopción sobre proteger el proyecto de una reventa como SaaS, que es lo que AGPL intenta.Profundizar
El modelo de eventos es track(nombre, metadata) más identify(rasgos), el mismo patrón que GA4, Segment y PostHog. Visitor.Name es un campo propio y no una propiedad más, para poder excluir el tráfico del dueño con exclude_named. Tabla de ports y adapters en el README.
Cinco decisiones que hoy tomaría al revés
Recap
pg_dump..js en una carpeta: engancha cada evento antes de guardarse y declara paneles. Corre dentro del binario con 100 ms de presupuesto, y un plugin roto nunca pierde un evento.time y el sistema de plugins entero es un decorador de un port. Los casos de uso no saben que existe.