Revue du branchement sur l'application — 26/10/2026
Revue critique de l'étape « brancher les paquets sur le site ». Elle complète l'audit d'entrée (audit.md), qui portait sur le dépôt avant modification.
Ce qui est mesuré, et comment
| Vérification | Résultat | Commande |
|---|---|---|
| Paquets en Node, données retrouvées chez les 4 services | 15 réussites, 0 échec | pnpm telemetry:test |
| Site construit, navigateur réel, données retrouvées | 6 réussites, 0 échec | pnpm telemetry:test:app |
| Formatage, lint, typage, tests | 64 tests, au vert | make check |
| Aucun secret dans le bundle navigateur | 23 660 fichiers, 0 trouvé | pnpm analytics:check-bundle |
| Construction complète après nettoyage | 23 449 pages, aucun chunk absent | rm -rf .next-saclay && build |
Le site de test était servi sur le port 3005, avec Umami et Rybbit activés.
Trois défauts trouvés en vérifiant, et corrigés
Illegal invocation.fetchconservé en champ de classe puis appelé parthis.fetchImpl(...)reçoit l'adaptateur comme receveur ; le navigateur refuse. Toutes les vues de page étaient perdues. Les trois adaptateurs détachent maintenant la référence, et un test de non-régression reproduit la forme d'appel réelle — un test qui appelait la fonction nue concluait à tort que tout allait bien.skipFirstsur un site à chargements complets. L'option saute la première vue de chaque session de navigation ; sur un site statique où chaque page est un chargement complet, « la première » est toutes les fois, et plus rien n'était jamais émis. La protection contre le double comptage Matomo est désormais portée par l'adaptateur (emitPageViews: false), ce qui la rend ciblée au lieu de valoir pour tous les fournisseurs.- Le test mesurait le filtre antirobot, pas l'application. Umami répond
200avec{"beep":"boop"}à un User-AgentHeadlessChromeet ne stocke rien ; et la lecture Rybbit traitait un tableau de tranches horaires comme un nombre, donc toujours zéro.
Le point commun des trois : chaque échec se présentait comme un succès. Un code HTTP 2xx ne prouve rien ; seule l'interrogation des données après coup a valeur de preuve.
Réserves sur l'état livré
Aucune bannière de consentement n'existe dans le site.Levée le 26/10/2026 : la bannière est en place (src/features/analytics/ConsentBanner.tsx), refuser coûte un clic comme accepter, et la décision se révoque depuis le pied de page. Vérifié dans un navigateur : aucun envoi avant la décision, deux envois acceptés après l'accord, plus aucun après le refus. Reste entier : l'exemption de consentement n'est établie que pour Matomo (privacy.md) — ici, c'est le moderequiredqui est servi, donc la question ne bloque plus, mais elle se reposera si quelqu'un veut passer enexempt.Seules les vues de page sont branchées.Levée le 27/10/2026 : Quatre des cinq événements du catalogue (search_performed,outbound_link_clicked,record_viewed,feature_used) ne sont émis par aucun composant :useTrackEventn'est importé nulle part danssrc/. Ils sont testés, pas utilisés.Le catalogue double l'instrumentation existante.Tranché le 27/10/2026 :/api/telemetrycollecte déjà l'issue de recherche et la catégorie de lien sortant. Câbler les mêmes événements dans le nouveau paquet sans supprimer l'ancien chemin créerait deux mesures du même geste. À arbitrer avant d'ajouter le moindre appel.Levée le 27/10/2026 : pas de lot, pas de réessai, pas de durée de vie, alors quepage()court-circuite la file d'attentetrack()en bénéficie. C'est exactement ce chemin qui a perdu les événements sans laisser de trace. L'asymétrie n'est pas justifiée.La file ne survit pas à une navigation.Levée le 27/10/2026 : Elle est en mémoire, et chaque page du site est un chargement complet : un événement en attente est perdu au clic suivant. Le vidage survisibilitychangeutilisefetch, que le navigateur peut interrompre ;sendBeaconest fait pour ce cas.Le test applicatif n'est pas discriminant.Levée le 27/10/2026 : Il conclut sur « au moins une vue dans la dernière heure », cumule les passages successifs et ne vérifie ni l'URL reçue ni le nombre de vues attendu. Il attraperait une panne totale, pas un double comptage ni une attribution fausse.- Matomo n'est pas exercé depuis l'application. Le build vérifié ne l'activait pas, et comme le site n'émet aucun événement métier, l'activer n'enverrait rien : l'adaptateur n'émet pas les vues de page. La non-régression du double comptage ne repose donc que sur le code et les tests unitaires.
- Trois fournisseurs simultanés restent un dispositif de comparaison, pas une cible. Rybbit coûte à lui seul 5 conteneurs et près de la moitié des 2,1 Gio mesurés.
Où en sont ces réserves
Six des huit sont levées, et la façon dont elles l'ont été figure dans l'historique Git. Ce qui reste :
Matomo n'est pas exercé depuis l'application.Levée le 27/10/2026. Build avec les deux traqueurs — l'historique pour les vues, l'adaptateur du paquet pour les seuls événements — puis mesure : 5 vues côté Matomo pour 5 côté Umami, donc aucun double comptage, etrecord_viewedetsearch_performedretrouvés parLive.getLastVisitsDetails. Le test de fumée couvre les deux points et distingue désormais vues et événements, sans quoi il concluait à des vues perdues là où il comptait des événements.- Trois fournisseurs en parallèle restent un dispositif de comparaison. Choisir est une décision, pas un développement.
Trouvé en exerçant Matomo, et sans rapport avec lui : classifyOutboundUrl comparait
url.hostname (sans port) à window.location.host (avec port). Hors du port 443, tout lien
interne était compté comme lien sortant — invisible en production, mais toute mesure de
développement, de préproduction et de test était fausse. Corrigé, avec un test de non-régression.
Et deux points neufs, trouvés en corrigeant : les vues de page étaient perdues quand la file n'était vidée qu'à l'échéance, puis quand elles partaient avant la fin de l'initialisation. Les deux sont corrigées et couvertes par des tests ; c'est le test de fumée rendu discriminant qui les a montrées, pas le raisonnement.
Suite proposée, par ordre d'utilité
- Trancher le consentement. Sans cela, tout le reste est théorique.
- Trancher le doublon avec
/api/telemetry, puis câbler deux ou trois événements réels — pas les cinq. - Faire passer
page()par la file, et vider ensendBeacon. - Rendre le test applicatif discriminant : compter avant et après, et vérifier les URL reçues.
- Choisir un fournisseur unique avant tout déploiement distant.
Ce que cette étape prouve, et ce qu'elle ne prouve pas
Elle prouve qu'un site Next statique peut envoyer des données à une pile d'analytics auto-hébergée, sous consentement, sans fuite de secret dans le bundle, et que ces données sont retrouvables chez le fournisseur.
Elle ne prouve pas que le site mesure quoi que ce soit : il n'y a ni consentement praticable, ni événement métier, ni rétention définie, ni déploiement distant.