Analytics overhaul: privacy fixes, All Time range, readability improvements; v2.0.29
Publish Container / docker (push) Successful in 14m17s
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:
1 parent
da5c1c5078
commit
4002f1ee93
6 files changed
+333
-231
No files matched your search
+30
-14
@@ -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) ---
|
||||
|
||||
Reference in new issue
Block a user