import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react'; import { AnalyticsClient } from './analytics-client'; import type { AnalyticsConfig, AnalyticsContext, ScrollDepth } from './types'; const AnalyticsContextInstance = createContext(null); interface AnalyticsProviderProps { config: AnalyticsConfig; children: React.ReactNode; } export const AnalyticsProvider: React.FC = ({ config, children, }) => { const client = useMemo(() => new AnalyticsClient(config), [config]); useEffect(() => { return () => { client.destroy(); }; }, [client]); const contextValue: AnalyticsContext = useMemo( () => ({ trackView: client.trackView.bind(client), trackEngagement: client.trackEngagement.bind(client), trackInteraction: client.trackInteraction.bind(client), flush: client.flush.bind(client), }), [client], ); // Automatic scroll tracking const scrollStateRef = useRef<{ reached: Set; pageLoadTime: number; }>({ reached: new Set(), pageLoadTime: Date.now() }); useEffect(() => { if (!config.scrollTracking?.enabled || typeof window === 'undefined') { return; } const thresholds = config.scrollTracking.thresholds ?? [25, 50, 75, 100]; const debounceMs = config.scrollTracking.debounceMs ?? 150; // Reset state on mount (handles route changes) scrollStateRef.current = { reached: new Set(), pageLoadTime: Date.now() }; let timeoutId: ReturnType | null = null; const calculateScrollDepth = (): number => { const scrollTop = window.scrollY || document.documentElement.scrollTop; const viewportHeight = window.innerHeight; const documentHeight = document.documentElement.scrollHeight; const scrollableHeight = documentHeight - viewportHeight; if (scrollableHeight <= 0) return 100; return Math.min(100, Math.round((scrollTop / scrollableHeight) * 100)); }; const handleScroll = () => { if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout(() => { const currentDepth = calculateScrollDepth(); const foldPosition = window.innerHeight; const isBeyondFold = window.scrollY > foldPosition; const { reached, pageLoadTime } = scrollStateRef.current; for (const threshold of thresholds) { if (currentDepth >= threshold && !reached.has(threshold)) { reached.add(threshold); client.trackInteraction({ type: 'scroll', data: { pageUrl: window.location.href, depth: threshold, timeToReachMs: Date.now() - pageLoadTime, isBeyondFold, }, }); } } }, debounceMs); }; window.addEventListener('scroll', handleScroll, { passive: true }); handleScroll(); // Check initial scroll position return () => { window.removeEventListener('scroll', handleScroll); if (timeoutId) clearTimeout(timeoutId); }; }, [client, config.scrollTracking]); return ( {children} ); }; export const useAnalytics = (): AnalyticsContext => { const context = useContext(AnalyticsContextInstance); if (!context) { throw new Error('useAnalytics must be used within an AnalyticsProvider'); } return context; };