AutoPodAutoPod

Het Meten van AI-Citaatimpact: Analyses, Logs en Panel Scrapes

•22 min leestijd
Audio-artikel
Het Meten van AI-Citaatimpact: Analyses, Logs en Panel Scrapes
0:00—:—
Het Meten van AI-Citaatimpact: Analyses, Logs en Panel Scrapes

Het Meten van AI-Citaatimpact: Analyses, Logs en Panel Scrapes

Introductie

AI-gedreven ontdekking is nu verdeeld over verschillende metingslagen. Een persoon kan een bedrijf zien dat wordt genoemd in een AI-antwoord, op een link klikken zonder nuttige verwijzer, via een browser bezoeken die campagnedata verwijdert, of een antwoord lezen zonder überhaupt te klikken. Ondertussen kan een AI-crawler de pagina van het bedrijf opvragen zonder een menselijke bezoeker te vertegenwoordigen.

Een nuttig meetsysteem moet daarom vier vragen scheiden:

  1. Werd de site gecrawld of opgehaald?
  2. Werd de site genoemd in een AI-antwoord?
  3. Heeft een persoon de site bezocht vanuit dat antwoord?
  4. Heeft dat bezoek geleid tot betekenisvolle betrokkenheid of omzet?

Vanaf 22 september 2026 verbetert de eerstelijnsrapportage. Google Search Console biedt een prestatierapport voor Generatieve AI voor AI-Overzichten en AI-Modus, inclusief vertoningen, pagina's, landen, apparaten en datums. Het rapport werd wereldwijd beschikbaar op 31 augustus 2026. Google's prestatierapport voor generatieve AI (support.google.com)

Microsoft biedt ook een AI-Prestatierapport in Bing Webmaster Tools. De openbare preview rapporteert het totale aantal citaten, geciteerde pagina's, onderliggende zoekopdrachten en citaattrends voor ondersteunde Microsoft AI-ervaringen. AI-Prestaties van Bing Webmaster Tools (blogs.bing.com)

Deze tools moeten als autoritatief worden beschouwd voor hun eigen oppervlakken, maar ze creëren geen compleet cross-assistent meetsysteem. De aanbevolen aanpak is om eerstelijnsrapporten, serverlogs, gecontroleerde trackinglinks, analyse-exports en geplande observaties van antwoordpanelen te combineren.


1. Waarom het Meten van AI-Verwijzingen Moeilijk Is

Traditionele webanalyse is sterk afhankelijk van de Referer-header en campagneparameters. Beide kunnen ontbreken of onvolledig zijn.

De HTTP Referer-header kan de vorige pagina, alleen de oorsprong van de vorige website, of helemaal geen waarde bevatten. Browser-verwijzingsbeleid kan die informatie opzettelijk beperken of verwijderen. MDN Referrer-Policy documentatie (developer.mozilla.org)

AI-systemen creëren aanvullende complicaties:

  • Een assistent kan naar een pagina linken zonder een herkenbare verwijzer te sturen.
  • Een assistent kan een omgeleide of herschreven URL gebruiken.
  • Een assistent kan inhoud server-side ophalen voordat een antwoord wordt getoond.
  • Een AI-crawler kan een pagina bezoeken zonder dat een mens deze ziet.
  • Het antwoord kan een canonieke URL citeren in plaats van de oorspronkelijk opgehaalde URL.
  • Het antwoord kan variëren afhankelijk van locatie, apparaat, taal, tijd of personalisatie.

OpenAI maakt bijvoorbeeld onderscheid tussen OAI-SearchBot voor zoekontdekking, GPTBot voor mogelijk trainingsgebruik, en ChatGPT-User voor door de gebruiker geïnitieerde acties. Perplexity identificeert afzonderlijk PerplexityBot en Perplexity-User. Anthropic identificeert ClaudeBot, Claude-SearchBot en Claude-User. Deze verzoeken mogen niet worden gecombineerd tot één generiek “AI-verkeer” getal. OpenAI crawler documentatie (openai.com) Perplexity crawler documentatie (docs.perplexity.ai) Anthropic crawler documentatie (support.anthropic.com)

De centrale regel is:

Crawleractiviteit is geen verwijzingsverkeer, zichtbaarheid van citaten is geen verkeer, en verkeer is geen bedrijfsimpact.


2. Het Meetkader

Gebruik vijf metingslagen.

LaagHoofdvraagAanbevolen gegevens
ToegangHeeft een AI-systeem de pagina opgevraagd?Webserverlogs, firewalllogs
ZichtbaarheidWerd de pagina getoond of geciteerd?Google Search Console, Bing Webmaster Tools, observaties van antwoordpanelen
AcquisitieKwam een persoon van een AI-oppervlak?Verwijzer, campagneparameters, shortlink-klikken, analyses
BetrokkenheidHeeft de bezoeker zinvol geïnteracteerd?Analyse-evenementen, paginaweergaven, betrokkenheidstijd
WaardeHeeft het bezoek het gewenste resultaat opgeleverd?Aanmeldingen, leads, aankopen, abonnementen

Aanbevolen hiërarchie van "source-of-truth"

Gebruik het sterkste beschikbare signaal voor elke vraag:

  1. Eerstelijns platformrapporten voor vertoningen en citaten.
  2. Observaties van antwoordpanelen voor zichtbaarheid op promptniveau en concurrentievergelijkingen.
  3. Redirect logs voor gecontroleerde linkkliks.
  4. Webanalyse voor sessies, betrokkenheid en conversies.
  5. Serverlogs voor crawler- en fetcheractiviteit.
  6. Gemodelleerde schattingen alleen wanneer direct bewijs niet beschikbaar is.

Combineer niet alle signalen in één enkel getal zonder de bron en het betrouwbaarheidsniveau te bewaren.


3. Kerndatamodel

Creëer een datawarehouse met afzonderlijke tabellen voor ruwe observaties en afgeleide metrics.

panel_runs

Eén rij voor elke prompt- en oppervlaktecontrole.

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

Eén rij voor elke link die in een antwoordpaneel is waargenomen.

text run_id surface topic prompt_id cited_url cited_domain citation_position link_scope target_domain target_cited

ai_crawler_requests

Geaggregeerde serverloggegevens.

text date bot_name bot_role path requests successful_requests error_requests unique_ip_count

shortener_clicks

Eén rij voor elke gecontroleerde-linkaanvraag.

text click_id occurred_at slug surface topic prompt_id asset_id referrer_host user_agent_family ip_hmac destination_path

ai_sessions

Een genormaliseerde analysetabel.

text session_id date surface topic prompt_id landing_page attribution_method confidence engaged engagement_time_seconds key_event revenue

Het veld confidence is belangrijk. Aanbevolen waarden zijn:

  • A — Direct: gecontroleerde shortlink-klik of vertrouwde campagneparameter.
  • B — Sterk: bekende assistent-verwijzer of geverifieerde user-fetch-aanvraag.
  • C — Waarschijnlijk: bewijs van campagne of landingspagina zonder een volledige verwijzer.
  • D — Gemodelleerd: afgeleid uit timing, panelobservaties of geaggregeerde wijzigingen.

Rapporteer A en B afzonderlijk van C en D.


4. Definitie van Metrics

Citaatdeel per Onderwerp

Citaatdeel per Onderwerp meet hoe vaak een doel-domein verschijnt onder de bronnen die voor een onderwerp zijn waargenomen.

[ \text{Citation Share}_{topic}

\frac{\text{Target-domain citation occurrences}} {\text{All citation occurrences}} ]

Voorbeeld:

  • 100 totale citaten waargenomen voor “project management software”
  • 12 citaten verwijzen naar uw domein
  • Citaatdeel per Onderwerp = 12%

Deze metric meet het bron-aandeel, niet of een antwoord uw site minstens één keer bevatte.

Citaatdekking

Gebruik een tweede metric voor aanwezigheid op antwoordniveau:

[ \text{Citation Coverage}_{topic}

\frac{\text{Answer runs containing a target citation}} {\text{Answer runs where a panel was present}} ]

Citaatdekking is vaak gemakkelijker te begrijpen voor niet-technische doelgroepen:

“Onze site verscheen in 34% van de waargenomen AI-antwoorden voor dit onderwerp.”

Citaatpositie

Registreer de positie van een citaat binnen het antwoord:

  • 1 = eerste waargenomen bron
  • 2 = tweede waargenomen bron
  • 3 = derde waargenomen bron

Omdat AI-interfaces bronnen vaak groeperen of herordenen, moet de positie worden behandeld als een beschrijvende observatie, niet als een universele rangschikking.


Assistent-Verwijzingspercentage

Assistent-Verwijzingspercentage meet het aandeel van menselijke sitesessies dat kan worden toegeschreven aan een AI-oppervlak.

[ \text{Assistant Referral Rate}

\frac{\text{AI-attributed human sessions}} {\text{All human sessions}} ]

Rapporteer ten minste twee versies:

text Confirmed Assistant Referral Rate = A-confidence AI sessions / all human sessions

Expanded Assistant Referral Rate = A + B + C-confidence AI sessions / all human sessions

De bevestigde versie is conservatiever. De uitgebreide versie is nuttig voor trendanalyse, maar mag niet worden gepresenteerd als volledig waargenomen verkeer.

Gebruik geen panel runs als vertoningen

Als u 500 prompts scrapt, betekent dat niet dat 500 mensen die antwoorden hebben gezien. Uw scraper meet een gecontroleerde steekproef van antwoordstaten. Gebruik eerstelijns platform-vertoningsrapporten wanneer deze beschikbaar zijn.


Betrokkenheid na Citaat

Betrokkenheid na Citaat meet of mensen die via een AI-citaat zijn binnengekomen, een zinvolle actie hebben ondernomen.

[ \text{Post-Citation Engagement}

\frac{\text{Engaged AI sessions}} {\text{AI-attributed sessions}} ]

Voor Google Analytics 4 duurt een betrokken sessie meer dan 10 seconden, omvat deze een belangrijke gebeurtenis, of omvat deze ten minste twee pagina- of schermweergaven. Documentatie over betrokkenheidspercentage van Google Analytics (support.google.com)

Rapporteer ook:

text Post-Citation Engagement Time = median engaged seconds per AI session

Post-Citation Conversion Rate = AI sessions with a key event / AI-attributed sessions

Post-Citation Revenue per Session = revenue from AI sessions / AI-attributed sessions

De mediane betrokkenheidstijd is meestal stabieler dan de gemiddelde betrokkenheidstijd, omdat een klein aantal zeer lange sessies het gemiddelde kan vertekenen.


Aanvullende Nuttige Metrics

Succespercentage AI-Crawler

[ \text{Crawler Success Rate}

\frac{\text{AI crawler requests returning 2xx or 3xx}} {\text{AI crawler requests}} ]

Splits dit uit per bot en pad. Een sterke daling van citaten in combinatie met een stijgend crawler-foutpercentage kan duiden op een probleem met hosting, firewall of robots-configuratie.

Citaat-naar-Klik Ratio

Voor gecontroleerde observaties van antwoordpanelen:

[ \text{Observed Citation-to-Click Rate}

\frac{\text{Confirmed short-link clicks}} {\text{Observed target citations}} ]

Dit is geen echte doorklikratio van gebruikers, tenzij de noemer werkelijke gebruikersimpressies vertegenwoordigt. Benoem het duidelijk als een waargenomen operationele metric.

Citaatversheid

Meet hoe vaak de geciteerde URL de huidige paginaversie weerspiegelt.

text Citation Freshness = cited pages with current content hash / cited pages

Dit is nuttig voor documentatie-, prijs-, product- en beleidspagina's die frequent veranderen.


5. Serverloganalyse

Gestructureerde Access Logs Configureren

Gestructureerde logs zijn gemakkelijker te analyseren dan traditionele, door spaties gescheiden logs.

Voorbeeld Nginx-configuratie:

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;

Sla de logbestanden op een beperkte locatie op. Stuur geen ruwe IP-adressen naar een algemene analysetabel, tenzij u een duidelijke operationele reden heeft.

AI-Verzoektypes Identificeren

Scheid minimaal:

text OpenAI:

  • OAI-SearchBot
  • ChatGPT-User
  • GPTBot

Anthropic:

  • Claude-SearchBot
  • Claude-User
  • ClaudeBot

Perplexity:

  • PerplexityBot
  • Perplexity-User

Search engines:

  • Googlebot
  • Bingbot

Google's AI-zoekfuncties gebruiken over het algemeen bestaande Google Zoek-crawlbesturingselementen in plaats van een aparte universele “AI Overview bot.” Google stelt dat Googlebot de toegang voor Zoeken, inclusief AI-functies, beheert. Google AI-functies en websites (developers.google.com)

User-agent strings kunnen worden vervalst. Voor operationele beslissingen combineert u user-agent matching met door de provider gepubliceerde IP-bereiken of geverifieerde reverse DNS waar beschikbaar. OpenAI en Perplexity publiceren IP-informatie voor hun relevante crawlers en fetchers. (openai.com)

Reproduceerbare Logsamenvatter

Sla het volgende op als 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])

Voer het uit met:

bash HMAC_SECRET='a-long-random-secret'
python ai_log_summary.py /var/log/nginx/ai-access.jsonl \

data/ai_crawler_daily.csv

Dit levert operationeel bewijs over ophaalactiviteit, niet over menselijk verkeer.


6. Aangepaste Link Verkorters voor AI-gerichte Samenvattingen

Een eerstelijns linkverkorter biedt een gecontroleerd meetpunt wanneer u samenvattingen of bronmaterialen publiceert die door AI-systemen kunnen worden gerefereerd.

Gebruik een domein dat u beheert, zoals:

text https://go.example.com/ai-docs

De bestemming moet campagneparameters bevatten, zoals:

text utm_source=perplexity utm_medium=ai_answer utm_campaign=product_documentation utm_content=pricing_summary_v3

Google raadt campagneparameters aan voor het identificeren van verkeersbronnen en campagnes. Google Analytics documentatie voor campagnelinks (support.google.com)

Aanbevolen linkvelden

text surface topic prompt_id asset_id content_version destination_path

Plaats geen persoonlijke informatie, e-mailadressen of zoekopdrachten van gebruikers in de link. Campagne-identificatoren moeten de gepubliceerde asset beschrijven, niet een persoon identificeren.

Belangrijke beperkingen

Een shortlink is een kalibratie-apparaat, geen universele oplossing:

  • De assistent kan de bestemmings-URL herschrijven.
  • De assistent kan in plaats daarvan de canonieke URL citeren.
  • De link kan worden weergegeven, maar nooit worden aangeklikt.
  • Een browser of privacytool kan de verwijzer verwijderen.
  • Sommige systemen kunnen de bestemming server-side ophalen.
  • Een shortlink meet geen gebruikers die het merk onthouden en later terugkeren.

Gebruik geen shortener om de bestemming te verbergen, veiligheidssystemen te omzeilen of citaten te manipuleren. Gebruik een duidelijke redirect naar een relevante, canonieke pagina.

Minimale Eerstelijns Shortener

Het volgende voorbeeld gebruikt FastAPI en SQLite. Bescherm het link-creatie-eindpunt met authenticatie voordat u het in productie gebruikt.

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)

Voer het uit met:

bash pip install fastapi uvicorn uvicorn shortener:app --host 0.0.0.0 --port 8080

Gebruik een 302-redirect voor meetlinks, zodat browsers en tussentijdse systemen de redirect minder snel permanent zullen cachen.


7. Gepland Scrapen van AI-Antwoordpanelen

Panel scraping is nuttig voor het beantwoorden van vragen die eerstelijnsanalyses mogelijk niet blootleggen:

  • Welke concurrerende bronnen worden geciteerd?
  • Welke pagina wordt geciteerd voor een specifieke prompt?
  • Verandert het antwoord per land of apparaat?
  • Verschijnt het citaat in het antwoord, de bronnenlijst, of beide?
  • Hoe stabiel is het antwoord over de tijd?
  • Citeert de assistent een shortlink of de canonieke URL?

Scrapen moet echter zorgvuldig gebeuren.

Regels voor verantwoord verzamelen

  1. Geef de voorkeur aan een officiële API, een gelicentieerde zoekdataprovide of een eerstelijnsrapport.
  2. Controleer de voorwaarden van de provider voordat u verzoeken automatiseert.
  3. Respecteer robots-instructies waar van toepassing.
  4. Omzeil geen CAPTCHA's, inlogbarrières, frequentielimieten of botbeveiligingen.
  5. Gebruik een kleine, vaste set prompts.
  6. Draai met lage frequentie.
  7. Sla de respons en tijdstempel op, zodat resultaten kunnen worden gecontroleerd.
  8. Stop wanneer een provider een uitdaging of een geweigerde-toegangsrespons retourneert.

Het Robots Exclusion Protocol is een crawler-voorkeursmechanisme, geen beveiligingssysteem. Het autoriseert geen toegang tot beschermde inhoud. RFC 9309: Robots Exclusion Protocol (rfc-editor.org)

Een Prompt Manifest Bouwen

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

Houd prompts stabiel. Als prompts veranderen, worden trendvergelijkingen minder betrouwbaar.

Sla ook op:

  • land
  • taal
  • apparaattype
  • browserversie
  • promptversie
  • inhoudversie
  • verzameltijd in UTC

Google merkt op dat AI Overview- en AI Mode-responsen kunnen variëren omdat ze verschillende systemen gebruiken en gerelateerde zoekopdrachten of “query fan-out” kunnen gebruiken. Google AI-functies en websitedocumentatie (developers.google.com)

Playwright Verzamelscript

Het volgende script is een algemeen onderzoeksraamwerk. Het registreert de gerenderde pagina, screenshot, paginalinks en antwoordtekst. Het omzeilt opzettelijk geen uitdagingen of probeert geen toegangscontroles te omzeilen.

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"))

Installeren en uitvoeren:

bash pip install playwright playwright install chromium python scrape_panels.py

Plan het in met een vergrendeling, zodat twee uitvoeringen elkaar niet kunnen overlappen:

cron 15 3 * * * flock -n /tmp/ai-panel.lock
/usr/bin/python3 /opt/ai-measure/scrape_panels.py

Verbeter de nauwkeurigheid met oppervlakte-adapters

Het fallback-script registreert links op paginaniveau. Dat is nuttig voor ontdekking, maar kan ook gewone zoekresultaatlinks bevatten. Voor productiegebruik:

  • Gebruik een gelicentieerde antwoordpaneel-API indien beschikbaar.
  • Creëer een aparte selector-adapter voor elk oppervlak.
  • Sla de selectorversie op.
  • Sla ruwe HTML en een screenshot op.
  • Markeer elke record met link_scope = "panel_selector" of link_scope = "page_fallback".

Bereken Citaatdeel alleen uit panel_selector-gegevens wanneer u het paneel betrouwbaar kunt identificeren. Houd fallback-observaties in een apart verkennend rapport.


8. Google Analytics en BigQuery Integratie

Google Analytics 4 kan ruwe eventdata exporteren naar BigQuery. Dagelijkse exports bieden stabiele tabellen zoals events_JJJJMMDD; streaming exports zijn nuttig voor bijna-realtime monitoring, maar kunnen onvolledig zijn totdat de dagelijkse tabel is gefinaliseerd. Google Analytics BigQuery Export (support.google.com)

Een vereenvoudigde sessiequery zou er zo uit kunnen zien:

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;

Voor een volwassen implementatie, voeg surface, topic en prompt_id toe aan de campagnestructuur of join ze vanuit de shortener-klik-tabel.

Creëer geen analytics-dimensie met hoge cardinaliteit voor elke individuele gebruiker of sessie. Google waarschuwt dat buitensporig hoge cardinaliteit van aangepaste dimensies de rapportagekwaliteit kan verminderen. Google Analytics aangepaste dimensies en metrics (support.google.com)


9. Dashboardontwerp

Een praktisch dashboard moet vijf weergaven bevatten.

Samenvatting voor Directie

Weergeven:

  • Citaatdeel per Onderwerp
  • Citaatdekking per Onderwerp
  • Bevestigd Assistent-Verwijzingspercentage
  • Uitgebreid Assistent-Verwijzingspercentage
  • Betrokkenheid na Citaat
  • Succespercentage AI-crawler
  • Door AI toegeschreven belangrijke gebeurtenissen of omzet

Citaatdeel Trend

Lijngrafiek:

text Date × Topic × Surface

Toon beide:

  • aantal citaten van het doel-domein
  • alle waargenomen citaatvoorkomens

Assistent Verkeerstrechter

text Waargenomen citaten ↓ Gecontroleerde-link kliks ↓ Aan AI toegeschreven sessies ↓ Betrokken AI-sessies ↓ Belangrijke gebeurtenissen

Elke fase moet de toewijzingsbetrouwbaarheid weergeven.

Crawler Gezondheid

Tabel per bot:

text bot_name requests successful_requests error_rate unique_ip_count top_paths

Waarschuw wanneer:

  • foutpercentage hoger is dan 2 procent
  • een bot plotseling verdwijnt
  • een eerder toegankelijke inhoudsectie 403 of 429 retourneert
  • crawler-verzoeken zich concentreren op een onbedoeld privépad

Contentmogelijkheden Tabel

Rangschik pagina's en onderwerpen op basis van:

text hoog citaatdeel + laag verwijzingspercentage hoge vertoningen + lage citaatdekking hoge betrokkenheid + lage citaatzichtbaarheid hoge crawlerfouten + hoge bedrijfsrelevantie

Deze combinaties leiden meestal tot betere beslissingen dan alleen te kijken naar het verkeersvolume.

Minimaal Streamlit Dashboard

Sla op als 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("AI Citaatimpact Dashboard")

if panels.empty: st.warning("Geen panelobservaties gevonden.") 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("Citaatdeel per Onderwerp") st.dataframe(citation_share, use_container_width=True) st.bar_chart( citation_share.set_index("topic")["citation_share"] )

with col2: st.subheader("Citaatdekking per Onderwerp") st.dataframe(coverage, use_container_width=True) st.bar_chart( coverage.set_index("topic")["citation_coverage"] )

st.subheader("Citaatpositie") 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("Dagelijkse Observaties") 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) )

Voer het uit met:

bash pip install streamlit pandas streamlit run dashboard.py

Voor productie, laad genormaliseerde tabellen in DuckDB, BigQuery, Snowflake of PostgreSQL in plaats van direct ruwe bestanden te lezen.


10. Kwaliteitscontroles

Een meetsysteem kan nauwkeurig ogende, maar misleidende cijfers produceren. Voeg deze controles toe.

Scheid bots van mensen

Tel nooit OAI-SearchBot, GPTBot, ClaudeBot of PerplexityBot mee als menselijke sessies. Dit zijn crawler- of ophaalsignalen.

Bewaar ruw bewijs

Bewaar voor elke panel run:

  • ruwe HTML
  • screenshot
  • antwoordtekst
  • tijdstempel
  • browser- en collectorversie
  • prompt en locale
  • antwoord hash

Dit maakt het mogelijk om uit te leggen waarom een metric is veranderd.

Volg de afwezigheid van panelen

Een ontbrekend AI-paneel is niet hetzelfde als een antwoord zonder uw citaat.

Gebruik:

text panel_not_present panel_present_no_target panel_present_target_cited blocked_or_challenged collection_error

Beheer bemonsteringsbias

Een geplande scrape wordt beïnvloed door:

  • locatie
  • apparaat
  • taal
  • tijd
  • personalisatie
  • browserstatus
  • promptformulering

Gebruik een vaste kernset van prompts en een roterende set van verkennende prompts. Houd de twee groepen gescheiden in rapporten.

Vermijd valse precisie

Rapporteer betrouwbaarheidsintervallen of minimale steekproefgroottes voor kleine onderwerpsgroepen. Een onderwerp met drie observaties mag niet direct worden vergeleken met een onderwerp dat 300 observaties bevat.

Vergelijk appels met appels

Houd bij het vergelijken van oppervlakken constant:

  • prompt
  • locale
  • land
  • apparaat
  • verzameltijd
  • regels voor het doel-domein
  • citaatparser-versie

11. Aanbevolen Implementatieplan

Week 1: Instrumentatie

  • Configureer gestructureerde serverlogs.
  • Creëer het botregister.
  • Creëer het shortener-domein.
  • Voeg campagnenaamgevingsregels toe.
  • Definieer de onderwerpstaxonomie.
  • Schakel Google Analytics 4 BigQuery-export in.

Week 2: Gecontroleerde links

  • Publiceer vijf tot tien getrackte samenvattingen.
  • Creëer links voor verschillende onderwerpen en inhoudversies.
  • Valideer redirect logs tegen analytics-sessies.
  • Bevestig dat er geen persoonlijke informatie in URL's of analyseparameters terechtkomt.

Week 3: Panelobservaties

  • Creëer een prompt manifest.
  • Begin met twee oppervlakken en 20 tot 50 stabiele prompts.
  • Voer één keer per dag uit.
  • Sla screenshots en HTML op.
  • Meet verzamelfouten afzonderlijk van citaat-afwezigheid.

Week 4: Dashboard en governance

  • Bouw topic- en surface-dashboards.
  • Voeg crawler-gezondheidswaarschuwingen toe.
  • Definieer betrouwbaarheidsregels.
  • Vergelijk eerstelijnsrapporten met panelobservaties.
  • Documenteer bewaarbeleid, toegangsbeleid en verwijderingsbeleid.

Conclusie

AI-citaatmeting moet worden opgebouwd als een multi-signaal attributiesysteem, niet als een enkel verwijzingsrapport.

Het meest betrouwbare framework combineert:

  • Google en Bing eerstelijns zichtbaarheidsrapporten
  • serverlogs voor crawler- en fetcheractiviteit
  • eerstelijns shortlinks voor gecontroleerde klikmeting
  • geplande, conforme observaties van antwoordpanelen
  • analyse- en conversiedata voor downstream waarde

De drie belangrijkste metrics zijn:

  1. Citaatdeel per Onderwerp — hoeveel van de waargenomen bronruimte toebehoort aan uw domein.
  2. Assistent-Verwijzingspercentage — hoeveel menselijk verkeer kan worden toegeschreven aan AI-oppervlakken.
  3. Betrokkenheid na Citaat — of door AI verwezen bezoekers zinvolle acties ondernemen.

Het systeem wordt nuttig wanneer elk getal zijn oppervlak, tijdsperiode, noemer, attributiemethode en betrouwbaarheidsniveau omvat. Deze discipline voorkomt dat crawler-hits worden verward met gebruikers, gescrapete antwoorden met vertoningen, en citaten met bedrijfsresultaten.

Gerelateerde artikelen

Vindt u deze content leuk?

Schrijf u in voor onze nieuwsbrief voor de nieuwste inzichten in contentmarketing en groeigidsen.

Dit artikel is uitsluitend bedoeld voor informatieve doeleinden. Content en strategieën kunnen variëren op basis van uw specifieke behoeften.
Het Meten van AI-Citaatimpact: Analyses, Logs en Panel Scrapes | AutoPod