Măsurarea Impactului Citării AI: Analitice, Jurnale și Scraping de Panouri
Introducere
Descoperirea bazată pe AI este acum împărțită pe mai multe niveluri de măsurare. O persoană poate vedea o companie citată într-un răspuns AI, poate da clic pe un link fără un referrer util, poate vizita printr-un browser care elimină datele campaniei sau poate citi un răspuns fără a da clic deloc. Între timp, un crawler AI poate solicita pagina companiei fără a reprezenta un vizitator uman.
Un sistem de măsurare util trebuie, prin urmare, să separe patru întrebări:
- A fost site-ul indexat sau recuperat?
- A fost site-ul citat într-un răspuns AI?
- O persoană a vizitat site-ul de pe acel răspuns?
- Acea vizită a produs o implicare semnificativă sau venituri?
Începând cu 22 septembrie 2026, raportarea de primă parte se îmbunătățește. Google Search Console oferă un raport de performanță pentru AI Generativ pentru AI Overviews și AI Mode, incluzând afișări, pagini, țări, dispozitive și date. Raportul a devenit disponibil la nivel mondial pe 31 august 2026. Raportul de performanță AI Generativ de la Google (support.google.com)
Microsoft oferă, de asemenea, un raport de Performanță AI în Bing Webmaster Tools. Previzualizarea sa publică raportează numărul total de citări, pagini citate, interogări de bază și tendințe de citare în toate experiențele AI Microsoft acceptate. Performanță AI în Bing Webmaster Tools (blogs.bing.com)
Aceste instrumente ar trebui tratate ca fiind autoritare pentru propriile lor suprafețe, dar nu creează un sistem complet de măsurare între asistenți. Abordarea recomandată este de a combina rapoarte de primă parte, jurnale de server, linkuri de urmărire controlate, exporturi de analize și observații programate ale panourilor de răspuns.
1. De ce este dificilă măsurarea referințelor AI
Analiza web tradițională depinde în mare măsură de antetul Referer și de parametrii campaniei. Ambele pot fi lipsă sau incomplete.
Antetul HTTP Referer poate conține pagina anterioară, doar originea site-ului web anterior sau nicio valoare. Politicile de referrer ale browserului pot limita sau elimina în mod intenționat aceste informații. Documentația Referrer-Policy MDN (developer.mozilla.org)
Sistemele AI creează complicații suplimentare:
- Un asistent poate trimite un link către o pagină fără a trimite un referrer recognoscibil.
- Un asistent poate utiliza o adresă URL de redirecționare sau rescrisă.
- Un asistent poate prelua conținut de pe server înainte de a afișa un răspuns.
- Un crawler AI poate vizita o pagină fără ca vreun om să o vadă.
- Răspunsul poate cita o adresă URL canonică în loc de adresa URL care a fost preluată inițial.
- Răspunsul se poate schimba în funcție de locație, dispozitiv, limbă, oră sau personalizare.
De exemplu, OpenAI distinge între OAI-SearchBot pentru descoperirea în căutare, GPTBot pentru o posibilă utilizare în antrenament și ChatGPT-User pentru acțiunile inițiate de utilizator. Perplexity identifică separat PerplexityBot și Perplexity-User. Anthropic identifică ClaudeBot, Claude-SearchBot și Claude-User. Aceste cereri nu ar trebui combinate într-un singur număr generic de „trafic AI”. Documentația crawlerului OpenAI (openai.com) Documentația crawlerului Perplexity (docs.perplexity.ai) Documentația crawlerului Anthropic (support.anthropic.com)
Regula centrală este:
Activitatea crawler-ului nu este trafic de referință, vizibilitatea citării nu este trafic, iar traficul nu este impact asupra afacerii.
2. Cadrul de Măsurare
Utilizați cinci straturi de măsurare.
| Strat | Întrebare principală | Date recomandate | |---| | Acces | Un sistem AI a solicitat pagina? | Jurnale server web, jurnale firewall | | Vizibilitate | Pagina a fost afișată sau citată? | Google Search Console, Bing Webmaster Tools, observații panouri de răspuns | | Achiziție | O persoană a ajuns de pe o suprafață AI? | Referrer, parametri campanie, clicuri pe linkuri scurte, analize | | Angajament | Vizitatorul a interacționat semnificativ? | Evenimente analitice, vizualizări pagini, timp de angajament | | Valoare | Vizita a produs rezultatul dorit? | Înregistrări, lead-uri, achiziții, abonamente |
Ierarhia sursei de adevăr recomandate
Utilizați cel mai puternic semnal disponibil pentru fiecare întrebare:
- Rapoarte de platformă de primă parte pentru afișări și citări.
- Observații ale panourilor de răspuns pentru vizibilitate la nivel de prompt și comparații competitive.
- Jurnale de redirecționare pentru clicuri pe linkuri controlate.
- Analize web pentru sesiuni, angajament și conversii.
- Jurnale de server pentru activitatea crawler-ului și fetcher-ului.
- Estimări modelate doar atunci când dovezile directe nu sunt disponibile.
Nu combinați toate semnalele într-un singur număr fără a păstra sursa și nivelul de încredere.
3. Modelul de Date Central
Creați un depozit de date cu tabele separate pentru observații brute și metrici derivate.
panel_runs
Un rând pentru fiecare prompt și verificare de suprafață.
text run_id observed_at surface topic prompt_id query_text locale country device panel_present pane_type answer_hash raw_html_path screenshot_path collector_version status
panel_citations
Un rând pentru fiecare link observat într-un panou de răspuns.
text run_id surface topic prompt_id cited_url cited_domain citation_position link_scope target_domain target_cited
ai_crawler_requests
Date agregate din jurnalele serverului.
text date bot_name bot_role path requests successful_requests error_requests unique_ip_count
shortener_clicks
Un rând pentru fiecare solicitare de link controlat.
text click_id occurred_at slug surface topic prompt_id asset_id referrer_host user_agent_family ip_hmac destination_path
ai_sessions
Un tabel analitic normalizat.
text session_id date surface topic prompt_id landing_page attribution_method confidence engaged engagement_time_seconds key_event revenue
Câmpul confidence este important. Valorile recomandate sunt:
- A — Direct: clic pe link scurt controlat sau parametru de campanie de încredere.
- B — Puternic: referrer cunoscut al asistentului sau cerere de preluare a utilizatorului verificată.
- C — Probabil: dovezi de campanie sau de pagină de destinație fără un referrer complet.
- D — Modelat: inferat din sincronizare, observații de panou sau modificări agregate.
Raportați A și B separat de C și D.
4. Definiții de Metrică
Cota de Citare pe Subiect
Cota de Citare pe Subiect măsoară cât de des apare un domeniu țintă printre sursele observate pentru un anumit subiect.
[ \text{Citation Share}_{topic}
\frac{\text{Target-domain citation occurrences}} {\text{All citation occurrences}} ]
Exemplu:
- 100 citări totale observate pentru „software de management de proiect”
- 12 citări indică domeniul dvs.
- Cota de Citare pe Subiect = 12%
Această metrică măsoară cota sursei, nu dacă un răspuns a conținut site-ul dvs. cel puțin o dată.
Acoperirea Citărilor
Utilizați o a doua metrică pentru prezența la nivel de răspuns:
[ \text{Citation Coverage}_{topic}
\frac{\text{Answer runs containing a target citation}} {\text{Answer runs where a panel was present}} ]
Acoperirea Citărilor este adesea mai ușor de înțeles pentru audiențele non-tehnice:
„Site-ul nostru a apărut în 34% din răspunsurile AI observate pentru acest subiect.”
Poziția Citării
Înregistrați poziția unei citări în cadrul răspunsului:
1= prima sursă observată2= a doua sursă observată3= a treia sursă observată
Deoarece interfețele AI grupează sau reordonează adesea sursele, tratați poziția ca pe o observație descriptivă, nu ca pe o clasificare universală.
Rata de Referință a Asistentului
Rata de Referință a Asistentului măsoară proporția sesiunilor umane pe site care pot fi atribuite unei suprafețe AI.
[ \text{Assistant Referral Rate}
\frac{\text{AI-attributed human sessions}} {\text{All human sessions}} ]
Raportați cel puțin două versiuni:
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
Versiunea confirmată este mai conservatoare. Versiunea extinsă este utilă pentru analiza tendințelor, dar nu ar trebui prezentată ca trafic complet observat.
Nu utilizați rulările de panouri ca afișări
Dacă extrageți 500 de prompturi, asta nu înseamnă că 500 de persoane au văzut acele răspunsuri. Programul dvs. de scraping măsoară un eșantion controlat de stări ale răspunsurilor. Utilizați rapoartele de afișări ale platformelor de primă parte atunci când sunt disponibile.
Angajament Post-Citare
Angajamentul Post-Citare măsoară dacă persoanele care au ajuns printr-o citare AI au întreprins o acțiune semnificativă.
[ \text{Post-Citation Engagement}
\frac{\text{Engaged AI sessions}} {\text{AI-attributed sessions}} ]
Pentru Google Analytics 4, o sesiune angajată durează mai mult de 10 secunde, include un eveniment cheie sau include cel puțin două vizualizări de pagină sau ecran. Documentația Google Analytics despre rata de angajament (support.google.com)
De asemenea, raportați:
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
Timpul median de angajament este de obicei mai stabil decât timpul mediu de angajament, deoarece un număr mic de sesiuni foarte lungi pot distorsiona media.
Metrici Suplimentare Utile
Rata de Succes a Crawler-ului AI
[ \text{Crawler Success Rate}
\frac{\text{AI crawler requests returning 2xx or 3xx}} {\text{AI crawler requests}} ]
Defalcați acest lucru pe bot și cale. O scădere mare a citărilor combinată cu o rată crescută de erori ale crawler-ului poate indica o problemă de găzduire, firewall sau configurare a fișierului robots.
Rata Citare-la-Clic
Pentru observațiile controlate ale panoului de răspuns:
[ \text{Observed Citation-to-Click Rate}
\frac{\text{Confirmed short-link clicks}} {\text{Observed target citations}} ]
Aceasta nu este o rată reală de clicuri a utilizatorilor, cu excepția cazului în care numitorul reprezintă afișări reale ale utilizatorilor. Etichetați-o clar ca o metrică operațională observată.
Actualitatea Citării
Măsurați cât de des adresa URL citată reflectă versiunea actuală a paginii.
text Citation Freshness = cited pages with current content hash / cited pages
Acest lucru este util pentru paginile de documentație, prețuri, produse și politici care se modifică frecvent.
5. Analiza Jurnalelor Serverului
Configurați Jurnale de Acces Structurate
Jurnalele structurate sunt mai ușor de analizat decât jurnalele tradiționale separate prin spații.
Exemplu de configurare 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;
Stocați fișierele jurnal într-o locație restricționată. Nu trimiteți adrese IP brute într-un tabel general de analize, decât dacă aveți un motiv operațional clar.
Identificați Tipurile de Cereri AI
Cel puțin, separați:
text OpenAI:
- OAI-SearchBot
- ChatGPT-User
- GPTBot
Anthropic:
- Claude-SearchBot
- Claude-User
- ClaudeBot
Perplexity:
- PerplexityBot
- Perplexity-User
Search engines:
- Googlebot
- Bingbot
Funcțiile de Căutare AI de la Google utilizează, în general, controalele existente de crawling ale Căutării Google, mai degrabă decât un „bot AI Overview” universal separat. Google afirmă că Googlebot controlează accesul pentru Căutare, inclusiv pentru funcțiile AI. Funcțiile AI Google și site-urile web (developers.google.com)
Șirurile de user-agent pot fi falsificate. Pentru decizii operaționale, combinați potrivirea user-agent-ului cu intervalele IP publicate de furnizor sau cu DNS-ul invers verificat, acolo unde este disponibil. OpenAI și Perplexity publică informații IP pentru crawlerii și fetcherii lor relevanți. (openai.com)
Rezumator de Jurnale Reproducibil
Salvați următoarele ca 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])
Rulați-l cu:
bash
HMAC_SECRET='a-long-random-secret'
python ai_log_summary.py /var/log/nginx/ai-access.jsonl \
data/ai_crawler_daily.csv
Aceasta produce dovezi operaționale despre activitatea de recuperare, nu despre traficul uman.
6. Scurtătoare de Linkuri Personalizate pentru Rezumatele Orientate către AI
Un scurtător de linkuri de primă parte oferă un punct de măsurare controlat atunci când publicați rezumate sau materiale sursă care pot fi referențiate de sistemele AI.
Utilizați un domeniu pe care îl controlați, cum ar fi:
text https://go.example.com/ai-docs
Destinația ar trebui să includă parametri de campanie precum:
text utm_source=perplexity utm_medium=ai_answer utm_campaign=product_documentation utm_content=pricing_summary_v3
Google recomandă parametri de campanie pentru identificarea surselor de trafic și a campaniilor. Documentația Google Analytics despre URL-urile campaniilor (support.google.com)
Câmpuri de link recomandate
text surface topic prompt_id asset_id content_version destination_path
Nu introduceți informații personale, adrese de e-mail sau interogări de căutare ale utilizatorilor în link. Identificatorii de campanie ar trebui să descrie activul publicat, nu să identifice o persoană.
Limitări importante
Un link scurt este un dispozitiv de calibrare, nu o soluție universală:
- Asistentul poate rescrie adresa URL de destinație.
- Asistentul poate cita adresa URL canonică în schimb.
- Linkul poate fi afișat, dar niciodată accesat.
- Un browser sau un instrument de confidențialitate poate elimina referrer-ul.
- Unele sisteme pot prelua destinația de pe partea serverului.
- Un link scurt nu măsoară utilizatorii care își amintesc brandul și revin mai târziu.
Nu utilizați un scurtător pentru a ascunde destinația, a ocoli sistemele de securitate sau a manipula citările. Utilizați o redirecționare clară către o pagină relevantă, canonică.
Scurtător de Primă Parte Minimal
Exemplul următor utilizează FastAPI și SQLite. Protejați punctul final de creare a linkurilor cu autentificare înainte de a-l utiliza în producție.
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)
Rulați-l cu:
bash pip install fastapi uvicorn uvicorn shortener:app --host 0.0.0.0 --port 8080
Utilizați o redirecționare 302 pentru linkurile de măsurare, astfel încât browserele și sistemele intermediare să fie mai puțin susceptibile de a memora în cache redirecționarea permanent.
7. Scraping Programat al Panourilor de Răspuns AI
Scraping-ul de panouri este util pentru a răspunde la întrebări pe care analizele de primă parte s-ar putea să nu le expună:
- Ce surse concurente sunt citate?
- Ce pagină este citată pentru un anumit prompt?
- Răspunsul se schimbă în funcție de țară sau dispozitiv?
- Citarea apare în răspuns, în lista de surse sau în ambele?
- Cât de stabil este răspunsul în timp?
- Asistentul citează un link scurt sau adresa URL canonică?
Totuși, scraping-ul trebuie făcut cu atenție.
Reguli pentru colectarea responsabilă
- Preferați un API oficial, un furnizor licențiat de date de căutare sau un raport de primă parte.
- Revizuiți termenii furnizorului înainte de a automatiza cererile.
- Respectați instrucțiunile robots acolo unde este cazul.
- Nu ocoliți CAPTCHA-urile, barierele de conectare, limitele de rată sau protecțiile bot.
- Utilizați un set mic și fix de prompturi.
- Rulați la o frecvență scăzută.
- Salvați răspunsul și marca temporală pentru ca rezultatele să poată fi auditate.
- Opriți-vă atunci când un furnizor returnează o provocare sau un răspuns de acces refuzat.
Protocolul de Excludere a Roboților este un mecanism de preferință pentru crawlere, nu un sistem de securitate. Nu autorizează accesul la conținut protejat. RFC 9309: Protocolul de Excludere a Roboților (rfc-editor.org)
Construiți un Manifest de Prompturi
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
Mențineți prompturile stabile. Dacă prompturile se modifică, comparațiile de tendințe devin mai puțin fiabile.
De asemenea, stocați:
- țară
- limbă
- tip de dispozitiv
- versiune de browser
- versiune de prompt
- versiune de conținut
- ora colectării în UTC
Google menționează că răspunsurile AI Overview și AI Mode pot varia deoarece utilizează sisteme diferite și pot folosi căutări similare sau „ramificarea interogării”. Documentația funcțiilor AI Google și a site-urilor web (developers.google.com)
Script de Colectare Playwright
Următorul script este un instrument de cercetare de uz general. Înregistrează pagina redată, captura de ecran, linkurile paginii și textul răspunsului. Nu ocolește în mod intenționat provocările și nu încearcă să învingă controalele de acces.
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"))
Instalați și rulați:
bash pip install playwright playwright install chromium python scrape_panels.py
Programați-l cu o blocare pentru ca două rulări să nu se suprapună:
cron
15 3 * * * flock -n /tmp/ai-panel.lock
/usr/bin/python3 /opt/ai-measure/scrape_panels.py
Îmbunătățiți precizia cu adaptoare de suprafață
Scriptul de rezervă înregistrează linkurile la nivel de pagină. Acest lucru este util pentru descoperire, dar poate include linkuri obișnuite de rezultate ale căutării. Pentru utilizare în producție:
- Utilizați un API licențiat pentru panoul de răspuns atunci când este disponibil.
- Creați un adaptor selector separat pentru fiecare suprafață.
- Stocați versiunea selectorului.
- Stocați HTML-ul brut și o captură de ecran.
- Marcați fiecare înregistrare cu
link_scope = "panel_selector"saulink_scope = "page_fallback".
Calculați cota de citare numai din datele panel_selector atunci când puteți identifica în mod fiabil panoul. Păstrați observațiile de rezervă într-un raport exploratoriu separat.
8. Integrarea Google Analytics și BigQuery
Google Analytics 4 poate exporta date brute de evenimente către BigQuery. Exporturile zilnice oferă tabele stabile precum events_YYYYMMDD; exporturile de streaming sunt utile pentru monitorizarea aproape în timp real, dar pot fi incomplete până la finalizarea tabelului zilnic. Export Google Analytics BigQuery (support.google.com)
O interogare simplificată a sesiunii ar putea arăta astfel:
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;
Pentru o implementare matură, adăugați surface, topic și prompt_id la structura campaniei sau uniți-le din tabelul de clicuri al scurtătorului.
Nu creați o dimensiune analitică de cardinalitate ridicată pentru fiecare utilizator sau sesiune individuală. Google avertizează că dimensiunile personalizate de cardinalitate excesiv de ridicată pot reduce calitatea raportării. Dimensiuni și metrici personalizate Google Analytics (support.google.com)
9. Designul Tabloului de Bord
Un tablou de bord practic ar trebui să conțină cinci vizualizări.
Rezumat Executiv
Afișați:
- Cota de Citare pe Subiect
- Acoperirea Citărilor pe Subiect
- Rata de Referință a Asistentului Confirmată
- Rata de Referință a Asistentului Extinsă
- Angajament Post-Citare
- Rata de succes a crawler-ului AI
- Evenimente cheie sau venituri atribuite AI
Tendința Cotei de Citare
Diagramă liniară:
text Date × Topic × Surface
Afișați ambele:
- apariții ale citărilor domeniului țintă
- toate aparițiile citărilor observate
Pâlnia de Trafic a Asistentului
text Observed citations ↓ Controlled-link clicks ↓ AI-attributed sessions ↓ Engaged AI sessions ↓ Key events
Fiecare etapă ar trebui să afișeze încrederea sa de atribuire.
Starea de Sănătate a Crawler-ului
Tabel pe bot:
text bot_name requests successful_requests error_rate unique_ip_count top_paths
Alertați când:
- rata de eroare depășește 2 procente
- un bot dispare brusc
- o secțiune de conținut anterior accesibilă returnează 403 sau 429
- cererile crawler-ului se concentrează pe o cale privată neintenționată
Tabel Oportunități de Conținut
Clasați paginile și subiectele după:
text high citation share + low referral rate high impressions + low citation coverage high engagement + low citation visibility high crawler errors + high business importance
Aceste combinații produc de obicei decizii mai bune decât o simplă examinare a volumului de trafic.
Tablou de Bord Minimal Streamlit
Salvați ca 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) )
Rulați-l cu:
bash pip install streamlit pandas streamlit run dashboard.py
Pentru producție, încărcați tabelele normalizate în DuckDB, BigQuery, Snowflake sau PostgreSQL în loc să citiți fișierele brute direct.
10. Controale de Calitate
Un sistem de măsurare poate produce numere care par precise, dar sunt înșelătoare. Adăugați aceste controale.
Separați roboții de oameni
Nu numărați niciodată OAI-SearchBot, GPTBot, ClaudeBot sau PerplexityBot ca sesiuni umane. Acestea sunt semnale de crawler sau de preluare.
Păstrați dovezile brute
Pentru fiecare rulare de panou, păstrați:
- HTML brut
- captură de ecran
- textul răspunsului
- marca temporală
- versiunea browserului și a colectorului
- promptul și localizarea
- hash-ul răspunsului
Acest lucru face posibilă explicarea motivului pentru care o metrică s-a modificat.
Urmăriți absența panoului
Un panou AI lipsă nu este același lucru cu un răspuns fără citarea dvs.
Utilizați:
text panel_not_present panel_present_no_target panel_present_target_cited blocked_or_challenged collection_error
Controlați prejudecata de eșantionare
O extragere programată este afectată de:
- locație
- dispozitiv
- limbă
- timp
- personalizare
- starea browserului
- formularea promptului
Utilizați un set fix de prompturi de bază și un set rotativ de prompturi exploratorii. Păstrați cele două grupuri separate în rapoarte.
Evitați precizia falsă
Raportați intervale de încredere sau dimensiuni minime ale eșantionului pentru grupuri mici de subiecte. Un subiect cu trei observații nu ar trebui comparat direct cu un subiect care conține 300 de observații.
Comparați elemente similare
Atunci când comparați suprafețe, mențineți constante:
- prompt
- localizare
- țară
- dispozitiv
- ora colectării
- reguli de domeniu țintă
- versiunea analizatorului de citări
11. Plan de Implementare Recomandat
Săptămâna 1: Instrumentare
- Configurați jurnalele structurate ale serverului.
- Creați registrul de boți.
- Creați domeniul scurtătorului.
- Adăugați reguli de denumire a campaniilor.
- Definiți taxonomia subiectelor.
- Activați exportul Google Analytics 4 BigQuery.
Săptămâna 2: Linkuri controlate
- Publicați cinci până la zece rezumate urmărite.
- Creați linkuri pentru diferite subiecte și versiuni de conținut.
- Validați jurnalele de redirecționare în raport cu sesiunile de analiză.
- Confirmați că nu sunt introduse informații personale în URL-uri sau parametri analitici.
Săptămâna 3: Observații panouri
- Creați un manifest de prompturi.
- Începeți cu două suprafețe și 20 până la 50 de prompturi stabile.
- Rulați o dată pe zi.
- Salvați capturi de ecran și HTML.
- Măsurați eșecurile de colectare separat de absența citării.
Săptămâna 4: Tablou de bord și guvernanță
- Construiți tablouri de bord pentru subiecte și suprafețe.
- Adăugați alerte pentru starea de sănătate a crawler-ului.
- Definiți regulile de încredere.
- Comparați rapoartele de primă parte cu observațiile panourilor.
- Documentați politicile de retenție, acces și ștergere.
Concluzie
Măsurarea citărilor AI ar trebui construită ca un sistem de atribuire multi-semnal, nu ca un singur raport de referință.
Cadrul cel mai fiabil combină:
- Rapoarte de vizibilitate de primă parte Google și Bing
- jurnale de server pentru activitatea crawler-ului și fetcher-ului
- linkuri scurte de primă parte pentru măsurarea clicurilor controlate
- observații programate, conforme ale panourilor de răspuns
- date de analiză și conversie pentru valoarea ulterioară
Cele trei metrici cele mai importante sunt:
- Cota de Citare pe Subiect — cât din spațiul sursei observate aparține domeniului dvs.
- Rata de Referință a Asistentului — cât trafic uman poate fi atribuit suprafețelor AI.
- Angajament Post-Citare — dacă vizitatorii referiți de AI întreprind acțiuni semnificative.
Sistemul devine util atunci când fiecare număr include suprafața, perioada de timp, numitorul, metoda de atribuire și nivelul de încredere. Această disciplină previne ca accesările de către crawler să fie confundate cu utilizatori, răspunsurile extrase să fie confundate cu afișări, iar citările să fie confundate cu rezultate de afaceri.
Auto