J'ai écrit mon propre tracker de clics pour une raison simple : je voulais savoir si les
visiteurs d'une page cliquaient sur le lien qu'elle contenait. Pas combien de temps ils
restaient, pas d'où ils venaient, pas leur empreinte de navigateur. Une question, deux
chiffres.

Le premier rapport a répondu : 127 vues, 0 clic.

Ma première réaction a été de chercher le bug. Ma seconde, plus utile, a été de vérifier la
mesure avant d'accuser les visiteurs.

Le tracker, en quarante lignes

Un fichier PHP, une table SQLite, deux types d'événements.

<?php // tracker.php — appelez-le en GET avec ?p=slug&e=vue ou ?p=slug&e=clic $db = new PDO('sqlite:' . __DIR__ . '/stats.db'); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $db->exec("CREATE TABLE IF NOT EXISTS evenements ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT NOT NULL, page TEXT NOT NULL, type TEXT NOT NULL CHECK (type IN ('vue','clic')), cible TEXT, ref TEXT, ua_hash TEXT )"); $page = substr(preg_replace('/[^a-z0-9-]/', '', $_GET['p'] ?? ''), 0, 64); $type = $_GET['e'] ?? ''; $cible = substr($_GET['c'] ?? '', 0, 200); if ($page === '' || !in_array($type, ['vue', 'clic'], true)) { http_response_code(204); exit; } $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; // on garde une empreinte tronquée, pas l'adresse IP en clair $empreinte = substr(hash('sha256', $ua), 0, 16); $stmt = $db->prepare( "INSERT INTO evenements (ts, page, type, cible, ref, ua_hash) VALUES (datetime('now'), ?, ?, ?, ?, ?)" ); $stmt->execute([ $page, $type, $cible, substr($_SERVER['HTTP_REFERER'] ?? '', 0, 200), $empreinte, ]); http_response_code(204); // pas de corps : rien à afficher, rien à charger 

Côté page, deux appels : un fetch au chargement, un sur le clic.

<a href="/destination" data-track="lien-principal">Voir la fiche</a> <script> fetch("tracker.php?p=ma-page&e=vue", {keepalive: true}); document.querySelectorAll("a[data-track]").forEach(a => { a.addEventListener("click", () => { fetch(`tracker.php?p=ma-page&e=clic&c=${a.dataset.track}`, {keepalive: true}); }); }); </script> 

keepalive: true est le seul détail technique qui compte : sans lui, le navigateur annule la
requête au moment où la page de destination commence à se charger, et le clic n'est jamais
enregistré. C'est le bug classique de tous les trackers maison, et il produit exactement le
symptôme que j'avais — sauf que ce n'était pas lui cette fois, ce qui m'a fait perdre une heure
de plus.

127 et 0 : vérifier avant de conclure

Trois vérifications, dans l'ordre.

1. La base reçoit-elle quelque chose ? Oui, 127 lignes de type vue. Le tracker écrivait.

2. Le type clic arrive-t-il quand on le provoque ? Test manuel :

curl -s -o /dev/null -w "%{http_code}\n" \ "https://exemple.fr/tracker.php?p=ma-page&e=clic&c=test" sqlite3 stats.db "SELECT type, COUNT(*) FROM evenements GROUP BY type;" 

La ligne apparaît. Le code d'écriture est correct.

3. La page envoie-t-elle vraiment un clic ? C'est là qu'était la réponse : le bouton
« Voir la fiche » n'était pas un lien. C'était une image avec un onclick qui appelait
window.location, dans une balise sans href. Le script de tracking écoutait les a, et il
n'y avait aucun a à écouter.

Le tracker mesurait correctement une page qui n'avait rien à cliquer.

Pourquoi le zéro vaut mieux que la moyenne

Un outil d'analytics classique m'aurait affiché autre chose. Il aurait compté une visite, une
page vue, un taux de rebond, une durée de session, et j'en aurais déduit que la page
« intéressait ». Aucun de ces chiffres ne répond à la question que je m'étais posée.

Le zéro, lui, répond. Il dit : personne n'a cliqué. Et comme il est inconfortable, il oblige à
descendre dans le détail — jusqu'à la balise <a> manquante.

C'est la différence entre une métrique de vanité et une mesure honnête :

  • une métrique de vanité monte quand on travaille plus, et ne descend jamais toute seule ;
  • une mesure honnête peut valoir zéro, et c'est précisément pour ça qu'on la croit quand elle vaut autre chose.

Une page vue n'est pas un intérêt. Un temps de lecture estimé n'est pas une lecture. Un taux de
clic de 0 % est un fait : il y a un lien à réparer, ou une audience qui n'est pas là.

Ce que j'ai changé : la page, pas le tracker

La tentation, quand un chiffre déplaît, c'est de changer le thermomètre. Ajouter une mesure de
défilement, compter les « intentions de clic », élargir la définition d'un clic jusqu'à ce
qu'elle inclue les survols.

J'ai fait l'inverse. J'ai corrigé le HTML, remis un vrai <a href>, et laissé le tracker
identique. La question « est-ce que les gens cliquent » n'a de valeur que si la réponse peut
être non.

Et j'ai gardé le zéro dans l'historique au lieu de le masquer. Un rapport qui commence par
« 127 vues, 0 clic » raconte une histoire vraie : une page a été vue, et elle n'offrait rien à
cliquer. Effacer cette ligne aurait rendu les chiffres suivants illisibles.

Les limites d'un tracker maison

Elles existent, et il faut les connaître avant de prendre une décision sur ses propres
chiffres.

  • Les robots. Une partie des 127 vues n'est peut-être pas humaine. Un filtre sur le User-Agent en élimine une partie, jamais la totalité.
  • Les bloqueurs. Un visiteur avec un bloqueur de publicité peut bloquer tracker.php s'il est sur une liste. La mesure est donc un plancher, pas un total.
  • Pas de déduplication. Un rechargement compte une vue de plus. C'est acceptable pour comparer deux périodes, faux pour compter des personnes.
  • L'attribution est approximative. Le Referer est absent dans beaucoup de cas, et falsifiable dans tous.
  • Le stockage. Une empreinte de User-Agent n'identifie personne, mais ce n'est pas anonyme pour autant. Je la tronque, je ne stocke pas l'IP, et je purge la table au bout de 90 jours.

Ces limites ne rendent pas la mesure inutile. Elles rendent le chiffre interprétable : 127
vues est un ordre de grandeur, pas une audience. 0 clic est un fait, pas une estimation.

Ce qu'un tableau de bord honnête doit accepter

Un tableau de bord n'est pas un miroir de son travail. C'est un miroir de ce qu'on a décidé de
mesurer — et cette décision est plus importante que l'outil.

J'ai construit celui-ci pour répondre à une question précise, avec deux colonnes et une
requête :

SELECT page, SUM(type = 'vue') AS vues, SUM(type = 'clic') AS clics FROM evenements WHERE ts >= datetime('now', '-30 day') GROUP BY page ORDER BY vues DESC; 

Deux colonnes, une période, pas de score composite. C'est tout ce que j'ai besoin de savoir, et
c'est suffisant pour constater qu'une page est vue et que son lien ne fonctionne pas.

Le reste — le temps passé, la profondeur de défilement, le parcours de conversion — ne
répondrait pas mieux. Il donnerait juste plus de chiffres à interpréter, et une pente plus
facile à trouver flatteuse.