Skip to content

feat: classify and exclude bot traffic at click ingestion#31

Merged
onamfc merged 1 commit into
mainfrom
brandon/sit-298-bot-classification
Jul 1, 2026
Merged

feat: classify and exclude bot traffic at click ingestion#31
onamfc merged 1 commit into
mainfrom
brandon/sit-298-bot-classification

Conversation

@onamfc

@onamfc onamfc commented Jul 1, 2026

Copy link
Copy Markdown
Member

What

Detects bot/automated traffic at the moment a click is recorded and persists the classification on the click_events row, so analytics can exclude it consistently. Detection belongs at ingestion - that's the only point with the full request context (user-agent, method, headers), and classifying once means every consumer reads one answer instead of re-deriving it from a stored user-agent.

Changes

  • Schema: click_events gains is_bot BOOLEAN NOT NULL DEFAULT false and bot_reason VARCHAR(16) [1], plus a partial index (WHERE is_bot = false) for the common human-only analytics filter [2]. Idempotent ALTER for existing databases [3]; backward compatible (legacy rows default to false until the backfill runs).
  • lib/bot-detection.ts: classifies via (1) an optional trusted upstream signal - honored only when TRUST_EDGE_BOT_HEADER=true [1], (2) HTTP method (HEAD/OPTIONS are probes/prefetch, never human), (3) the isbot user-agent database [2]. Keep-and-flag, not drop - the row is still recorded and flagged, preserving an audit trail and enabling "% automated" reporting.
  • Wired into both click-recording paths (redirect [1] + SDK resolve [2]).
  • Analytics now excludes is_bot rows from all click counts [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11].
  • scripts/backfill-bot-flags.ts (npm run backfill:bot-flags [2]): one-time, keyset-paginated backfill that flags historical rows from their stored user-agent [1].

Behavior change (release note)

Analytics click counts now exclude detected bots - reported numbers will drop to the human-only figure. Run the backfill once to apply this retroactively to existing data.

Verification

  • 139 unit tests pass, including classifier cases (crawler UAs, HEAD/OPTIONS, edge signal, empty UA).
  • Integration check against Postgres: schema applies, flags persist (bot→true/ua, human→false/null), and the analytics filter excludes bots.

Bot detection belongs at ingestion, where the full request context exists and
the classification can be persisted once for every consumer to read — rather
than re-deriving it from a stored user-agent at read time.

- click_events: add is_bot (NOT NULL DEFAULT false) + bot_reason columns, with a
  partial index for the common human-only analytics filter. Idempotent ALTER for
  existing databases.
- lib/bot-detection.ts: classify each click via the isbot user-agent database,
  the HTTP method (HEAD/OPTIONS are probes, never human), and an optional trusted
  upstream bot signal (honored only when TRUST_EDGE_BOT_HEADER=true). Keep-and-
  flag, not drop — the row is recorded and flagged, preserving an audit trail.
- Wire classification into both click-recording paths (redirect + SDK resolve).
- Analytics: exclude is_bot rows from all click counts.
- scripts/backfill-bot-flags.ts: one-time, keyset-paginated backfill that flags
  historical rows from their stored user-agent (SIT-298).

Verified: 139 unit tests pass (incl. classifier cases) and an integration check
confirms the schema applies, flags persist, and analytics excludes bots.
@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 48.62385% with 56 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/lib/database.ts 0.00% 21 Missing ⚠️
src/routes/redirect.ts 7.14% 13 Missing ⚠️
src/routes/analytics.ts 0.00% 11 Missing ⚠️
src/routes/sdk.ts 8.33% 11 Missing ⚠️

📢 Thoughts on this report? Let us know!

@onamfc onamfc merged commit d9ddefa into main Jul 1, 2026
12 of 13 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 1, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant