Bilans

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érificationRésultatCommande
Paquets en Node, données retrouvées chez les 4 services15 réussites, 0 échecpnpm telemetry:test
Site construit, navigateur réel, données retrouvées6 réussites, 0 échecpnpm telemetry:test:app
Formatage, lint, typage, tests64 tests, au vertmake check
Aucun secret dans le bundle navigateur23 660 fichiers, 0 trouvépnpm analytics:check-bundle
Construction complète après nettoyage23 449 pages, aucun chunk absentrm -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

  1. Illegal invocation. fetch conservé en champ de classe puis appelé par this.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.
  2. skipFirst sur 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.
  3. Le test mesurait le filtre antirobot, pas l'application. Umami répond 200 avec {"beep":"boop"} à un User-Agent HeadlessChrome et 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é

  1. 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 mode required qui est servi, donc la question ne bloque plus, mais elle se reposera si quelqu'un veut passer en exempt.
  2. 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 : useTrackEvent n'est importé nulle part dans src/. Ils sont testés, pas utilisés.
  3. Le catalogue double l'instrumentation existante. Tranché le 27/10/2026 : /api/telemetry collecte 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.
  4. page() court-circuite la file d'attente Levée le 27/10/2026 : pas de lot, pas de réessai, pas de durée de vie, alors que track() 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.
  5. 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 sur visibilitychange utilise fetch, que le navigateur peut interrompre ; sendBeacon est fait pour ce cas.
  6. 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.
  7. 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.
  8. 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, et record_viewed et search_performed retrouvés par Live.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é

  1. Trancher le consentement. Sans cela, tout le reste est théorique.
  2. Trancher le doublon avec /api/telemetry, puis câbler deux ou trois événements réels — pas les cinq.
  3. Faire passer page() par la file, et vider en sendBeacon.
  4. Rendre le test applicatif discriminant : compter avant et après, et vérifier les URL reçues.
  5. 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.