Wszystko na jednej stronie.
Instalacja
Dodaj stronę w panelu, a potem wklej to w <head> każdej podstrony. Zmień domenę na swoją.
<script defer data-domain="yoursite.com" src="https://annuora.com/v.js"></script>
WordPress: wklej przez wtyczkę typu „Insert Headers and Footers” albo w header.php motywu. Shopify, Webflow, Framer, Wix: pole „custom code / head”. Google Tag Manager: tag Custom HTML na wszystkich stronach.
Subdomeny: dodaj główną domenę, a skrypt z tym samym data-domain wstaw na każdej subdomenie.
Aplikacje SPA
React, Vue, Next, Nuxt, Svelte - nic nie trzeba robić. Skrypt łapie history.pushState i popstate. Routing po #? Dodaj atrybut data-hash.
Zdarzenia i przychód
visitant("Signup");
visitant("Signup", { props: { plan: "pro", source: "pricing" } });
visitant("Purchase", { revenue: { amount: 49.00, currency: "USD" } });
// zdarzenia wywołane zanim skrypt się załaduje
window.visitant = window.visitant || function(){ (visitant.q = visitant.q || []).push(arguments) };
Każde zdarzenie widać od razu w panelu „Cele i zdarzenia”. Żeby liczyć konwersję, dodaj je jako cel w ustawieniach strony. Cele mogą być też stronami, np. /dziekujemy albo /blog/*.
Zdarzenia bez kodu
<a href="/signup" data-v-event="Signup CTA" data-v-prop-position="hero">Start</a>
Śledzone automatycznie
- Odsłony, wejścia, wyjścia, źródła, kampanie UTM, kraje (ze strefy czasowej, bez IP), urządzenia.
- Kliknięcia w linki wychodzące (Outbound Link: Click) i pobrania plików (File Download).
- Czas zaangażowania (tylko gdy karta jest widoczna) i głębokość przewinięcia.
- Core Web Vitals: LCP, INP, CLS, TTFB.
Boty AI (snippet serwerowy)
Boty nie uruchamiają JavaScriptu, więc potrzebny jest jeden z poniższych fragmentów. Klucz serwera znajdziesz w Ustawienia - Śledzenie botów AI. Wysyłane są tylko żądania wyglądające na boty - ruch ludzi nie wychodzi z Twojego serwera.
Save as wp-content/mu-plugins/visitant-bots.php. Works with page caching off or on (cached pages still hit PHP only on misses; for full coverage use the Cloudflare or nginx option).
<?php
/* Plugin Name: Visitant AI crawler tracking */
add_action('init', function () {
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
if (!preg_match('/bot|crawl|spider|GPTBot|ChatGPT-User|OAI-SearchBot|Claude|anthropic|Perplexity|Google-|GoogleOther|Applebot|meta-external|Meta-WebIndexer|Amazonbot|Amzn-|Bytespider|CCBot|MistralAI|DuckAssist|YouBot|xAI|Grok|cohere|Diffbot|Kimi|Qwen|Manus|Firecrawl/i', $ua)) return;
$body = json_encode(['ua' => $ua, 'path' => $_SERVER['REQUEST_URI'] ?? '/',
'ip' => $_SERVER['REMOTE_ADDR'] ?? '', 'status' => 200]);
wp_remote_post('https://annuora.com/api/bot', ['blocking' => false, 'timeout' => 1,
'headers' => ['Content-Type' => 'application/json', 'X-Visitant-Key' => 'TWOJ_KLUCZ_SERWERA'], 'body' => $body]);
});Put at the top of your front controller (index.php).
<?php
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
if (preg_match('/bot|crawl|spider|GPTBot|ChatGPT-User|OAI-SearchBot|Claude|anthropic|Perplexity|Google-|GoogleOther|Applebot|meta-external|Meta-WebIndexer|Amazonbot|Amzn-|Bytespider|CCBot|MistralAI|DuckAssist|YouBot|xAI|Grok|cohere|Diffbot|Kimi|Qwen|Manus|Firecrawl/i', $ua)) {
$ch = curl_init('https://annuora.com/api/bot');
curl_setopt_array($ch, [CURLOPT_POST => 1, CURLOPT_TIMEOUT_MS => 800, CURLOPT_RETURNTRANSFER => 1,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-Visitant-Key: TWOJ_KLUCZ_SERWERA'],
CURLOPT_POSTFIELDS => json_encode(['ua' => $ua, 'path' => $_SERVER['REQUEST_URI'] ?? '/',
'ip' => $_SERVER['REMOTE_ADDR'] ?? '', 'status' => http_response_code() ?: 200])]);
curl_exec($ch); curl_close($ch);
}Express, Fastify (as onRequest hook) or any Node server.
const AI_UA = /bot|crawl|spider|GPTBot|ChatGPT-User|OAI-SearchBot|Claude|anthropic|Perplexity|Google-|GoogleOther|Applebot|meta-external|Meta-WebIndexer|Amazonbot|Amzn-|Bytespider|CCBot|MistralAI|DuckAssist|YouBot|xAI|Grok|cohere|Diffbot|Kimi|Qwen|Manus|Firecrawl/i;
app.use((req, res, next) => {
const ua = req.get('user-agent') || '';
if (AI_UA.test(ua)) res.on('finish', () => fetch('https://annuora.com/api/bot', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Visitant-Key': 'TWOJ_KLUCZ_SERWERA' },
body: JSON.stringify({ ua, path: req.originalUrl, ip: req.ip, status: res.statusCode })
}).catch(() => {}));
next();
});middleware.ts in the project root.
import { NextResponse } from 'next/server';
const AI_UA = /bot|crawl|spider|GPTBot|ChatGPT-User|OAI-SearchBot|Claude|anthropic|Perplexity|Google-|GoogleOther|Applebot|meta-external|Meta-WebIndexer|Amazonbot|Amzn-|Bytespider|CCBot|MistralAI|DuckAssist|YouBot|xAI|Grok|cohere|Diffbot|Kimi|Qwen|Manus|Firecrawl/i;
export function middleware(req, ev) {
const ua = req.headers.get('user-agent') || '';
if (AI_UA.test(ua)) ev.waitUntil(fetch('https://annuora.com/api/bot', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Visitant-Key': 'TWOJ_KLUCZ_SERWERA' },
body: JSON.stringify({ ua, path: req.nextUrl.pathname, ip: req.headers.get('x-forwarded-for') || '' })
}).catch(() => {}));
return NextResponse.next();
}Covers every request, including cached pages. Add a route for your domain.
const AI_UA = /bot|crawl|spider|GPTBot|ChatGPT-User|OAI-SearchBot|Claude|anthropic|Perplexity|Google-|GoogleOther|Applebot|meta-external|Meta-WebIndexer|Amazonbot|Amzn-|Bytespider|CCBot|MistralAI|DuckAssist|YouBot|xAI|Grok|cohere|Diffbot|Kimi|Qwen|Manus|Firecrawl/i;
export default {
async fetch(req, env, ctx) {
const res = await fetch(req);
const ua = req.headers.get('user-agent') || '';
if (AI_UA.test(ua)) ctx.waitUntil(fetch('https://annuora.com/api/bot', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Visitant-Key': 'TWOJ_KLUCZ_SERWERA' },
body: JSON.stringify({ ua, path: new URL(req.url).pathname, ip: req.headers.get('cf-connecting-ip'), status: res.status })
}));
return res;
}
};Ships the access log every 5 minutes from cron. Works for any server that writes the combined log format.
# crontab: */5 * * * * /usr/local/bin/visitant-ship.sh
LOG=/var/log/nginx/access.log; POS=/var/tmp/visitant.pos
N=$(wc -l < "$LOG"); P=$(cat "$POS" 2>/dev/null || echo 0); [ "$N" -lt "$P" ] && P=0
tail -n +$((P+1)) "$LOG" | grep -iE 'bot|crawl|spider|GPTBot|ChatGPT-User|OAI-SearchBot|Claude|anthropic|Perplexity|Google-|GoogleOther|Applebot|meta-external|Meta-WebIndexer|Amazonbot|Amzn-|Bytespider|CCBot|MistralAI|DuckAssist|YouBot|xAI|Grok|cohere|Diffbot|Kimi|Qwen|Manus|Firecrawl' | python3 -c '
import sys, re, json
rx = re.compile(r"^(\S+) .*?\[(.+?)\] \"\S+ (\S+)[^\"]*\" (\d{3}) .*?\"[^\"]*\" \"([^\"]*)\"")
out = [dict(ip=m[1], path=m[3], status=int(m[4]), ua=m[5]) for m in map(rx.match, sys.stdin) if m]
print(json.dumps(out[:1000]))' | curl -s -X POST 'https://annuora.com/api/bot' -H 'X-Visitant-Key: TWOJ_KLUCZ_SERWERA' -H 'Content-Type: application/json' --data-binary @-
echo "$N" > "$POS"
Format żądania, jeśli piszesz własną integrację:
POST https://annuora.com/api/bot
X-Visitant-Key: sk_...
Content-Type: application/json
[{"ua": "Mozilla/5.0 ... GPTBot/1.4", "path": "/blog/post", "ip": "20.171.0.1", "status": 200, "ts": 1790000000}]
Jak liczymy ruch z AI
- Potwierdzony: referrer asystenta (chatgpt.com, perplexity.ai, claude.ai, gemini.google.com, copilot.microsoft.com i inne), referrer aplikacji (android-app://com.openai.chatgpt) albo utm_source/utm_medium dodany przez asystenta.
- Prawdopodobny: wejście z fragmentem tekstu (#:~:text=) bez referrera albo z Google - tak linkują odpowiedzi AI i AI Overviews. Pokazywany osobno, nigdy nie doliczany.
- Agenci: wizyty przeglądarek sterowanych przez AI, które się podpisują (nagłówek Signature-Agent), i przeglądarek automatycznych (navigator.webdriver). Domyślnie wyłączone z liczb ludzi.
Wykluczanie siebie
W konsoli przeglądarki na swojej stronie:
localStorage.visitant_ignore = "1"
Ścieżki (np. /admin/*) wykluczysz w ustawieniach strony.
API
Klucz API wygenerujesz w zakładce Konto. Plan Pro i wyżej.
curl -H "Authorization: Bearer vt_..." \
"https://annuora.com/api/v1/stats/yoursite.com?period=30d&dimension=source×eries=1&ai=1"
period = day | yesterday | 7d | 30d | 90d | month | 12mo | custom (&from=2026-09-01&to=2026-09-24)
dimension = page | entry | exit | source | channel | referrer | utm_source | utm_medium | utm_campaign
country | browser | os | device | event | ai_source | prop:<key>
f = {"channel":"ai","country":"PL"} (URL-encoded JSON; "!" negates, "~" contains)
Model prywatności
Brak cookies i identyfikatorów w przeglądarce. Odwiedzającego liczymy jako hash(sól_dnia, strona, IP, user-agent) skrócony do 64 bitów. Sól zmienia się co dobę i jest kasowana po 48 godzinach, więc nie da się połączyć wizyt z różnych dni ani odtworzyć IP. IP nie jest nigdzie zapisywane, kraj bierzemy ze strefy czasowej przeglądarki.