NookDocs ships a first-party analytics beacon that writes anonymous events directly to the platform's own Supabase tables. The data powers the per-project Analytics dashboard and feeds the feedback backlog.
This is independent of third-party integrations — GA4, PostHog, Plausible, Mixpanel, and the dozen other providers in the integrations.* block continue to run regardless of whether the first-party beacon is on.
What's captured
| Event type | Emitted by | Metadata |
view | Every docs page load | referrer, loadTime |
search | Search modal submission | query, resultCount |
feedback_up / feedback_down | The Feedback widget | reason, hasComment |
assistant_open / assistant_message | The AI chat bubble | queryLength, responseTime |
llm_export | An LLM/agent fetching llms.txt, llms-full.txt, or a page's .md | kind |
Every event records:
session_id— anonymous per-tab random string, lives insessionStorage. Resets on browser close. No cookies, no localStorage, no cross-tab dedupe.user_agent(truncated to 512 chars) — server-side classified intois_bot+bot_family(gpt|claude|perplexity|google|bing|other), and into browser / OS / device for the tech breakdowns.referrer(from the browser'sdocument.referrer).path— the normalized docs path (no host, no query string).Approximate location —
country, and (going forward)region+city— derived from your visitor's connection at the edge. Country is available historically; region and city are captured for new visits only.UTM tags —
utm_source/utm_medium/utm_campaign/utm_term/utm_content, read from the landing URL's query string when present. Powers the Channel / Campaign breakdowns. Captured going forward.
No IPs, no emails, no user identifiers.
Bot detection
The ingestion endpoint classifies the user-agent into a bot family so the dashboard can split AI-mediated traffic from human traffic. Matched patterns:
gpt —
GPTBot,ChatGPT-User,OAI-SearchBotclaude —
ClaudeBot,Claude-Web,anthropic-aiperplexity —
PerplexityBot,Perplexity-Usergoogle —
Googlebot,Google-Extended,GoogleOther,Storebot-Googlebing —
bingbot,BingPreview,msnbotother — generic
bot,crawler,spider,HeadlessChrome,curl/,wget/
The Google-Extended token is the AI-training opt-out UA — any scraper asking for its content still shows up in the AI column.
Dashboard
Dashboard → your project → sidebar → Analytics
The page renders in a single fetch and updates when the toolbar changes:
Date range — a preset menu (Now, Today, Yesterday, Last 24 hours, Last 7 / 30 days, Last 12 months, Week / Month / Year to date, All time) plus Custom for an arbitrary start–end range via a two-month calendar. The ‹ › arrows step the window back/forward by its own length. Ranges resolve in your local timezone, so "Today" means your midnight.
Now is the live view: a rolling 30-minute window, bucketed per minute, auto-refreshing. It has no granularity selector (the grain is fixed).
Granularity — for every range except Now, bucket the Traffic chart by Hourly / Daily / Weekly / Monthly. Options are constrained to grains that fit the window, so you never plot thousands of hourly points or a single monthly bar: short windows offer hourly, long windows offer weekly/monthly.
Window Available grains Today · Last 24 hours Hourly Last 7 days · Week to date Hourly · Daily Last 30 days · Month to date Daily · Weekly up to ~4 months Daily · Weekly · Monthly Last 12 months · Year to date · All time Weekly · Monthly Hide bot traffic (default on) — Strips UA-classified crawlers from view / visitor totals. The AI-vs-human split panel always shows both sides regardless of this toggle.
Panels
| Panel | What it shows |
| Views tile | Total view events in the window. |
| Unique visitors tile | Distinct session_ids seen in the window. |
| Searches tile | Total search events + the top query label. |
| Feedback tile | Thumbs-up count + thumbs-down count. |
| Traffic chart | Area (views) + unique-visitor line, bucketed by the chosen granularity. Empty buckets show as zero, not gaps. |
| Locations | Tabbed: Map (world choropleth shaded by visitors, hover for the count) · Country · Region · City — each a ranked visitor list. |
| Tech | Tabbed: Browser · OS · Device — ranked visitor list per client, with real brand logos. |
| Traffic sources | Tabbed: Channel (Direct / Organic Search / Social / AI / Referral / Campaign…) · Referrer (referring site, with favicon) · Campaign (UTM-tagged visits) · Keyword (utm_term). |
| AI-vs-human split | Stacked horizontal bar: Human + ChatGPT + Claude + Perplexity + Google + Bing + Other bots. Per-family counts + percentages in the legend. |
| AI & LLMs | Crawl views, LLM exports, Ask-AI usage, and AI referrals at a glance, plus tabs for Crawlers (by bot), AI pages (what bots read most), and Exports (llms.txt / llms-full.txt / page .md fetches). |
| Top pages | Same ranked-list card with a Views / Unique metric toggle — top 10. |
| Top searches | Query / count / avg result count — top 10. |
| Recent feedback | Last 20 page_feedback rows with rating, reason, comment, and path. |
Every breakdown list has a sort toggle (highest-first by default) — Visitors on most cards, Views / Unique on Top pages.
Drill-down filters
Click any breakdown row — a page, browser, OS, device, country, region, city, referrer, channel, or keyword — to filter the entire dashboard by it. Filters stack (AND): e.g. Country = Germany and Browser = Firefox. A hover filter icon marks clickable rows. The always-visible Filters control (next to the date range) shows how many are active and lets you remove them individually or Clear all. Totals, the traffic chart, and every breakdown all respect the active filters.
Campaign tracking (UTM)
Tag the links that point at your docs so the Traffic sources → Campaign / Channel breakdowns can attribute the visits:
https://docs.example.com/quickstart?utm_source=newsletter&utm_medium=email&utm_campaign=launchutm_source,utm_medium,utm_campaign,utm_term,utm_contentare all picked up from the landing URL.The medium drives the Channel:
cpc/ppc/paid→ Paid Search,email/newsletter→ Email, otherwise → Campaign.Tags are read from the landing page (the first page opened with them) and recorded going forward — past visits aren't backfilled.
AI & LLM insights
NookDocs is built for the LLM era, so the dashboard surfaces how AI actually uses your docs — not just human pageviews:
Crawlers — views from AI/search bots, grouped by family (ChatGPT/GPT, Claude, Perplexity, Google, Bing, other). Same classification as the AI-vs-human split.
AI pages — which pages those bots read most, so you can see what AI is learning from your docs.
Exports — how often LLMs/agents fetch your machine-readable surfaces:
llms.txt,llms-full.txt, and per-page Markdown (/<page>.md). Each fetch is counted.AI referrals — humans who arrived from an AI assistant. Visits carrying an AI source (e.g. ChatGPT appends
?utm_source=chatgpt.com) or an AI referrer are classified as the AI channel in Traffic sources, not lumped into Campaign/Referral.Ask AI — opens and questions on your docs' built-in assistant.
All of these respect the active drill-down filters and date range.
Data latency
Events land in page_events within ~1 second of a page view (edge ingestion). The dashboard reads directly from live tables — no materialized-view rebuild to wait for. For tenants with high volume, per-day rollups (materialized views) are on the roadmap.
Configure from the dashboard
Dashboard → your project → Editor → Configurations → AI & Advanced → Analytics
One toggle: First-party beacon (default on). Turning it off disables first-party event capture entirely for this tenant — useful for regulated industries or readers who want to rely on their own analytics stack.
Config reference
analytics.enabledbooleandefault: trueEnable the first-party beacon. When false, no events are written to page_events / visitors and the Analytics dashboard shows "No data" for this project.
Example
{
"analytics": {
"enabled": false
}
}API
Ingestion
POST /api/v1/projects/:projectId/events — public ingestion.
{
"event_type": "view",
"path": "/guides/auth",
"session_id": "abc123…",
"metadata": { "referrer": "https://google.com/", "loadTime": 142 }
}Returns 202 Accepted on success, 400 on malformed input, 404 on unknown project. Payload caps: 500-char path, 2 KB metadata JSON. CORS open — the endpoint accepts cross-origin POSTs from any tenant subdomain.
Summary (dashboard-only)
GET /api/v1/projects/:projectId/analytics/summary?from=…&to=…&unit=day&tz=Europe/Istanbul&bots=excluded — dashboard read.
Requires a JWT or API-key Authorization: Bearer … header. Returns a single JSON blob with totals, timeseries, aiSplit, topPages, topSearches, geo (countries / regions / cities), tech (browsers / os / devices), sources (channels / referrers / campaigns / keywords), llm (usage / crawlers / pages / exports), and recentFeedback — everything the Analytics page renders. Query params:
from,to— ISO timestamps bounding the window[from, to). Default: last 30 days.unit— timeseries bucket grain:hour|day|week|month(defaultday).tz— IANA timezone for bucket boundaries (defaultUTC). The dashboard sends the viewer's zone.bots—excluded(default) |included. Whenexcluded, view / visitor totals and breakdowns skip UA-classified bots.
Retention
Raw events (
page_events) — 90 days. The platform runsprune_old_page_events()on a schedule.Daily aggregates — summary rollups (planned) retain indefinitely.
Visitors (
visitors) — no time cap; the composite(project_id, session_id)PK dedupes sessions across reloads.
Privacy stance
The beacon does NOT record:
IP addresses. Approximate country / region / city come from coarse, pre-resolved geolocation at the edge — the raw IP is never read or stored.
Cookies or localStorage identifiers.
Query strings beyond the normalized path.
Session state that persists across browser restarts (sessionStorage clears on close).
Do Not Track is honored via the analytics.enabled toggle — tenants that need wholesale opt-out flip it off and ship.