MÀta AI-citeringspÄverkan: Analysdata, loggar och skrapning av svarspaneler
Introduktion
AI-driven upptÀckt Àr nu uppdelad i flera mÀtlager. En person kan se ett företag citerat i ett AI-svar, klicka pÄ en lÀnk utan anvÀndbar hÀnvisare, besöka via en webblÀsare som tar bort kampanjdata, eller lÀsa ett svar utan att klicka alls. Samtidigt kan en AI-bot begÀra företagets sida utan att representera en mÀnsklig besökare.
Ett anvÀndbart mÀtsystem mÄste dÀrför separera fyra frÄgor:
- HÀmtades eller hÀmtades webbplatsen?
- Citerades webbplatsen i ett AI-svar?
- Besökte en person webbplatsen frÄn det svaret?
- Ledde det besöket till meningsfullt engagemang eller intÀkter?
FrÄn och med den 22 september 2026 förbÀttras förstapartsrapporteringen. Google Search Console tillhandahÄller en prestandarapport för generativ AI för AI Overviews och AI Mode, inklusive exponeringar, sidor, lÀnder, enheter och datum. Rapporten blev tillgÀnglig globalt den 31 augusti 2026. Googles rapport om generativ AI-prestanda (support.google.com)
Microsoft tillhandahÄller ocksÄ en AI Performance-rapport i Bing Webmaster Tools. Dess offentliga förhandsvisning rapporterar totala citeringar, citerade sidor, grundlÀggande frÄgor och citeringstrender över stödda Microsoft AI-upplevelser. Bing Webmaster Tools AI Performance (blogs.bing.com)
Dessa verktyg bör behandlas som auktoritativa för sina egna ytor, men de skapar inte ett komplett mÀtsystem över olika assistenter. Den rekommenderade metoden Àr att kombinera förstapartrapporter, serverloggar, kontrollerade spÄrningslÀnkar, analysdataexporter och schemalagda observationer av svarspaneler.
1. Varför AI-hÀnvisningsmÀtning Àr svÄrt
Traditionell webbanalys Àr starkt beroende av Referer-huvudet och kampanjparametrar. BÄda kan saknas eller vara ofullstÀndiga.
HTTP Referer-huvudet kan innehÄlla föregÄende sida, endast den föregÄende webbplatsens ursprung, eller inget vÀrde alls. WebblÀsarpolicyer för hÀnvisare kan avsiktligt begrÀnsa eller ta bort den informationen. MDN Referrer-Policy dokumentation (developer.mozilla.org)
AI-system skapar ytterligare komplikationer:
- En assistent kan lÀnka till en sida utan att skicka en igenkÀnnbar hÀnvisare.
- En assistent kan anvÀnda en omdirigerande eller omskriven URL.
- En assistent kan hÀmta innehÄll serversidan innan ett svar visas.
- En AI-bot kan besöka en sida utan att nÄgon mÀnniska ser den.
- Svaret kan citera en kanonisk URL istÀllet för den URL som ursprungligen hÀmtades.
- Svaret kan Àndras beroende pÄ plats, enhet, sprÄk, tid eller personalisering.
Till exempel skiljer OpenAI mellan OAI-SearchBot för sökupptÀckt, GPTBot för möjlig trÀningsanvÀndning och ChatGPT-User för anvÀndarinitierade ÄtgÀrder. Perplexity identifierar separat PerplexityBot och Perplexity-User. Anthropic identifierar ClaudeBot, Claude-SearchBot och Claude-User. Dessa förfrÄgningar bör inte kombineras till ett generiskt "AI-trafik"-nummer. OpenAI bot-dokumentation (openai.com) Perplexity bot-dokumentation (docs.perplexity.ai) Anthropic bot-dokumentation (support.anthropic.com)
Den centrala regeln Àr:
Botaktivitet Àr inte hÀnvisningstrafik, citeringssynlighet Àr inte trafik, och trafik Àr inte affÀrspÄverkan.
2. Ramverket för mÀtning
AnvÀnd fem mÀtlager.
| Lager | HuvudfrÄga | Rekommenderad data |
|---|---|---|
| à tkomst | BegÀrde ett AI-system sidan? | Webbserverloggar, brandvÀggsloggar |
| Synlighet | Visades eller citerades sidan? | Google Search Console, Bing Webmaster Tools, observationer av svarspaneler |
| FörvÀrv | Kom en person frÄn en AI-yta? | HÀnvisare, kampanjparametrar, klick pÄ korta lÀnkar, analysdata |
| Engagemang | Interagerade besökaren meningsfullt? | AnalyshÀndelser, sidvisningar, engagemangstid |
| VÀrde | Ledde besöket till ett önskat resultat? | Registreringar, leads, köp, prenumerationer |
Rekommenderad kÀllhierarki för sanningen
AnvÀnd den starkaste tillgÀngliga signalen för varje frÄga:
- Förstapartplattformsrapporter för exponeringar och citeringar.
- Observationer av svarspaneler för synlighet pÄ prompt-nivÄ och jÀmförelser med konkurrenter.
- Omdirigeringsloggar för kontrollerade lÀnkklick.
- Webbanalys för sessioner, engagemang och konverteringar.
- Serverloggar för bot- och hÀmtningsaktivitet.
- Modellerade uppskattningar endast nÀr direkta bevis saknas.
Kombinera inte alla signaler till ett enda nummer utan att bevara kÀlla och konfidensnivÄ.
3. GrundlÀggande datamodell
Skapa ett datalager med separata tabeller för rÄa observationer och hÀrledda mÄtt.
panel_runs
En rad för varje prompt- och ytkontroll.
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
En rad för varje lÀnk som observerats i en svarspanel.
text run_id surface topic prompt_id cited_url cited_domain citation_position link_scope target_domain target_cited
ai_crawler_requests
Aggregerad serverloggdata.
text date bot_name bot_role path requests successful_requests error_requests unique_ip_count
shortener_clicks
En rad för varje kontrollerad lÀnkbegÀran.
text click_id occurred_at slug surface topic prompt_id asset_id referrer_host user_agent_family ip_hmac destination_path
ai_sessions
En normaliserad analysdata-tabell.
text session_id date surface topic prompt_id landing_page attribution_method confidence engaged engagement_time_seconds key_event revenue
confidence-fÀltet Àr viktigt. Rekommenderade vÀrden Àr:
- A â Direkt: kontrollerat klick pĂ„ kortlĂ€nk eller betrodd kampanjparameter.
- B â Stark: kĂ€nd assistent-hĂ€nvisare eller verifierad anvĂ€ndar-hĂ€mtningsförfrĂ„gan.
- C â Sannolik: bevis frĂ„n kampanj eller mĂ„lsida utan en komplett hĂ€nvisare.
- D â Modellerad: hĂ€rledd frĂ„n tidpunkt, panelobservationer eller aggregerade förĂ€ndringar.
Rapportera A och B separat frÄn C och D.
4. Metrikdefinitioner
Citeringsandel per Àmne
Citeringsandel per Àmne mÀter hur ofta en mÄldomÀn förekommer bland de kÀllor som observerats för ett Àmne.
\[ \text{Citeringsandel}_{Ă€mne}
\frac{\text{Antal citeringar av mÄldomÀnen}} {\text{Alla citeringsförekomster}} \]
Exempel:
- 100 totala citeringar observerade för "programvara för projekthantering"
- 12 citeringar pekar pÄ din domÀn
- Citeringsandel per Àmne = 12%
Detta mÄtt mÀter kÀllandel, inte om ett svar innehöll din webbplats minst en gÄng.
Citeringsomfattning
AnvÀnd ett andra mÄtt för nÀrvaro pÄ svarsnivÄ:
\[ \text{Citeringsomfattning}_{Ă€mne}
\frac{\text{Antal svar som innehÄller en mÄlciteraring}} {\text{Antal svar dÀr en panel fanns}} \]
Citeringsomfattning Àr ofta lÀttare för icke-tekniska mÄlgrupper att förstÄ:
"VÄr webbplats syntes i 34% av observerade AI-svar för detta Àmne."
Citeringsposition
Registrera positionen för en citering inom svaret:
1= första observerade kÀlla2= andra observerade kÀlla3= tredje observerade kÀlla
Eftersom AI-grÀnssnitt ofta grupperar eller omordnar kÀllor, behandla position som en beskrivande observation, inte en universell ranking.
AssistenthÀnvisningsfrekvens
AssistenthÀnvisningsfrekvens mÀter andelen mÀnskliga webbplatssessioner som kan tillskrivas en AI-yta.
\[ \text{AssistenthÀnvisningsfrekvens}
\frac{\text{AI-tillskrivna mÀnskliga sessioner}} {\text{Alla mÀnskliga sessioner}} \]
Rapportera minst tvÄ versioner:
text BekrÀftad AssistenthÀnvisningsfrekvens = A-konfidens AI-sessioner / alla mÀnskliga sessioner
Utökad AssistenthÀnvisningsfrekvens = A + B + C-konfidens AI-sessioner / alla mÀnskliga sessioner
Den bekrÀftade versionen Àr mer konservativ. Den utökade versionen Àr anvÀndbar för trendanalys men bör inte presenteras som fullt observerad trafik.
AnvÀnd inte panelkörningar som exponeringar
Om du skrapar 500 prompter betyder det inte att 500 personer sÄg dessa svar. Din skrapare mÀter ett kontrollerat urval av svarstillstÄnd. AnvÀnd förstapartplattforms exponeringrapporter nÀr de Àr tillgÀngliga.
Engagemang efter citering
Engagemang efter citering mÀter om personer som kom via en AI-citering utförde en meningsfull ÄtgÀrd.
\[ \text{Engagemang efter citering}
\frac{\text{Engagerade AI-sessioner}} {\text{AI-tillskrivna sessioner}} \]
För Google Analytics 4 varar en engagerad session lÀngre Àn 10 sekunder, inkluderar en nyckelhÀndelse, eller inkluderar minst tvÄ sid- eller skÀrmvisningar. Google Analytics dokumentation om engagemangsfrekvens (support.google.com)
Rapportera ocksÄ:
text Engagemangstid efter citering = median engagerade sekunder per AI-session
Konverteringsfrekvens efter citering = AI-sessioner med en nyckelhÀndelse / AI-tillskrivna sessioner
IntÀkt per session efter citering = intÀkt frÄn AI-sessioner / AI-tillskrivna sessioner
Median engagemangstid Àr vanligtvis stabilare Àn genomsnittlig engagemangstid eftersom ett litet antal mycket lÄnga sessioner kan förvrÀnga genomsnittet.
Ytterligare anvÀndbara mÀtvÀrden
AI-botars framgÄngsfrekvens
\[ \text{Botars framgÄngsfrekvens}
\frac{\text{AI-botbegÀranden som returnerar 2xx eller 3xx}} {\text{AI-botbegÀranden}} \]
Bryt ner detta per bot och sökvÀg. En hög citeringsminskning i kombination med en ökande bot-felprocent kan indikera ett problem med hosting, brandvÀgg eller robots-konfiguration.
Citerings-till-klick-frekvens
För kontrollerade svarspanelobservationer:
\[ \text{Observerad Citerings-till-klick-frekvens}
\frac{\text{BekrÀftade klick pÄ kortlÀnk}} {\text{Observerade mÄlcitationer}} \]
Detta Àr inte en sann anvÀndares klickfrekvens om inte nÀmnaren representerar faktiska anvÀndarexponeringar. MÀrk det tydligt som ett observerat operationellt mÄtt.
Citeringsaktualitet
MĂ€t hur ofta den citerade URL:en speglar den aktuella sidversionen.
text Citeringsaktualitet = citerade sidor med nuvarande innehÄllshash / citerade sidor
Detta Àr anvÀndbart för dokumentation, prissÀttning, produkter och policysidor som ofta Àndras.
5. Serverlogganalys
Konfigurera strukturerade Ätkomstloggar
Strukturerade loggar Àr lÀttare att analysera Àn traditionella mellanslagseparerade loggar.
Exempel pÄ Nginx-konfiguration:
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;
Lagr loggfilerna pÄ en begrÀnsad plats. Skicka inte rÄa IP-adresser till en generell analysdata-tabell om du inte har en tydlig operativ anledning.
Identifiera AI-förfrÄgningstyper
Separera minst:
text OpenAI:
- OAI-SearchBot
- ChatGPT-User
- GPTBot
Anthropic:
- Claude-SearchBot
- Claude-User
- ClaudeBot
Perplexity:
- PerplexityBot
- Perplexity-User
Sökmotorer:
- Googlebot
- Bingbot
Googles AI Search-funktioner anvÀnder generellt befintliga Google Search-genomsökningskontroller snarare Àn en separat universell "AI Overview bot." Google anger att Googlebot kontrollerar Ätkomst för Sök, inklusive AI-funktioner. Googles AI-funktioner och webbplatser (developers.google.com)
User-agent-strÀngar kan förfalskas. För operationella beslut, kombinera user-agent-matchning med leverantörspublicerade IP-intervall eller verifierad omvÀnd DNS dÀr tillgÀngligt. OpenAI och Perplexity publicerar IP-information för sina relevanta botar och hÀmtare. (openai.com)
Reproducerbar loggsammanfattare
Spara följande som 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])
Kör den med:
bash HMAC_SECRET='a-long-random-secret' \ python ai_log_summary.py /var/log/nginx/ai-access.jsonl \
data/ai_crawler_daily.csv
Detta ger operationella bevis om hÀmtningsaktivitet, inte mÀnsklig trafik.
6. Anpassade lÀnkkortare för AI-riktade sammanfattningar
En förstaparts-lÀnkkortare tillhandahÄller en kontrollerad mÀtpunkt nÀr du publicerar sammanfattningar eller kÀllmaterial som kan refereras av AI-system.
AnvÀnd en domÀn du kontrollerar, till exempel:
text https://go.example.com/ai-docs
Destinationen bör inkludera kampanjparametrar som:
text utm_source=perplexity utm_medium=ai_answer utm_campaign=product_documentation utm_content=pricing_summary_v3
Google rekommenderar kampanjparametrar för att identifiera trafikkÀllor och kampanjer. Google Analytics kampanj-URL-dokumentation (support.google.com)
Rekommenderade lÀnkfÀlt
text surface topic prompt_id asset_id content_version destination_path
LÀgg inte in personlig information, e-postadresser eller anvÀndares sökfrÄgor i lÀnken. Kampanjidentifierare bör beskriva den publicerade tillgÄngen, inte identifiera en person.
Viktiga begrÀnsningar
En kort lÀnk Àr ett kalibreringsverktyg, inte en universell lösning:
- Assistenten kan skriva om destinations-URL:en.
- Assistenten kan istÀllet citera den kanoniska URL:en.
- LĂ€nken kan visas men aldrig klickas.
- En webblÀsare eller integritetsverktyg kan ta bort hÀnvisaren.
- Vissa system kan hÀmta destinationen serversidan.
- En kort lÀnk mÀter inte anvÀndare som minns varumÀrket och ÄtervÀnder senare.
AnvÀnd inte en kortare för att dölja destinationen, kringgÄ sÀkerhetssystem eller manipulera citeringar. AnvÀnd en tydlig omdirigering till en relevant, kanonisk sida.
Minimal förstaparts-lÀnkkortare
Följande exempel anvÀnder FastAPI och SQLite. Skydda lÀnkskapande endpoint med autentisering innan den anvÀnds i produktion.
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)
Kör den med:
bash pip install fastapi uvicorn uvicorn shortener:app --host 0.0.0.0 --port 8080
AnvÀnd en 302-omdirigering för mÀtlÀnkar sÄ att webblÀsare och mellansystem Àr mindre benÀgna att cachelagra omdirigeringen permanent.
7. Schemalagd skrapning av AI-svarspaneler
Panel-skrapning Àr anvÀndbart för att besvara frÄgor som förstapartanalyser kanske inte exponerar:
- Vilka konkurrerande kÀllor citeras?
- Vilken sida citeras för en specifik prompt?
- Ăndras svaret beroende pĂ„ land eller enhet?
- Förekommer citeringen i svaret, kÀlllistan eller bÄda?
- Hur stabilt Àr svaret över tid?
- Citerar assistenten en kort lÀnk eller den kanoniska URL:en?
Skrapning mÄste dock göras försiktigt.
Regler för ansvarsfull insamling
- Föredra ett officiellt API, licensierad sökdataleverantör eller förstapartsrapport.
- Granska leverantörens villkor innan du automatiserar förfrÄgningar.
- Respektera robots-instruktioner dÀr det Àr tillÀmpligt.
- KringgÄ inte CAPTCHA:s, inloggningshinder, hastighetsbegrÀnsningar eller bot-skydd.
- AnvÀnd en liten, fast promptuppsÀttning.
- Kör med lÄg frekvens.
- Spara svaret och tidsstÀmpeln sÄ att resultaten kan granskas.
- Stoppa nÀr en leverantör returnerar en utmaning eller ett nekad Ätkomst-svar.
Protokoll för exkludering av robotar Àr en preferensmekanism för botar, inte ett sÀkerhetssystem. Det auktoriserar inte Ätkomst till skyddat innehÄll. RFC 9309: Protokoll för exkludering av robotar (rfc-editor.org)
Bygg ett prompt-manifest
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
HÄll prompterna stabila. Om prompterna Àndras blir trendjÀmförelser mindre tillförlitliga.
Lagra Àven:
- land
- sprÄk
- enhetstyp
- webblÀsarversion
- promptversion
- innehÄllsversion
- insamlingstid i UTC
Google noterar att AI Overview och AI Mode-svar kan variera eftersom de anvÀnder olika system och kan anvÀnda relaterade sökningar eller "query fan-out." Googles AI-funktioner och webbplatsdokumentation (developers.google.com)
Playwright insamlingsskript
Följande skript Àr ett allmÀnt forskningsverktyg. Det registrerar den renderade sidan, skÀrmbilden, sidlÀnkar och svarstext. Det kringgÄr avsiktligt inte utmaningar eller försöker besegra Ätkomstkontroller.
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) + "\
") output.flush()
# Keep request volume low.
await asyncio.sleep(8)
await browser.close()
if name == "main": asyncio.run(main("prompts.csv"))
Installera och kör:
bash pip install playwright playwright install chromium python scrape_panels.py
SchemalÀgg det med ett lÄs sÄ att tvÄ körningar inte kan överlappa varandra:
cron 15 3 * * * flock -n /tmp/ai-panel.lock \ /usr/bin/python3 /opt/ai-measure/scrape_panels.py
FörbÀttra noggrannheten med yt-adaptrar
Fallbacks-skriptet registrerar lÀnkar pÄ sidnivÄ. Detta Àr anvÀndbart för upptÀckt men kan inkludera vanliga sökresultatlÀnkar. För produktionsanvÀndning:
- AnvÀnd ett licensierat svarspanel-API nÀr det Àr tillgÀngligt.
- Skapa en separat vÀljar-adapter för varje yta.
- Lagra vÀljarversionen.
- Lagra rÄ HTML och en skÀrmbild.
- Markera varje post med
link_scope = "panel_selector"ellerlink_scope = "page_fallback".
BerÀkna endast citeringsandel frÄn panel_selector-data nÀr du pÄ ett tillförlitligt sÀtt kan identifiera panelen. HÄll fallback-observationer i en separat utforskande rapport.
8. Google Analytics och BigQuery-integration
Google Analytics 4 kan exportera rÄa hÀndelsedata till BigQuery. Dagliga exporter ger stabila tabeller som events_YYYYMMDD; streamingexporter Àr anvÀndbara för nÀra realtidsövervakning men kan vara ofullstÀndiga tills den dagliga tabellen Àr fÀrdigstÀlld. Google Analytics BigQuery Export (support.google.com)
En förenklad sessionsfrÄga kan se ut sÄ hÀr:
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;
För en mogen implementering, lÀgg till surface, topic och prompt_id till kampanjstrukturen eller koppla dem frÄn klick-tabellen för lÀnkkortaren.
Skapa inte en dimension med hög kardinalitet för varje enskild anvÀndare eller session. Google varnar för att alltför mÄnga anpassade dimensioner med hög kardinalitet kan minska rapporteringskvaliteten. Google Analytics anpassade dimensioner och mÀtvÀrden (support.google.com)
9. Dashboard-design
En praktisk instrumentpanel bör innehÄlla fem vyer.
Sammanfattning för ledningen
Visa:
- Citeringsandel per Àmne
- Citeringsomfattning per Àmne
- BekrÀftad assistenthÀnvisningsfrekvens
- Utökad assistenthÀnvisningsfrekvens
- Engagemang efter citering
- AI-botars framgÄngsfrekvens
- AI-tillskrivna nyckelhÀndelser eller intÀkter
Trend för citeringsandel
Linjediagram:
text Datum Ă Ămne Ă Yta
Visa bÄda:
- förekomster av citeringar av mÄldomÀnen
- alla observerade citeringsförekomster
Assistenttrafiktratt
text Observerade citeringar â Klick pĂ„ kontrollerade lĂ€nkar â AI-tillskrivna sessioner â Engagerade AI-sessioner â NyckelhĂ€ndelser
Varje steg bör visa sin attribution-konfidens.
Bot-status
Tabell per bot:
text bot_name requests successful_requests error_rate unique_ip_count top_paths
Varna nÀr:
- felprocenten överstiger 2 procent
- en bot plötsligt försvinner
- en tidigare tillgÀnglig innehÄllssektion returnerar 403 eller 429
- bot-förfrÄgningar koncentreras till en oavsiktlig privat sökvÀg
Tabell för innehÄllsmöjligheter
Rankningssidor och Àmnen efter:
text hög citeringsandel + lÄg hÀnvisningsfrekvens höga exponeringar + lÄg citeringsomfattning högt engagemang + lÄg citeringssynlighet höga botfel + hög affÀrsmÀssiga betydelse
Dessa kombinationer ger vanligtvis bÀttre beslut Àn att bara titta pÄ trafikvolym.
Minimal Streamlit-instrumentpanel
Spara som 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) )
Kör den med:
bash pip install streamlit pandas streamlit run dashboard.py
För produktion, ladda normaliserade tabeller till DuckDB, BigQuery, Snowflake eller PostgreSQL istÀllet för att lÀsa rÄa filer direkt.
10. Kvalitetskontroller
Ett mÀtsystem kan producera exakt utseende men missvisande siffror. LÀgg till dessa kontroller.
Separera botar frÄn mÀnniskor
RÀkna aldrig OAI-SearchBot, GPTBot, ClaudeBot eller PerplexityBot som mÀnskliga sessioner. Dessa Àr signaler frÄn botar eller hÀmtningar.
Bevara rÄa bevis
För varje panelkörning, behÄll:
- rÄ HTML
- skÀrmbild
- svarstext
- tidsstÀmpel
- webblÀsar- och samlarversion
- prompt och lokalisering
- svarshash
Detta gör det möjligt att förklara varför ett mÄtt Àndrades.
SpÄra panelens frÄnvaro
En saknad AI-panel Àr inte samma sak som ett svar utan din citering.
AnvÀnd:
text panel_not_present panel_present_no_target panel_present_target_cited blocked_or_challenged collection_error
Kontrollera samplingsbias
En schemalagd skrapning pÄverkas av:
- plats
- enhet
- sprÄk
- tid
- personalisering
- webblÀsarstatus
- promptens formulering
AnvÀnd en fast kÀrnuppsÀttning av prompter och en roterande uppsÀttning utforskande prompter. HÄll de tvÄ grupperna separata i rapporter.
Undvik falsk precision
Rapportera konfidensintervall eller minsta sampelstorlekar för smÄ Àmnesgrupper. Ett Àmne med tre observationer bör inte jÀmföras direkt med ett Àmne som innehÄller 300 observationer.
JÀmför lika med lika
NÀr du jÀmför ytor, hÄll konstant:
- prompt
- lokalisering
- land
- enhet
- insamlingstimme
- regler för mÄldomÀn
- version av citeringsparser
11. Rekommenderad utrullningsplan
Vecka 1: Instrumentering
- Konfigurera strukturerade serverloggar.
- Skapa botregistret.
- Skapa lÀnkkortare-domÀnen.
- LÀgg till regler för kampanjnamngivning.
- Definiera Àmnestaxonomin.
- Aktivera Google Analytics 4 BigQuery-export.
Vecka 2: Kontrollerade lÀnkar
- Publicera fem till tio spÄrade sammanfattningar.
- Skapa lÀnkar för olika Àmnen och innehÄllsversioner.
- Validera omdirigeringsloggar mot analyssessioner.
- BekrÀfta att ingen personlig information hamnar i URL:er eller analysparametrar.
Vecka 3: Panelobservationer
- Skapa ett prompt-manifest.
- Börja med tvÄ ytor och 20 till 50 stabila prompter.
- Kör en gÄng per dag.
- Spara skÀrmbilder och HTML.
- MÀt insamlingsfel separat frÄn citeringens frÄnvaro.
Vecka 4: Instrumentpanel och styrning
- Bygg instrumentpaneler för Àmnen och ytor.
- LĂ€gg till bot-statusvarningar.
- Definiera konfidensregler.
- JÀmför förstapartrapporter med panelobservationer.
- Dokumentera policyer för lagring, Ätkomst och radering.
Slutsats
AI-citeringsmÀtning bör byggas som ett fler-signalstillskrivningssystem, inte som en enkel hÀnvisningsrapport.
Den mest tillförlitliga ramen kombinerar:
- Googles och Bings förstapartsrapporter för synlighet
- serverloggar för bot- och hÀmtningsaktivitet
- förstaparts-kortlÀnkar för kontrollerad klickmÀtning
- schemalagda, kompatibla observationer av svarspaneler
- analys- och konverteringsdata för nedströmsvÀrde
De tre viktigaste mÀtvÀrdena Àr:
- Citeringsandel per Ă€mne â hur stor del av det observerade kĂ€llutrymmet som tillhör din domĂ€n.
- AssistenthĂ€nvisningsfrekvens â hur mycket mĂ€nsklig trafik som kan tillskrivas AI-ytor.
- Engagemang efter citering â om AI-hĂ€nvisade besökare utför meningsfulla Ă„tgĂ€rder.
Systemet blir anvÀndbart nÀr varje nummer inkluderar dess yta, tidsperiod, nÀmnare, attribueringsmetod och konfidensnivÄ. Denna disciplin förhindrar att bot-trÀffar misstas för anvÀndare, att skrapade svar misstas för exponeringar och att citeringar misstas för affÀrsresultat.
Auto