Add comprehensive visitor engagement tracking and scroll-to-top on navigation

Tracks scroll depth (25/50/75/90%), time on page, UTM parameters, outbound
link clicks, search queries, audio pause/completion/listen time, and 404s.
Logged-in study users are now tied to their visitor record and surfaced in
the admin recent visits table. Scroll position resets on every route change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-06-17 14:06:21 -04:00
parent a6f0c39c35
commit 31426fccba
11 changed files with 498 additions and 4 deletions
+27 -1
View File
@@ -13,11 +13,17 @@ import { useGlobalSearch } from './hooks/useGlobalSearch'
import { GlobalSearch } from './components/GlobalSearch'
import { EpisodeAudioPlayer } from './components/EpisodeAudioPlayer'
import './App.css'
import { sendEvent, useScrollDepthTracking, useTimeOnPage, useUTMCapture, useOutboundLinkTracking } from './analytics'
const CONSENT_KEY = 'vbn_analytics_consent_choice'
const HEADLINER_WIDGET_ID = 'WI_cmou3b4q7000701p0o9qmmcfj'
function ScrollToTop() {
const { pathname } = useLocation()
useEffect(() => { window.scrollTo(0, 0) }, [pathname])
return null
}
function usePageTracking() {
const location = useLocation()
useEffect(() => {
@@ -2308,6 +2314,10 @@ export default function App() {
const navigate = useNavigate()
const location = useLocation()
usePageTracking()
useScrollDepthTracking()
useTimeOnPage()
useUTMCapture()
useOutboundLinkTracking()
useEffect(() => {
fetch('/api/admin-content')
@@ -2375,6 +2385,7 @@ export default function App() {
return (
<>
<ScrollToTop />
<Routes>
<Route path="/" element={<LandingPage content={content} />} />
<Route path="/start-here" element={<StartHerePage content={content} />} />
@@ -2418,12 +2429,27 @@ export default function App() {
/>
)}
/>
<Route path="*" element={<NotFoundPage />} />
</Routes>
<BackToTopButton />
</>
)
}
function NotFoundPage() {
const location = useLocation()
useEffect(() => {
sendEvent('not_found', { path: location.pathname })
}, [location.pathname])
return (
<main style={{ padding: '4rem 2rem', textAlign: 'center' }}>
<h1>Page not found</h1>
<p>The page <code>{location.pathname}</code> doesn't exist.</p>
<Link to="/" style={{ marginTop: '1rem', display: 'inline-block' }}>← Back to home</Link>
</main>
)
}
function BackToTopButton() {
const [visible, setVisible] = useState(false)