Вимірювання впливу цитування ШІ: аналітика, журнали та збір даних з панелей
Вступ
Виявлення, що керується ШІ, тепер розподілене на кілька рівнів вимірювання. Людина може побачити компанію, цитовану у відповіді ШІ, перейти за посиланням без корисного реферера, відвідати сайт через браузер, який видаляє дані кампанії, або прочитати відповідь, взагалі не натискаючи на посилання. Водночас, краулер ШІ може запитувати сторінку компанії, не представляючи людського відвідувача.
Таким чином, корисна система вимірювання повинна розділяти чотири питання:
- Чи було сайт проскановано або вилучено?
- Чи було сайт цитовано у відповіді ШІ?
- Чи відвідала людина сайт з цієї відповіді?
- Чи призвів цей візит до значущої взаємодії або доходу?
Станом на 22 вересня 2026 року покращується звітність першої сторони. Google Search Console надає звіт про продуктивність генеративного ШІ для AI Overviews (Оглядів ШІ) та AI Mode (Режиму ШІ), що включає покази, сторінки, країни, пристрої та дати. Звіт став доступним у всьому світі 31 серпня 2026 року. Звіт Google про продуктивність генеративного ШІ (support.google.com)
Microsoft також надає звіт AI Performance (Продуктивність ШІ) в Bing Webmaster Tools. Його публічна попередня версія звітує про загальну кількість цитувань, цитовані сторінки, запити обґрунтування та тенденції цитувань у підтримуваних ШІ-досвідах Microsoft. Продуктивність ШІ в Bing Webmaster Tools (blogs.bing.com)
Ці інструменти слід розглядати як авторитетні для їхніх власних поверхонь, але вони не створюють повної системи вимірювання між асистентами. Рекомендований підхід полягає у поєднанні звітів від першої сторони, журналів сервера, контрольованих посилань для відстеження, експортів аналітики та планових спостережень за панелями відповідей.
1. Чому вимірювання рефералів ШІ є складним
Традиційна веб-аналітика значною мірою залежить від заголовка Referer та параметрів кампанії. Обидва можуть бути відсутніми або неповними.
Заголовок HTTP Referer може містити попередню сторінку, лише походження попереднього веб-сайту або взагалі не мати значення. Політики реферерів браузера можуть навмисно обмежувати або видаляти цю інформацію. Документація Referrer-Policy на MDN (developer.mozilla.org)
Системи ШІ створюють додаткові ускладнення:
- Асистент може посилатися на сторінку, не надсилаючи розпізнаваного реферера.
- Асистент може використовувати URL-адресу з перенаправленням або переписану URL-адресу.
- Асистент може отримувати контент на стороні сервера перед показом відповіді.
- Краулер ШІ може відвідувати сторінку, не бачачи її жодною людиною.
- Відповідь може цитувати канонічну URL-адресу замість URL-адреси, яка була спочатку вилучена.
- Відповідь може змінюватися залежно від місцезнаходження, пристрою, мови, часу або персоналізації.
Наприклад, OpenAI розрізняє OAI-SearchBot для пошукового виявлення, GPTBot для можливого використання в навчанні та ChatGPT-User для дій, ініційованих користувачем. Perplexity окремо ідентифікує PerplexityBot та Perplexity-User. Anthropic ідентифікує ClaudeBot, Claude-SearchBot та Claude-User. Ці запити не слід об'єднувати в одне загальне число "трафіку ШІ". Документація краулерів OpenAI (openai.com) Документація краулерів Perplexity (docs.perplexity.ai) Документація краулерів Anthropic (support.anthropic.com)
Основне правило таке:
Діяльність краулера — це не реферальний трафік, видимість цитування — це не трафік, а трафік — це не вплив на бізнес.
2. Структура вимірювання
Використовуйте п'ять рівнів вимірювання.
| Рівень | Основне питання | Рекомендовані дані |
|---|---|---|
| Доступ | Чи система ШІ запитувала сторінку? | Журнали веб-сервера, журнали міжмережевого екрану |
| Видимість | Чи була сторінка показана або цитована? | Google Search Console, Bing Webmaster Tools, спостереження за панелями відповідей |
| Залучення | Чи людина прибула з поверхні ШІ? | Реферер, параметри кампанії, кліки за короткими посиланнями, аналітика |
| Взаємодія | Чи відвідувач значуще взаємодіяв? | Події аналітики, перегляди сторінок, час взаємодії |
| Цінність | Чи візит призвів до бажаного результату? | Реєстрації, ліди, покупки, підписки |
Рекомендована ієрархія джерел істини
Використовуйте найсильніший доступний сигнал для кожного питання:
- Звіти платформ першої сторони для показів та цитувань.
- Спостереження за панелями відповідей для видимості на рівні запиту та порівнянь з конкурентами.
- Журнали перенаправлень для контрольованих кліків за посиланнями.
- Веб-аналітика для сесій, взаємодії та конверсій.
- Журнали сервера для діяльності краулерів та фетчерів.
- Модельовані оцінки лише тоді, коли прямі докази недоступні.
Не об'єднуйте всі сигнали в одне число без збереження джерела та рівня довіри.
3. Основна модель даних
Створіть сховище даних з окремими таблицями для необроблених спостережень та похідних метрик.
panel_runs
Один рядок для кожного запиту та перевірки поверхні.
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
Один рядок для кожного посилання, виявленого на панелі відповідей.
text run_id surface topic prompt_id cited_url cited_domain citation_position link_scope target_domain target_cited
ai_crawler_requests
Агреговані дані журналів сервера.
text date bot_name bot_role path requests successful_requests error_requests unique_ip_count
shortener_clicks
Один рядок для кожного запиту контрольованого посилання.
text click_id occurred_at slug surface topic prompt_id asset_id referrer_host user_agent_family ip_hmac destination_path
ai_sessions
Нормалізована таблиця аналітики.
text session_id date surface topic prompt_id landing_page attribution_method confidence engaged engagement_time_seconds key_event revenue
Поле confidence є важливим. Рекомендовані значення:
- A — Прямий: клік за контрольованим коротким посиланням або довірений параметр кампанії.
- B — Сильний: відомий реферер асистента або перевірений запит користувача.
- C — Ймовірний: докази кампанії або цільової сторінки без повного реферера.
- D — Модельований: виведений з часу, спостережень за панелями або агрегованих змін.
Повідомляйте A та B окремо від C та D.
4. Визначення метрик
Частка цитувань за темою
Частка цитувань за темою вимірює, як часто цільовий домен з'являється серед джерел, спостережених для певної теми.
[ \text{Citation Share}_{topic}
\frac{\text{Target-domain citation occurrences}} {\text{All citation occurrences}} ]
Приклад:
- 100 загальних цитувань, спостережених для «програмного забезпечення для управління проєктами»
- 12 цитувань вказують на ваш домен
- Частка цитувань за темою = 12%
Ця метрика вимірює частку джерела, а не те, чи містила відповідь ваш сайт принаймні один раз.
Охоплення цитуванням
Використовуйте другу метрику для присутності на рівні відповіді:
[ \text{Citation Coverage}_{topic}
\frac{\text{Answer runs containing a target citation}} {\text{Answer runs where a panel was present}} ]
Охоплення цитуванням часто легше зрозуміти нетехнічній аудиторії:
«Наш сайт з'явився у 34% спостережених відповідей ШІ для цієї теми».
Позиція цитування
Зафіксуйте позицію цитування у відповіді:
1= перше спостережене джерело2= друге спостережене джерело3= третє спостережене джерело
Оскільки інтерфейси ШІ часто групують або змінюють порядок джерел, розглядайте позицію як описове спостереження, а не як універсальний рейтинг.
Коефіцієнт рефералів асистента
Коефіцієнт рефералів асистента вимірює частку людських сесій на сайті, які можуть бути віднесені до поверхні ШІ.
[ \text{Assistant Referral Rate}
\frac{\text{AI-attributed human sessions}} {\text{All human sessions}} ]
Повідомляйте принаймні дві версії:
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
Підтверджена версія є більш консервативною. Розширена версія корисна для аналізу тенденцій, але її не слід представляти як повністю спостережений трафік.
Не використовуйте панельні запуски як покази
Якщо ви збираєте дані з 500 запитів, це не означає, що 500 людей побачили ці відповіді. Ваш скрепер вимірює контрольований зразок станів відповідей. Використовуйте звіти про покази від платформ першої сторони, коли вони доступні.
Взаємодія після цитування
Взаємодія після цитування вимірює, чи люди, які прибули через цитування ШІ, здійснили значущу дію.
[ \text{Post-Citation Engagement}
\frac{\text{Engaged AI sessions}} {\text{AI-attributed sessions}} ]
Для Google Analytics 4 залучена сесія триває більше 10 секунд, включає ключову подію або включає щонайменше два перегляди сторінок чи екранів. Документація Google Analytics щодо коефіцієнта залучення (support.google.com)
Також повідомляйте:
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
Медіанний час взаємодії зазвичай стабільніший, ніж середній час взаємодії, оскільки невелика кількість дуже довгих сесій може спотворити середнє значення.
Додаткові корисні метрики
Коефіцієнт успішності краулера ШІ
[ \text{Crawler Success Rate}
\frac{\text{AI crawler requests returning 2xx or 3xx}} {\text{AI crawler requests}} ]
Розбийте це за ботом та шляхом. Високий спад цитувань у поєднанні зі зростанням кількості помилок краулера може вказувати на проблему з хостингом, міжмережевим екраном або конфігурацією файлу robots.txt.
Коефіцієнт цитування-до-кліку
Для контрольованих спостережень за панелями відповідей:
[ \text{Observed Citation-to-Click Rate}
\frac{\text{Confirmed short-link clicks}} {\text{Observed target citations}} ]
Це не є справжнім коефіцієнтом кліків користувачів, якщо знаменник не представляє фактичні покази користувачів. Чітко позначайте його як спостережену операційну метрику.
Актуальність цитування
Виміряйте, як часто цитована URL-адреса відображає поточну версію сторінки.
text Citation Freshness = cited pages with current content hash / cited pages
Це корисно для сторінок документації, ціноутворення, продуктів та політик, які часто змінюються.
5. Аналіз журналів сервера
Налаштування структурованих журналів доступу
Структуровані журнали легше аналізувати, ніж традиційні журнали з роздільниками-пробілами.
Приклад конфігурації 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;
Зберігайте файли журналів у захищеному місці. Не надсилайте сирі IP-адреси до загальної таблиці аналітики, якщо у вас немає чіткої операційної причини.
Визначення типів запитів ШІ
Мінімум, розділіть:
text OpenAI:
- OAI-SearchBot
- ChatGPT-User
- GPTBot
Anthropic:
- Claude-SearchBot
- Claude-User
- ClaudeBot
Perplexity:
- PerplexityBot
- Perplexity-User
Search engines:
- Googlebot
- Bingbot
Функції ШІ-пошуку Google зазвичай використовують існуючі засоби керування скануванням Google Search, а не окремого універсального «бота AI Overview». Google заявляє, що Googlebot контролює доступ до Пошуку, включаючи функції ШІ. Функції ШІ Google та веб-сайти (developers.google.com)
Рядки user-agent можуть бути підробленими. Для операційних рішень комбінуйте зіставлення user-agent з IP-діапазонами, опублікованими постачальниками, або перевіреним зворотним DNS, де це можливо. OpenAI та Perplexity публікують інформацію про IP-адреси для своїх відповідних краулерів та фетчерів. (openai.com)
Відтворюваний підсумовувач журналів
Збережіть наступне як 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])
Запустіть за допомогою:
bash
HMAC_SECRET='a-long-random-secret'
python ai_log_summary.py /var/log/nginx/ai-access.jsonl \
data/ai_crawler_daily.csv
Це надає операційні докази щодо діяльності з отримання даних, а не людського трафіку.
6. Власні скорочувачі посилань для резюме, що орієнтовані на ШІ
Скорочувач посилань від першої сторони надає контрольовану точку вимірювання, коли ви публікуєте резюме або вихідні матеріали, на які можуть посилатися системи ШІ.
Використовуйте домен, який ви контролюєте, наприклад:
text https://go.example.com/ai-docs
Призначення повинно включати параметри кампанії, такі як:
text utm_source=perplexity utm_medium=ai_answer utm_campaign=product_documentation utm_content=pricing_summary_v3
Google рекомендує використовувати параметри кампанії для ідентифікації джерел трафіку та кампаній. Документація Google Analytics щодо URL-адрес кампаній (support.google.com)
Рекомендовані поля посилань
text surface topic prompt_id asset_id content_version destination_path
Не додавайте до посилання особисту інформацію, адреси електронної пошти або пошукові запити користувачів. Ідентифікатори кампанії повинні описувати опублікований актив, а не ідентифікувати особу.
Важливі обмеження
Коротке посилання — це калібрувальний пристрій, а не універсальне рішення:
- Асистент може переписати цільову URL-адресу.
- Асистент може цитувати канонічну URL-адресу замість цього.
- Посилання може бути відображено, але ніколи не натиснуто.
- Браузер або інструмент конфіденційності може видалити реферер.
- Деякі системи можуть отримувати ціль на стороні сервера.
- Коротке посилання не вимірює користувачів, які пам'ятають бренд і повертаються пізніше.
Не використовуйте скорочувач, щоб приховати ціль, обійти системи безпеки або маніпулювати цитуваннями. Використовуйте чітке перенаправлення на відповідну, канонічну сторінку.
Мінімальний скорочувач посилань першої сторони
Наступний приклад використовує FastAPI та SQLite. Захистіть кінцеву точку створення посилань за допомогою автентифікації перед використанням у виробництві.
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)
Запустіть за допомогою:
bash pip install fastapi uvicorn uvicorn shortener:app --host 0.0.0.0 --port 8080
Використовуйте перенаправлення 302 для вимірювальних посилань, щоб браузери та проміжні системи рідше кешували перенаправлення назавжди.
7. Плановий збір даних з панелей відповідей ШІ
Збір даних з панелей корисний для відповідей на питання, які аналітика першої сторони може не розкривати:
- Які конкуруючі джерела цитуються?
- Яка сторінка цитується для певного запиту?
- Чи змінюється відповідь залежно від країни чи пристрою?
- Чи з'являється цитування у відповіді, списку джерел або в обох місцях?
- Наскільки стабільна відповідь з часом?
- Чи асистент цитує коротке посилання чи канонічну URL-адресу?
Однак збір даних слід проводити обережно.
Правила відповідального збору даних
- Віддавайте перевагу офіційному API, ліцензованому постачальнику пошукових даних або звіту від першої сторони.
- Перегляньте умови постачальника перед автоматизацією запитів.
- Дотримуйтесь інструкцій robots.txt, де це застосовно.
- Не обходьте CAPTCHA, бар'єри входу, обмеження частоти запитів або захист від ботів.
- Використовуйте невеликий, фіксований набір запитів.
- Запускайте з низькою частотою.
- Зберігайте відповідь і часову мітку, щоб результати можна було перевірити.
- Зупиняйтеся, коли постачальник повертає виклик або відповідь про відмову в доступі.
Протокол виключення роботів — це механізм переваги для краулерів, а не система безпеки. Він не дозволяє доступ до захищеного контенту. RFC 9309: Протокол виключення роботів (rfc-editor.org)
Створіть маніфест запитів
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
Зберігайте запити стабільними. Якщо запити змінюються, порівняння тенденцій стають менш надійними.
Також зберігайте:
- країна
- мова
- тип пристрою
- версія браузера
- версія запиту
- версія контенту
- час збору в UTC
Google зазначає, що відповіді AI Overview (Огляд ШІ) та AI Mode (Режим ШІ) можуть відрізнятися, оскільки вони використовують різні системи та можуть застосовувати пов'язані пошукові запити або «розширення запитів». Документація Google AI features and website (developers.google.com)
Скрипт збору даних Playwright
Наступний скрипт є універсальним інструментом для досліджень. Він записує відрендерену сторінку, знімок екрана, посилання на сторінку та текст відповіді. Він навмисно не обходить перешкоди та не намагається подолати засоби контролю доступу.
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"))
Встановіть та запустіть:
bash pip install playwright playwright install chromium python scrape_panels.py
Заплануйте його за допомогою блокування, щоб два запуски не перекривалися:
cron
15 3 * * * flock -n /tmp/ai-panel.lock
/usr/bin/python3 /opt/ai-measure/scrape_panels.py
Поліпшення точності за допомогою адаптерів поверхні
Резервний скрипт записує посилання на рівні сторінки. Це корисно для виявлення, але може включати звичайні посилання результатів пошуку. Для виробничого використання:
- Використовуйте ліцензований API панелі відповідей, якщо він доступний.
- Створіть окремий адаптер селекторів для кожної поверхні.
- Зберігайте версію селектора.
- Зберігайте необроблений HTML та знімок екрана.
- Позначайте кожен запис як
link_scope = "panel_selector"абоlink_scope = "page_fallback".
Розраховуйте частку цитувань з даних panel_selector лише тоді, коли ви можете надійно ідентифікувати панель. Зберігайте резервні спостереження в окремому дослідницькому звіті.
8. Інтеграція Google Analytics та BigQuery
Google Analytics 4 може експортувати необроблені дані подій у BigQuery. Щоденні експорти надають стабільні таблиці, такі як events_YYYYMMDD; потокові експорти корисні для моніторингу в майже реальному часі, але можуть бути неповними до завершення формування щоденної таблиці. Експорт Google Analytics у BigQuery (support.google.com)
Спрощений запит сесії може виглядати так:
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;
Для розвиненої реалізації додайте surface, topic та prompt_id до структури кампанії або приєднайте їх з таблиці кліків скорочувача.
Не створюйте вимір аналітики з високою кардинальністю для кожного окремого користувача або сесії. Google попереджає, що надмірні власні виміри з високою кардинальністю можуть знизити якість звітів. Користувацькі виміри та метрики Google Analytics (support.google.com)
9. Дизайн інформаційної панелі
Практична інформаційна панель повинна містити п'ять переглядів.
Резюме для керівництва
Відображайте:
- Частка цитувань за темою
- Охоплення цитуванням за темою
- Підтверджений коефіцієнт рефералів асистента
- Розширений коефіцієнт рефералів асистента
- Взаємодія після цитування
- Коефіцієнт успішності краулера ШІ
- Ключові події або дохід, віднесені до ШІ
Тенденція частки цитувань
Лінійний графік:
text Date × Topic × Surface
Показуйте обидва:
- випадки цитування цільового домену
- всі спостережені випадки цитування
Воронка трафіку асистента
text Observed citations ↓ Controlled-link clicks ↓ AI-attributed sessions ↓ Engaged AI sessions ↓ Key events
Кожен етап повинен відображати свою впевненість у атрибуції.
Стан краулера
Таблиця за ботами:
text bot_name requests successful_requests error_rate unique_ip_count top_paths
Сповіщайте, коли:
- коефіцієнт помилок перевищує 2 відсотки
- бот раптово зникає
- раніше доступний розділ контенту повертає 403 або 429
- запити краулера зосереджені на ненавмисному приватному шляху
Таблиця можливостей контенту
Ранжуйте сторінки та теми за:
text high citation share + low referral rate high impressions + low citation coverage high engagement + low citation visibility high crawler errors + high business importance
Ці комбінації зазвичай дають кращі рішення, ніж розгляд лише обсягу трафіку.
Мінімальна панель інструментів Streamlit
Збережіть як 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) )
Запустіть за допомогою:
bash pip install streamlit pandas streamlit run dashboard.py
Для виробництва завантажуйте нормалізовані таблиці в DuckDB, BigQuery, Snowflake або PostgreSQL замість прямого читання необроблених файлів.
10. Контроль якості
Система вимірювання може генерувати числа, які виглядають точними, але є оманливими. Додайте ці засоби контролю.
Відокремлюйте ботів від людей
Ніколи не рахуйте OAI-SearchBot, GPTBot, ClaudeBot або PerplexityBot як людські сесії. Це сигнали краулерів або вилучення даних.
Зберігайте необроблені докази
Для кожного запуску панелі зберігайте:
- необроблений HTML
- знімок екрана
- текст відповіді
- часова мітка
- версія браузера та збирача
- запит та локаль
- хеш відповіді
Це дозволяє пояснити, чому метрика змінилася.
Відстежуйте відсутність панелі
Відсутня панель ШІ — це не те саме, що відповідь без вашого цитування.
Використовуйте:
text panel_not_present panel_present_no_target panel_present_target_cited blocked_or_challenged collection_error
Контролюйте упередженість вибірки
Запланований збір даних залежить від:
- місцезнаходження
- пристрій
- мова
- час
- персоналізація
- стан браузера
- формулювання запиту
Використовуйте фіксований основний набір запитів та змінний набір дослідницьких запитів. Тримайте ці дві групи окремо у звітах.
Уникайте хибної точності
Повідомляйте довірчі інтервали або мінімальні розміри вибірки для невеликих груп тем. Тему з трьома спостереженнями не слід порівнювати безпосередньо з темою, що містить 300 спостережень.
Порівнюйте подібне з подібним
При порівнянні поверхонь зберігайте постійними:
- запит
- локаль
- країна
- пристрій
- година збору
- правила цільового домену
- версія парсера цитувань
11. Рекомендований план впровадження
Тиждень 1: Інструментування
- Налаштуйте структуровані журнали сервера.
- Створіть реєстр ботів.
- Створіть домен для скорочувача.
- Додайте правила іменування кампаній.
- Визначте таксономію тем.
- Увімкніть експорт Google Analytics 4 у BigQuery.
Тиждень 2: Контрольовані посилання
- Опублікуйте п'ять-десять відстежуваних резюме.
- Створіть посилання для різних тем та версій контенту.
- Перевірте журнали перенаправлень з сесіями аналітики.
- Переконайтеся, що особиста інформація не потрапляє в URL-адреси або параметри аналітики.
Тиждень 3: Спостереження за панелями
- Створіть маніфест запитів.
- Почніть з двох поверхонь та 20-50 стабільних запитів.
- Запускайте раз на день.
- Зберігайте знімки екрана та HTML.
- Вимірюйте збої збору даних окремо від відсутності цитувань.
Тиждень 4: Інформаційна панель та управління
- Створіть інформаційні панелі за темами та поверхнями.
- Додайте сповіщення про стан краулерів.
- Визначте правила довіри.
- Порівняйте звіти першої сторони зі спостереженнями за панелями.
- Документуйте політики зберігання, доступу та видалення даних.
Висновок
Вимірювання цитувань ШІ слід будувати як систему атрибуції з багатьма сигналами, а не як єдиний звіт про реферали.
Найбільш надійна система поєднує:
- Звіти Google та Bing про видимість від першої сторони
- журнали сервера для діяльності краулерів та фетчерів
- короткі посилання від першої сторони для контрольованого вимірювання кліків
- планові, сумісні спостереження за панелями відповідей
- дані аналітики та конверсій для подальшої цінності
Три найважливіші метрики:
- Частка цитувань за темою — яка частина спостережуваного джерельного простору належить вашому домену.
- Коефіцієнт рефералів асистента — скільки людського трафіку можна віднести до поверхонь ШІ.
- Взаємодія після цитування — чи здійснюють відвідувачі, спрямовані ШІ, значущі дії.
Система стає корисною, коли кожне число включає свою поверхню, період часу, знаменник, метод атрибуції та рівень довіри. Ця дисципліна запобігає помилковому сприйняттю звернень краулерів за користувачів, зібраних відповідей за покази, а цитувань за бізнес-результати.
Auto