Todos los proyectos
Open Source · v0.1.0Proyecto 02
Error trackingSelf-hostedOpen Source

trapline

Un binario, un fichero SQLite y cuatro clientes de la misma API: REST, panel web, CLI y MCP. Diseñado para que un agente lea el error, parchee el código, corra los tests y lo cierre para la próxima release sin salir de tu máquina.

GoSQLite (sin CGO)FTS5MCPHexagonalAGPL-3.0

Sentry self-hosted pide 16 GB de RAM. trapline hace el trabajo con 19,7 MB.

Error tracking, tracing, uptime, crons y status page en un binario de 18 MB. Los SDKs oficiales de Sentry siguen igual: cambias el DSN y funciona.

Sentry self-hosted · mínimo que publica su repo16 GB
trapline · medido en reposo, gate nocturno19,7 MB
A escala real la barra de trapline mide el 0,12 % de la otra. Comparación de instalación pequeña: a escala de empresa grande esa pila hace cosas que aquí no existen.
19,7 MBRAM en reposo, perfil mínimo
611 ev/singesta medida en bench
6SDKs oficiales probados en CI
01 · Cómo funciona

Un error viaja del SDK al agente sin cambiar de proceso

Pulsa los pasos o deja que avance. Cada paso ilumina la pieza de la que habla.

Tu app
SDK oficial de Sentry
Ingesta
parser puro
SQLite
una transacción
/api/v1
una sola API
4 clientes
panel · CLI · REST · MCP
Paso 1 de 4 · avanza solo
Tu app lanza una excepción
El SDK oficial de Sentry la captura y la empaqueta en un envelope, el mismo formato de siempre. trapline no tiene SDK propio: reutiliza los seis que ya están probados en producción. Lo único que cambia en tu código es el DSN.

La versión mínima que funciona: no instalas un SDK nuevo, cambias una variable.

# antes
SENTRY_DSN=https://<clave>@o123.ingest.sentry.io/456
# después · ejemplo
SENTRY_DSN=https://<clave>@errores.tu-dominio.com/1
02 · Un ejemplo de punta a punta

Del throw al issue cerrado

Un caso inventado con números pequeños, para ver qué pasa en cada tramo antes de hablar de arquitectura.

1
14:02 · producción · release 2.4.0
Se rompe el checkout
Una app Node lanza TypeError: Cannot read properties of undefined (reading 'id') en checkout.js:42. El SDK oficial lo manda tal cual.
2
14:02 → 14:40
Tres eventos, un issue
La huella se calcula sobre el stacktrace normalizado, sin el directorio de la release ni números volátiles. Los tres eventos de la tarde caen en el mismo issue, no en tres. Cada deploy no estrena issues nuevos.
3
CLI o MCP
Un documento en vez de cinco llamadas
trapline issues bundle -project 1 -issue 17 devuelve un markdown: excepción, stacktrace symbolicado con contexto, breadcrumbs, tags agregados, frecuencia a 24 h y 14 d, y los commits sospechosos con la razón de la sospecha.
4
skill /fix-error
El agente escribe el test que falla, parchea y corre la suite
Lee el bundle antes de abrir ningún fichero. Cierra el issue como resuelto en la próxima release. Nunca hace push, nunca despliega: lo que llega a producción lo decide una persona.
5
release 2.4.1
Si vuelve, se reabre solo
La regla de reapertura vive en la misma transacción de ingesta: un evento del issue en una release posterior a la que lo resolvió lo reabre y avisa. Una regresión no se entierra bajo un "resuelto".
03 · Los retos

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.

01

La cifra que se vende, medida cada noche

Lo obvio
Arrancar el proceso, mirar cuánta RAM usa, publicar el número.
Por qué falla
Ese número es cierto solo con nadie usando el producto. El repositorio publicó tres cifras falsas: un login con Argon2id a 64 MiB subía la RSS de 9,6 a 142 MB y no volvía a bajar; el encoder de zstd reservaba estado por núcleo, 16 MB antes del primer evento; 200 logins simultáneos llevaban el proceso a 1,03 GB.
■ antes■ despuésRAM en el pico
En reposo, máquina de 24 hilos (zstd por núcleo → concurrencia 1)
33 MB
17 MB
200 logins a la vez (límite 10/min por IP + 2 hashes en vuelo)
1,03 GB
52,9 MB
Lo que se hizo
Se mide el pico (VmHWM, la marca de agua del kernel) y no una muestra de VmRSS, reiniciando la marca antes de cada medición. Un script lo mide y un gate nocturno lo vuelve a medir y falla si sube. Argon2id bajó al parámetro que recomienda OWASP y el proceso devuelve el working set tras la ráfaga.
19,7 MBen reposo con el perfil mínimo. 34,2 MB con todo encendido, 38,7 MB de pico ingiriendo.
Ver detalle técnico

El gate comprueba además que los seis jobs de fondo del perfil cargado están corriendo: un subsistema que no arrancó haría la cifra falsamente buena.

Resto de la huella, medida por scripts/footprint.sh: binario de 18,13 MB contra un presupuesto de 30 MB que rompe el build; 695 bytes por item en disco (86,1 MB para 130 000 items con payload zstd, índices, FTS5, buckets y sketches); 611 a 628 eventos/s de mediana en make bench con el umbral del gate en 150.

Medido en una sola máquina (WSL2, Linux 6.6, Go 1.26.6). Un número de una máquina es una anécdota; lo que lo convierte en dato es que se vuelve a medir cada noche.

02

Compatible con SDKs que no controlas

Lo obvio
Leer la documentación del protocolo y escribir envelopes de prueba a mano.
Por qué falla
Eso prueba la idea que el proyecto tiene de lo que manda un SDK, que es justo lo que está en duda. El protocolo es polimórfico: timestamps en cuatro formas, tags como objeto o como pares, excepciones envueltas o desnudas. Y el fallo más grave lo pasaron todos los tests: un middleware CSRF aplicado a la ingesta hacía que ningún SDK oficial pudiera entregar un evento.
Go
✓ cada PR
Python
✓ cada PR
Node
✓ cada PR
Browser
✓ cada noche
PHP
✓ cada noche
Dart
✓ cada noche
sentry-cli: su superficie /api/0/ se implementó grabando el tráfico de la herramienta real, no leyendo su documentación.
Lo que se hizo
Cada fila de la tabla de compatibilidad es una suite que manda errores reales con el SDK oficial, a la versión pineada, contra un servidor real, y comprueba qué llegó y cómo se agrupó. El parser es puro y está fuzzeado.
5,8 Mejecuciones de fuzzing sobre el parser sin un crash. 6 SDKs y sentry-cli en CI.
Ver detalle técnico

CSRF solo tiene sentido donde hay una credencial ambiente, la cookie del panel. La ingesta se autentica con una clave que el cliente presenta, así que el middleware no aplica ahí. Lo cazó la suite con SDKs reales, no los tests unitarios de la API (ADR 02).

La superficie /api/0/ que espera sentry-cli (releases new, set-commits, finalize, deploys new, sourcemaps upload) tiene los fixtures grabados commiteados en compat/sentry-cli/fixtures/ (ADR 13).

03

Mil peticiones, no una

Lo obvio
Poner topes por petición: 20 MiB por envelope, 4 MiB por item, 100 items, y un lector que corta la expansión de gzip y zstd.
Por qué falla
Todos existían y eran correctos, y ninguno respondía a la pregunta del atacante: cuántas peticiones pueden sostener esos topes a la vez. 128 bombas zstd, 151 KB en total, llevaban el proceso a 1,46 GB.
■ antes■ despuésMB de RAM en el pico
128 bombas gzip 976:1, concurrencia 32
649 MB
121 MB
128 bombas zstd 21 346:1, concurrencia 32
1,46 GB
93 MB
512 bombas zstd, concurrencia 128
crecía sin tope
121 MB
Lo que se hizo
Un presupuesto de memoria de 32 MiB compartido por todas las peticiones de ingesta en vuelo, reservado por adelantado para el descompresor. Debajo, dos limitadores por IP distintos: ingesta a 48 000 por minuto, aplicado antes de leer el body; autenticación a 10 por minuto, porque lo que se raciona son 19 MiB por intento.
+4 %de memoria al cuadruplicar la avalancha gzip (+30 % en zstd), no +400 %. El gate lo afirma como propiedad.
Ver detalle técnico

El 429 de ingesta va pelado, con Retry-After y nunca con X-Sentry-Rate-Limits: ese header hace que el SDK deje de enviar la categoría durante minutos, y eso convertiría una defensa en pérdida de datos (ADR 39).

La identidad del cliente la decide un paquete puro, internal/clientip: X-Forwarded-For solo se cree si el peer directo está en TrustedProxies, IPv6 se cuenta por /64 y el mapa está acotado a 16 384 entradas (ADR 23).

04

Percentiles sin guardar filas

Lo obvio
Guardar el p95 de cada minuto y promediar los sesenta para tener el de la hora.
Por qué falla
Los percentiles no se componen. El promedio de sesenta p95 no es el p95 de la hora, y un minuto con tres requests lentos pesa igual que uno con diez mil rápidos.
Minuto A · 3 requests
p95 = 900 ms
Minuto B · 10 000 requests
p95 = 40 ms
Promediar los p95
470 ms ✗
Tres requests lentos pesan igual que diez mil rápidos.
Fusionar los histogramas
≈ 40 ms ✓
10 003 requests, 3 lentos: el p95 real de los dos minutos juntos.
Lo que se hizo
Cada ventana guarda un histograma logarítmico tipo DDSketch cuya fusión es exacta: la hora construida plegando sesenta minutos es, bit a bit, la que esos requests habrían producido. El percentil se calcula al leer y nunca se persiste. Release health sigue la misma regla: cuatro contadores por (release, entorno, hora), nunca una fila por session.
1 %de error relativo en el sketch, con merge exacto. Cero filas por request y cero filas por session.
Ver detalle técnico

Contar sessions distintas exige estado: una ventana en memoria acotada a 50 000 entradas con TTL de una hora, volcada cada 60 s y al apagar. Un reinicio pierde lo que estaba en vuelo; cuando la ventana se llena se sacrifica precisión, nunca estabilidad. Las dos cosas se declaran al usuario en cada respuesta (ADR 07, ADR 20, ADR 08).

Guardar cada session por separado es otro producto, analítica de sesiones, que es donde el incumbente gasta su infraestructura. Está en la lista de lo que este producto no hará nunca.

05

Agent-first: el issue como documento

Lo obvio
Darle al agente la API REST y que se apañe.
Por qué falla
Para un solo bug hace cinco llamadas y de cada JSON vuelve a deducir la forma. Peor: un mensaje de excepción con un salto de línea, escrito por cualquiera que tenga un DSN, podría abrir un encabezado propio en un documento sobre el que el agente está a punto de actuar.
REST · 5 llamadas
issueeventstagscommitshistory
y de cada JSON deducir la forma otra vez
→
bundle · 1 documento
# TypeError: … (reading 'id') ## Stacktrace ## Breadcrumbs ## Tags · Frecuencia ## Suspect commits
Lo que se hizo
El bundle es un markdown determinista: todo mapa se ordena antes de escribirse, no lee ningún reloj, tiene fixtures dorados y todo valor que viene de un evento pasa por una función que colapsa saltos de línea. El servidor MCP va embebido en el binario y no llama a los casos de uso: cada tool declara una petición a /api/v1 con la credencial con la que llegó. Un token de solo lectura no puede resolver un issue por la misma línea que rechaza el POST.
10 toolsMCP por stdio o POST /mcp. El SDK oficial costó 2,03 MB de binario, medidos.
Ver detalle técnico

La skill /fix-error nunca marca resuelto algo cuyo test no pasa. Deuda consciente: el bundle no incluye el waterfall de una transaction lenta ni el crash-free de la release; son la respuesta a otra pregunta y tienen sus propias tools. Meterlo todo en un documento lo volvería el volcado que nadie lee entero (ADR 22, ADR 06).

"Un número de una sola máquina es una anécdota."
Cada cifra pública del producto tiene un script que la mide y un gate que la vuelve a medir: huella, tamaño de binario, eventos por segundo, compatibilidad con cada SDK, paridad de los cuatro clientes y hasta los bloques de código de la documentación, que se ejecutan contra un servidor real.
04 · Decisiones

Un solo filtro: ¿esto añade un proceso, una dependencia o memoria en reposo?

Si la respuesta era sí, se buscó la alternativa. Si no la había, se midió y se publicó con su gate.

WHY
¿Por qué SQLite y no un almacén columnar?
Porque el producto es un binario sin dependencias, y cada proceso extra erosiona la única razón para elegirlo. Lo que hace viable a SQLite son los agregados precalculados escritos en la transacción de ingesta: ninguna pantalla escanea payloads.
Profundizar

DuckDB o ClickHouse son la elección correcta si se mira solo el perfil de queries. DuckDB queda diferido, no descartado: el puerto de storage permite añadir un adapter columnar sin tocar dominio (ADR 01, ADR 04, ADR 10).

La medición ordena prioridades: con un evento realista de 14,6 KB, decodificar el JSON cuesta más que toda la transacción de SQLite (421,9 µs contra 790,0 µs por cuatro sentencias más de las que había). El escritor único convierte la concurrencia en cola: 8 clientes dan 667 a 1 090 ev/s frente a 705 a 747 con uno solo.

El ADR describía un batching de ingesta que nunca se implementó. El benchmark midió lo que valdría agrupar, ~1,5×, a cambio de una ventana en la que el SDK tiene el evento confirmado y todavía no es durable. Se eliminó el texto en vez de implementarlo.
WHY
¿Por qué SQLite en Go puro, sin CGO?
Porque se instala con curl | sh en amd64 y arm64. Con CGO la compilación cruzada exige toolchains C por arquitectura y el binario deja de ser estático. Es más lento en microbenchmarks y el diseño lo absorbe: lecturas contra agregados, no sobre millones de filas.
Profundizar

Una librería C en el proceso es además superficie de memoria no gestionada en un servicio cuyo endpoint de ingesta es público. modernc.org/sqlite con CGO_ENABLED=0; el gate de benchmark en CI es el juez. Los adapters se prueban contra SQLite real, nunca contra mocks de base de datos (ADR 09).

WHY
Todo subsistema es opt-in y apagado cuesta cero
Un toggle que sigue costando recursos es una casilla de configuración. Los jobs de uptime, crons, status page y digest no arrancan: no son un bucle que mira una flag y duerme, no existen hasta que su subsistema tiene trabajo. Una instalación nueva arranca solo con error tracking y un job de fondo.
Profundizar

El scheduler ofrece cada job junto con la pregunta de si su subsistema tiene trabajo y solo existen los que responden que sí; la pregunta se repite al cambiar la configuración, sin reinicio (ADR 05, ADR 14). Para la ingesta, donde el trabajo lo inicia el SDK, se usa el propio protocolo: categoría apagada devuelve 429 con X-Sentry-Rate-Limits y el SDK deja de enviarla.

El costo es marginal ~cero, no literalmente cero: el SDK respeta el límite durante la ventana indicada y al expirar reintenta. Sobrevenderlo como "cero absoluto" es regalar la refutación del claim central.
WHY
AGPL-3.0 para el producto, Apache-2.0 para el motor
Usarlo no obliga a nada: la AGPL aplica solo si se distribuye o se sirve por red una versión modificada. internal/engine no importa nada del repositorio y será una librería aparte bajo Apache-2.0, porque es código pensado para reutilizarse.
Profundizar

Esa frontera, junto con internal/domain (solo stdlib) y cinco paquetes puros más, la verifica un test en internal/arch en cada go test y depguard en el lint. La duplicación es intencional: el test corre aunque no tengas golangci-lint instalado.

trapline
Sentry self-hosted
Provisionar
1 núcleo, 512 MB de RAM
4 núcleos, 16 GB de RAM, 20 GB de disco
Desplegar
un binario estático
~40 contenedores: Kafka, ZooKeeper, ClickHouse, PostgreSQL, Redis, Snuba, Relay…
Actualizar
reemplazar el binario
install.sh con migraciones de varios servicios
Backup
copiar un fichero (VACUUM INTO, en caliente)
pg_dump + ClickHouse + volúmenes
No hará nunca
session replay, profiling, metrics, SSO, multi-nodo: contrato, no backlog
lo tiene todo, y a escala de miles de organizaciones
16 GB es su mínimo publicado, no una medición propia; ha cambiado más de una vez. No hacen lo mismo: esa pila tiene un motor de búsqueda de eventos que aquí no existe. Cifras de septiembre de 2026.
05 · Qué haría distinto

Cinco decisiones que hoy tomaría al revés

Hice
Publicar la huella midiendo un proceso en reposo
Hoy haría
Medir desde el primer gate el pico bajo carga y bajo ataque. Las tres cifras equivocadas eran ciertas justo en el único momento que no le importa a nadie.
Hice
Citar "54 contenedores" de memoria en el ADR 001
Hoy haría
Contarlos antes de escribirlos. Eran ~40. El ADR no se edita: la corrección se añade debajo, con fecha.
Hice
Describir en un ADR un batching que no existía
Hoy haría
Escribir solo lo implementado. La medición dio ~1,5× a cambio de una ventana de no durabilidad; se borró el texto.
Hice
Middleware CSRF global, también en la ingesta
Hoy haría
Suite con SDKs reales desde el primer endpoint. Los tests de la API pasaban con un middleware que impedía entregar un evento.
Hice
Grouping con el directorio de release en la huella
Hoy haría
Corpus dorado de grouping desde el inicio. Cada deploy creaba issues nuevos y enterraba la señal de regresión.
06 · Si te llevas tres cosas

Recap

1
Cabe en un VPS de 512 MB y la cifra se vuelve a medir cada noche. Si sube, el build se pone rojo.
2
No cambias de SDK: la compatibilidad se prueba con seis SDKs oficiales y con el tráfico grabado de sentry-cli, no con envelopes escritos a mano.
3
Está diseñado para que un agente cierre el bug: un documento en vez de cinco llamadas, MCP embebido con la misma credencial, y una persona decide qué llega a producción.
Enlaces