Analytics overhaul: privacy fixes, All Time range, readability improvements; v2.0.29
Publish Container / docker (push) Successful in 14m17s

- Remove raw IP storage; store only SHA-256 hash prefix in visitor records
- Non-blocking geo lookup: recordVisitor now synchronous, geo resolved in background
- UTM capture race condition fixed via sessionStorage pre-consent capture
- Add consent + bot checks to play endpoint (was missing)
- Add rate limiter to pageview endpoint (60 req/min per IP)
- Add 'All time' range option alongside 7d/30d/90d with trendData aggregation
- AnalyticsPanel: tabbed layout (Traffic/Engagement/System), dynamic chart titles,
  range buttons with active state, visitor log uses ipHash instead of raw IP,
  trendData drives main chart with weekly bucketing for all-time view
- Re-add prop-types dependency (required by react-simple-maps bundler)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-09-17 16:27:53 -04:00
1 parent da5c1c5078
commit 4002f1ee93
6 files changed
+333 -231

No files matched your search

+30 -14
View File
@@ -2,11 +2,28 @@ import { useEffect, useRef } from 'react'
import { useLocation } from 'react-router-dom'
const CONSENT_KEY = 'vbn_analytics_consent_choice'
const UTM_PENDING_KEY = 'vbn_pending_utm'
function hasConsent(): boolean {
return localStorage.getItem(CONSENT_KEY) === 'accepted'
}
// Capture UTM params immediately on module load, before consent is given.
// They're held in sessionStorage so they survive a consent-banner accept.
;(function captureUtmEarly() {
try {
const params = new URLSearchParams(window.location.search)
const utm: Record<string, string> = {}
for (const key of ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']) {
const val = params.get(key)
if (val) utm[key] = val
}
if (Object.keys(utm).length > 0) {
sessionStorage.setItem(UTM_PENDING_KEY, JSON.stringify({ path: window.location.pathname, ...utm }))
}
} catch { /* sessionStorage unavailable */ }
})()
export function sendEvent(type: string, data: Record<string, unknown>): void {
if (!hasConsent()) return
fetch('/api/analytics/event', {
@@ -69,22 +86,21 @@ export function useTimeOnPage() {
}, [location.pathname])
}
// --- UTM capture (runs once per page load) ---
// --- UTM capture ---
// UTM params are captured into sessionStorage before consent (see top of file).
// This hook drains sessionStorage and sends the event once consent is present.
// Running without a dep array so it retries after the consent banner triggers a re-render.
export function useUTMCapture() {
const captured = useRef(false)
useEffect(() => {
if (captured.current || !hasConsent()) return
const params = new URLSearchParams(window.location.search)
const utm: Record<string, string> = {}
for (const key of ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term']) {
const val = params.get(key)
if (val) utm[key] = val
}
if (Object.keys(utm).length === 0) return
captured.current = true
sendEvent('utm', { path: window.location.pathname, ...utm })
}, [])
if (!hasConsent()) return
try {
const raw = sessionStorage.getItem(UTM_PENDING_KEY)
if (!raw) return
const utm = JSON.parse(raw) as Record<string, string>
sessionStorage.removeItem(UTM_PENDING_KEY)
sendEvent('utm', utm)
} catch { /* ignore */ }
})
}
// --- All link clicks (internal + external) ---