Skip to content

Lead analytics with human visits; show bot crawls apart - #230

Merged
ralyodio merged 1 commit into
masterfrom
tracker-human-split
Sep 5, 2026
Merged

ralyodio merged 1 commit into
masterfrom
tracker-human-split

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Why

Every headline number on /dashboard, /dashboard/analytics and the per-project stats page was bot-inclusive. On genrewatch.com 99% of ~257k weekly hits are Meta's training crawler, and the card read as "80k pageviews a day" for a site with a few hundred real readers.

Definition (SQL and TS agree)

  • human = bucket does NOT start with bot: (AI referrals, search, social, referral, direct). A person arriving from ChatGPT is a person.
  • bot = bucket starts with bot: (named AI crawlers + bot:other).
  • humans + bots = events, exactly. Lives in lib/tracker/humans.ts and the migration header.

Migration: NOT APPLIED

supabase/migrations/20260905120000_tracker_human_split.sql must be applied by hand via the Supabase MCP (project ywcizjsgrcmhgyplldac) before this merges. It:

  • adds dashboard_project_traffic(p_project_ids uuid[], p_since date) -> (project_id, day, humans, bots) from tracker_daily_stats (grant authenticated, service_role); dashboard_project_pageviews is left in place
  • tracker_project_totals: + humans, prev_humans
  • tracker_daily_series, tracker_daily_series_multi: + humans
  • tracker_project_daily_series: + humans, bots
  • tracker_recent_series: + humans

Return types change, so each is drop function if exists + create function. Argument signatures are identical and columns are only appended, so existing callers keep working. All stay security invoker; grants are re-issued after the drops. tracker_top_pages_multi / tracker_top_actions_multi / tracker_top_exit_pages_multi (which carry set work_mem from an applied migration) are deliberately untouched.

Until the migration lands, the code derives the missing humans column as events - bots (exact for every bucket-based leg), so nothing renders as "0 human visits" in the gap. The one thing that needs the migration is the /dashboard cards, which call the new dashboard_project_traffic RPC; before it exists they show the existing "unavailable / query failed, not zero" state rather than a wrong number.

UI

  • /dashboard cards: {n} human visits headline, {n} bot hits · Past 7 days beneath, sparkline of humans.
  • /dashboard/analytics: tiles are Human visits (accent), AI referrals, Bot crawls, All events (muted). Verdict, property ranking, per-property trend and sparklines are all human-based. "By property" table: Humans / Previous / Change / Trend / AI / Bots. Portfolio chart stacks human visits per property with bot crawls (all properties) as one dashed, unstacked overlay.
  • Project stats: tiles are Human visits, AI referrals, Bot crawls. Traffic pulse stacks humans + bots (the bot-inclusive pageview band is gone; it double-counted bots), with AI referrals and interactions as overlays. SeriesPoint gains humans, so the SSR page and /api/projects/:id/tracker-stats agree.
  • Every labelled figure carries its definition as hover text and a caption.
  • humans is never backfilled from the bot-inclusive event table on pre-bucket-rollup days; those days honestly show 0 humans.

Tests

  • tests/tracker-human-split.test.ts (new): humansFrom / toCount, toProjectTotals, sumTotals, totalsTrends (humans trend down while bot-inflated events trend up), buildBucketAxis humans.
  • tests/portfolio-analytics.test.ts: humans on series rows, no legacy backfill of humans, pre-migration fallback.
  • tsc --noEmit: clean. vitest run: 153 files passed, 1 skipped; 2008 tests passed, 7 skipped.
  • Not run: next lint (eslint is not present in the node_modules available to the worktree).

🤖 Generated with Claude Code

https://claude.ai/code/session_01WJaXiqE9BDoNfoJBhfXroC

Every headline number on /dashboard, /dashboard/analytics and the
per-project stats page was bot-inclusive. On one property 99% of ~257k
weekly hits were a single AI training crawler, and the card read as
"80k pageviews a day" for a site with a few hundred real readers.

Definition, used in SQL and in lib/tracker/humans.ts: a hit is a human
when its bucket does NOT start with `bot:` (AI referrals, search, social,
referral, direct), a bot when it does. humans + bots = events, exactly.

Migration 20260905120000_tracker_human_split.sql (NOT applied yet):
- new dashboard_project_traffic(p_project_ids, p_since) ->
  (project_id, day, humans, bots) from tracker_daily_stats
- tracker_project_totals gains humans / prev_humans
- tracker_daily_series and tracker_daily_series_multi gain humans
- tracker_project_daily_series gains humans and bots
- tracker_recent_series gains humans
All are drop + create (return types change), same argument signatures,
columns only appended, security invoker, grants re-issued. The three
*_multi functions carrying `set work_mem` are deliberately untouched.

UI:
- /dashboard cards: "{n} human visits" headline, "{n} bot hits · Past 7
  days" beneath, sparkline of humans, read from dashboard_project_traffic
- /dashboard/analytics: tiles are Human visits, AI referrals, Bot crawls,
  All events; verdict, ranking, per-property trend and sparklines are all
  human-based; the By property table has Humans / Previous / Change /
  Trend / AI / Bots; the portfolio chart stacks human visits per property
  with bot crawls as one dashed unstacked overlay
- project stats: tiles are Human visits, AI referrals, Bot crawls;
  Traffic pulse stacks humans + bots (the bot-inclusive pageview band is
  gone), with AI referrals and interactions as overlays
- every labelled figure carries the definition as hover text and a
  caption

Before the migration is applied the missing `humans` column is derived
as events - bots, which is exact for every bucket-based leg, so the
pages are correct either way. `humans` is never backfilled from the
bot-inclusive event table on pre-bucket days.

Tests: lib/tracker/totals.ts and humans.ts are covered in
tests/tracker-human-split.test.ts; portfolio-analytics.test.ts covers
the new series field and the fallback.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJaXiqE9BDoNfoJBhfXroC
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

39 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 28 | LOW: 9

Severity Rule Location
HIGH tls-verification-disabled lib/onion.ts:47
HIGH secret-generic-credential lib/sp/platforms/facebook.ts:32
MEDIUM js-unescaped-html-sink app/(app)/dashboard/admin/email-broadcast/EmailBroadcastForm.tsx:125
MEDIUM js-unescaped-html-sink app/(app)/dashboard/projects/[id]/autoblog/articles/[articleId]/page.tsx:214
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:67
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:97
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:104
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:110
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:186
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:190
MEDIUM js-unescaped-html-sink app/c/[project]/[slug]/page.tsx:77
MEDIUM js-unescaped-html-sink app/c/[project]/page.tsx:57
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:228
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:285
MEDIUM js-unescaped-html-sink app/layout.tsx:129
MEDIUM js-open-redirect app/login/form.tsx:39
MEDIUM js-unescaped-html-sink app/r/[token]/page.tsx:176
MEDIUM js-open-redirect app/signup/form.tsx:43
MEDIUM js-open-redirect components/billing/buy-credits-modal.tsx:98
MEDIUM js-unescaped-html-sink components/json-ld.tsx:8
MEDIUM js-unescaped-html-sink components/report/markdown-view.tsx:15
MEDIUM js-unescaped-html-sink lib/careers/page-templates.ts:198
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:41
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:324
MEDIUM redos-nested-quantifier lib/lx/articleGen.ts:99
MEDIUM redos-nested-quantifier lib/tracker/agent-gate.ts:61
MEDIUM sh-remote-script-execution prober/deploy/provision.sh:30
MEDIUM sql-template-interpolation scripts/detect-slot-themes.ts:31
MEDIUM sql-template-interpolation scripts/purge-constructed-keywords.ts:163
MEDIUM sql-template-interpolation scripts/purge-offniche-keywords.ts:124
LOW secret-generic-credential app/(marketing)/docs/autoblog-webhook/page.tsx:145
LOW secret-generic-credential lib/sp/platforms/linkedin.ts:25
LOW js-dynamic-code-execution tests/careers-page-templates.test.ts:21
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:19
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:69
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:51
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:52
LOW secret-generic-credential tests/contract/posthog-integration.test.ts:13
LOW secret-generic-credential tests/lead-campaign.test.ts:16

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio marked this pull request as ready for review September 5, 2026 16:31
@ralyodio
ralyodio merged commit 3edaa04 into master Sep 5, 2026
10 checks passed
@ralyodio
ralyodio deleted the tracker-human-split branch September 5, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant