AutoPodAutoPod

Mesurer l'impact des citations d'IA : Analyses, journaux et extractions de panneaux

•27 min de lecture
Article audio
Mesurer l'impact des citations d'IA : Analyses, journaux et extractions de panneaux
0:00—:—
Mesurer l'impact des citations d'IA : Analyses, journaux et extractions de panneaux

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 :

  1. Le site a-t-il été exploré ou récupéré ?
  2. Le site a-t-il été cité dans une réponse d'IA ?
  3. Une personne a-t-elle visité le site à partir de cette réponse ?
  4. 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.

CoucheQuestion principaleDonnées recommandées
AccèsUn 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
AcquisitionUne personne est-elle arrivée d'une surface d'IA ?Référent, paramètres de campagne, clics sur liens courts, analyses
EngagementLe visiteur a-t-il interagi de manière significative ?Événements d'analyse, pages vues, temps d'engagement
ValeurLa 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 :

  1. Rapports propriétaires de plateformes pour les impressions et les citations.
  2. Observations des panneaux de réponses pour la visibilité au niveau des invites et les comparaisons concurrentielles.
  3. Journaux de redirection pour les clics sur les liens contrôlés.
  4. Analyses web pour les sessions, l'engagement et les conversions.
  5. Journaux de serveur pour l'activité des robots d'exploration et des récupérateurs.
  6. 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ée
  • 2 = deuxième source observée
  • 3 = 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

  1. Préférez une API officielle, un fournisseur de données de recherche sous licence ou un rapport propriétaire.
  2. Examinez les conditions du fournisseur avant d'automatiser les requêtes.
  3. Respectez les instructions des robots, le cas échéant.
  4. Ne contournez pas les CAPTCHA, les barrières de connexion, les limites de taux ou les protections anti-robots.
  5. Utilisez un petit ensemble d'invites fixe.
  6. Exécutez à basse fréquence.
  7. Enregistrez la réponse et l'horodatage pour que les résultats puissent être vérifiés.
  8. 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" ou link_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 :

  1. La part de citation par sujet — la part de l'espace de source observé appartenant à votre domaine.
  2. Le taux de référence de l'assistant — le trafic humain pouvant être attribué aux surfaces d'IA.
  3. 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.

Articles connexes

Vous aimez ce contenu ?

Abonnez-vous à notre newsletter pour les dernières analyses en marketing de contenu et guides de croissance.

Cet article est fourni à titre informatif uniquement. Les contenus et stratégies peuvent varier selon vos besoins spécifiques.
Mesurer l'impact des citations d'IA : Analyses, journaux et extractions de panneaux | AutoPod