AutoPodAutoPod

Medición del impacto de las citas de IA: analíticas, registros y raspado de paneles

•Lectura de 26 min
Artículo en audio
Medición del impacto de las citas de IA: analíticas, registros y raspado de paneles
0:00—:—
Medición del impacto de las citas de IA: analíticas, registros y raspado de paneles

Medición del impacto de las citas de IA: analíticas, registros y raspado de paneles

Introducción

El descubrimiento impulsado por la IA se divide ahora en varias capas de medición. Una persona puede ver una empresa citada en una respuesta de IA, hacer clic en un enlace sin un referente útil, visitar a través de un navegador que elimina los datos de la campaña o leer una respuesta sin hacer clic en absoluto. Mientras tanto, un rastreador de IA puede solicitar la página de la empresa sin representar a un visitante humano.

Por lo tanto, un sistema de medición útil debe separar cuatro preguntas:

  1. ¿Fue el sitio rastreado o recuperado?
  2. ¿Fue el sitio citado en una respuesta de IA?
  3. ¿Una persona visitó el sitio desde esa respuesta?
  4. ¿Esa visita produjo una interacción o ingresos significativos?

A partir del 22 de septiembre de 2026, los informes de primera parte están mejorando. Google Search Console proporciona un informe de rendimiento de IA Generativa para AI Overviews y AI Mode, que incluye impresiones, páginas, países, dispositivos y fechas. El informe estuvo disponible en todo el mundo el 31 de agosto de 2026. Informe de rendimiento de IA Generativa de Google (support.google.com)

Microsoft también ofrece un informe de Rendimiento de IA en Bing Webmaster Tools. Su vista previa pública informa las citas totales, las páginas citadas, las consultas de fundamento y las tendencias de citas en las experiencias de IA de Microsoft compatibles. Rendimiento de IA en Bing Webmaster Tools (blogs.bing.com)

Estas herramientas deben considerarse autorizadas para sus propias superficies, pero no crean un sistema de medición completo entre asistentes. El enfoque recomendado es combinar informes de primera parte, registros del servidor, enlaces de seguimiento controlados, exportaciones de analíticas y observaciones programadas de paneles de respuestas.


1. Por qué la medición de referencias de IA es difícil

Las analíticas web tradicionales dependen en gran medida del encabezado Referer y de los parámetros de campaña. Ambos pueden faltar o estar incompletos.

El encabezado HTTP Referer puede contener la página anterior, solo el origen del sitio web anterior, o ningún valor en absoluto. Las políticas de referencia del navegador pueden limitar o eliminar intencionadamente esa información. Documentación de Referrer-Policy de MDN (developer.mozilla.org)

Los sistemas de IA crean complicaciones adicionales:

  • Un asistente puede enlazar a una página sin enviar un referente reconocible.
  • Un asistente puede utilizar una URL de redirección o reescrita.
  • Un asistente puede obtener contenido del lado del servidor antes de mostrar una respuesta.
  • Un rastreador de IA puede visitar una página sin que ningún humano la vea.
  • La respuesta puede citar una URL canónica en lugar de la URL que se recuperó originalmente.
  • La respuesta puede cambiar según la ubicación, el dispositivo, el idioma, la hora o la personalización.

Por ejemplo, OpenAI distingue entre OAI-SearchBot para el descubrimiento de búsqueda, GPTBot para un posible uso en entrenamiento y ChatGPT-User para acciones iniciadas por el usuario. Perplexity identifica por separado a PerplexityBot y Perplexity-User. Anthropic identifica a ClaudeBot, Claude-SearchBot y Claude-User. Estas solicitudes no deben combinarse en un único número genérico de "tráfico de IA". Documentación del rastreador de OpenAI (openai.com) Documentación del rastreador de Perplexity (docs.perplexity.ai) Documentación del rastreador de Anthropic (support.anthropic.com)

La regla central es:

La actividad del rastreador no es tráfico de referencia, la visibilidad de las citas no es tráfico, y el tráfico no es impacto comercial.


2. El marco de medición

Utilice cinco capas de medición.

CapaPregunta principalDatos recomendados
Acceso¿Un sistema de IA solicitó la página?Registros del servidor web, registros del firewall
Visibilidad¿Se mostró o citó la página?Google Search Console, Bing Webmaster Tools, observaciones de paneles de respuestas
Adquisición¿Una persona llegó desde una superficie de IA?Referente, parámetros de campaña, clics en enlaces cortos, analíticas
Interacción¿El visitante interactuó significativamente?Eventos de analíticas, vistas de página, tiempo de interacción
Valor¿La visita produjo el resultado deseado?Registros, clientes potenciales, compras, suscripciones

Jerarquía de fuentes de verdad recomendada

Utilice la señal más fuerte disponible para cada pregunta:

  1. Informes de plataformas de primera parte para impresiones y citas.
  2. Observaciones de paneles de respuestas para visibilidad a nivel de prompt y comparaciones competitivas.
  3. Registros de redirección para clics en enlaces controlados.
  4. Analíticas web para sesiones, interacción y conversiones.
  5. Registros del servidor para actividad de rastreadores y recolectores.
  6. Estimaciones modeladas solo cuando la evidencia directa no está disponible.

No combine todas las señales en un solo número sin preservar la fuente y el nivel de confianza.


3. Modelo de datos central

Cree un almacén de datos con tablas separadas para observaciones en bruto y métricas derivadas.

panel_runs

Una fila por cada prompt y verificación de superficie.

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

Una fila por cada enlace observado en un panel de respuestas.

text run_id surface topic prompt_id cited_url cited_domain citation_position link_scope target_domain target_cited

ai_crawler_requests

Datos agregados de registros del servidor.

text date bot_name bot_role path requests successful_requests error_requests unique_ip_count

shortener_clicks

Una fila por cada solicitud de enlace controlado.

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

ai_sessions

Una tabla de analíticas normalizada.

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

El campo confidence es importante. Los valores recomendados son:

  • A — Directo: clic en enlace corto controlado o parámetro de campaña de confianza.
  • B — Fuerte: referente de asistente conocido o solicitud de recuperación de usuario verificada.
  • C — Probable: evidencia de campaña o página de destino sin un referente completo.
  • D — Modelado: inferido a partir del tiempo, observaciones del panel o cambios agregados.

Informe A y B por separado de C y D.


4. Definiciones de métricas

Cuota de citas por tema

La Cuota de citas por tema mide con qué frecuencia un dominio objetivo aparece entre las fuentes observadas para un tema.

\[ \text{Cuota de citas}_{tema}

\frac{\text{Ocurrencias de citas del dominio objetivo}} {\text{Todas las ocurrencias de citas}} \]

Ejemplo:

  • 100 citas totales observadas para “software de gestión de proyectos”
  • 12 citas apuntan a su dominio
  • Cuota de citas por tema = 12%

Esta métrica mide la cuota de fuente, no si una respuesta contuvo su sitio al menos una vez.

Cobertura de citas

Utilice una segunda métrica para la presencia a nivel de respuesta:

\[ \text{Cobertura de citas}_{tema}

\frac{\text{Ejecuciones de respuestas que contienen una cita objetivo}} {\text{Ejecuciones de respuestas donde un panel estuvo presente}} \]

La Cobertura de citas suele ser más fácil de entender para audiencias no técnicas:

“Nuestro sitio apareció en el 34% de las respuestas de IA observadas para este tema.”

Posición de la cita

Registre la posición de una cita dentro de la respuesta:

  • 1 = primera fuente observada
  • 2 = segunda fuente observada
  • 3 = tercera fuente observada

Dado que las interfaces de IA a menudo agrupan o reordenan las fuentes, trate la posición como una observación descriptiva, no como una clasificación universal.


Tasa de referencia de asistente

La Tasa de referencia de asistente mide la proporción de sesiones humanas en el sitio que pueden atribuirse a una superficie de IA.

\[ \text{Tasa de referencia de asistente}

\frac{\text{Sesiones humanas atribuidas a IA}} {\text{Todas las sesiones humanas}} \]

Informe al menos dos versiones:

text Tasa de referencia de asistente confirmada = Sesiones de IA de confianza A / todas las sesiones humanas

Tasa de referencia de asistente expandida = Sesiones de IA de confianza A + B + C / todas las sesiones humanas

La versión confirmada es más conservadora. La versión expandida es útil para el análisis de tendencias, pero no debe presentarse como tráfico totalmente observado.

No utilice ejecuciones de paneles como impresiones

Si raspa 500 prompts, eso no significa que 500 personas vieron esas respuestas. Su scraper mide una muestra controlada de estados de respuesta. Utilice los informes de impresiones de la plataforma de primera parte cuando estén disponibles.


Interacción post-cita

La Interacción post-cita mide si las personas que llegaron a través de una cita de IA realizaron una acción significativa.

\[ \text{Interacción post-cita}

\frac{\text{Sesiones de IA con interacción}} {\text{Sesiones atribuidas a IA}} \]

Para Google Analytics 4, una sesión con interacción dura más de 10 segundos, incluye un evento clave o incluye al menos dos vistas de página o pantalla. Documentación de la tasa de interacción de Google Analytics (support.google.com)

También informe:

text Tiempo de interacción post-cita = mediana de segundos de interacción por sesión de IA

Tasa de conversión post-cita = Sesiones de IA con un evento clave / Sesiones atribuidas a IA

Ingresos por sesión post-cita = ingresos de sesiones de IA / Sesiones atribuidas a IA

El tiempo medio de interacción suele ser más estable que el tiempo promedio de interacción porque un pequeño número de sesiones muy largas puede distorsionar el promedio.


Métricas adicionales útiles

Tasa de éxito del rastreador de IA

\[ \text{Tasa de éxito del rastreador}

\frac{\text{Solicitudes del rastreador de IA que devuelven 2xx o 3xx}} {\text{Solicitudes del rastreador de IA}} \]

Desglose esto por bot y ruta. Una alta disminución de citas combinada con una tasa de error creciente del rastreador puede indicar un problema de alojamiento, firewall o configuración de robots.

Tasa de citas a clics

Para observaciones controladas de paneles de respuestas:

\[ \text{Tasa de citas a clics observada}

\frac{\text{Clics confirmados en enlaces cortos}} {\text{Citas objetivo observadas}} \]

Esto no es una verdadera tasa de clics de usuario a menos que el denominador represente impresiones de usuario reales. Etiquételo claramente como una métrica operativa observada.

Actualidad de la cita

Mida con qué frecuencia la URL citada refleja la versión actual de la página.

text Actualidad de la cita = páginas citadas con hash de contenido actual / páginas citadas

Esto es útil para páginas de documentación, precios, productos y políticas que cambian con frecuencia.


5. Análisis de registros del servidor

Configurar registros de acceso estructurados

Los registros estructurados son más fáciles de analizar que los registros tradicionales separados por espacios.

Ejemplo de configuración de 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;

Almacene los archivos de registro en una ubicación restringida. No envíe direcciones IP sin procesar a una tabla de análisis general a menos que tenga una razón operativa clara.

Identificar tipos de solicitudes de IA

Como mínimo, separe:

text OpenAI:

  • OAI-SearchBot
  • ChatGPT-User
  • GPTBot

Anthropic:

  • Claude-SearchBot
  • Claude-User
  • ClaudeBot

Perplexity:

  • PerplexityBot
  • Perplexity-User

Motores de búsqueda:

  • Googlebot
  • Bingbot

Las funciones de búsqueda de IA de Google generalmente utilizan los controles de rastreo existentes de Google Search en lugar de un "bot de Resumen de IA" universal separado. Google afirma que Googlebot controla el acceso para la Búsqueda, incluidas las funciones de IA. Funciones de IA y sitios web de Google (developers.google.com)

Las cadenas de agente de usuario pueden ser falsificadas. Para decisiones operativas, combine la coincidencia de agente de usuario con rangos de IP publicados por el proveedor o DNS inverso verificado donde estén disponibles. OpenAI y Perplexity publican información de IP para sus rastreadores y recolectores relevantes. (openai.com)

Resumidor de registros reproducible

Guarde lo siguiente 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])

Ejecútelo con:

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

data/ai_crawler_daily.csv

Esto produce evidencia operativa sobre la actividad de recuperación, no sobre el tráfico humano.


6. Acortadores de enlaces personalizados para resúmenes orientados a la IA

Un acortador de enlaces de primera parte proporciona un punto de medición controlado cuando publica resúmenes o materiales fuente que pueden ser referenciados por sistemas de IA.

Utilice un dominio que controle, como:

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

El destino debe incluir parámetros de campaña como:

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

Google recomienda parámetros de campaña para identificar fuentes de tráfico y campañas. Documentación de URL de campaña de Google Analytics (support.google.com)

Campos de enlace recomendados

text surface topic prompt_id asset_id content_version destination_path

No introduzca información personal, direcciones de correo electrónico o consultas de búsqueda de usuario en el enlace. Los identificadores de campaña deben describir el activo publicado, no identificar a una persona.

Limitaciones importantes

Un enlace corto es un dispositivo de calibración, no una solución universal:

  • El asistente puede reescribir la URL de destino.
  • El asistente puede citar la URL canónica en su lugar.
  • El enlace puede mostrarse pero nunca hacerse clic.
  • Un navegador o herramienta de privacidad puede eliminar el referente.
  • Algunos sistemas pueden recuperar el destino desde el lado del servidor.
  • Un enlace corto no mide a los usuarios que recuerdan la marca y regresan más tarde.

No utilice un acortador para ocultar el destino, eludir los sistemas de seguridad o manipular las citas. Utilice una redirección clara a una página relevante y canónica.

Acortador de primera parte mínimo

El siguiente ejemplo utiliza FastAPI y SQLite. Proteja el endpoint de creación de enlaces con autenticación antes de usarlo en producción.

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)

Ejecútelo con:

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

Utilice una redirección 302 para los enlaces de medición para que los navegadores y los sistemas intermedios tengan menos probabilidades de almacenar en caché la redirección de forma permanente.


7. Raspado programado de paneles de respuestas de IA

El raspado de paneles es útil para responder preguntas que las analíticas de primera parte pueden no exponer:

  • ¿Qué fuentes competidoras se citan?
  • ¿Qué página se cita para un prompt específico?
  • ¿La respuesta cambia por país o dispositivo?
  • ¿La cita aparece en la respuesta, en la lista de fuentes o en ambas?
  • ¿Qué tan estable es la respuesta a lo largo del tiempo?
  • ¿El asistente cita un enlace corto o la URL canónica?

Sin embargo, el raspado debe hacerse con cuidado.

Reglas para una recopilación responsable

  1. Prefiera una API oficial, un proveedor de datos de búsqueda con licencia o un informe de primera parte.
  2. Revise los términos del proveedor antes de automatizar las solicitudes.
  3. Respete las instrucciones de robots cuando sea aplicable.
  4. No eluda CAPTCHAs, barreras de inicio de sesión, límites de tasa o protecciones contra bots.
  5. Utilice un conjunto de prompts pequeño y fijo.
  6. Ejecute con baja frecuencia.
  7. Guarde la respuesta y la marca de tiempo para que los resultados puedan ser auditados.
  8. Deténgase cuando un proveedor devuelva un desafío o una respuesta de acceso denegado.

El Protocolo de Exclusión de Robots es un mecanismo de preferencia del rastreador, no un sistema de seguridad. No autoriza el acceso a contenido protegido. RFC 9309: Protocolo de Exclusión de Robots (rfc-editor.org)

Construya un manifiesto 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

Mantenga los prompts estables. Si los prompts cambian, las comparaciones de tendencias se vuelven menos fiables.

También almacene:

  • país
  • idioma
  • tipo de dispositivo
  • versión del navegador
  • versión del prompt
  • versión del contenido
  • hora de recopilación en UTC

Google señala que las respuestas de AI Overview y AI Mode pueden variar porque utilizan diferentes sistemas y pueden usar búsquedas relacionadas o "dispersión de consultas" (query fan-out). Documentación de funciones de IA y sitio web de Google (developers.google.com)

Script de recopilación con Playwright

El siguiente script es una herramienta de investigación de propósito general. Registra la página renderizada, la captura de pantalla, los enlaces de la página y el texto de la respuesta. Intencionalmente no elude desafíos ni intenta anular los controles de acceso.

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

Instale y ejecute:

bash pip install playwright playwright install chromium python scrape_panels.py

Prográmelo con un bloqueo para que dos ejecuciones no se superpongan:

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

Mejorar la precisión con adaptadores de superficie

El script de fallback registra enlaces a nivel de página. Esto es útil para el descubrimiento, pero puede incluir enlaces de resultados de búsqueda ordinarios. Para uso en producción:

  • Utilice una API de panel de respuestas con licencia cuando esté disponible.
  • Cree un adaptador de selector separado para cada superficie.
  • Almacene la versión del selector.
  • Almacene el HTML sin procesar y una captura de pantalla.
  • Marque cada registro con link_scope = "panel_selector" o link_scope = "page_fallback".

Calcule la Cuota de Citas solo a partir de datos de panel_selector cuando pueda identificar el panel de forma fiable. Mantenga las observaciones de fallback en un informe exploratorio separado.


8. Integración con Google Analytics y BigQuery

Google Analytics 4 puede exportar datos de eventos brutos a BigQuery. Las exportaciones diarias proporcionan tablas estables como events_YYYYMMDD; las exportaciones en streaming son útiles para la monitorización casi en tiempo real, pero pueden estar incompletas hasta que se finalice la tabla diaria. Exportación de Google Analytics a BigQuery (support.google.com)

Una consulta de sesión simplificada podría verse así:

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 una implementación madura, añada surface, topic y prompt_id a la estructura de la campaña o únalos desde la tabla de clics del acortador.

No cree una dimensión de análisis de alta cardinalidad para cada usuario o sesión individual. Google advierte que las dimensiones personalizadas de alta cardinalidad excesivas pueden reducir la calidad de los informes. Dimensiones y métricas personalizadas de Google Analytics (support.google.com)


9. Diseño del panel de control

Un panel de control práctico debería contener cinco vistas.

Resumen ejecutivo

Mostrar:

  • Cuota de citas por tema
  • Cobertura de citas por tema
  • Tasa de referencia de asistente confirmada
  • Tasa de referencia de asistente expandida
  • Interacción post-cita
  • Tasa de éxito del rastreador de IA
  • Eventos clave o ingresos atribuidos a la IA

Tendencia de la cuota de citas

Gráfico de líneas:

text Fecha × Tema × Superficie

Mostrar ambos:

  • ocurrencias de citas de dominio objetivo
  • todas las ocurrencias de citas observadas

Embudo de tráfico de asistente

text Citas observadas ↓ Clics en enlaces controlados ↓ Sesiones atribuidas a IA ↓ Sesiones de IA con interacción ↓ Eventos clave

Cada etapa debe mostrar su confianza de atribución.

Salud del rastreador

Tabla por bot:

text bot_name requests successful_requests error_rate unique_ip_count top_paths

Alertar cuando:

  • la tasa de error supera el 2 por ciento
  • un bot desaparece repentinamente
  • una sección de contenido previamente accesible devuelve 403 o 429
  • las solicitudes del rastreador se concentran en una ruta privada no intencionada

Tabla de oportunidades de contenido

Clasificar páginas y temas por:

text alta cuota de citas + baja tasa de referencia altas impresiones + baja cobertura de citas alta interacción + baja visibilidad de citas altos errores del rastreador + alta importancia comercial

Estas combinaciones suelen producir mejores decisiones que mirar solo el volumen de tráfico.

Panel de control Streamlit mínimo

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

Ejecútelo con:

bash pip install streamlit pandas streamlit run dashboard.py

Para producción, cargue las tablas normalizadas en DuckDB, BigQuery, Snowflake o PostgreSQL en lugar de leer los archivos directamente.


10. Controles de calidad

Un sistema de medición puede producir números que parecen precisos pero son engañosos. Añada estos controles.

Separar bots de personas

Nunca cuente OAI-SearchBot, GPTBot, ClaudeBot o PerplexityBot como sesiones humanas. Estas son señales de rastreo o recuperación.

Preservar evidencia en bruto

Para cada ejecución de panel, conserve:

  • HTML en bruto
  • captura de pantalla
  • texto de respuesta
  • marca de tiempo
  • versión del navegador y del recolector
  • prompt y configuración regional
  • hash de respuesta

Esto permite explicar por qué una métrica cambió.

Rastrear ausencia de panel

Un panel de IA ausente no es lo mismo que una respuesta sin su cita.

Utilice:

text panel_not_present panel_present_no_target panel_present_target_cited blocked_or_challenged collection_error

Controlar el sesgo de muestreo

Un raspado programado se ve afectado por:

  • ubicación
  • dispositivo
  • idioma
  • tiempo
  • personalización
  • estado del navegador
  • redacción del prompt

Utilice un conjunto central fijo de prompts y un conjunto rotatorio de prompts exploratorios. Mantenga los dos grupos separados en los informes.

Evitar la falsa precisión

Informe intervalos de confianza o tamaños de muestra mínimos para grupos de temas pequeños. Un tema con tres observaciones no debe compararse directamente con un tema que contenga 300 observaciones.

Comparar lo similar con lo similar

Al comparar superficies, mantenga constante:

  • prompt
  • configuración regional
  • país
  • dispositivo
  • hora de recopilación
  • reglas de dominio objetivo
  • versión del analizador de citas

11. Plan de implementación recomendado

Semana 1: Instrumentación

  • Configure registros de servidor estructurados.
  • Cree el registro de bots.
  • Cree el dominio del acortador.
  • Añada reglas de denominación de campaña.
  • Defina la taxonomía de temas.
  • Habilite la exportación de Google Analytics 4 a BigQuery.

Semana 2: Enlaces controlados

  • Publique de cinco a diez resúmenes con seguimiento.
  • Cree enlaces para diferentes temas y versiones de contenido.
  • Valide los registros de redirección con las sesiones de análisis.
  • Confirme que no se introduce información personal en URLs o parámetros de análisis.

Semana 3: Observaciones de panel

  • Cree un manifiesto de prompts.
  • Comience con dos superficies y de 20 a 50 prompts estables.
  • Ejecute una vez al día.
  • Guarde capturas de pantalla y HTML.
  • Mida los fallos de recopilación por separado de la ausencia de citas.

Semana 4: Panel de control y gobernanza

  • Construya paneles de control por tema y superficie.
  • Añada alertas de salud del rastreador.
  • Defina reglas de confianza.
  • Compare los informes de primera parte con las observaciones de panel.
  • Documente las políticas de retención, acceso y eliminación.

Conclusión

La medición de citas de IA debe construirse como un sistema de atribución multi-señal, no como un único informe de referencia.

El marco más fiable combina:

  • Informes de visibilidad de primera parte de Google y Bing
  • registros del servidor para actividad de rastreadores y recolectores
  • enlaces cortos de primera parte para la medición controlada de clics
  • observaciones programadas y conformes de paneles de respuestas
  • datos de analíticas y conversión para el valor posterior

Las tres métricas más importantes son:

  1. Cuota de citas por tema — cuánto del espacio de fuentes observado pertenece a su dominio.
  2. Tasa de referencia de asistente — cuánto tráfico humano puede atribuirse a las superficies de IA.
  3. Interacción post-cita — si los visitantes referidos por IA realizan acciones significativas.

El sistema se vuelve útil cuando cada número incluye su superficie, período de tiempo, denominador, método de atribución y nivel de confianza. Esa disciplina evita que los accesos de rastreadores se confundan con usuarios, que las respuestas raspadas se confundan con impresiones y que las citas se confundan con resultados comerciales.

Artículos relacionados

¿Te gusta este contenido?

Suscríbete a nuestro boletín para recibir las últimas novedades en marketing de contenidos y guías de crecimiento.

Este artículo es solo para fines informativos. El contenido y las estrategias pueden variar según tus necesidades específicas.
Medición del impacto de las citas de IA: analíticas, registros y raspado de paneles | AutoPod