# 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](/analytics/bilans/audit)), 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

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](/analytics/conception/privacy)) — 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.
