Admin step A: privacy-respecting first-party event logging
- events table (kind, article_id, visitor_hash, day) with a UNIQUE key that dedups to one row per visitor-day — caps volume and makes counts mean distinct visitor-days. NO ip/ua/referrer/url. Groupings derived from article_id at query time, never stored. - POST /api/events (public): whitelisted kinds (visit/open/share_ub/copy_source/ native_share/source_click); visitor token hashed server-side (never raw). - Frontend analytics.js: random localStorage visitor token; track() via sendBeacon; visit once/day; open on article click; share_ub/copy_source/native_share from the share menu; /a landing pages fire source_click. 127 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,21 @@ def render_share_page(article: dict, base_url: str, summary: str | None = None)
|
||||
}}
|
||||
go();
|
||||
}})();
|
||||
</script>"""
|
||||
|
||||
# Fire a privacy-respecting 'source_click' when the reader heads to the source
|
||||
# (reuses the SPA's anonymous visitor token from same-origin localStorage).
|
||||
src_click = f"""<script>
|
||||
(function(){{
|
||||
var b=document.querySelector('[data-src-click]'); if(!b) return;
|
||||
b.addEventListener('click',function(){{
|
||||
try{{
|
||||
var v=localStorage.getItem('goodnews:visitor')||'';
|
||||
var body=JSON.stringify({{kind:'source_click',article_id:{aid},visitor:v}});
|
||||
if(navigator.sendBeacon) navigator.sendBeacon('/api/events',new Blob([body],{{type:'application/json'}}));
|
||||
}}catch(e){{}}
|
||||
}});
|
||||
}})();
|
||||
</script>"""
|
||||
|
||||
return f"""<!doctype html>
|
||||
@@ -134,6 +149,7 @@ def render_share_page(article: dict, base_url: str, summary: str | None = None)
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
{src_click}
|
||||
{poll}
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
Reference in New Issue
Block a user