Mesurer l'impact des citations d'IA : Analyses, journaux et extractions de panneaux
Introduction
La découverte pilotée par l'IA est désormais répartie sur plusieurs couches de mesure. Une personne peut voir une entreprise citée dans une réponse d'IA, cliquer sur un lien sans référent utile, visiter via un navigateur qui supprime les données de campagne, ou lire une réponse sans cliquer du tout. Pendant ce temps, un robot d'exploration d'IA peut demander la page de l'entreprise sans représenter un visiteur humain.
Un système de mesure utile doit donc séparer quatre questions :
- Le site a-t-il été exploré ou récupéré ?
- Le site a-t-il été cité dans une réponse d'IA ?
- Une personne a-t-elle visité le site à partir de cette réponse ?
- Cette visite a-t-elle produit un engagement ou des revenus significatifs ?
Depuis le 22 septembre 2026, les rapports propriétaires s'améliorent. Google Search Console fournit un rapport de performances de l'IA générative pour les AI Overviews et le AI Mode, incluant les impressions, les pages, les pays, les appareils et les dates. Le rapport est devenu disponible dans le monde entier le 31 août 2026. Rapport sur les performances de l'IA générative de Google (support.google.com)
Microsoft fournit également un rapport AI Performance dans les outils pour webmasters de Bing. Sa prévisualisation publique rapporte le total des citations, les pages citées, les requêtes d'ancrage et les tendances de citation à travers les expériences d'IA Microsoft prises en charge. Performances de l'IA dans les outils pour webmasters de Bing (blogs.bing.com)
Ces outils doivent être considérés comme faisant autorité pour leurs propres surfaces, mais ils ne créent pas un système de mesure complet et inter-assistants. L'approche recommandée consiste à combiner les rapports propriétaires, les journaux de serveur, les liens de suivi contrôlés, les exportations d'analyse et les observations planifiées des panneaux de réponses.
1. Pourquoi la mesure des références d'IA est difficile
L'analyse web traditionnelle dépend fortement de l'en-tête Referer et des paramètres de campagne. Les deux peuvent être manquants ou incomplets.
L'en-tête HTTP Referer peut contenir la page précédente, seulement l'origine du site web précédent, ou aucune valeur du tout. Les politiques de référencement des navigateurs peuvent intentionnellement limiter ou supprimer cette information. Documentation MDN sur Referrer-Policy (developer.mozilla.org)
Les systèmes d'IA créent des complications supplémentaires :
- Un assistant peut lier à une page sans envoyer de référent reconnaissable.
- Un assistant peut utiliser une URL de redirection ou réécrite.
- Un assistant peut récupérer du contenu côté serveur avant d'afficher une réponse.
- Un robot d'exploration d'IA peut visiter une page sans qu'aucun humain ne la voie.
- La réponse peut citer une URL canonique au lieu de l'URL initialement récupérée.
- La réponse peut varier en fonction de l'emplacement, de l'appareil, de la langue, de l'heure ou de la personnalisation.
Par exemple, OpenAI distingue OAI-SearchBot pour la découverte de recherche, GPTBot pour une éventuelle utilisation à des fins d'entraînement, et ChatGPT-User pour les actions initiées par l'utilisateur. Perplexity identifie séparément PerplexityBot et Perplexity-User. Anthropic identifie ClaudeBot, Claude-SearchBot et Claude-User. Ces requêtes ne doivent pas être combinées en un seul nombre générique de « trafic d'IA ». Documentation des robots d'exploration OpenAI (openai.com) Documentation des robots d'exploration Perplexity (docs.perplexity.ai) Documentation des robots d'exploration Anthropic (support.anthropic.com)
La règle centrale est la suivante :
L'activité des robots d'exploration n'est pas du trafic de référence, la visibilité des citations n'est pas du trafic, et le trafic n'est pas un impact commercial.
2. Le cadre de mesure
Utilisez cinq couches de mesure.
| Couche | Question principale | Données recommandées |
|---|---|---|
| Accès | Un système d'IA a-t-il demandé la page ? | Journaux du serveur web, journaux du pare-feu |
| Visibilité | La page a-t-elle été affichée ou citée ? | Google Search Console, Bing Webmaster Tools, observations des panneaux de réponses |
| Acquisition | Une personne est-elle arrivée d'une surface d'IA ? | Référent, paramètres de campagne, clics sur liens courts, analyses |
| Engagement | Le visiteur a-t-il interagi de manière significative ? | Événements d'analyse, pages vues, temps d'engagement |
| Valeur | La visite a-t-elle produit un résultat souhaité ? | Inscriptions, prospects, achats, abonnements |
Hiérarchie recommandée de la source de vérité
Utilisez le signal le plus fort disponible pour chaque question :
- Rapports propriétaires de plateformes pour les impressions et les citations.
- Observations des panneaux de réponses pour la visibilité au niveau des invites et les comparaisons concurrentielles.
- Journaux de redirection pour les clics sur les liens contrôlés.
- Analyses web pour les sessions, l'engagement et les conversions.
- Journaux de serveur pour l'activité des robots d'exploration et des récupérateurs.
- Estimations modélisées uniquement lorsque les preuves directes ne sont pas disponibles.
Ne combinez pas tous les signaux en un seul nombre sans préserver la source et le niveau de confiance.
3. Modèle de données de base
Créez un entrepôt avec des tables séparées pour les observations brutes et les métriques dérivées.
panel_runs
Une ligne pour chaque vérification d'invite et de surface.
text run_id observed_at surface topic prompt_id query_text locale country device panel_present panel_type answer_hash raw_html_path screenshot_path collector_version status
panel_citations
Une ligne pour chaque lien observé dans un panneau de réponses.
text run_id surface topic prompt_id cited_url cited_domain citation_position link_scope target_domain target_cited
ai_crawler_requests
Données agrégées des journaux de serveur.
text date bot_name bot_role path requests successful_requests error_requests unique_ip_count
shortener_clicks
Une ligne pour chaque requête de lien contrôlé.
text click_id occurred_at slug surface topic prompt_id asset_id referrer_host user_agent_family ip_hmac destination_path
ai_sessions
Une table d'analyse normalisée.
text session_id date surface topic prompt_id landing_page attribution_method confidence engaged engagement_time_seconds key_event revenue
Le champ confidence est important. Les valeurs recommandées sont :
- A — Direct : clic sur lien court contrôlé ou paramètre de campagne fiable.
- B — Fort : référent d'assistant connu ou requête de récupération utilisateur vérifiée.
- C — Probable : preuve de campagne ou de page de destination sans référent complet.
- D — Modélisé : inféré à partir du timing, des observations de panneau ou des changements agrégés.
Signalez A et B séparément de C et D.
4. Définitions des métriques
Part de citation par sujet
La part de citation par sujet mesure la fréquence à laquelle un domaine cible apparaît parmi les sources observées pour un sujet.
[ \text{Part de citation}_{sujet}
\frac{\text{Occurrences de citation du domaine cible}} {\text{Toutes les occurrences de citation}} ]
Exemple :
- 100 citations totales observées pour « logiciel de gestion de projet »
- 12 citations pointent vers votre domaine
- Part de citation par sujet = 12 %
Cette métrique mesure la part de la source, et non si une réponse contenait votre site au moins une fois.
Couverture des citations
Utilisez une deuxième métrique pour la présence au niveau de la réponse :
[ \text{Couverture des citations}_{sujet}
\frac{\text{Exécutions de réponses contenant une citation cible}} {\text{Exécutions de réponses où un panneau était présent}} ]
La couverture des citations est souvent plus facile à comprendre pour un public non technique :
« Notre site est apparu dans 34 % des réponses d'IA observées pour ce sujet. »
Position de la citation
Enregistrez la position d'une citation dans la réponse :
1= première source observée2= deuxième source observée3= troisième source observée
Étant donné que les interfaces d'IA regroupent ou réorganisent souvent les sources, traitez la position comme une observation descriptive, non comme un classement universel.
Taux de référence de l'assistant
Le taux de référence de l'assistant mesure la proportion de sessions humaines sur le site pouvant être attribuées à une surface d'IA.
[ \text{Taux de référence de l'assistant}
\frac{\text{Sessions humaines attribuées à l'IA}} {\text{Toutes les sessions humaines}} ]
Signalez au moins deux versions :
text Taux de référence d'assistant confirmé = sessions IA de confiance A / toutes les sessions humaines
Taux de référence d'assistant étendu = sessions IA de confiance A + B + C / toutes les sessions humaines
La version confirmée est plus conservatrice. La version étendue est utile pour l'analyse des tendances mais ne doit pas être présentée comme un trafic entièrement observé.
N'utilisez pas les exécutions de panneaux comme impressions
Si vous extrayez 500 invites, cela ne signifie pas que 500 personnes ont vu ces réponses. Votre outil d'extraction mesure un échantillon contrôlé d'états de réponse. Utilisez les rapports d'impressions des plateformes propriétaires lorsqu'ils sont disponibles.
Engagement post-citation
L'engagement post-citation mesure si les personnes arrivées via une citation d'IA ont effectué une action significative.
[ \text{Engagement post-citation}
\frac{\text{Sessions IA engagées}} {\text{Sessions attribuées à l'IA}} ]
Pour Google Analytics 4, une session engagée dure plus de 10 secondes, comprend un événement clé ou comprend au moins deux pages ou vues d'écran. Documentation du taux d'engagement de Google Analytics (support.google.com)
Signalez également :
text Temps d'engagement post-citation = temps médian d'engagement en secondes par session IA
Taux de conversion post-citation = sessions IA avec un événement clé / sessions attribuées à l'IA
Revenus post-citation par session = revenus des sessions IA / sessions attribuées à l'IA
Le temps d'engagement médian est généralement plus stable que le temps d'engagement moyen, car un petit nombre de sessions très longues peut fausser la moyenne.
Métriques utiles supplémentaires
Taux de succès des robots d'exploration d'IA
[ \text{Taux de succès des robots d'exploration}
\frac{\text{Requêtes des robots d'exploration d'IA renvoyant 2xx ou 3xx}} {\text{Requêtes des robots d'exploration d'IA}} ]
Ventilez ceci par bot et par chemin. Un taux de déclin de citation élevé combiné à un taux d'erreur de robot d'exploration croissant peut indiquer un problème d'hébergement, de pare-feu ou de configuration des robots.
Taux de citation-à-clic
Pour les observations contrôlées des panneaux de réponses :
[ \text{Taux de citation-à-clic observé}
\frac{\text{Clics confirmés sur liens courts}} {\text{Citations cibles observées}} ]
Ce n'est pas un véritable taux de clics utilisateur, sauf si le dénominateur représente les impressions utilisateur réelles. Étiquetez-le clairement comme une métrique opérationnelle observée.
Fraîcheur des citations
Mesurez la fréquence à laquelle l'URL citée reflète la version actuelle de la page.
text Fraîcheur des citations = pages citées avec hachage de contenu actuel / pages citées
Ceci est utile pour les pages de documentation, de tarification, de produits et de politiques qui changent fréquemment.
5. Analyse des journaux de serveur
Configurer les journaux d'accès structurés
Les journaux structurés sont plus faciles à analyser que les journaux traditionnels séparés par des espaces.
Exemple de configuration Nginx :
nginx log_format ai_json escape=json '{' '"ts":"$time_iso8601",' '"remote_addr":"$remote_addr",' '"request":"$request",' '"status":$status,' '"bytes":$body_bytes_sent,' '"referer":"$http_referer",' '"user_agent":"$http_user_agent",' '"host":"$host"' '}';
access_log /var/log/nginx/ai-access.jsonl ai_json;
Stockez les fichiers journaux dans un emplacement restreint. N'envoyez pas d'adresses IP brutes à une table d'analyse générale, sauf si vous avez une raison opérationnelle claire.
Identifier les types de requêtes d'IA
Au minimum, séparez :
text OpenAI:
- OAI-SearchBot
- ChatGPT-User
- GPTBot
Anthropic:
- Claude-SearchBot
- Claude-User
- ClaudeBot
Perplexity:
- PerplexityBot
- Perplexity-User
Moteurs de recherche:
- Googlebot
- Bingbot
Les fonctionnalités de recherche d'IA de Google utilisent généralement les contrôles d'exploration existants de Google Search plutôt qu'un « bot de présentation d'IA » universel séparé. Google déclare que Googlebot contrôle l'accès pour la recherche, y compris les fonctionnalités d'IA. Fonctionnalités et sites web de Google AI (developers.google.com)
Les chaînes d'agent utilisateur peuvent être falsifiées. Pour les décisions opérationnelles, combinez la correspondance d'agent utilisateur avec les plages d'IP publiées par les fournisseurs ou la résolution DNS inversée vérifiée, si disponible. OpenAI et Perplexity publient des informations IP pour leurs robots d'exploration et récupérateurs pertinents. (openai.com)
Résumeur de journaux reproductible
Enregistrez ce qui suit sous ai_log_summary.py.
python #!/usr/bin/env python3
import csv import hashlib import hmac import json import os import re import sys from collections import Counter, defaultdict from datetime import datetime from urllib.parse import urlsplit
BOT_PATTERNS = { "OAI-SearchBot": ("openai", re.compile(r"\bOAI-SearchBot\b", re.I)), "ChatGPT-User": ("openai_user", re.compile(r"\bChatGPT-User\b", re.I)), "GPTBot": ("openai_training", re.compile(r"\bGPTBot\b", re.I)), "Claude-SearchBot": ("anthropic", re.compile(r"\bClaude-SearchBot\b", re.I)), "Claude-User": ("anthropic_user", re.compile(r"\bClaude-User\b", re.I)), "ClaudeBot": ("anthropic_training", re.compile(r"\bClaudeBot\b", re.I)), "PerplexityBot": ("perplexity", re.compile(r"\bPerplexityBot\b", re.I)), "Perplexity-User": ("perplexity_user", re.compile(r"\bPerplexity-User\b", re.I)), "Googlebot": ("google_search", re.compile(r"\bGooglebot\b", re.I)), "Bingbot": ("bing_search", re.compile(r"\bbingbot\b", re.I)), }
SECRET = os.environ.get("HMAC_SECRET", "replace-this-in-production").encode()
def hmac_ip(ip): return hmac.new(SECRET, ip.encode(), hashlib.sha256).hexdigest()[:16]
def classify_bot(user_agent): for bot_name, (role, pattern) in BOT_PATTERNS.items(): if pattern.search(user_agent or ""): return bot_name, role return None, None
def parse_path(request_line): try: parts = request_line.split() return urlsplit(parts[1]).path except Exception: return "/"
def main(input_path): totals = defaultdict(lambda: { "requests": 0, "success": 0, "errors": 0, "ips": set(), "paths": Counter(), })
with open(input_path, "r", encoding="utf-8") as source:
for line in source:
try:
record = json.loads(line)
except json.JSONDecodeError:
continue
bot_name, bot_role = classify_bot(record.get("user_agent", ""))
if not bot_name:
continue
timestamp = record.get("ts", "")
date = datetime.fromisoformat(
timestamp.replace("Z", "+00:00")
).date().isoformat()
status = int(record.get("status", 0))
path = parse_path(record.get("request", ""))
key = (date, bot_name, bot_role)
totals[key]["requests"] += 1
totals[key]["ips"].add(hmac_ip(record.get("remote_addr", "")))
totals[key]["paths"][path] += 1
if 200 <= status < 400:
totals[key]["success"] += 1
else:
totals[key]["errors"] += 1
writer = csv.writer(sys.stdout)
writer.writerow([
"date",
"bot_name",
"bot_role",
"requests",
"successful_requests",
"error_requests",
"unique_ip_count",
"top_paths",
])
for (date, bot_name, bot_role), values in sorted(totals.items()):
top_paths = values["paths"].most_common(10)
writer.writerow([
date,
bot_name,
bot_role,
values["requests"],
values["success"],
values["errors"],
len(values["ips"]),
json.dumps(top_paths),
])
if name == "main": if len(sys.argv) != 2: print("Usage: python ai_log_summary.py /path/to/access.jsonl") sys.exit(1)
main(sys.argv[1])
Exécutez-le avec :
bash
HMAC_SECRET='a-long-random-secret'
python ai_log_summary.py /var/log/nginx/ai-access.jsonl \
data/ai_crawler_daily.csv
Ceci produit des preuves opérationnelles concernant l'activité de récupération, et non le trafic humain.
6. Raccourcisseurs de liens personnalisés pour les résumés destinés à l'IA
Un raccourcisseur de liens propriétaire fournit un point de mesure contrôlé lorsque vous publiez des résumés ou des documents sources susceptibles d'être référencés par des systèmes d'IA.
Utilisez un domaine que vous contrôlez, tel que :
text https://go.example.com/ai-docs
La destination doit inclure des paramètres de campagne tels que :
text utm_source=perplexity utm_medium=ai_answer utm_campaign=product_documentation utm_content=pricing_summary_v3
Google recommande les paramètres de campagne pour identifier les sources de trafic et les campagnes. Documentation sur les URL de campagne de Google Analytics (support.google.com)
Champs de lien recommandés
text surface topic prompt_id asset_id content_version destination_path
N'insérez pas d'informations personnelles, d'adresses e-mail ou de requêtes de recherche d'utilisateur dans le lien. Les identifiants de campagne doivent décrire l'actif publié, et non identifier une personne.
Limitations importantes
Un lien court est un dispositif de calibration, pas une solution universelle :
- L'assistant peut réécrire l'URL de destination.
- L'assistant peut citer l'URL canonique à la place.
- Le lien peut être affiché mais jamais cliqué.
- Un navigateur ou un outil de confidentialité peut supprimer le référent.
- Certains systèmes peuvent récupérer la destination côté serveur.
- Un lien court ne mesure pas les utilisateurs qui se souviennent de la marque et reviennent plus tard.
N'utilisez pas un raccourcisseur pour masquer la destination, contourner les systèmes de sécurité ou manipuler les citations. Utilisez une redirection claire vers une page pertinente et canonique.
Raccourcisseur propriétaire minimal
L'exemple suivant utilise FastAPI et SQLite. Protégez le point de terminaison de création de liens avec une authentification avant de l'utiliser en production.
python
shortener.py
import hashlib import hmac import os import secrets import sqlite3 from datetime import datetime, timezone from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
from fastapi import FastAPI, HTTPException, Request from fastapi.responses import RedirectResponse from pydantic import BaseModel
DB_PATH = os.environ.get("SHORTENER_DB", "shortener.db") HMAC_SECRET = os.environ.get( "HMAC_SECRET", "replace-this-in-production" ).encode()
ALLOWED_HOST = "www.example.com"
app = FastAPI()
def db(): connection = sqlite3.connect(DB_PATH) connection.row_factory = sqlite3.Row return connection
def init_db(): connection = db() connection.execute(""" CREATE TABLE IF NOT EXISTS links ( slug TEXT PRIMARY KEY, destination TEXT NOT NULL, surface TEXT NOT NULL, topic TEXT, prompt_id TEXT, asset_id TEXT, created_at TEXT NOT NULL ) """) connection.execute(""" CREATE TABLE IF NOT EXISTS clicks ( id INTEGER PRIMARY KEY AUTOINCREMENT, occurred_at TEXT NOT NULL, slug TEXT NOT NULL, surface TEXT, topic TEXT, prompt_id TEXT, referrer_host TEXT, user_agent TEXT, ip_hmac TEXT ) """) connection.commit() connection.close()
def append_query(url, params): parsed = urlsplit(url) query = dict(parse_qsl(parsed.query, keep_blank_values=True)) query.update(params) return urlunsplit(( parsed.scheme, parsed.netloc, parsed.path, urlencode(query), parsed.fragment, ))
def hash_ip(ip): return hmac.new( HMAC_SECRET, (ip or "").encode(), hashlib.sha256 ).hexdigest()[:16]
class LinkRequest(BaseModel): destination_path: str surface: str topic: str | None = None prompt_id: str | None = None asset_id: str | None = None
@app.on_event("startup") def startup(): init_db()
@app.post("/links") def create_link(payload: LinkRequest): destination = f"https://{ALLOWED_HOST}{payload.destination_path}"
if not payload.destination_path.startswith("/"):
raise HTTPException(400, "destination_path must be a site-relative path")
slug = secrets.token_urlsafe(7).replace("_", "-").replace("/", "-")
target = append_query(destination, {
"utm_source": payload.surface,
"utm_medium": "ai_answer",
"utm_campaign": "citation_measurement",
"utm_content": payload.asset_id or "unspecified",
})
connection = db()
connection.execute("""
INSERT INTO links
(slug, destination, surface, topic, prompt_id, asset_id, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (
slug,
target,
payload.surface,
payload.topic,
payload.prompt_id,
payload.asset_id,
datetime.now(timezone.utc).isoformat(),
))
connection.commit()
connection.close()
return {"slug": slug, "url": f"https://go.example.com/{slug}"}
@app.get("/{slug}") def redirect(slug: str, request: Request): connection = db() row = connection.execute( "SELECT * FROM links WHERE slug = ?", (slug,) ).fetchone()
if not row:
connection.close()
raise HTTPException(404, "Unknown link")
referrer = request.headers.get("referer", "")
referrer_host = urlsplit(referrer).hostname or ""
connection.execute("""
INSERT INTO clicks
(occurred_at, slug, surface, topic, prompt_id,
referrer_host, user_agent, ip_hmac)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (
datetime.now(timezone.utc).isoformat(),
slug,
row["surface"],
row["topic"],
row["prompt_id"],
referrer_host,
request.headers.get("user-agent", "")[:300],
hash_ip(request.client.host if request.client else ""),
))
connection.commit()
connection.close()
return RedirectResponse(row["destination"], status_code=302)
Exécutez-le avec :
bash pip install fastapi uvicorn uvicorn shortener:app --host 0.0.0.0 --port 8080
Utilisez une redirection 302 pour les liens de mesure afin que les navigateurs et les systèmes intermédiaires soient moins susceptibles de mettre la redirection en cache de manière permanente.
7. Extraction planifiée des panneaux de réponses d'IA
L'extraction de panneaux est utile pour répondre à des questions que les analyses propriétaires ne peuvent pas toujours révéler :
- Quelles sources concurrentes sont citées ?
- Quelle page est citée pour une invite spécifique ?
- La réponse change-t-elle par pays ou par appareil ?
- La citation apparaît-elle dans la réponse, la liste des sources, ou les deux ?
- Quelle est la stabilité de la réponse au fil du temps ?
- L'assistant cite-t-il un lien court ou l'URL canonique ?
Cependant, l'extraction doit être effectuée avec précaution.
Règles pour une collecte responsable
- Préférez une API officielle, un fournisseur de données de recherche sous licence ou un rapport propriétaire.
- Examinez les conditions du fournisseur avant d'automatiser les requêtes.
- Respectez les instructions des robots, le cas échéant.
- Ne contournez pas les CAPTCHA, les barrières de connexion, les limites de taux ou les protections anti-robots.
- Utilisez un petit ensemble d'invites fixe.
- Exécutez à basse fréquence.
- Enregistrez la réponse et l'horodatage pour que les résultats puissent être vérifiés.
- Arrêtez-vous lorsqu'un fournisseur renvoie un défi ou une réponse d'accès refusé.
Le protocole d'exclusion des robots est un mécanisme de préférence pour les robots d'exploration, pas un système de sécurité. Il n'autorise pas l'accès à du contenu protégé. RFC 9309 : Protocole d'exclusion des robots (rfc-editor.org)
Construire un Manifeste d'Invites
csv prompt_id,topic,query,locale,surface,target_domain pm_001,project-management,"best project management software for remote teams",en-US,google,example.com pm_001,project-management,"best project management software for remote teams",en-US,bing,example.com pm_002,analytics,"how to measure AI referral traffic",en-US,google,example.com
Maintenez les invites stables. Si les invites changent, les comparaisons de tendances deviennent moins fiables.
Stockez également :
- pays
- langue
- type d'appareil
- version du navigateur
- version de l'invite
- version du contenu
- heure de collecte en UTC
Google note que les réponses des AI Overview et AI Mode peuvent varier car elles utilisent des systèmes différents et peuvent utiliser des recherches connexes ou une « répartition des requêtes ». Documentation des fonctionnalités et du site web de Google AI (developers.google.com)
Script de collecte Playwright
Le script suivant est un cadre de recherche à usage général. Il enregistre la page rendue, la capture d'écran, les liens de la page et le texte de la réponse. Il ne contourne intentionnellement pas les défis et n'essaie pas de déjouer les contrôles d'accès.
python
scrape_panels.py
import asyncio import csv import hashlib import json import os import re from datetime import datetime, timezone from pathlib import Path from urllib.parse import quote_plus, urljoin, urlsplit
from playwright.async_api import async_playwright
DATA_DIR = Path("data/panels") DATA_DIR.mkdir(parents=True, exist_ok=True)
SURFACE_URLS = { "google": "https://www.google.com/search?q={query}&hl=en&gl=us", "bing": "https://www.bing.com/search?q={query}&setlang=en-us", }
PANEL_MARKERS = { "google": ["AI Overview", "AI Mode"], "bing": ["Copilot", "Generative search"], }
BLOCK_MARKERS = [ "captcha", "unusual traffic", "verify you are human", "access denied", ]
def clean_text(value): return re.sub(r"\s+", " ", value or "").strip()
def host_matches(url, target_domain): host = (urlsplit(url).hostname or "").lower() target = target_domain.lower().lstrip("www.") return host == target or host.endswith("." + target)
def page_hash(text): normalized = clean_text(text).encode("utf-8") return hashlib.sha256(normalized).hexdigest()
async def collect_one(page, row): surface = row["surface"] query = row["query"] target_domain = row["target_domain"]
search_url = SURFACE_URLS[surface].format(
query=quote_plus(query)
)
await page.goto(
search_url,
wait_until="domcontentloaded",
timeout=45_000,
)
await page.wait_for_timeout(2500)
text = await page.locator("body").inner_text()
lowered = text.lower()
if any(marker in lowered for marker in BLOCK_MARKERS):
return {
**row,
"observed_at": datetime.now(timezone.utc).isoformat(),
"status": "blocked_or_challenged",
"panel_present": False,
"links": [],
}
links = await page.locator("a").evaluate_all("""
anchors => anchors.map(a => ({
href: a.href || "",
text: (a.innerText || "").trim()
})).filter(x => x.href)
""")
panel_present = any(
marker.lower() in lowered
for marker in PANEL_MARKERS.get(surface, [])
)
observed_at = datetime.now(timezone.utc)
run_id = hashlib.sha256(
f"{surface}|{row['prompt_id']}|{observed_at.isoformat()}".encode()
).hexdigest()[:20]
html_path = DATA_DIR / f"{run_id}.html"
screenshot_path = DATA_DIR / f"{run_id}.png"
await page.screenshot(path=str(screenshot_path), full_page=True)
html_path.write_text(await page.content(), encoding="utf-8")
output_links = []
for position, link in enumerate(links, start=1):
output_links.append({
"href": urljoin(search_url, link["href"]),
"text": clean_text(link["text"])[:300],
"position": position,
"target_cited": host_matches(
link["href"],
target_domain
),
})
return {
**row,
"run_id": run_id,
"observed_at": observed_at.isoformat(),
"status": "ok",
"panel_present": panel_present,
"answer_hash": page_hash(text),
"answer_text": clean_text(text)[:50_000],
"html_path": str(html_path),
"screenshot_path": str(screenshot_path),
"link_scope": "page_fallback",
"links": output_links,
}
async def main(manifest_path): rows = [] with open(manifest_path, newline="", encoding="utf-8") as source: rows = list(csv.DictReader(source))
output_path = DATA_DIR / (
datetime.now(timezone.utc).strftime("%Y-%m-%d") + ".jsonl"
)
async with async_playwright() as playwright:
browser = await playwright.chromium.launch(headless=True)
context = await browser.new_context(
locale="en-US",
timezone_id="UTC",
)
page = await context.new_page()
with open(output_path, "w", encoding="utf-8") as output:
for row in rows:
try:
result = await collect_one(page, row)
except Exception as exc:
result = {
**row,
"observed_at": datetime.now(timezone.utc).isoformat(),
"status": f"error:{type(exc).__name__}",
"panel_present": False,
"links": [],
}
output.write(json.dumps(result) + "\n")
output.flush()
# Keep request volume low.
await asyncio.sleep(8)
await browser.close()
if name == "main": asyncio.run(main("prompts.csv"))
Installez et exécutez :
bash pip install playwright playwright install chromium python scrape_panels.py
Planifiez-le avec un verrou afin que deux exécutions ne puissent pas se chevaucher :
cron
15 3 * * * flock -n /tmp/ai-panel.lock
/usr/bin/python3 /opt/ai-measure/scrape_panels.py
Améliorer la précision avec les adaptateurs de surface
Le script de secours enregistre les liens au niveau de la page. C'est utile pour la découverte mais peut inclure des liens de résultats de recherche ordinaires. Pour une utilisation en production :
- Utilisez une API de panneau de réponses sous licence lorsqu'elle est disponible.
- Créez un adaptateur de sélecteur distinct pour chaque surface.
- Stockez la version du sélecteur.
- Stockez le HTML brut et une capture d'écran.
- Marquez chaque enregistrement avec
link_scope = "panel_selector"oulink_scope = "page_fallback".
Ne calculez la part de citation qu'à partir des données panel_selector lorsque vous pouvez identifier de manière fiable le panneau. Gardez les observations de secours dans un rapport exploratoire séparé.
8. Intégration de Google Analytics et BigQuery
Google Analytics 4 peut exporter les données d'événements brutes vers BigQuery. Les exportations quotidiennes fournissent des tables stables telles que events_AAAAJJMM; les exportations en streaming sont utiles pour la surveillance quasi en temps réel mais peuvent être incomplètes tant que la table quotidienne n'est pas finalisée. Exportation Google Analytics vers BigQuery (support.google.com)
Une requête de session simplifiée pourrait ressembler à ceci :
sql WITH page_events AS ( SELECT event_date, user_pseudo_id,
(
SELECT value.int_value
FROM UNNEST(event_params)
WHERE key = 'ga_session_id'
) AS session_id,
(
SELECT value.string_value
FROM UNNEST(event_params)
WHERE key = 'page_location'
) AS page_location,
(
SELECT value.string_value
FROM UNNEST(event_params)
WHERE key = 'session_engaged'
) AS session_engaged,
event_name
FROM PROJECT.analytics_PROPERTY.events_*
WHERE _TABLE_SUFFIX BETWEEN '20260901' AND '20260921'
),
session_rollup AS ( SELECT event_date, user_pseudo_id, session_id,
MAX(
CASE
WHEN REGEXP_CONTAINS(
LOWER(page_location),
r'[?&]utm_medium=ai_answer'
)
THEN 1 ELSE 0
END
) AS ai_session,
MAX(
CASE
WHEN session_engaged = '1'
THEN 1 ELSE 0
END
) AS engaged_session,
MAX(
CASE
WHEN event_name IN ('purchase', 'generate_lead', 'sign_up')
THEN 1 ELSE 0
END
) AS key_event
FROM page_events GROUP BY event_date, user_pseudo_id, session_id )
SELECT event_date, COUNTIF(ai_session = 1) AS ai_sessions, COUNTIF(ai_session = 1 AND engaged_session = 1) AS engaged_ai_sessions, COUNTIF(ai_session = 1 AND key_event = 1) AS ai_key_events, SAFE_DIVIDE( COUNTIF(ai_session = 1 AND engaged_session = 1), COUNTIF(ai_session = 1) ) AS post_citation_engagement_rate FROM session_rollup GROUP BY event_date ORDER BY event_date;
Pour une implémentation mature, ajoutez surface, topic et prompt_id à la structure de la campagne ou joignez-les à partir de la table des clics du raccourcisseur.
Ne créez pas de dimension d'analyse à forte cardinalité pour chaque utilisateur ou session individuel. Google avertit qu'un nombre excessif de dimensions personnalisées à forte cardinalité peut réduire la qualité des rapports. Dimensions et métriques personnalisées de Google Analytics (support.google.com)
9. Conception du tableau de bord
Un tableau de bord pratique devrait contenir cinq vues.
Résumé exécutif
Afficher :
- Part de citation par sujet
- Couverture des citations par sujet
- Taux de référence d'assistant confirmé
- Taux de référence d'assistant étendu
- Engagement post-citation
- Taux de succès des robots d'exploration d'IA
- Événements clés ou revenus attribués à l'IA
Tendance de la part de citation
Graphique linéaire :
text Date × Sujet × Surface
Afficher les deux :
- occurrences de citation du domaine cible
- toutes les occurrences de citation observées
Entonnoir de trafic de l'assistant
text Citations observées ↓ Clics sur liens contrôlés ↓ Sessions attribuées à l'IA ↓ Sessions IA engagées ↓ Événements clés
Chaque étape doit afficher sa confiance d'attribution.
Santé des robots d'exploration
Tableau par robot :
text nom_du_bot requêtes requêtes_réussies taux_d'erreur nombre_d'adresses_ip_uniques principaux_chemins
Alerter lorsque :
- le taux d'erreur dépasse 2 pour cent
- un bot disparaît soudainement
- une section de contenu précédemment accessible renvoie 403 ou 429
- les requêtes des robots se concentrent sur un chemin privé non intentionnel
Tableau des opportunités de contenu
Classez les pages et les sujets par :
text forte part de citation + faible taux de référence nombre élevé d'impressions + faible couverture de citation engagement élevé + faible visibilité des citations erreurs de robot d'exploration élevées + grande importance commerciale
Ces combinaisons produisent généralement de meilleures décisions que de ne considérer que le volume de trafic.
Tableau de bord Streamlit minimal
Enregistrez sous dashboard.py.
python import glob import json from pathlib import Path
import pandas as pd import streamlit as st
st.set_page_config( page_title="AI Citation Impact", layout="wide", )
@st.cache_data(ttl=300) def load_panel_rows(): rows = []
for filename in glob.glob("data/panels/*.jsonl"):
with open(filename, encoding="utf-8") as source:
for line in source:
record = json.loads(line)
run_id = record.get("run_id")
for link in record.get("links", []):
hostname = link.get("href", "").split("/")[2]
rows.append({
"run_id": run_id,
"observed_at": record.get("observed_at"),
"surface": record.get("surface"),
"topic": record.get("topic"),
"prompt_id": record.get("prompt_id"),
"panel_present": record.get("panel_present"),
"target_domain": record.get("target_domain"),
"cited_domain": hostname,
"target_cited": link.get("target_cited", False),
"position": link.get("position"),
"link_scope": record.get("link_scope"),
})
return pd.DataFrame(rows)
panels = load_panel_rows()
st.title("Tableau de bord de l'impact des citations d'IA")
if panels.empty: st.warning("Aucune observation de panneau trouvée.") st.stop()
panels["date"] = pd.to_datetime( panels["observed_at"], errors="coerce" ).dt.date
citation_share = ( panels.groupby("topic") .apply( lambda group: group["target_cited"].sum() / max(len(group), 1) ) .reset_index(name="citation_share") )
coverage = ( panels.groupby(["topic", "run_id"], dropna=False) .agg( panel_present=("panel_present", "max"), target_cited=("target_cited", "max"), ) .reset_index() )
coverage = ( coverage[coverage["panel_present"] == True] .groupby("topic") .apply(lambda group: group["target_cited"].mean()) .reset_index(name="citation_coverage") )
col1, col2 = st.columns(2)
with col1: st.subheader("Part de citation par sujet") st.dataframe(citation_share, use_container_width=True) st.bar_chart( citation_share.set_index("topic")["citation_share"] )
with col2: st.subheader("Couverture des citations par sujet") st.dataframe(coverage, use_container_width=True) st.bar_chart( coverage.set_index("topic")["citation_coverage"] )
st.subheader("Position de la citation") st.dataframe( panels.groupby(["topic", "surface"], dropna=False) .agg( citations=("target_cited", "sum"), median_position=("position", "median"), observations=("run_id", "nunique"), ) .reset_index(), use_container_width=True, )
st.subheader("Observations quotidiennes") daily = ( panels.groupby(["date", "surface"], dropna=False) .agg( citations=("target_cited", "sum"), observed_links=("cited_domain", "count"), ) .reset_index() )
st.line_chart( daily.pivot( index="date", columns="surface", values="citations" ).fillna(0) )
Exécutez-le avec :
bash pip install streamlit pandas streamlit run dashboard.py
Pour la production, chargez les tables normalisées dans DuckDB, BigQuery, Snowflake ou PostgreSQL au lieu de lire directement les fichiers bruts.
10. Contrôles qualité
Un système de mesure peut produire des chiffres précis en apparence mais trompeurs. Ajoutez ces contrôles.
Séparer les robots des personnes
Ne comptez jamais OAI-SearchBot, GPTBot, ClaudeBot ou PerplexityBot comme des sessions humaines. Ce sont des signaux de robot d'exploration ou de récupération.
Préserver les preuves brutes
Pour chaque exécution de panneau, conservez :
- le HTML brut
- la capture d'écran
- le texte de la réponse
- l'horodatage
- la version du navigateur et du collecteur
- l'invite et le paramètre régional
- le hachage de la réponse
Cela permet d'expliquer pourquoi une métrique a changé.
Suivre l'absence de panneau
Un panneau d'IA manquant n'est pas la même chose qu'une réponse sans votre citation.
Utilisez :
text panneau_non_présent panneau_présent_sans_cible panneau_présent_cible_citée bloqué_ou_mis_au_défi erreur_de_collecte
Contrôler le biais d'échantillonnage
Une extraction planifiée est affectée par :
- l'emplacement
- l'appareil
- la langue
- le temps
- la personnalisation
- l'état du navigateur
- la formulation de l'invite
Utilisez un ensemble d'invites de base fixe et un ensemble rotatif d'invites exploratoires. Gardez les deux groupes séparés dans les rapports.
Éviter la fausse précision
Signalez les intervalles de confiance ou les tailles d'échantillon minimales pour les petits groupes de sujets. Un sujet avec trois observations ne doit pas être comparé directement à un sujet contenant 300 observations.
Comparer des choses comparables
Lors de la comparaison de surfaces, maintenez constants :
- l'invite
- le paramètre régional
- le pays
- l'appareil
- l'heure de collecte
- les règles du domaine cible
- la version de l'analyseur de citations
11. Plan de déploiement recommandé
Semaine 1 : Instrumentation
- Configurez les journaux de serveur structurés.
- Créez le registre des robots.
- Créez le domaine du raccourcisseur.
- Ajoutez des règles de dénomination de campagne.
- Définissez la taxonomie des sujets.
- Activez l'exportation BigQuery de Google Analytics 4.
Semaine 2 : Liens contrôlés
- Publiez cinq à dix résumés suivis.
- Créez des liens pour différents sujets et versions de contenu.
- Validez les journaux de redirection par rapport aux sessions d'analyse.
- Confirmez qu'aucune information personnelle n'entre dans les URL ou les paramètres d'analyse.
Semaine 3 : Observations des panneaux
- Créez un manifeste d'invites.
- Commencez avec deux surfaces et 20 à 50 invites stables.
- Exécutez une fois par jour.
- Enregistrez les captures d'écran et le HTML.
- Mesurez les échecs de collecte séparément de l'absence de citation.
Semaine 4 : Tableau de bord et gouvernance
- Construisez des tableaux de bord par sujet et par surface.
- Ajoutez des alertes sur la santé des robots d'exploration.
- Définissez des règles de confiance.
- Comparez les rapports propriétaires avec les observations des panneaux.
- Documentez les politiques de rétention, d'accès et de suppression.
Conclusion
La mesure des citations d'IA doit être conçue comme un système d'attribution multi-signaux, et non comme un simple rapport de référence.
Le cadre le plus fiable combine :
- les rapports de visibilité propriétaires de Google et Bing
- les journaux de serveur pour l'activité des robots d'exploration et des récupérateurs
- les liens courts propriétaires pour la mesure contrôlée des clics
- les observations planifiées et conformes des panneaux de réponses
- les données d'analyse et de conversion pour la valeur en aval
Les trois métriques les plus importantes sont :
- La part de citation par sujet — la part de l'espace de source observé appartenant à votre domaine.
- Le taux de référence de l'assistant — le trafic humain pouvant être attribué aux surfaces d'IA.
- L'engagement post-citation — si les visiteurs référés par l'IA effectuent des actions significatives.
Le système devient utile lorsque chaque chiffre inclut sa surface, sa période, son dénominateur, sa méthode d'attribution et son niveau de confiance. Cette discipline empêche de confondre les visites de robots avec des utilisateurs, les réponses extraites avec des impressions, et les citations avec des résultats commerciaux.
Auto