Back to deblo
deblo

El segfault que no era nuestro: cómo lanzamos el tracking del día de lanzamiento de Déblo en la noche del despliegue — analítica condicionada por entorno, atribución nativa de las tiendas, tres bugs que el compilador no podía ver y un build sin memoria que diagnosticamos en lugar de revertir

El 1 de julio de 2026 — el día del lanzamiento — el riesgo nunca fue el texto. Era que las campañas de pago salieran a ciegas. Este es el build-log de cómo desplegamos la analítica y la atribución de instalaciones de Déblo como código en la noche del lanzamiento: etiquetas GA4, Meta y LinkedIn condicionadas por entorno que se despliegan sin riesgo antes de que existan las cuentas publicitarias; atribución enrutada por los canales nativos de las tiendas en lugar del pixel web; una auditoría adversarial que atrapó tres bugs que tanto el typechecker como el build dieron por buenos; y un despliegue en Easypanel que hizo segfault en el primer build — que demostramos que no era nuestro código antes de tocar una sola línea.

Juste A. Gnimavo (Thales) & Claude | July 1, 2026 18 min deblo
EN/ FR/ ES
deblolaunch-dayclaude-opus-4.8claude-codesveltekitsvelte-5-runesadapter-nodega4meta-pixellinkedin-insight-tagenv-dynamic-publicutmplay-install-referrerapple-campaign-tokengoogle-play-cloakingsoft-404install-attributionpost-mortemsegfaultexit-139oomnode-optionsmax-old-space-sizeeasypanelhetznerdockerfileadversarial-auditchrome-devtools-protocolheadless-verificationopen-redirect

Por Thales (CEO, ZeroSuite) & Claude Opus 4.8 — Claude Code instance

El día del lanzamiento, las apps eran la parte fácil. Déblo salió al público en la App Store y en Google Play el 1 de julio de 2026, y para cuando nos sentamos a hacer el push de la tarde, las fichas de las tiendas estaban en vivo, los enlaces de descarga resolvían, y el texto de lanzamiento tanto para los canales founder como para los canales de producto estaba escrito y revisado. Lo que de verdad podía salir mal el día del lanzamiento no era el contenido. Era el pago.

Un lanzamiento dispara primero lo orgánico, luego lo pagado. Pero las campañas de pago gastan dinero en el instante en que se ejecutan, y si los pixeles y la atribución no están en su sitio, lo gastan a ciegas. No puedes ver qué canal convirtió, no puedes hacer retargeting al visitante que rebotó, y no puedes distinguir la instalación que vino de un anuncio de LinkedIn de la que vino del mensaje de WhatsApp de un primo. El tracking aún no estaba construido. Ese era el verdadero riesgo del día del lanzamiento, y fue el que dedicamos la noche a cerrar.

Este post es el build-log de esa noche. Cubre cuatro cosas que cada una merece un párrafo en el cuaderno de cualquiera: una ficha de tienda que devolvía 404 a nuestras comprobaciones estando perfectamente en vivo en un navegador; una integración de analítica diseñada para desplegarse sin riesgo antes de que existieran las cuentas publicitarias; atribución de instalaciones enrutada por los canales propios de las tiendas en lugar del pixel web; y un build de producción que hizo segfault en el primer despliegue — que diagnosticamos como no nuestro código antes de tocar una sola línea. La última es la razón del título, y es la parte con la lección más transferible, así que se lleva el mayor espacio.


La tienda que devolvía 404

Lo primero que hay que verificar el día del lanzamiento es aburrido e innegociable: ¿los enlaces de descarga llegan de verdad a una app instalable? Los enlaces de lanzamiento de Déblo no apuntan a URLs de tienda en crudo. Apuntan a deblo.ai/ios, deblo.ai/android y deblo.ai/app — un conjunto de rutas de SvelteKit que detectan la plataforma del visitante a partir del user-agent y lo redirigen con un 302 a la tienda correcta, con un fallback de escritorio que renderiza una landing con códigos QR. Así que la comprobación era: curl a la redirección, seguirla, confirmar que la página de la tienda es una ficha real.

Ambas tiendas devolvían 404.

deblo.ai/ios     → 302 → App Store listing … 404
deblo.ai/android → 302 → Play listing      … 404

Durante unos minutos esto parecía indicar que las apps aún no se habían publicado de verdad en producción — que seguían retenidas en los estados de "pending developer release" y "manual publish" que ambas tiendas usan. Esa era la lectura correcta para la App Store: siguiendo la redirección con un user-agent de navegador y dejándola resolver, la URL canónica de Apple devolvía 200 con el título completo de la app, lo que significaba que Apple estaba en vivo. Google Play era la trampa. Google Play devuelve HTTP 200 para una página que no existe — un soft 404, donde la línea de estado miente y el cuerpo lleva el mensaje "we're sorry, the requested URL was not found". Una comprobación ingenua del código de estado lo lee como éxito. Una comprobación de contenido lee el <title> vacío y los marcadores de not-found y lo lee como muerto.

Entonces el founder abrió ambas páginas en un navegador con sesión iniciada y las dos estaban inequívocamente en vivo, con botón de Install incluido.

La reconciliación merece decirse con claridad, porque ahora es una regla: Google Play hace cloaking. Sirve una respuesta recortada o un soft 404 a las IPs de datacenter y a los user-agents de bot, mientras sirve la ficha completa a los navegadores reales. Para la pregunta "¿está la ficha de Play en vivo?", un curl desde un servidor de build no es autoritativo y una petición headless tampoco lo es. El navegador humano con sesión iniciada sí lo es. Habíamos construido un poller de liveness basado en contenido — comprobar el título de la App Store por el nombre de la app, comprobar el título de Play por la cadena "Google Play" — y era más correcto que una comprobación de estado, y aun así iba a reportar Play como muerto porque el cloak también lo estaba derrotando. La lección costó veinte minutos y son los veinte minutos más baratos de este post.

El orden que se deduce de todo esto es estricto, y es el orden que mantuvimos: publicar ambas apps y confirmar que ambas fichas resuelven en un navegador antes de que salga un solo post o anuncio de lanzamiento. Cada enlace del pack de lanzamiento apuntaba a esas páginas de tienda. Publicar el pin del founder o disparar el broadcast de WhatsApp mientras cualquiera de las tiendas estuviera a oscuras habría gastado la ventana más valiosa del día — las primeras horas, cuando la atención está más alta — en un enlace muerto, y una primera impresión no se puede des-enviar.


Analítica que puedes desplegar antes de que existan las cuentas

Aquí está la restricción que dio forma a todo el diseño del tracking: en la noche del lanzamiento, ninguna de las cuentas publicitarias existía todavía. No había propiedad de GA4, ni pixel de Meta, ni LinkedIn Insight Tag. Esos se crean en los gestores de anuncios, y crearlos es una tarea del founder que no había ocurrido. Pero el código para dispararlos tenía que desplegarse ya, para que en el momento en que los IDs existieran pudieran colocarse sin otro despliegue.

El patrón que satisface ambas cosas son las etiquetas condicionadas por entorno. Un único componente de Svelte lee tres variables — PUBLIC_GA4_MEASUREMENT_ID, PUBLIC_META_PIXEL_ID, PUBLIC_LINKEDIN_PARTNER_ID — desde $env/dynamic/public, y cada etiqueta se renderiza solo si su ID está presente. Una variable vacía o ausente significa que la etiqueta no se carga en absoluto: ninguna etiqueta de script inyectada, ninguna llamada de red hecha. El componente es un no-op genuino hasta que aparece un ID, lo que lo hace completamente seguro de desplegar antes de que existan las cuentas.

La razón de que sea $env/dynamic/public y no $env/static/public importa. El entorno público estático se incrusta en tiempo de build; una variable ausente ahí o bien se hornea como undefined o rompe el build según cómo se referencie, y cambiar un ID significa recompilar. El entorno público dinámico se lee en tiempo de ejecución por el adaptador de Node cuando el servidor arranca. Eso significa que el founder puede pegar los tres IDs en el entorno de Easypanel y reiniciar el contenedor — sin recompilar, sin cambio de código — y las etiquetas cobran vida. En una noche de lanzamiento donde las cuentas publicitarias pueden crearse a cualquier hora, "reinicia, no recompiles" es la propiedad que quieres.

Las etiquetas mismas se inyectan de la forma en que lo hacen los propios snippets de "instalación programática" de los proveedores — en onMount, solo en el navegador, construyendo a mano el stub de dataLayer/gtag, la cola de fbq y el array del partner-id de LinkedIn, y luego añadiendo el script cargador asíncrono. Los scripts insertados a través de innerHTML nunca se ejecutan, así que un enfoque de <svelte:head> con {@html} silenciosamente no haría nada; el camino programático es el correcto, y además mantiene todo el asunto completamente fuera del renderizado del lado del servidor, que es lo que quieres para analítica de terceros en un sitio de marketing prerrenderizado.

Hay una laguna honesta, y la escribimos en el código como un comentario en lugar de fingir que no existía: las etiquetas se disparan al cargar sin ninguna barrera de consentimiento. Para un producto cuya audiencia dominante está en África Occidental y la diáspora, esa fue la decisión deliberada del día del lanzamiento, con una plataforma de gestión de consentimiento para los visitantes de la UE archivada como un follow-up documentado en lugar de un bloqueante del lanzamiento. Nombrar la laguna en el código fuente es más barato que descubrirla en una auditoría más tarde — y, da la casualidad, la auditoría confirmó que era la única laguna de ese tipo.


La atribución pertenece a la tienda, no al pixel

Un pixel web te dice quién visitó deblo.ai. No puede decirte quién instaló la app, porque la instalación ocurre dentro de la App Store o de Google Play, al otro lado de una redirección, donde ningún pixel web sigue. La atribución de instalaciones tiene que viajar por el propio canal de la tienda.

Así que las tres rutas de redirección — /ios, /android, /app — ya no se limitan a rebotar al visitante hacia una URL de tienda fija. Leen los parámetros UTM entrantes y los reenvían al mecanismo de atribución nativo de cada tienda:

  • Google Play toma un Install Referrer: la cadena UTM va a un parámetro de consulta referrer=, URL-encoded exactamente una vez, y Play se lo expone a la app a través de la Install Referrer API.
  • Apple toma un campaign token: utm_campaign se convierte en el valor ct del enlace de la App Store, limitado al máximo de cuarenta caracteres de Apple, junto a un provider token opcional y mt=8.

Las URLs base de las tiendas son constantes de módulo. Nada suministrado por el usuario toca jamás el host de la redirección — solo entran los valores UTM codificados, y solo como parámetros de consulta. Eso es deliberado: una ruta de redirección que construye su destino a partir de la entrada de la petición es un open-redirect esperando a ocurrir, y la forma de evitarlo es hacer que el host sea inderivable a partir de cualquier cosa que el llamante controle.

Todo el asunto cuelga de una única convención UTM, porque la atribución solo es tan buena como la disciplina de los enlaces que la alimentan. Un único punto de entrada — deblo.ai/app?utm_source=…&utm_medium=…&utm_campaign=launch-j0&utm_content=… — con valores en kebab-case, sin acentos (la campaña es launch-j0, el source es la plataforma exacta, el medium separa paid de organic-social de broadcast, y el content nombra el creativo para que los perdedores puedan cortarse). El lado web lo ve en GA4; el lado de la instalación lo ve en el referrer de Play y en el token de Apple. Mismo enlace, ambos lados de la redirección.


La auditoría encontró tres bugs que el compilador no pudo

Antes de que nada de esto se desplegara, pasó por el pase de auditoría de solo lectura que ejecutamos sobre cualquier cosa que toque un camino de datos — un agente separado, instruido como un revisor senior, con la orden de intentar romperlo. El typecheck estaba limpio en los 5.572 archivos. El build de producción estaba en verde. Ninguno de esos puede ver un bug semántico, y la auditoría encontró tres.

El primero fue el peor, porque silenciosamente derrotaba todo el propósito del ejercicio. GA4 estaba configurado con send_page_view: false — la jugada estándar cuando pretendes enviar las page views manualmente en una single-page app — pero la única page view manual vivía en el handler afterNavigate, que deliberadamente se salta su primera invocación para evitar el doble conteo del hit de entrada. El resultado neto: GA4 nunca registraba la page view de la landing en absoluto. Una sesión sin navegación dentro de la app reportaría cero page views. El día del lanzamiento, con tráfico de pago aterrizando y rebotando, GA4 habría mostrado una fracción de la realidad y nosotros nos lo habríamos creído. El fix era una línea — disparar explícitamente la page view de entrada en el init — pero el bug era invisible para toda comprobación estática porque el código era perfectamente válido; era el comportamiento lo que estaba mal.

El segundo era una condición de carrera. El skip de afterNavigate estaba protegido por un flag ready que podía, según el orden de onMount frente al primer afterNavigate, tragarse también la primera navegación real. El fix fue desacoplar el skip de la readiness para que el hit de la landing venga siempre del init y el flag cambie de forma determinista en la primera llamada sin importar el orden.

El tercero era un bug de codificación en el referrer de Play. Los valores UTM se estaban codificando una vez cuando se ensamblaba la cadena y luego otra vez cuando la cadena completa se colocaba en el parámetro referrer — un doble encode que la mayoría de los parsers de Install Referrer, que decodifican una vez, mostrarían como mojibake y atribuirían mal. El fix fue codificar exactamente una vez, en la frontera, y apoyarse en la convención de que los valores UTM son ASCII kebab-case para que no haya ambigüedad estructural dentro de un valor.

Tres bugs reales, cero quejas del compilador. Este es el argumento entero de por qué existe el pase adversarial: el typechecker demuestra que el código está bien formado y el build demuestra que compila, y ninguno de los dos puede decirte que tu analítica va a sub-contar cada sesión el día más ajetreado del año.

Fixes aplicados, re-verificados y commiteados como de8e757. Luego pasó al despliegue, y el despliegue es donde la noche se puso interesante.


El segfault que no era nuestro

El primer build de de8e757 en Easypanel murió con esto:

Segmentation fault (core dumped)
ERROR: process "/bin/sh -c npm run build" did not complete successfully: exit code: 139

Exit 139 es SIGSEGV — un fallo de segmentación, una violación de acceso a memoria a nivel nativo. Aterrizó a mitad del build, a mitad de compilar los componentes de Svelte. En la noche del lanzamiento, con el commit del tracking encima, la lectura refleja es: el código nuevo rompió el build, revíertelo. Ese reflejo está equivocado lo bastante a menudo como para que valga la pena tener una regla contra actuar sobre él, y la regla es: demuestra de quién es el bug antes de arreglarlo.

La evidencia se ensambló rápido, y toda ella apuntaba lejos del código:

  • El mismo build pasaba localmente. Ejecutar el build de producción en la máquina del desarrollador con exactamente los tres IDs de tracking establecidos — los mismos valores que Easypanel estaba pasando — salía con 0, source maps y todo. Un error de compilación no pasa en una máquina y hace segfault en otra.
  • El crash se movía. El primer build fallido murió en un componente; el segundo build fallido, mismo commit, murió en un componente distinto, no relacionado. Un bug en el código falla de forma determinista en el mismo sitio cada vez. Un crash que vaga por los archivos con cada ejecución es un fallo a nivel nativo — presión de memoria — no un error de lógica.
  • Las dependencias no habían cambiado. La capa de Docker que ejecuta npm ci estaba cacheada y sin cambios desde el último despliegue exitoso, así que los binarios nativos que hacían la compilación — los mismos que habían construido la app bien días antes — eran byte por byte idénticos.

El mismo código que compila localmente, más una ubicación de crash no determinista, más dependencias cacheadas sin cambios, es igual a una conclusión: el host de build se quedó sin memoria, y el compilador murió con un segfault bajo la presión en lugar de con un mensaje limpio de out-of-memory. No era nuestro código. Revertir el commit del tracking no habría desplegado nada ni diagnosticado nada, porque el siguiente build habría chocado con el mismo muro — la app simplemente había crecido hasta el borde de lo que la memoria del contenedor de build permitía, y esta era la primera compilación fresca, sin caché, en cruzarlo.

El fix duradero tiene dos partes, y solo una de ellas está en el repositorio. En la etapa builder del Dockerfile, antes de npm run build, le dimos a V8 espacio explícito para el heap:

dockerfileENV NODE_OPTIONS=--max-old-space-size=4096

Eso ayuda cuando el host tiene la memoria pero Node no la estaba alcanzando. La otra mitad no es código en absoluto — es que el host de build necesita suficiente RAM real o swap para una compilación grande de SvelteKit, lo cual es una palanca de infraestructura, no un commit. Desplegamos el cambio del Dockerfile como 35f4e99 y nombramos la mitad de infraestructura en voz alta, porque un fix que escribes en el repo y que silenciosamente depende de que una máquina tenga más memoria es un fix que confundirá a la siguiente persona que lea solo el diff.

El punto de esta sección no es la línea de NODE_OPTIONS. Son los quince minutos de no revertir. En la noche del lanzamiento, con un día de hospital a las espaldas del founder y todo el push social esperando, la presión de agarrar el cambio más reciente y tirarlo por la borda es enorme, y habría costado el tracking, desplegado nada, y dejado la causa real — un techo de memoria — para que detonara en el siguiente despliegue. La disciplina que importó fue leer el crash con honestidad: no determinismo, reproducción, deps cacheadas. Tres hechos, una conclusión, sin pánico.


Verificar en producción con un navegador headless

Una vez que el build estaba en verde y el founder había pegado los tres IDs en el entorno de Easypanel y reiniciado el contenedor, la última pregunta era la única que cuenta: ¿las etiquetas se disparan de verdad en producción, con los IDs reales, leyéndolos en tiempo de ejecución tal como pretendía el diseño?

Las etiquetas se inyectan del lado del cliente, así que no puedes verlas con curl — necesitas un navegador que ejecute el JavaScript. Condujimos un Chrome headless a través del DevTools Protocol contra el deblo.ai/app en vivo, navegamos con una URL de auto-test etiquetada con UTM, esperamos a que onMount y los cargadores asíncronos se ejecutaran, y leímos la página de vuelta:

json{ "ga4": true,  "ga4Script": true,  "dataLayer": 3,
  "meta": true, "metaScript": true,
  "linkedin": "…", "linkedinScript": true }

gtag definido y dataLayer poblado, googletagmanager.com/gtag/js cargado; fbq definido, connect.facebook.net/fbevents.js cargado; el partner id de LinkedIn establecido y snap.licdn.com/li.lms-analytics/insight.min.js cargado. Las tres etiquetas en vivo, leyendo sus IDs del entorno de ejecución a través de $env/dynamic/public, exactamente como se diseñó. El founder confirmó lo mismo desde el otro extremo — GA4 Realtime y el event tester de Meta ambos recibiendo. De extremo a extremo, con evidencia, no con suposición.


De qué iba realmente la noche

Quita las particularidades y lo que queda es un conjunto de pequeñas disciplinas que solo rinden bajo presión, que es el único momento en que son difíciles de mantener:

  • Condiciona las etiquetas de terceros a variables de entorno para que la integración se despliegue sin riesgo antes de que existan las cuentas y cobre vida con un reinicio, no con una recompilación.
  • Enruta la atribución de instalaciones por el canal nativo de la tienda, nunca por el pixel web, y nunca dejes que una redirección construya su host a partir de la entrada de la petición.
  • Ejecuta el pase adversarial incluso cuando el typechecker y el build están ambos en verde, porque no pueden ver una page view que nunca se dispara ni un referrer codificado dos veces.
  • Lee un crash antes de culpar al cambio más nuevo. Una ubicación no determinista más una reproducción local limpia más dependencias cacheadas sin cambios no es tu código, y los quince minutos que cuesta establecerlo te ahorrarán revertir algo que nunca fue el problema.

Nada de esto es exótico. Es la disciplina ordinaria de desplegar, aplicada en la única noche en que saltársela habría sido lo más tentador y lo más caro. Las apps eran la parte fácil. El tracking — la fontanería sin glamur que decide si el dinero que estás a punto de gastar te enseña algo — era el lanzamiento, y salió con las luces encendidas.

Déblo está en vivo en iOS y Android. El pago puede ver.


Prueba Déblo

Déblo está en vivo — una IA de voz y ojos en tiempo real a la que le hablas y le muestras cosas. Desde 100 FCFA (~$0.16).

Sigue a Déblo:

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles

Thales & Claude thales

Trece agentes, cuarenta y tres minutos: la primera sesión Workflow de Claude Fable 5, y lo que un script de orquestación determinista cambia en los builds multiagente

Un prompt, trece agentes, cuarenta y tres minutos: la primera sesión de producción con Claude Fable 5 y la herramienta Workflow de Claude Code entregó un sitio web de producción completo de siete páginas más un endpoint backend de captura de leads, en un solo commit. La bitácora: el script de orquestación determinista, el patrón de inyección de contrato entre fases, la economía por agente del fan-out paralelo, y el suspenso del límite de sesión que el diario de reanudación convirtió en un no-evento.

23 min Jun 12, 2026
claude-fable-5claude-codeworkflow-toolmulti-agent +10
Thales & Claude casp

La puerta detectó su propia deriva: un día dentro de CASP con Claude Fable 5

Le entregamos al modelo Claude más autónomo hasta la fecha las llaves de CASP — la CLI open source que mantiene honestos a los agentes de código IA frente a git — con la autoridad de rechazar nuestra propia roadmap. Rechazó cinco cosas, encontró dos bugs reales en el validador al hacerle dogfooding, los corrigió bajo una puerta de dos auditores, y dejó casp check completamente en verde sobre su propio repositorio por primera vez. CASP 0.3.0 es el resultado.

15 min Jun 10, 2026
caspzerosuiteworkflowai-cto +9
Thales & Claude zerosuite

El trasplante del CASP: cómo la disciplina de los seis archivos pasó de Conductor a un ERP de transporte antifraude, qué añade la competencia /next cuando el operador solo escribe «next», y por qué el coste de una deriva del CASP sube cuando el proyecto es el dinero de otros

La disciplina del CASP que pilotó treinta y cinco sesiones de Conductor es agnóstica al producto. El cuaderno de bitácora de su trasplante a KASSIA, un ERP de transporte antifraude para un operador de flota en Costa de Marfil: lo que migró, lo que no (el validador a medida — y lo que cuesta su ausencia), qué añade la competencia /next cuando el operador escribe una sola palabra, y dónde se detiene el CASP — el bug de despliegue que no podía ver porque registra la intención, no la realidad de la infraestructura.

22 min Jun 8, 2026
kassiaerp-kassia-transport-logistiquezerosuiteCASP +15