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:
@@ -103,3 +103,20 @@ def test_admin_stats_games_funnel_aggregates(app_db):
|
||||
assert games["by_game"]["word"] == {"arrival": 2, "started": 1, "completed": 0, "shared": 1}
|
||||
assert games["by_game"]["match"]["completed"] == 1
|
||||
assert games["totals"]["arrival"] == 2 and games["totals"]["shared"] == 1
|
||||
|
||||
|
||||
def test_engaged_readers_metric(app_db):
|
||||
"""Engaged readers counts the gesture-gated 'engaged' beacon + deliberate actions,
|
||||
NOT auto-fired visit/summary_viewed or a game-share arrival."""
|
||||
app, db = app_db
|
||||
tc = TestClient(app, headers=_BROWSER)
|
||||
tc.post("/api/events", json={"kind": "engaged", "visitor": "a"}) # gesture beacon
|
||||
tc.post("/api/events", json={"kind": "source_click", "article_id": 5, "visitor": "b"}) # deliberate
|
||||
tc.post("/api/events", json={"kind": "visit", "visitor": "c"}) # raw visit only
|
||||
tc.post("/api/events", json={"kind": "summary_viewed", "article_id": 5, "visitor": "c"}) # auto-fired
|
||||
tc.post("/api/events", json={"kind": "word_arrival", "visitor": "d"}) # share-loop landing
|
||||
from goodnews.db import connect
|
||||
from goodnews import queries
|
||||
cn = connect(str(db)); v = queries.admin_stats(cn, days=30)["visitors"]; cn.close()
|
||||
assert v["engaged_today"] == 2 # a (engaged) + b (source_click)
|
||||
assert v["today"] == 1 # only c fired a raw visit
|
||||
|
||||
Reference in New Issue
Block a user