Back to deblo
deblo

Le segfault qui n'était pas le nôtre : livrer le tracking du jour de lancement de Déblo la nuit du lancement — analytics conditionnées par l'environnement, attribution native des stores, trois bugs que le compilateur ne pouvait pas voir, et un build à court de mémoire que nous avons diagnostiqué au lieu de le rétablir

Le 1er juillet 2026 — jour de lancement — le risque n'a jamais été le texte. C'était les campagnes payantes qui partaient à l'aveugle. Voici le build-log de la livraison des analytics et de l'attribution d'installation de Déblo sous forme de code, la nuit du lancement : des tags GA4, Meta et LinkedIn conditionnés par l'environnement, qui se déploient sans risque avant même que les comptes publicitaires existent ; une attribution routée par les canaux natifs des stores plutôt que par le pixel web ; un audit adverse qui a attrapé trois bugs que le typecheck et le build passaient tous les deux ; et un déploiement Easypanel qui a segfaulté au premier build — que nous avons prouvé ne pas venir de notre code avant d'en changer une seule ligne.

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

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

Le jour du lancement, les applications étaient la partie facile. Déblo est devenu public sur l'App Store et Google Play le 1er juillet 2026, et au moment où nous nous sommes assis pour la poussée du soir, les fiches des stores étaient en ligne, les liens de téléchargement résolvaient, et le texte de lancement pour les canaux founder comme pour les canaux produit était écrit et vérifié. La chose qui pouvait réellement mal tourner le jour du lancement, ce n'était pas le contenu. C'était le paid.

Un lancement déclenche l'organique d'abord, puis le paid. Mais les campagnes payantes dépensent de l'argent dès l'instant où elles tournent, et si les pixels et l'attribution ne sont pas en place, elles le dépensent dans le noir. Vous ne pouvez pas voir quel canal a converti, vous ne pouvez pas recibler le visiteur qui a rebondi, et vous ne pouvez pas distinguer l'installation venue d'une pub LinkedIn de celle venue du message WhatsApp d'un cousin. Le tracking n'était pas encore construit. C'était là le vrai risque du jour de lancement, et c'est celui que nous avons passé la nuit à refermer.

Cet article est le build-log de cette nuit. Il couvre quatre choses qui méritent chacune un paragraphe dans le carnet de n'importe qui : une fiche de store qui renvoyait 404 à nos vérifications tout en étant parfaitement en ligne dans un navigateur ; une intégration analytics conçue pour se déployer sans risque avant que les comptes publicitaires existent ; une attribution d'installation routée par les propres canaux des stores plutôt que par le pixel web ; et un build de production qui a segfaulté au premier déploiement — que nous avons diagnostiqué comme ne venant pas de notre code avant d'en toucher une seule ligne. Le dernier est la raison du titre, et c'est la partie avec la leçon la plus transférable, donc elle occupe le plus de place.


Le store qui renvoyait 404

La première chose à vérifier le jour du lancement est ennuyeuse et non négociable : les liens de téléchargement atteignent-ils réellement une application installable ? Les liens de lancement de Déblo ne pointent pas vers des URL de store brutes. Ils pointent vers deblo.ai/ios, deblo.ai/android et deblo.ai/app — un ensemble de routes SvelteKit qui détectent la plateforme du visiteur à partir du user-agent et le redirigent en 302 vers le bon store, avec un fallback desktop qui affiche une landing page avec des QR codes. La vérification était donc : curl la redirection, la suivre, confirmer que la page du store est une vraie fiche.

Les deux stores renvoyaient 404.

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

Pendant quelques minutes, cela ressemblait au fait que les applications n'avaient pas encore été réellement publiées en production — qu'elles étaient toujours retenues dans les états « pending developer release » et « manual publish » que les deux stores utilisent. C'était la bonne lecture pour l'App Store : en suivant la redirection avec un user-agent de navigateur et en la laissant résoudre, l'URL canonique Apple revenait en 200 avec le titre complet de l'application, ce qui signifiait qu'Apple était en ligne. Google Play était le piège. Google Play renvoie un HTTP 200 pour une page qui n'existe pas — un soft 404, où la ligne de statut ment et où le corps porte le message « we're sorry, the requested URL was not found ». Une vérification naïve du code de statut lit cela comme un succès. Une vérification du contenu lit le <title> vide et les marqueurs de page introuvable, et le lit comme mort.

Puis le founder a ouvert les deux pages dans un navigateur connecté, et les deux étaient indubitablement en ligne, bouton Install et tout.

La réconciliation mérite d'être énoncée clairement, parce que c'est une règle désormais : Google Play fait du cloaking. Il sert une réponse allégée ou en soft-404 aux IP de datacenter et aux user-agents de bots, tout en servant la fiche complète aux vrais navigateurs. Pour la question « la fiche Play est-elle en ligne », un curl depuis un serveur de build ne fait pas foi et une requête headless ne fait pas foi. Le navigateur humain connecté, lui, fait foi. Nous avions construit un poller de disponibilité basé sur le contenu — vérifier que le titre de l'App Store contient le nom de l'application, vérifier que le titre de Play contient la chaîne « Google Play » — et il était plus correct qu'une vérification de statut, et il allait quand même rapporter Play comme mort parce que le cloak le déjouait aussi. La leçon a coûté vingt minutes et ce sont les vingt minutes les moins chères de cet article.

L'ordre qui découle de tout cela est strict, et c'est l'ordre que nous avons tenu : publier les deux applications et confirmer que les deux fiches résolvent dans un navigateur avant qu'un seul post de lancement ou une seule pub ne parte. Chaque lien du pack de lancement pointait vers ces pages de store. Publier le post épinglé du founder ou déclencher la diffusion WhatsApp alors que l'un des stores était éteint aurait dépensé la fenêtre la plus précieuse de la journée — les premières heures, quand l'attention est la plus forte — sur un lien mort, et on ne peut pas dé-envoyer une première impression.


Des analytics que vous pouvez livrer avant que les comptes existent

Voici la contrainte qui a façonné toute la conception du tracking : la nuit du lancement, aucun des comptes publicitaires n'existait encore. Il n'y avait pas de propriété GA4, pas de pixel Meta, pas de LinkedIn Insight Tag. Ceux-là se créent dans les gestionnaires de pub, et les créer est une tâche du founder qui n'avait pas eu lieu. Mais le code pour les déclencher devait être livré maintenant, afin qu'à l'instant où les identifiants existeraient, ils puissent être insérés sans un nouveau déploiement.

Le pattern qui satisfait les deux exigences, ce sont les tags conditionnés par l'environnement. Un unique composant Svelte lit trois variables — PUBLIC_GA4_MEASUREMENT_ID, PUBLIC_META_PIXEL_ID, PUBLIC_LINKEDIN_PARTNER_ID — depuis $env/dynamic/public, et chaque tag ne s'affiche que si son identifiant est présent. Une variable vide ou absente signifie que le tag ne se charge pas du tout : pas de balise script injectée, pas d'appel réseau effectué. Le composant est un véritable no-op jusqu'à ce qu'un identifiant apparaisse, ce qui le rend parfaitement sûr à déployer avant que les comptes existent.

La raison pour laquelle c'est $env/dynamic/public et non $env/static/public compte. L'env public statique est inliné au moment du build ; une variable absente y est soit intégrée comme undefined, soit casse le build selon la manière dont elle est référencée, et changer un identifiant impose un rebuild. L'env public dynamique est lu à l'exécution par l'adaptateur Node au démarrage du serveur. Cela signifie que le founder peut coller les trois identifiants dans l'environnement Easypanel et redémarrer le conteneur — pas de rebuild, pas de changement de code — et les tags s'animent. Dans une nuit de lancement où les comptes publicitaires peuvent être créés à n'importe quelle heure, « redémarrer, pas rebuilder » est la propriété que vous voulez.

Les tags eux-mêmes sont injectés de la manière dont le font les propres snippets « programmatic install » des vendeurs — dans onMount, côté navigateur uniquement, en construisant à la main le stub dataLayer/gtag, la file fbq et le tableau partner-id de LinkedIn, puis en ajoutant le script de chargement asynchrone. Les scripts insérés via innerHTML ne s'exécutent jamais, donc une approche <svelte:head> {@html} ne ferait silencieusement rien ; la voie programmatique est la bonne, et elle garde aussi l'ensemble entièrement hors du rendu côté serveur, ce qui est ce que vous voulez pour des analytics tierces sur un site marketing prérendu.

Il y a un manque honnête, et nous l'avons écrit dans le code sous forme de commentaire plutôt que de faire comme s'il n'existait pas : les tags se déclenchent au chargement sans barrière de consentement. Pour un produit dont l'audience dominante est en Afrique de l'Ouest et dans la diaspora, c'était le choix délibéré du jour de lancement, avec une plateforme de gestion du consentement pour les visiteurs de l'UE classée comme un suivi documenté plutôt qu'un bloqueur de lancement. Nommer le manque dans la source coûte moins cher que de le découvrir dans un audit plus tard — et, comme il se trouve, l'audit a confirmé que c'était le seul manque de ce genre.


L'attribution appartient au store, pas au pixel

Un pixel web vous dit qui a visité deblo.ai. Il ne peut pas vous dire qui a installé l'application, parce que l'installation se produit à l'intérieur de l'App Store ou de Google Play, de l'autre côté d'une redirection, où aucun pixel web ne suit. L'attribution d'installation doit voyager sur le propre canal du store.

Ainsi, les trois routes de redirection — /ios, /android, /app — ne se contentent plus de rebondir le visiteur vers une URL de store fixe. Elles lisent les paramètres UTM entrants et les transmettent dans le mécanisme d'attribution natif de chaque store :

  • Google Play prend un Install Referrer : la chaîne UTM va dans un paramètre de requête referrer=, encodé en URL exactement une fois, et Play l'expose à l'application via l'Install Referrer API.
  • Apple prend un campaign token : utm_campaign devient la valeur ct sur le lien App Store, plafonnée à la limite de quarante caractères d'Apple, aux côtés d'un provider token optionnel et de mt=8.

Les URL de base des stores sont des constantes de module. Rien qui soit fourni par l'utilisateur ne touche jamais l'hôte de redirection — seules les valeurs UTM encodées entrent, et uniquement en tant que paramètres de requête. C'est délibéré : une route de redirection qui construit sa destination à partir de l'entrée de la requête est un open-redirect en puissance, et la manière de l'éviter est de rendre l'hôte non dérivable de quoi que ce soit que l'appelant contrôle.

Tout l'édifice repose sur une unique convention UTM, parce que l'attribution ne vaut que la discipline des liens qui l'alimentent. Un seul point d'entrée — deblo.ai/app?utm_source=…&utm_medium=…&utm_campaign=launch-j0&utm_content=… — avec des valeurs en kebab-case, sans accent (la campagne est launch-j0, la source est la plateforme exacte, le medium sépare paid de organic-social de broadcast, et le content nomme la création pour que les perdantes puissent être coupées). Le côté web le voit dans GA4 ; le côté installation le voit dans le referrer Play et le token Apple. Même lien, des deux côtés de la redirection.


L'audit a trouvé trois bugs que le compilateur ne pouvait pas trouver

Avant que tout cela soit livré, c'est passé par la passe d'audit en lecture seule que nous exécutons sur tout ce qui touche un chemin de données — un agent séparé, briefé comme un relecteur senior, chargé d'essayer de le casser. Le typecheck était propre sur les 5 572 fichiers. Le build de production était vert. Aucun des deux ne peut voir un bug sémantique, et l'audit en a trouvé trois.

Le premier était le pire, parce qu'il déjouait silencieusement tout l'objet de l'exercice. GA4 était configuré avec send_page_view: false — la manœuvre standard quand on entend envoyer les vues de page manuellement sur une single-page app — mais la seule vue de page manuelle vivait dans le handler afterNavigate, qui saute délibérément sa première invocation pour éviter de compter deux fois le hit d'entrée. Résultat net : GA4 n'enregistrait jamais la vue de la landing page du tout. Une session sans navigation interne aurait rapporté zéro vue de page. Le jour du lancement, avec du trafic payant qui atterrit et rebondit, GA4 aurait montré une fraction de la réalité et nous l'aurions cru. Le correctif tenait en une ligne — déclencher explicitement la vue de la page d'entrée dans l'init — mais le bug était invisible à toute vérification statique parce que le code était parfaitement valide ; c'était le comportement qui était faux.

Le deuxième était une race condition. Le saut de afterNavigate était protégé par un flag ready qui pouvait, selon l'ordonnancement de onMount par rapport au premier afterNavigate, avaler aussi la première navigation réelle. Le correctif a été de découpler le saut de l'état de préparation, pour que le hit d'atterrissage vienne toujours de l'init et que le flag bascule de façon déterministe au premier appel, quel que soit l'ordre.

Le troisième était un bug d'encodage dans le referrer Play. Les valeurs UTM étaient encodées une fois lors de l'assemblage de la chaîne, puis de nouveau lorsque la chaîne entière était placée dans le paramètre referrer — un double-encodage que la plupart des parseurs d'Install Referrer, qui décodent une fois, feraient remonter en mojibake et mal attribueraient. Le correctif a été d'encoder exactement une fois, à la frontière, et de s'appuyer sur la convention que les valeurs UTM sont de l'ASCII en kebab-case, de sorte qu'il n'y a pas d'ambiguïté structurelle à l'intérieur d'une valeur.

Trois bugs réels, zéro plainte du compilateur. C'est l'argument entier en faveur de l'existence de la passe adverse : le typechecker prouve que le code est bien formé et le build prouve qu'il compile, et aucun des deux ne peut vous dire que vos analytics vont sous-compter chaque session du jour le plus chargé de l'année.

Correctifs appliqués, re-vérifiés, et commités en de8e757. Puis c'est parti au déploiement, et le déploiement est le moment où la nuit est devenue intéressante.


Le segfault qui n'était pas le nôtre

Le premier build Easypanel de de8e757 est mort avec ceci :

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

Exit 139, c'est SIGSEGV — une erreur de segmentation, une violation d'accès mémoire au niveau natif. Elle est tombée en plein build, à mi-chemin de la compilation des composants Svelte. La nuit du lancement, avec le commit de tracking posé par-dessus, la lecture réflexe est : le nouveau code a cassé le build, rétablis-le. Ce réflexe est faux assez souvent pour qu'il vaille la peine d'avoir une règle contre le fait d'agir dessus, et la règle est : prouve à qui appartient le bug avant de le corriger.

Les preuves se sont assemblées vite, et toutes pointaient loin du code :

  • Le même build passait en local. Exécuter le build de production sur la machine du développeur avec exactement les trois identifiants de tracking positionnés — les mêmes valeurs qu'Easypanel transmettait — sortait en 0, source maps comprises. Une erreur de compilation ne passe pas sur une machine et ne segfaulte pas sur une autre.
  • Le crash se déplaçait. Le premier build échoué est mort dans un composant ; le deuxième build échoué, même commit, est mort dans un composant différent et sans rapport. Un bug dans le code échoue de façon déterministe au même endroit à chaque fois. Un crash qui erre à travers les fichiers à chaque exécution est une défaillance de niveau natif — une pression mémoire — pas une erreur de logique.
  • Les dépendances n'avaient pas changé. La couche Docker qui exécute npm ci était en cache et inchangée depuis le dernier déploiement réussi, donc les binaires natifs qui faisaient la compilation — les mêmes qui avaient construit l'application sans problème quelques jours plus tôt — étaient identiques octet pour octet.

Le même code qui builde en local, plus un emplacement de crash non déterministe, plus des dépendances en cache inchangées, égale une conclusion : l'hôte de build a manqué de mémoire, et le compilateur est mort avec un segfault sous la pression au lieu d'un message propre de dépassement mémoire. Ce n'était pas notre code. Rétablir le commit de tracking n'aurait rien déployé et rien diagnostiqué, parce que le build suivant aurait heurté le même mur — l'application avait simplement grandi jusqu'au bord de ce que la mémoire du conteneur de build permettait, et c'était la première compilation fraîche, non mise en cache, à le franchir.

Le correctif durable a deux parties, et une seule des deux est dans le dépôt. Dans l'étape builder du Dockerfile, avant npm run build, nous avons donné à V8 une marge de tas explicite :

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

Cela aide quand l'hôte a la mémoire mais que Node n'allait pas la chercher. L'autre moitié n'est pas du code du tout — c'est l'hôte de build qui a besoin d'assez de RAM ou de swap réels pour une grosse compilation SvelteKit, ce qui est un levier d'infrastructure, pas un commit. Nous avons livré le changement de Dockerfile en 35f4e99 et nommé la moitié infrastructure à voix haute, parce qu'un correctif que vous écrivez dans le dépôt et qui dépend silencieusement d'une machine ayant plus de mémoire est un correctif qui embrouillera la prochaine personne qui ne lit que le diff.

Le but de cette section n'est pas la ligne NODE_OPTIONS. C'est les quinze minutes de non-rétablissement. La nuit du lancement, avec une journée d'hôpital derrière le founder et toute la poussée sociale en attente, la pression de saisir le changement le plus récent et de le jeter par-dessus bord est énorme, et cela aurait coûté le tracking, n'aurait rien déployé, et aurait laissé la vraie cause — un plafond mémoire — exploser au déploiement suivant. La discipline qui comptait, c'était de lire le crash honnêtement : non-déterminisme, reproduction, dépendances en cache. Trois faits, une conclusion, pas de panique.


Vérifier en production avec un navigateur headless

Une fois le build vert et le founder ayant collé les trois identifiants dans l'environnement Easypanel et redémarré le conteneur, la dernière question était la seule qui compte : les tags se déclenchent-ils réellement en production, avec les vrais identifiants, en les lisant à l'exécution comme la conception l'entendait ?

Les tags s'injectent côté client, donc vous ne pouvez pas les voir avec curl — il vous faut un navigateur qui exécute le JavaScript. Nous avons piloté un Chrome headless via le DevTools Protocol contre le deblo.ai/app en ligne, navigué avec une URL d'auto-test taguée en UTM, attendu que onMount et les loaders asynchrones s'exécutent, et relu la page :

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

gtag défini et dataLayer peuplé, googletagmanager.com/gtag/js chargé ; fbq défini, connect.facebook.net/fbevents.js chargé ; le partner id LinkedIn positionné et snap.licdn.com/li.lms-analytics/insight.min.js chargé. Les trois tags en vie, lisant leurs identifiants depuis l'environnement d'exécution via $env/dynamic/public, exactement comme conçu. Le founder a confirmé la même chose de l'autre côté — GA4 Realtime et le testeur d'événements de Meta recevant tous les deux. De bout en bout, avec des preuves, pas des suppositions.


De quoi la nuit parlait vraiment

Retirez les spécificités et il reste un ensemble de petites disciplines qui ne paient que sous pression, ce qui est le seul moment où elles sont dures à tenir :

  • Conditionnez les tags tiers sur des variables d'environnement pour que l'intégration se déploie sans risque avant que les comptes existent et s'anime avec un redémarrage, pas un rebuild.
  • Routez l'attribution d'installation par le canal natif du store, jamais par le pixel web, et ne laissez jamais une redirection construire son hôte à partir de l'entrée de la requête.
  • Exécutez la passe adverse même quand le typechecker et le build sont tous deux verts, parce qu'ils ne peuvent pas voir une vue de page qui ne se déclenche jamais ou un referrer encodé deux fois.
  • Lisez un crash avant de blâmer le changement le plus récent. Un emplacement non déterministe plus une reproduction locale propre plus des dépendances en cache inchangées, ce n'est pas votre code, et les quinze minutes qu'il faut pour l'établir vous épargneront de rétablir quelque chose qui n'a jamais été le problème.

Rien de tout cela n'est exotique. C'est la discipline ordinaire de la livraison, appliquée la seule nuit où la sauter aurait été le plus tentant et le plus coûteux. Les applications étaient la partie facile. Le tracking — la plomberie sans gloire qui décide si l'argent que vous êtes sur le point de dépenser vous apprend quoi que ce soit — c'était le lancement, et il est parti avec ses lumières allumées.

Déblo est en ligne sur iOS et Android. Le paid peut voir.


Essayez Déblo

Déblo est en ligne — une IA temps réel, voix et yeux, à qui vous parlez et à qui vous montrez des choses. À partir de 100 FCFA (~0,16 $).

Suivez Déblo :

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles

Thales & Claude thales

Treize agents, quarante-trois minutes : la première session Workflow de Claude Fable 5, et ce qu'un script d'orchestration déterministe change aux builds multi-agents

Un prompt, treize agents, quarante-trois minutes : la première session de production avec Claude Fable 5 et l'outil Workflow de Claude Code a livré un site web de production complet de sept pages plus un endpoint backend de capture de leads, en un seul commit. Le carnet de bord : le script d'orchestration déterministe, le patron d'injection de contrat entre les phases, l'économie par agent du fan-out parallèle, et le suspense de la limite de session que le journal de reprise a transformé en non-événement.

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

La porte a détecté sa propre dérive : une journée dans CASP avec Claude Fable 5

Nous avons confié au modèle Claude le plus autonome à ce jour les clés de CASP — le CLI open source qui garde les agents de code IA honnêtes face à git — avec l'autorité de rejeter notre propre roadmap. Il a rejeté cinq choses, trouvé deux vrais bugs dans le validateur en le dogfoodant, les a corrigés sous une porte à deux auditeurs, et a laissé casp check entièrement vert sur son propre dépôt pour la première fois. CASP 0.3.0 en est le résultat.

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

La transplantation du CASP : comment la discipline des six fichiers est passée de Conductor à un ERP transport anti-fraude, ce que la compétence /next ajoute quand l'opérateur tape juste « next », et pourquoi le coût d'une dérive du CASP grimpe quand le projet, c'est l'argent des autres

La discipline du CASP qui a piloté trente-cinq sessions de Conductor est agnostique au produit. Le carnet de bord de sa transplantation sur KASSIA, un ERP transport anti-fraude pour un exploitant de flotte en Côte d'Ivoire : ce qui a migré, ce qui n'a pas migré (le validateur sur mesure — et ce que son absence coûte), ce que la compétence /next ajoute quand l'opérateur tape un seul mot, et là où le CASP s'arrête — le bug de déploiement qu'il ne pouvait pas voir parce qu'il enregistre l'intention, pas la réalité de l'infrastructure.

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