Medindo o Impacto das Citações de IA: Análise, Registros e Raspagens de Painéis
Introdução
A descoberta impulsionada por IA está agora dividida em várias camadas de medição. Uma pessoa pode ver uma empresa citada numa resposta de IA, clicar num link sem um referrer útil, visitar através de um navegador que remove dados de campanha, ou ler uma resposta sem clicar em nada. Enquanto isso, um crawler de IA pode solicitar a página da empresa sem representar um visitante humano.
Um sistema de medição útil deve, portanto, separar quatro questões:
- O site foi rastreado ou recuperado?
- O site foi citado numa resposta de IA?
- Uma pessoa visitou o site a partir dessa resposta?
- Essa visita gerou engajamento ou receita significativos?
A partir de 22 de setembro de 2026, os relatórios primários estão a melhorar. O Google Search Console fornece um relatório de desempenho de IA Generativa para AI Overviews e AI Mode, incluindo impressões, páginas, países, dispositivos e datas. O relatório ficou disponível mundialmente em 31 de agosto de 2026. Relatório de desempenho de IA Generativa do Google (support.google.com)
A Microsoft também fornece um relatório de Desempenho de IA nas Ferramentas para Webmasters do Bing. A sua pré-visualização pública relata o total de citações, páginas citadas, grounding queries e tendências de citação em todas as experiências de IA da Microsoft suportadas. Desempenho de IA das Ferramentas para Webmasters do Bing (blogs.bing.com)
Essas ferramentas devem ser tratadas como autoritárias para as suas próprias superfícies, mas não criam um sistema de medição completo entre assistentes. A abordagem recomendada é combinar relatórios primários, registros de servidor, links de rastreamento controlados, exportações de análise e observações programadas de painéis de resposta.
1. Por Que a Medição de Referência de IA é Difícil
A análise web tradicional depende fortemente do cabeçalho Referer e dos parâmetros de campanha. Ambos podem estar ausentes ou incompletos.
O cabeçalho HTTP Referer pode conter a página anterior, apenas a origem do site anterior, ou nenhum valor. As políticas de referrer do navegador podem intencionalmente limitar ou remover essa informação. Documentação Referrer-Policy da MDN (developer.mozilla.org)
Os sistemas de IA criam complicações adicionais:
- Um assistente pode linkar para uma página sem enviar um referrer reconhecível.
- Um assistente pode usar um URL de redirecionamento ou reescrito.
- Um assistente pode buscar conteúdo no lado do servidor antes de exibir uma resposta.
- Um crawler de IA pode visitar uma página sem que nenhum humano a veja.
- A resposta pode citar um URL canónico em vez do URL que foi originalmente recuperado.
- A resposta pode mudar dependendo da localização, dispositivo, idioma, tempo ou personalização.
Por exemplo, a OpenAI distingue entre OAI-SearchBot para descoberta de pesquisa, GPTBot para possível uso de treinamento e ChatGPT-User para ações iniciadas pelo utilizador. A Perplexity identifica separadamente PerplexityBot e Perplexity-User. A Anthropic identifica ClaudeBot, Claude-SearchBot e Claude-User. Essas solicitações não devem ser combinadas num único número genérico de “tráfego de IA”. Documentação do crawler da OpenAI (openai.com) Documentação do crawler da Perplexity (docs.perplexity.ai) Documentação do crawler da Anthropic (support.anthropic.com)
A regra central é:
A atividade de crawler não é tráfego de referência, a visibilidade de citação não é tráfego, e tráfego não é impacto nos negócios.
2. O Framework de Medição
Use cinco camadas de medição.
| Camada | Pergunta principal | Dados recomendados |
|---|---|---|
| Acesso | Um sistema de IA solicitou a página? | Registros do servidor web, registros do firewall |
| Visibilidade | A página foi mostrada ou citada? | Google Search Console, Bing Webmaster Tools, observações do painel de resposta |
| Aquisição | Uma pessoa chegou de uma superfície de IA? | Referrer, parâmetros de campanha, cliques em links curtos, análise |
| Engajamento | O visitante interagiu de forma significativa? | Eventos de análise, visualizações de página, tempo de engajamento |
| Valor | A visita produziu um resultado desejado? | Inscrições, leads, compras, assinaturas |
Hierarquia de fonte de verdade recomendada
Use o sinal mais forte disponível para cada pergunta:
- Relatórios primários da plataforma para impressões e citações.
- Observações de painéis de resposta para visibilidade a nível de prompt e comparações competitivas.
- Registros de redirecionamento para cliques em links controlados.
- Análise web para sessões, engajamento e conversões.
- Registros de servidor para atividade de crawler e fetcher.
- Estimativas modeladas apenas quando a evidência direta não estiver disponível.
Não combine todos os sinais num único número sem preservar a fonte e o nível de confiança.
3. Modelo de Dados Central
Crie um warehouse com tabelas separadas para observações brutas e métricas derivadas.
panel_runs
Uma linha para cada prompt e verificação de superfície.
text run_id observed_at surface topic prompt_id query_text locale country device pane_present pane_type answer_hash raw_html_path screenshot_path collector_version status
panel_citations
Uma linha para cada link observado num painel de resposta.
text run_id surface topic prompt_id cited_url cited_domain citation_position link_scope target_domain target_cited
ai_crawler_requests
Dados agregados de registros de servidor.
text date bot_name bot_role path requests successful_requests error_requests unique_ip_count
shortener_clicks
Uma linha para cada solicitação de link controlado.
text click_id occurred_at slug surface topic prompt_id asset_id referrer_host user_agent_family ip_hmac destination_path
ai_sessions
Uma tabela de análise normalizada.
text session_id date surface topic prompt_id landing_page attribution_method confidence engaged engagement_time_seconds key_event revenue
O campo confidence é importante. Os valores recomendados são:
- A — Direto: clique em link curto controlado ou parâmetro de campanha confiável.
- B — Forte: referrer de assistente conhecido ou solicitação de busca de utilizador verificada.
- C — Provável: evidência de campanha ou página de destino sem um referrer completo.
- D — Modelado: inferido a partir de tempo, observações de painel ou alterações agregadas.
Reporte A e B separadamente de C e D.
4. Definições de Métricas
Quota de Citação por Tópico
A Quota de Citação por Tópico mede a frequência com que um domínio alvo aparece entre as fontes observadas para um tópico.
[ \text{Citation Share}_{topic}
\frac{\text{Target-domain citation occurrences}} {\text{All citation occurrences}} ]
Exemplo:
- 100 citações totais observadas para “software de gestão de projetos”
- 12 citações apontam para o seu domínio
- Quota de Citação por Tópico = 12%
Esta métrica mede a quota de fonte, não se uma resposta continha o seu site pelo menos uma vez.
Cobertura de Citação
Use uma segunda métrica para a presença a nível de resposta:
[ \text{Citation Coverage}_{topic}
\frac{\text{Answer runs containing a target citation}} {\text{Answer runs where a panel was present}} ]
A Cobertura de Citação é frequentemente mais fácil de entender para públicos não técnicos:
“O nosso site apareceu em 34% das respostas de IA observadas para este tópico.”
Posição da Citação
Registre a posição de uma citação dentro da resposta:
1= primeira fonte observada2= segunda fonte observada3= terceira fonte observada
Como as interfaces de IA frequentemente agrupam ou reordenam as fontes, trate a posição como uma observação descritiva, não como um ranking universal.
Taxa de Referência do Assistente
A Taxa de Referência do Assistente mede a proporção de sessões humanas no site que podem ser atribuídas a uma superfície de IA.
[ \text{Assistant Referral Rate}
\frac{\text{AI-attributed human sessions}} {\text{All human sessions}} ]
Reporte pelo menos duas versões:
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
A versão confirmada é mais conservadora. A versão expandida é útil para análise de tendências, mas não deve ser apresentada como tráfego totalmente observado.
Não use execuções de painel como impressões
Se você raspar 500 prompts, isso não significa que 500 pessoas viram essas respostas. O seu scraper mede uma amostra controlada de estados de resposta. Use relatórios de impressões da plataforma primária quando disponíveis.
Engajamento Pós-Citação
O Engajamento Pós-Citação mede se as pessoas que chegaram através de uma citação de IA realizaram uma ação significativa.
[ \text{Post-Citation Engagement}
\frac{\text{Engaged AI sessions}} {\text{AI-attributed sessions}} ]
Para o Google Analytics 4, uma sessão engajada dura mais de 10 segundos, inclui um evento chave ou inclui pelo menos duas visualizações de página ou tela. Documentação da taxa de engajamento do Google Analytics (support.google.com)
Reporte também:
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
O tempo médio de engajamento (mediana) é geralmente mais estável do que o tempo médio de engajamento (média), porque um pequeno número de sessões muito longas pode distorcer a média.
Métricas Úteis Adicionais
Taxa de Sucesso do Crawler de IA
[ \text{Crawler Success Rate}
\frac{\text{AI crawler requests returning 2xx or 3xx}} {\text{AI crawler requests}} ]
Divida isso por bot e caminho. Um alto declínio de citação combinado com uma taxa crescente de erros do crawler pode indicar um problema de hospedagem, firewall ou configuração de robots.
Taxa de Citação para Clique
Para observações controladas de painel de resposta:
[ \text{Observed Citation-to-Click Rate}
\frac{\text{Confirmed short-link clicks}} {\text{Observed target citations}} ]
Esta não é uma taxa de cliques de utilizador verdadeira, a menos que o denominador represente impressões reais do utilizador. Rotule-a claramente como uma métrica operacional observada.
Atualidade da Citação
Meça a frequência com que o URL citado reflete a versão atual da página.
text Citation Freshness = cited pages with current content hash / cited pages
Isso é útil para páginas de documentação, preços, produtos e políticas que mudam frequentemente.
5. Análise de Registros de Servidor
Configurar Registros de Acesso Estruturados
Os registros estruturados são mais fáceis de analisar do que os registros tradicionais separados por espaços.
Exemplo de configuração 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;
Armazene os arquivos de registro num local restrito. Não envie endereços IP brutos para uma tabela de análise geral, a menos que tenha uma razão operacional clara.
Identificar Tipos de Solicitações de IA
No mínimo, separe:
text OpenAI:
- OAI-SearchBot
- ChatGPT-User
- GPTBot
Anthropic:
- Claude-SearchBot
- Claude-User
- ClaudeBot
Perplexity:
- PerplexityBot
- Perplexity-User
Search engines:
- Googlebot
- Bingbot
As funcionalidades de Pesquisa de IA do Google geralmente usam os controles de crawling existentes do Google Search, em vez de um "bot de Visão Geral de IA" universal separado. O Google afirma que o Googlebot controla o acesso para a Pesquisa, incluindo as funcionalidades de IA. Funcionalidades de IA e sites do Google (developers.google.com)
As strings de user-agent podem ser falsificadas. Para decisões operacionais, combine a correspondência de user-agent com os intervalos de IP publicados pelo provedor ou DNS reverso verificado, quando disponível. OpenAI e Perplexity publicam informações de IP para os seus crawlers e fetchers relevantes. (openai.com)
Sumarizador de Registros Reproduzível
Salve o seguinte como 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])
Execute com:
bash
HMAC_SECRET='a-long-random-secret'
python ai_log_summary.py /var/log/nginx/ai-access.jsonl \
data/ai_crawler_daily.csv
Isso produz evidências operacionais sobre a atividade de recuperação, não tráfego humano.
6. Encurtadores de Link Personalizados para Resumos Voltados para IA
Um encurtador de links primário oferece um ponto de medição controlado ao publicar resumos ou materiais de origem que podem ser referenciados por sistemas de IA.
Use um domínio que você controle, como:
text https://go.example.com/ai-docs
O destino deve incluir parâmetros de campanha como:
text utm_source=perplexity utm_medium=ai_answer utm_campaign=product_documentation utm_content=pricing_summary_v3
O Google recomenda parâmetros de campanha para identificar fontes de tráfego e campanhas. Documentação de URL de campanha do Google Analytics (support.google.com)
Campos de link recomendados
text surface topic prompt_id asset_id content_version destination_path
Não coloque informações pessoais, endereços de e-mail ou consultas de pesquisa de utilizadores no link. Os identificadores de campanha devem descrever o ativo publicado, não identificar uma pessoa.
Limitações importantes
Um link curto é um dispositivo de calibração, não uma solução universal:
- O assistente pode reescrever o URL de destino.
- O assistente pode citar o URL canónico em vez disso.
- O link pode ser exibido, mas nunca clicado.
- Um navegador ou ferramenta de privacidade pode remover o referrer.
- Alguns sistemas podem buscar o destino no lado do servidor.
- Um link curto não mede utilizadores que se lembram da marca e retornam mais tarde.
Não use um encurtador para ocultar o destino, contornar sistemas de segurança ou manipular citações. Use um redirecionamento claro para uma página relevante e canónica.
Encurtador de Link Primário Mínimo
O exemplo a seguir usa FastAPI e SQLite. Proteja o endpoint de criação de link com autenticação antes de usá-lo em produção.
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)
Execute com:
bash pip install fastapi uvicorn uvicorn shortener:app --host 0.0.0.0 --port 8080
Use um redirecionamento 302 para links de medição, para que os navegadores e sistemas intermediários tenham menos probabilidade de armazenar o redirecionamento em cache permanentemente.
7. Raspagem Agendada de Painéis de Respostas de IA
A raspagem de painéis é útil para responder a perguntas que a análise primária pode não expor:
- Quais fontes concorrentes são citadas?
- Qual página é citada para um prompt específico?
- A resposta muda por país ou dispositivo?
- A citação aparece na resposta, na lista de fontes ou em ambos?
- Quão estável é a resposta ao longo do tempo?
- O assistente cita um link curto ou o URL canónico?
No entanto, a raspagem deve ser feita com cuidado.
Regras para coleta responsável
- Prefira uma API oficial, um provedor de dados de pesquisa licenciado ou um relatório primário.
- Revise os termos do provedor antes de automatizar as solicitações.
- Respeite as instruções de robots quando aplicável.
- Não ignore CAPTCHAs, barreiras de login, limites de taxa ou proteções contra bots.
- Use um conjunto pequeno e fixo de prompts.
- Execute com baixa frequência.
- Salve a resposta e o timestamp para que os resultados possam ser auditados.
- Pare quando um provedor retornar um desafio ou uma resposta de acesso negado.
O Protocolo de Exclusão de Robôs é um mecanismo de preferência de crawler, não um sistema de segurança. Não autoriza o acesso a conteúdo protegido. RFC 9309: Protocolo de Exclusão de Robôs (rfc-editor.org)
Criar um Manifesto de Prompts
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
Mantenha os prompts estáveis. Se os prompts mudarem, as comparações de tendências tornam-se menos confiáveis.
Armazene também:
- país
- idioma
- tipo de dispositivo
- versão do navegador
- versão do prompt
- versão do conteúdo
- tempo de coleta em UTC
O Google observa que as respostas do AI Overview e do AI Mode podem variar porque usam sistemas diferentes e podem usar pesquisas relacionadas ou “expansão de consulta”. Documentação das funcionalidades de IA e site do Google (developers.google.com)
Script de Coleta com Playwright
O script a seguir é uma ferramenta de pesquisa de propósito geral. Ele registra a página renderizada, a captura de tela, os links da página e o texto da resposta. Intencionalmente, não ignora desafios nem tenta burlar os controles de acesso.
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"))
Instalar e executar:
bash pip install playwright playwright install chromium python scrape_panels.py
Agende-o com um bloqueio para que duas execuções não se sobreponham:
cron
15 3 * * * flock -n /tmp/ai-panel.lock
/usr/bin/python3 /opt/ai-measure/scrape_panels.py
Melhorar a precisão com adaptadores de superfície
O script de fallback registra links a nível de página. Isso é útil para descoberta, mas pode incluir links de resultados de pesquisa comuns. Para uso em produção:
- Use uma API de painel de resposta licenciada quando disponível.
- Crie um adaptador de seletor separado para cada superfície.
- Armazene a versão do seletor.
- Armazene o HTML bruto e uma captura de tela.
- Marque cada registro com
link_scope = "panel_selector"oulink_scope = "page_fallback".
Calcule a Quota de Citação apenas a partir de dados de panel_selector quando puder identificar o painel de forma confiável. Mantenha as observações de fallback num relatório exploratório separado.
8. Integração Google Analytics e BigQuery
O Google Analytics 4 pode exportar dados de eventos brutos para o BigQuery. As exportações diárias fornecem tabelas estáveis como events_YYYYMMDD; as exportações de streaming são úteis para monitoramento quase em tempo real, mas podem estar incompletas até que a tabela diária seja finalizada. Exportação do Google Analytics para BigQuery (support.google.com)
Uma consulta de sessão simplificada pode parecer com isto:
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;
Para uma implementação madura, adicione surface, topic e prompt_id à estrutura da campanha ou junte-os da tabela de cliques do encurtador.
Não crie uma dimensão analítica de alta cardinalidade para cada utilizador ou sessão individual. O Google avisa que dimensões personalizadas de alta cardinalidade excessivas podem reduzir a qualidade dos relatórios. Dimensões e métricas personalizadas do Google Analytics (support.google.com)
9. Design do Dashboard
Um dashboard prático deve conter cinco visualizações.
Resumo Executivo
Exibir:
- Quota de Citação por Tópico
- Cobertura de Citação por Tópico
- Taxa de Referência Confirmada do Assistente
- Taxa de Referência Expandida do Assistente
- Engajamento Pós-Citação
- Taxa de sucesso do crawler de IA
- Eventos chave ou receita atribuídos à IA
Tendência da Quota de Citação
Gráfico de linha:
text Date × Topic × Surface
Mostre ambos:
- ocorrências de citação do domínio alvo
- todas as ocorrências de citação observadas
Funil de Tráfego do Assistente
text Observed citations ↓ Controlled-link clicks ↓ AI-attributed sessions ↓ Engaged AI sessions ↓ Key events
Cada etapa deve exibir a sua confiança de atribuição.
Saúde do Crawler
Tabela por bot:
text bot_name requests successful_requests error_rate unique_ip_count top_paths
Alerte quando:
- a taxa de erro excede 2 por cento
- um bot desaparece subitamente
- uma seção de conteúdo anteriormente acessível retorna 403 ou 429
- as solicitações do crawler se concentram num caminho privado não intencional
Tabela de Oportunidades de Conteúdo
Classifique páginas e tópicos por:
text high citation share + low referral rate high impressions + low citation coverage high engagement + low citation visibility high crawler errors + high business importance
Essas combinações geralmente produzem decisões melhores do que olhar apenas para o volume de tráfego.
Dashboard Minimalista com Streamlit
Salve como 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="Impacto das Citações de IA", 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("Dashboard de Impacto das Citações de IA")
if panels.empty: st.warning("Nenhuma observação de painel encontrada.") 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("Quota de Citação por Tópico") st.dataframe(citation_share, use_container_width=True) st.bar_chart( citation_share.set_index("topic")["citation_share"] )
with col2: st.subheader("Cobertura de Citação por Tópico") st.dataframe(coverage, use_container_width=True) st.bar_chart( coverage.set_index("topic")["citation_coverage"] )
st.subheader("Posição da Citação") 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("Observações Diárias") 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) )
Execute com:
bash pip install streamlit pandas streamlit run dashboard.py
Para produção, carregue tabelas normalizadas em DuckDB, BigQuery, Snowflake ou PostgreSQL, em vez de ler arquivos brutos diretamente.
10. Controles de Qualidade
Um sistema de medição pode produzir números que parecem precisos, mas são enganosos. Adicione estes controles.
Separe bots de pessoas
Nunca conte OAI-SearchBot, GPTBot, ClaudeBot ou PerplexityBot como sessões humanas. Estes são sinais de crawler ou recuperação.
Preserve a evidência bruta
Para cada execução de painel, mantenha:
- HTML bruto
- captura de tela
- texto da resposta
- timestamp
- versão do navegador e do coletor
- prompt e locale
- hash da resposta
Isso torna possível explicar por que uma métrica mudou.
Rastrear a ausência do painel
Um painel de IA ausente não é o mesmo que uma resposta sem a sua citação.
Use:
text pane_not_present pane_present_no_target pane_present_target_cited blocked_or_challenged collection_error
Controlar o viés de amostragem
Uma raspagem agendada é afetada por:
- localização
- dispositivo
- idioma
- tempo
- personalização
- estado do navegador
- redação do prompt
Use um conjunto fixo de prompts principais e um conjunto rotativo de prompts exploratórios. Mantenha os dois grupos separados nos relatórios.
Evite falsa precisão
Relate intervalos de confiança ou tamanhos mínimos de amostra para pequenos grupos de tópicos. Um tópico com três observações não deve ser comparado diretamente com um tópico contendo 300 observações.
Compare similar com similar
Ao comparar superfícies, mantenha constante:
- prompt
- locale
- país
- dispositivo
- hora de coleta
- regras de domínio alvo
- versão do parser de citação
11. Plano de Implementação Recomendado
Semana 1: Instrumentação
- Configure registros de servidor estruturados.
- Crie o registro de bots.
- Crie o domínio do encurtador.
- Adicione regras de nomenclatura de campanha.
- Defina a taxonomia de tópicos.
- Ative a exportação do Google Analytics 4 para BigQuery.
Semana 2: Links controlados
- Publique de cinco a dez resumos rastreados.
- Crie links para diferentes tópicos e versões de conteúdo.
- Valide os registros de redirecionamento em relação às sessões de análise.
- Confirme que nenhuma informação pessoal entra nos URLs ou parâmetros de análise.
Semana 3: Observações de painel
- Crie um manifesto de prompts.
- Comece com duas superfícies e 20 a 50 prompts estáveis.
- Execute uma vez por dia.
- Salve capturas de tela e HTML.
- Meça falhas de coleta separadamente da ausência de citação.
Semana 4: Dashboard e governança
- Crie dashboards de tópico e superfície.
- Adicione alertas de saúde do crawler.
- Defina regras de confiança.
- Compare relatórios primários com observações de painel.
- Documente políticas de retenção, acesso e exclusão.
Conclusão
A medição de citação de IA deve ser construída como um sistema de atribuição multi-sinal, não como um único relatório de referência.
O framework mais confiável combina:
- Relatórios de visibilidade primários do Google e Bing
- registros de servidor para atividade de crawler e fetcher
- links curtos primários para medição de cliques controlados
- observações programadas e conformes de painéis de resposta
- dados de análise e conversão para valor a jusante
As três métricas mais importantes são:
- Quota de Citação por Tópico — quanto do espaço de fonte observado pertence ao seu domínio.
- Taxa de Referência do Assistente — quanto tráfego humano pode ser atribuído a superfícies de IA.
- Engajamento Pós-Citação — se os visitantes referenciados por IA realizam ações significativas.
O sistema torna-se útil quando cada número inclui a sua superfície, período de tempo, denominador, método de atribuição e nível de confiança. Essa disciplina impede que acessos de crawler sejam confundidos com utilizadores, respostas raspadas com impressões, e citações com resultados de negócios.
Auto