analytics: honest engagement metric — Engaged readers vs Recorded visits (Codex)

Admin now shows two numbers:
- Recorded visits: the existing raw count (one daily 'visit' beacon; still includes
  UA-spoofing bots that slip past the UA filter).
- Engaged readers: distinct visitor-day with DELIBERATE activity — either the new
  gesture-gated 'engaged' beacon (fires once/day only after ~8s visible AND a real
  scroll/pointer/key/touch) or a deliberate action (source_click, full_story, share,
  replace_used, paywall_replace, not_today/less_like_this/hide_topic, game start/
  complete/share). Explicitly EXCLUDES auto-fired visit/summary_viewed/open, replace_none,
  and game *_arrival (a share-loop landing, not engagement).

armEngaged() in analytics.js (wired in the global layout) + a mirrored vanilla-JS beacon
on the server-rendered /a/<id> share pages. 'engaged' added to the event allowlist and
fired with article_id=0 so the uniqueness constraint dedups it per day. queries.admin_stats
gains engaged_today/d7/d30. Bots are doubly excluded (UA filter at the beacon + the
gesture gate). Tests cover the metric (engaged + deliberate counted; visit/summary/arrival
not). 447 backend + 36 frontend tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-30 14:07:24 -04:00
parent 8a7606e20d
commit f416e13700
7 changed files with 103 additions and 5 deletions
+1
View File
@@ -644,6 +644,7 @@ _EVENT_KINDS = {
"share_ub", "copy_source", "native_share",
"not_today", "less_like_this", "hide_topic",
"replace_used", "replace_none", "paywall_replace", "paywalled_source_open",
"engaged", # genuine engagement: ~8s visible + a real gesture (vs. a raw visit)
"client_error", # boot-failure seatbelt beacon (blank-screen risk signal)
} | _GAME_EVENT_KINDS