Měření dopadu citací AI: Analýza, protokoly a scrapování panelů
Úvod
Objevování řízené umělou inteligencí je nyní rozděleno do několika měřicích vrstev. Člověk může vidět společnost citovanou v odpovědi AI, kliknout na odkaz bez užitečného refereru, navštívit stránku prohlížečem, který odstraňuje data kampaně, nebo si odpověď přečíst, aniž by vůbec klikl. Mezitím může crawler AI vyžádat stránku společnosti, aniž by představoval lidského návštěvníka.
Užitečný měřicí systém proto musí oddělit čtyři otázky:
- Byla stránka procházena nebo načtena?
- Byla stránka citována v odpovědi AI?
- Navštívil stránku z této odpovědi člověk?
- Vytvořila tato návštěva smysluplnou interakci nebo příjem?
K 22. září 2026 se zlepšuje reportování z první strany. Google Search Console poskytuje report výkonu generativní AI pro Přehledy AI a Režim AI, včetně zobrazení, stránek, zemí, zařízení a dat. Report se stal celosvětově dostupným 31. srpna 2026. Report výkonu generativní AI od Googlu (support.google.com)
Microsoft také poskytuje report Výkon AI v nástrojích Bing Webmaster Tools. Jeho veřejná ukázka reportuje celkové citace, citované stránky, základní dotazy a trendy citací napříč podporovanými AI zážitky Microsoftu. Výkon AI v Bing Webmaster Tools (blogs.bing.com)
Tyto nástroje by měly být považovány za autoritativní pro své vlastní povrchy, ale nevytvářejí kompletní systém měření napříč asistenty. Doporučený přístup spočívá v kombinaci reportů z první strany, serverových protokolů, řízených sledovacích odkazů, exportů z analytických nástrojů a plánovaných pozorování panelů odpovědí.
1. Proč je měření doporučení AI obtížné
Tradiční webová analytika silně závisí na hlavičce Referer a parametrech kampaně. Obojí může chybět nebo být neúplné.
Hlavička HTTP Referer může obsahovat předchozí stránku, pouze původ předchozího webu, nebo žádnou hodnotu. Zásady refereru prohlížeče mohou záměrně omezit nebo odstranit tyto informace. Dokumentace Referrer-Policy na MDN (developer.mozilla.org)
Systémy AI vytvářejí další komplikace:
- Asistent může odkazovat na stránku, aniž by odeslal rozpoznatelný referer.
- Asistent může použít přesměrovanou nebo přepsanou URL adresu.
- Asistent může načíst obsah na straně serveru před zobrazením odpovědi.
- Crawler AI může navštívit stránku, aniž by ji viděl jakýkoli člověk.
- Odpověď může citovat kanonickou URL namísto URL, která byla původně načtena.
- Odpověď se může měnit v závislosti na poloze, zařízení, jazyce, čase nebo personalizaci.
Například OpenAI rozlišuje mezi OAI-SearchBot pro objevování ve vyhledávání, GPTBot pro možné použití k trénování a ChatGPT-User pro akce iniciované uživatelem. Perplexity samostatně identifikuje PerplexityBot a Perplexity-User. Anthropic identifikuje ClaudeBot, Claude-SearchBot a Claude-User. Tyto požadavky by neměly být kombinovány do jednoho obecného čísla „návštěvnosti AI“. Dokumentace crawlerů OpenAI (openai.com) Dokumentace crawlerů Perplexity (docs.perplexity.ai) Dokumentace crawlerů Anthropic (support.anthropic.com)
Hlavní pravidlo zní:
Aktivita crawleru není doporučená návštěvnost, viditelnost citace není návštěvnost a návštěvnost není obchodní dopad.
2. Rámec měření
Použijte pět měřicích vrstev.
| Vrstva | Hlavní otázka | Doporučená data |
|---|---|---|
| Přístup | Vyžádal si stránku systém AI? | Protokoly webového serveru, protokoly firewallu |
| Viditelnost | Byla stránka zobrazena nebo citována? | Google Search Console, Bing Webmaster Tools, pozorování panelů odpovědí |
| Akvizice | Přišel člověk z povrchu AI? | Referer, parametry kampaně, kliknutí na krátký odkaz, analytika |
| Angažovanost | Interagoval návštěvník smysluplně? | Události analytiky, zobrazení stránek, doba angažovanosti |
| Hodnota | Přinesla návštěva požadovaný výsledek? | Registrace, leady, nákupy, předplatné |
Doporučená hierarchie zdrojů pravdy
Pro každou otázku použijte nejsilnější dostupný signál:
- Reporty platforem z první strany pro zobrazení a citace.
- Pozorování panelů odpovědí pro viditelnost na úrovni dotazů a srovnání s konkurencí.
- Protokoly přesměrování pro řízená kliknutí na odkazy.
- Webová analytika pro relace, angažovanost a konverze.
- Serverové protokoly pro aktivitu crawlerů a načítacích botů.
- Modelované odhady pouze v případě, že nejsou k dispozici přímé důkazy.
Nekombinujte všechny signály do jednoho čísla bez zachování zdroje a úrovně spolehlivosti.
3. Základní datový model
Vytvořte datový sklad se samostatnými tabulkami pro surová pozorování a odvozené metriky.
panel_runs
Jeden řádek pro každý dotaz a kontrolu povrchu.
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
Jeden řádek pro každý odkaz pozorovaný v panelu odpovědí.
text run_id surface topic prompt_id cited_url cited_domain citation_position link_scope target_domain target_cited
ai_crawler_requests
Agregovaná data serverových protokolů.
text date bot_name bot_role path requests successful_requests error_requests unique_ip_count
shortener_clicks
Jeden řádek pro každý požadavek řízeného odkazu.
text click_id occurred_at slug surface topic prompt_id asset_id referrer_host user_agent_family ip_hmac destination_path
ai_sessions
Normalizovaná analytická tabulka.
text session_id date surface topic prompt_id landing_page attribution_method confidence engaged engagement_time_seconds key_event revenue
Pole confidence je důležité. Doporučené hodnoty jsou:
- A — Přímá: řízené kliknutí na krátký odkaz nebo důvěryhodný parametr kampaně.
- B — Silná: známý referer asistenta nebo ověřený požadavek na načtení uživatelem.
- C — Pravděpodobná: důkazy z kampaně nebo vstupní stránky bez úplného refereru.
- D — Modelovaná: odvozená z časování, pozorování panelů nebo agregovaných změn.
Reportujte A a B odděleně od C a D.
4. Definice metrik
Podíl citací dle tématu
Podíl citací dle tématu měří, jak často se cílová doména objevuje mezi zdroji pozorovanými pro dané téma.
[ \text{Citation Share}_{topic}
\frac{\text{Target-domain citation occurrences}} {\text{All citation occurrences}} ]
Příklad:
- Celkem 100 citací pozorovaných pro „software pro řízení projektů“
- 12 citací odkazuje na vaši doménu
- Podíl citací dle tématu = 12%
Tato metrika měří podíl zdroje, nikoli to, zda odpověď obsahovala vaši stránku alespoň jednou.
Pokrytí citacemi
Použijte druhou metriku pro přítomnost na úrovni odpovědí:
[ \text{Citation Coverage}_{topic}
\frac{\text{Answer runs containing a target citation}} {\text{Answer runs where a panel was present}} ]
Pokrytí citacemi je pro netechnické publikum často snáze pochopitelné:
„Naše stránky se objevily ve 34 % pozorovaných odpovědí AI pro toto téma.“
Pozice citace
Zaznamenejte pozici citace v rámci odpovědi:
1= první pozorovaný zdroj2= druhý pozorovaný zdroj3= třetí pozorovaný zdroj
Protože rozhraní AI často seskupují nebo mění pořadí zdrojů, považujte pozici za popisné pozorování, nikoli za univerzální pořadí.
Míra doporučení asistentem
Míra doporučení asistentem měří podíl lidských relací na webu, které lze připsat povrchu AI.
[ \text{Assistant Referral Rate}
\frac{\text{AI-attributed human sessions}} {\text{All human sessions}} ]
Reportujte alespoň dvě verze:
text Potvrzená míra doporučení asistentem = relace AI se spolehlivostí A / všechny lidské relace
Rozšířená míra doporučení asistentem = relace AI se spolehlivostí A + B + C / všechny lidské relace
Potvrzená verze je konzervativnější. Rozšířená verze je užitečná pro analýzu trendů, ale neměla by být prezentována jako plně pozorovaná návštěvnost.
Nepoužívejte spuštění panelů jako zobrazení
Pokud scrapujete 500 dotazů, neznamená to, že těchto 500 odpovědí vidělo 500 lidí. Váš scraper měří řízený vzorek stavů odpovědí. Použijte reporty zobrazení platforem z první strany, pokud jsou k dispozici.
Angažovanost po citaci
Angažovanost po citaci měří, zda lidé, kteří přišli prostřednictvím citace AI, provedli smysluplnou akci.
[ \text{Post-Citation Engagement}
\frac{\text{Engaged AI sessions}} {\text{AI-attributed sessions}} ]
Pro Google Analytics 4 trvá angažovaná relace déle než 10 sekund, zahrnuje klíčovou událost nebo zahrnuje alespoň dvě zobrazení stránky či obrazovky. Dokumentace míry angažovanosti Google Analytics (support.google.com)
Také reportujte:
text Doba angažovanosti po citaci = medián angažovaných sekund na relaci AI
Konverzní poměr po citaci = relace AI s klíčovou událostí / relace připsané AI
Příjem z relace po citaci = příjem z relací AI / relace připsané AI
Medián doby angažovanosti je obvykle stabilnější než průměrná doba angažovanosti, protože malý počet velmi dlouhých relací může průměr zkreslit.
Další užitečné metriky
Míra úspěšnosti crawleru AI
[ \text{Crawler Success Rate}
\frac{\text{AI crawler requests returning 2xx or 3xx}} {\text{AI crawler requests}} ]
Rozdělte to podle bota a cesty. Vysoký pokles citací v kombinaci s rostoucí chybovostí crawleru může naznačovat problém s hostingem, firewallem nebo konfigurací robotů.
Míra prokliku z citace
Pro řízená pozorování panelů odpovědí:
[ \text{Observed Citation-to-Click Rate}
\frac{\text{Confirmed short-link clicks}} {\text{Observed target citations}} ]
Toto není skutečná míra prokliku uživatele, pokud jmenovatel nepředstavuje skutečné uživatelské zobrazení. Jasně ji označte jako pozorovanou provozní metriku.
Aktuálnost citace
Měřte, jak často citovaná URL odráží aktuální verzi stránky.
text Aktuálnost citace = citované stránky s aktuálním hash kódem obsahu / citované stránky
To je užitečné pro stránky s dokumentací, cenami, produkty a zásadami, které se často mění.
5. Analýza serverových protokolů
Konfigurace strukturovaných přístupových protokolů
Strukturované protokoly se snáze analyzují než tradiční protokoly oddělené mezerami.
Příklad konfigurace 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;
Ukládejte soubory protokolů na omezeném místě. Neodesílejte nezpracované IP adresy do obecné analytické tabulky, pokud k tomu nemáte jasný provozní důvod.
Identifikace typů požadavků AI
Minimálně oddělte:
text OpenAI:
- OAI-SearchBot
- ChatGPT-User
- GPTBot
Anthropic:
- Claude-SearchBot
- Claude-User
- ClaudeBot
Perplexity:
- PerplexityBot
- Perplexity-User
Vyhledávače:
- Googlebot
- Bingbot
Funkce vyhledávání AI od Googlu obecně používají stávající ovládací prvky procházení Google Search namísto samostatného univerzálního „bota pro Přehled AI“. Google uvádí, že Googlebot kontroluje přístup pro Vyhledávání, včetně funkcí AI. Funkce a webové stránky AI od Googlu (developers.google.com)
Řetězce user-agent lze padělat. Pro provozní rozhodnutí kombinujte shodu user-agent s rozsahy IP adres zveřejněnými poskytovatelem nebo ověřeným reverzním DNS, kde je k dispozici. OpenAI a Perplexity zveřejňují informace o IP adresách pro své relevantní crawlery a načítací boty. (openai.com)
Reprodukovatelný sumarizátor protokolů
Uložte následující jako 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])
Spusťte to s:
bash
HMAC_SECRET='a-long-random-secret'
python ai_log_summary.py /var/log/nginx/ai-access.jsonl \
data/ai_crawler_daily.csv
To vytváří provozní důkazy o aktivitě načítání, nikoli o lidské návštěvnosti.
6. Vlastní zkracovače odkazů pro souhrny směřující k AI
Zkracovač odkazů z první strany poskytuje řízený měřicí bod, když publikujete souhrny nebo zdrojové materiály, které mohou být referencovány systémy AI.
Použijte doménu, kterou ovládáte, například:
text https://go.example.com/ai-docs
Cíl by měl zahrnovat parametry kampaně, jako jsou:
text utm_source=perplexity utm_medium=ai_answer utm_campaign=product_documentation utm_content=pricing_summary_v3
Google doporučuje parametry kampaně pro identifikaci zdrojů návštěvnosti a kampaní. Dokumentace URL kampaně Google Analytics (support.google.com)
Doporučená pole odkazu
text surface topic prompt_id asset_id content_version destination_path
Nevkládejte do odkazu osobní údaje, e-mailové adresy ani uživatelské vyhledávací dotazy. Identifikátory kampaně by měly popisovat publikovaný asset, nikoli identifikovat osobu.
Důležitá omezení
Krátký odkaz je kalibrační zařízení, nikoli univerzální řešení:
- Asistent může přepsat cílovou URL adresu.
- Asistent může místo toho citovat kanonickou URL adresu.
- Odkaz může být zobrazen, ale nikdy na něj nebude kliknuto.
- Prohlížeč nebo nástroj pro ochranu soukromí může odstranit referer.
- Některé systémy mohou načíst cíl na straně serveru.
- Krátký odkaz neměří uživatele, kteří si značku zapamatují a vrátí se později.
Nepoužívejte zkracovač k skrytí cíle, obcházení bezpečnostních systémů nebo manipulaci s citacemi. Použijte jasné přesměrování na relevantní, kanonickou stránku.
Minimální zkracovač odkazů z první strany
Následující příklad používá FastAPI a SQLite. Před použitím v produkci chraňte koncový bod pro vytváření odkazů autentizací.
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)
Spusťte to s:
bash pip install fastapi uvicorn uvicorn shortener:app --host 0.0.0.0 --port 8080
Použijte přesměrování 302 pro měřicí odkazy, aby prohlížeče a mezilehlé systémy méně pravděpodobně trvale cachovaly přesměrování.
7. Plánované scrapování panelů odpovědí AI
Scrapování panelů je užitečné pro zodpovězení otázek, které analytika z první strany nemusí odhalit:
- Které konkurenční zdroje jsou citovány?
- Která stránka je citována pro konkrétní dotaz?
- Mění se odpověď podle země nebo zařízení?
- Objevuje se citace v odpovědi, v seznamu zdrojů, nebo v obojím?
- Jak stabilní je odpověď v čase?
- Cituje asistent krátký odkaz nebo kanonickou URL?
Scrapování však musí být prováděno opatrně.
Pravidla pro zodpovědný sběr
- Upřednostněte oficiální API, licencovaného poskytovatele vyhledávacích dat nebo report z první strany.
- Před automatizací požadavků si prostudujte podmínky poskytovatele.
- Respektujte pokyny robotů, kde je to relevantní.
- Neobcházejte CAPTCHA, přihlašovací bariéry, omezení rychlosti nebo ochrany proti botům.
- Použijte malou, pevnou sadu dotazů.
- Spouštějte s nízkou frekvencí.
- Uložte odpověď a časové razítko, aby bylo možné výsledky auditovat.
- Zastavte, když poskytovatel vrátí výzvu nebo odpověď „přístup odepřen“.
Protokol pro vyloučení robotů je mechanismus preferencí crawleru, nikoli bezpečnostní systém. Neautorizuje přístup k chráněnému obsahu. RFC 9309: Protokol pro vyloučení robotů (rfc-editor.org)
Vytvořte manifest dotazů
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
Udržujte dotazy stabilní. Pokud se dotazy změní, srovnání trendů se stane méně spolehlivým.
Uložte také:
- země
- jazyk
- typ zařízení
- verze prohlížeče
- verze dotazu
- verze obsahu
- čas sběru v UTC
Google poznamenává, že odpovědi AI Overview a AI Mode se mohou lišit, protože používají různé systémy a mohou využívat související vyhledávání nebo „rozšíření dotazů“. Dokumentace funkcí a webových stránek AI od Googlu (developers.google.com)
Skript pro sběr pomocí Playwright
Následující skript je univerzální nástroj pro výzkum. Zaznamenává vykreslenou stránku, snímek obrazovky, odkazy na stránce a text odpovědi. Záměrně neobchází výzvy ani se nepokouší překonat kontrolu přístupu.
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"))
Nainstalujte a spusťte:
bash pip install playwright playwright install chromium python scrape_panels.py
Naplánujte to se zámkem, aby se dva běhy nepřekrývaly:
cron
15 3 * * * flock -n /tmp/ai-panel.lock
/usr/bin/python3 /opt/ai-measure/scrape_panels.py
Zlepšete přesnost pomocí adaptérů pro povrchy
Záložní skript zaznamenává odkazy na úrovni stránky. To je užitečné pro objevování, ale může zahrnovat běžné odkazy z výsledků vyhledávání. Pro produkční použití:
- Použijte licencované API panelu odpovědí, pokud je k dispozici.
- Vytvořte samostatný adaptér selektoru pro každý povrch.
- Uložte verzi selektoru.
- Uložte nezpracované HTML a snímek obrazovky.
- Označte každý záznam
link_scope = "panel_selector"nebolink_scope = "page_fallback".
Vypočítávejte podíl citací pouze z dat panel_selector, když můžete spolehlivě identifikovat panel. Pozorování z fallbacku udržujte v samostatném průzkumném reportu.
8. Integrace Google Analytics a BigQuery
Google Analytics 4 může exportovat nezpracovaná data událostí do BigQuery. Denní exporty poskytují stabilní tabulky jako events_YYYYMMDD; streamovací exporty jsou užitečné pro monitorování téměř v reálném čase, ale mohou být neúplné, dokud nebude denní tabulka finalizována. Export Google Analytics do BigQuery (support.google.com)
Zjednodušený dotaz na relaci může vypadat takto:
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;
Pro pokročilou implementaci přidejte surface, topic a prompt_id do struktury kampaně nebo je připojte z tabulky kliknutí zkracovače odkazů.
Nevytvářejte analytickou dimenzi s vysokou kardinalitou pro každého jednotlivého uživatele nebo relaci. Google varuje, že nadměrné vlastní dimenze s vysokou kardinalitou mohou snížit kvalitu reportování. Vlastní dimenze a metriky Google Analytics (support.google.com)
9. Návrh dashboardu
Praktický dashboard by měl obsahovat pět zobrazení.
Souhrn pro management
Zobrazte:
- Podíl citací dle tématu
- Pokrytí citacemi dle tématu
- Potvrzená míra doporučení asistentem
- Rozšířená míra doporučení asistentem
- Angažovanost po citaci
- Míra úspěšnosti crawleru AI
- Klíčové události nebo příjem připsané AI
Trend podílu citací
Spojnicový graf:
text Date × Topic × Surface
Zobrazte obojí:
- výskyty citací cílové domény
- všechny pozorované výskyty citací
Trychtýř návštěvnosti asistenta
text Pozorované citace ↓ Kliknutí na řízené odkazy ↓ Relace připsané AI ↓ Angažované relace AI ↓ Klíčové události
Každá fáze by měla zobrazovat svou spolehlivost atribuce.
Stav crawlerů
Tabulka dle bota:
text bot_name requests successful_requests error_rate unique_ip_count top_paths
Upozornění, když:
- chybovost přesáhne 2 procenta
- bot náhle zmizí
- dříve přístupná sekce obsahu vrátí 403 nebo 429
- požadavky crawleru se soustředí na nezamýšlenou soukromou cestu
Tabulka příležitostí pro obsah
Seřaďte stránky a témata podle:
text vysoký podíl citací + nízká míra doporučení vysoká zobrazení + nízké pokrytí citacemi vysoká angažovanost + nízká viditelnost citace vysoká chybovost crawleru + vysoká obchodní důležitost
Tyto kombinace obvykle vedou k lepším rozhodnutím než pouhé sledování objemu návštěvnosti.
Minimální Streamlit dashboard
Uložte jako 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 Citation Impact Dashboard")
if panels.empty: st.warning("No panel observations found.") 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("Citation Share by Topic") st.dataframe(citation_share, use_container_width=True) st.bar_chart( citation_share.set_index("topic")["citation_share"] )
with col2: st.subheader("Citation Coverage by Topic") st.dataframe(coverage, use_container_width=True) st.bar_chart( coverage.set_index("topic")["citation_coverage"] )
st.subheader("Citation Position") 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("Daily Observations") 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) )
Spusťte to s:
bash pip install streamlit pandas streamlit run dashboard.py
Pro produkci načtěte normalizované tabulky do DuckDB, BigQuery, Snowflake nebo PostgreSQL namísto přímého čtení surových souborů.
10. Kontroly kvality
Měřicí systém může produkovat přesně vypadající, ale zavádějící čísla. Přidejte tyto kontroly.
Oddělte boty od lidí
Nikdy nepočítejte OAI-SearchBot, GPTBot, ClaudeBot nebo PerplexityBot jako lidské relace. Jedná se o signály crawlerů nebo načítacích botů.
Zachovejte surové důkazy
Pro každé spuštění panelu zachovejte:
- surové HTML
- snímek obrazovky
- text odpovědi
- časové razítko
- verze prohlížeče a sběrače
- dotaz a lokalizace
- hash odpovědi
To umožňuje vysvětlit, proč se metrika změnila.
Sledování nepřítomnosti panelu
Chybějící panel AI není totéž jako odpověď bez vaší citace.
Použijte:
text panel_not_present panel_present_no_target panel_present_target_cited blocked_or_challenged collection_error
Kontrola zkreslení výběru vzorku
Plánované scrapování je ovlivněno:
- poloha
- zařízení
- jazyk
- čas
- personalizace
- stav prohlížeče
- formulace dotazu
Použijte pevnou základní sadu dotazů a rotující sadu průzkumných dotazů. Tyto dvě skupiny udržujte v reportech oddělené.
Vyhněte se falešné přesnosti
Reportujte intervaly spolehlivosti nebo minimální velikosti vzorku pro malé tematické skupiny. Téma se třemi pozorováními by nemělo být přímo srovnáváno s tématem obsahujícím 300 pozorování.
Srovnávejte srovnatelné
Při srovnávání povrchů udržujte konstantní:
- dotaz
- lokalizace
- země
- zařízení
- hodina sběru
- pravidla cílové domény
- verze parseru citací
11. Doporučený plán implementace
Týden 1: Instrumentace
- Konfigurujte strukturované serverové protokoly.
- Vytvořte registr botů.
- Vytvořte doménu pro zkracovač odkazů.
- Přidejte pravidla pro pojmenování kampaní.
- Definujte taxonomii témat.
- Povolte export Google Analytics 4 do BigQuery.
Týden 2: Řízené odkazy
- Publikujte pět až deset sledovaných souhrnů.
- Vytvořte odkazy pro různá témata a verze obsahu.
- Ověřte protokoly přesměrování oproti analytickým relacím.
- Potvrďte, že do URL adres nebo analytických parametrů nevstupují žádné osobní údaje.
Týden 3: Pozorování panelů
- Vytvořte manifest dotazů.
- Začněte se dvěma povrchy a 20 až 50 stabilními dotazy.
- Spouštějte jednou denně.
- Ukládejte snímky obrazovky a HTML.
- Měřte selhání sběru dat odděleně od nepřítomnosti citací.
Týden 4: Dashboard a správa
- Vytvořte dashboardy pro témata a povrchy.
- Přidejte upozornění na stav crawlerů.
- Definujte pravidla spolehlivosti.
- Porovnejte reporty z první strany s pozorováními panelů.
- Dokumentujte zásady uchovávání, přístupu a mazání dat.
Závěr
Měření citací AI by mělo být postaveno jako vícesignálový atribuční systém, nikoli jako jediný report doporučení.
Nejspolehlivější rámec kombinuje:
- Reporty viditelnosti z první strany od Googlu a Bingu
- serverové protokoly pro aktivitu crawlerů a načítacích botů
- krátké odkazy z první strany pro řízené měření kliknutí
- plánovaná, vyhovující pozorování panelů odpovědí
- analytická a konverzní data pro následnou hodnotu
Tři nejdůležitější metriky jsou:
- Podíl citací dle tématu — kolik pozorovaného zdrojového prostoru patří vaší doméně.
- Míra doporučení asistentem — kolik lidské návštěvnosti lze připsat povrchům AI.
- Angažovanost po citaci — zda návštěvníci doporučení AI provádějí smysluplné akce.
Systém se stává užitečným, když každé číslo zahrnuje svůj povrch, časové období, jmenovatel, atribuční metodu a úroveň spolehlivosti. Tato disciplína zabraňuje záměně zásahů crawlerů za uživatele, seškrábaných odpovědí za zobrazení a citací za obchodní výsledky.
Auto