Québec, Canada

403-1381 1re Avenue

+1 581.849.27.96

bdgouthiere@gmail.com

Mesurer son audience sans JavaScript : ce que racontent les journaux du serveur

Ou : Compter ses lecteurs avec ce que le serveur sait déjà

Ce site n’embarque aucun script de mesure d’audience. Pas de Google Analytics, pas de Matomo, pas de pixel : la configuration de Hugo désactive même explicitement le module de Google. Ce n’est pas de l’ascétisme. Un serveur web note de toute façon chaque requête dans son journal, que tu le lui demandes ou non, et ce journal contient déjà l’essentiel de ce qu’un outil de suivi te vendrait : quelle page, quand, venue d’où, demandée par quel logiciel. Les données restent chez moi, la page ne charge rien de plus, et personne n’a à cliquer sur une bannière. La contrepartie est qu’un journal ne sait que ce que le serveur voit. Il ne sait rien de ce qui se passe dans le navigateur, et il croit sur parole tout ce qu’on lui raconte.

Une ligne par requête, et rien d’autre

Le serveur, Caddy, écrit son journal au format « combined », celui qu’Apache a popularisé. Une requête, une ligne :

1
192.0.2.10 - - [01/Oct/2026:09:00:00 +0000] "GET /blog/ HTTP/2.0" 200 5631 "https://duckduckgo.com/" "Mozilla/5.0 (X11; Linux x86_64) ..."

L’adresse IP, la date, la méthode et le chemin, le code de réponse, la taille, la page d’où vient le visiteur, et l’identifiant que le logiciel donne de lui-même, le user-agent. L’adresse de cet exemple appartient à une plage réservée à la documentation : je ne publie pas celles de mes lecteurs.

Le circuit tient en un script. Toutes les heures, une tâche planifiée de mon poste rapatrie le journal par rsync, avec une clé SSH qui ne sert qu’à ça : côté serveur, elle est bridée en lecture seule sur le dossier des journaux, sans shell ni redirection de port. Le script lance ensuite GoAccess, qui produit des rapports HTML pour le jour, les 7 et les 30 derniers jours, avec deux options : --anonymize-ip, qui met à zéro le dernier octet des adresses IPv4 et les 80 derniers bits des IPv6, et --ignore-crawlers, qui écarte les robots1. Un simple grep isole à part les lignes qui mentionnent Googlebot, Bingbot et quelques autres, pour des rapports consacrés aux robots.

Ce qu’ils disent

Sur les trente jours qui se terminent le 4 octobre 2026, le serveur a reçu 77 795 requêtes. Un peu plus de la moitié, 40 508, ont reçu une erreur 4xx. Ce n’est pas le signe d’un site cassé : 14 959 requêtes visaient des chemins WordPress ou PHP, wp-login.php, xmlrpc.php et leurs cousins, sur un site qui a quitté WordPress pour Hugo. Ce sont des robots qui testent des portes, comme ils les testent sur tout Internet.

Le journal dit aussi d’où viennent les lecteurs. Sur la période du 17 août au 15 septembre, les moteurs de recherche envoyaient 127 visiteurs depuis Google, 86 depuis DuckDuckGo et 25 depuis Qwant. Aucun outil de suivi ne m’aurait appris quelque chose de plus utile sur ce public : il cherche ailleurs que chez Google dans une proportion que les tableaux de bord habituels laissent rarement voir.

Et le journal raconte le passage des robots d’indexation. Googlebot a fait 686 requêtes sur trente jours, de 3 certains jours à 157 d’autres. Comme le journal contient le chemin, je sais quelles sections il visite et lesquelles il ignore, requête par requête, là où les statistiques d’exploration de la Search Console se contentent d’exemples.

Les robots qui se font passer pour du public

C’est ici que l’option --ignore-crawlers mérite un test. GoAccess reconnaît les robots grâce à une liste interne de user-agents. J’ai pris une ligne réelle de chaque robot présent dans mon journal, remplacé l’adresse par une adresse de documentation, et demandé à GoAccess 1.8.1 s’il la comptait. Googlebot, Bingbot, GPTBot, OAI-SearchBot, PerplexityBot, Amazonbot et SemrushBot sont bien écartés. ClaudeBot et Bytespider, le robot de ByteDance, ne le sont pas : ils sont comptés comme des visiteurs humains. Sur les trente derniers jours, ils représentent 4 922 requêtes. Depuis décembre 2025, Bytespider seul en totalise 20 288.

La seconde mise en garde est plus fondamentale. Le user-agent est une déclaration, pas une preuve. N’importe quel script peut s’annoncer comme Googlebot. Google documente d’ailleurs comment vérifier qu’une requête vient vraiment de chez lui, par une recherche DNS inverse sur l’adresse suivie d’une recherche directe qui doit retomber sur la même adresse2. Mes 686 requêtes de Googlebot sont donc, en toute rigueur, 686 requêtes qui se disent Googlebot.

Pour savoir à quoi ressemble vraiment mon trafic, j’ai écrit un classificateur d’une trentaine de lignes, sans dépendance.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import re
import sys
from collections import Counter

LINE = re.compile(r'^\S+ \S+ \S+ \[[^\]]+\] "(\S+) (\S+) [^"]*" (\d{3}) \S+ "[^"]*" "([^"]*)"')
FAMILIES = [  # first match wins: specific names before the generic catch-all
    ("google", re.compile(r"Googlebot|AdsBot-Google", re.I)),
    ("bing", re.compile(r"bingbot", re.I)),
    ("ai", re.compile(r"GPTBot|OAI-SearchBot|ClaudeBot|PerplexityBot|Bytespider|Amazonbot|CCBot", re.I)),
    ("other bots", re.compile(r"bot\b|spider|crawler|SemrushBot|AhrefsBot", re.I)),
]

def family(ua):
    for name, rx in FAMILIES:
        if rx.search(ua):
            return name
    return "human?"  # unknown agents only: the question mark is deliberate

counts, probes = Counter(), 0
for line in open(sys.argv[1], encoding="utf-8", errors="replace"):
    m = LINE.match(line)
    if not m:
        continue
    method, path, status, ua = m.groups()
    if status.startswith("4") and re.search(r"wp-|xmlrpc|\.php", path):
        probes += 1
        continue
    counts[family(ua)] += 1

for name, n in counts.most_common():
    print(f"{name:12} {n}")
print(f"{'probes':12} {probes}")

Passé sur le journal complet, du 14 décembre 2025 au 4 octobre 2026, il donne ceci.

FamilleRequêtesPart
Inconnus, peut-être humains304 88855,8 %
Sondes WordPress et PHP155 34228,4 %
Autres robots déclarés42 5997,8 %
Robots d’IA36 1366,6 %
Googlebot5 2411,0 %
Bingbot2 6330,5 %

Le point d’interrogation de la première ligne n’est pas décoratif. « Inconnu » veut dire « ne s’est pas annoncé comme robot », ce qui inclut tes lecteurs, mais aussi tous les scripts qui ont l’élégance de se faire passer pour Chrome.

Ce qu’ils taisent

Un journal ne voit que des requêtes. Il ne sait pas combien de temps tu restes sur une page, puisque rien ne lui signale ton départ. Il ne voit pas les pages relues depuis le cache de ton navigateur. Il ne suit pas un parcours de façon fiable : GoAccess considère comme un même visiteur les requêtes qui partagent l’adresse IP, la date et le user-agent1, et cette définition se trompe dans les deux sens. Une entreprise ou un opérateur mobile peut faire sortir des centaines de personnes par la même adresse, et un téléphone qui passe du Wi-Fi à la 5G en change en cours de lecture.

DevOps Dave : J’ai 5 635 visiteurs uniques sur trente jours, c’est écrit dans le rapport.

Security Sarah : Combien sont des robots qui ont oublié de le dire ?

DevOps Dave : Le rapport ignore les robots, j’ai coché l’option.

Security Sarah : Il ignore ceux qu’il connaît.

L’absence de bannière ne dispense de rien. Une adresse IP, même dynamique, est une donnée personnelle dès que l’exploitant du site dispose de moyens légaux de faire identifier la personne, grâce aux informations que détient son fournisseur d’accès, a jugé la Cour de justice de l’Union européenne dans l’arrêt Breyer de 20163. L’anonymisation de GoAccess ne s’applique qu’aux rapports : le fichier brut, lui, garde les adresses complètes. Il lui faut donc une base légale, une mention dans la politique de confidentialité et surtout une durée de conservation limitée.

Quant au consentement, la règle des cookies vise ce qu’on dépose ou lit dans le terminal du visiteur, ce qu’un journal ne fait pas. Le Comité européen de la protection des données a toutefois écrit en 2024 qu’un suivi fondé sur la seule adresse IP peut relever de la même règle quand cette adresse provient du terminal, ce qui est le cas des adresses IPv64. Un rapport agrégé n’est pas un suivi de navigation personne par personne, mais je ne suis pas juriste, et la frontière n’est pas tracée au cordeau. Le plus sûr reste de garder peu, peu longtemps, et de l’écrire.

Le serveur sait déjà qui passe. Il note tout, croit tout le monde, et n’oublie rien tant qu’on ne le lui demande pas. Ce sont trois défauts qu’un bon outil de mesure s’efforce de corriger, et trois choses qu’il vaut mieux savoir avant de présenter ses chiffres à qui que ce soit.



  1. Le manuel de GoAccess définit le visiteur unique ainsi : « HTTP requests containing the same IP, same date, and same user agent are considered a unique visitor ». Il précise que l’anonymisation met à zéro le dernier octet des adresses IPv4 et les 80 derniers bits des adresses IPv6. La liste des robots reconnus par --ignore-crawlers est interne au programme : mon test porte sur la version 1.8.1, et une version plus récente peut en reconnaître davantage. ↩︎ ↩︎

  2. La documentation de Google sur la vérification de Googlebot décrit deux méthodes : une recherche DNS inverse sur l’adresse, dont le nom doit appartenir à googlebot.com, google.com ou googleusercontent.com, suivie d’une recherche directe qui doit redonner l’adresse de départ ; ou la comparaison avec les plages d’adresses que Google publie en JSON. ↩︎

  3. CJUE, 19 octobre 2016, Patrick Breyer contre Bundesrepublik Deutschland, affaire C-582/14. Le requérant reprochait aux sites fédéraux allemands d’enregistrer les adresses IP de leurs visiteurs. La Cour a aussi jugé que l’objectif d’assurer le bon fonctionnement du site, face aux attaques notamment, doit pouvoir entrer dans la balance de l’intérêt légitime, ce qui tombe plutôt bien quand plus d’un quart des requêtes sont des sondes. ↩︎

  4. Les lignes directrices 2/2023 du Comité européen de la protection des données (PDF, version 2.0 adoptée le 7 octobre 2024) consacrent une section au « tracking based on IP only ». Elles indiquent que l’accès à l’adresse IP relève de l’article 5(3) de la directive ePrivacy « in cases where this information originates from the terminal equipment », ce qui vise notamment les adresses IPv6, et demandent à l’entité de prendre toutes les mesures prévues par cet article sauf si elle peut garantir le contraire. ↩︎