analytics/examples/nextjs/analytics-provider.tsx
2026-01-29 08:20:58 -08:00

131 lines
4.5 KiB
TypeScript

/**
* AnalyticsProvider - Next.js client-side analytics wrapper
*
* Handles client-side initialization and automatic route tracking.
*/
'use client';
import { createContext, useContext, useEffect, useMemo, type ReactNode } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';
import { AnalyticsClient, type AnalyticsConfig } from '@analytics/client';
// ─────────────────────────────────────────────────────────────────────────────
// Context
// ─────────────────────────────────────────────────────────────────────────────
interface AnalyticsContextValue {
client: AnalyticsClient;
trackEvent: (type: string, action: string, metadata?: Record<string, unknown>) => void;
trackClick: (element: string, metadata?: Record<string, unknown>) => void;
identify: (userId: string, traits?: Record<string, unknown>) => void;
}
const AnalyticsContext = createContext<AnalyticsContextValue | null>(null);
// ─────────────────────────────────────────────────────────────────────────────
// Provider
// ─────────────────────────────────────────────────────────────────────────────
interface AnalyticsProviderProps {
children: ReactNode;
collectorUrl: string;
appName: string;
enabled?: boolean;
debug?: boolean;
}
export function AnalyticsProvider({
children,
collectorUrl,
appName,
enabled = true,
debug = false,
}: AnalyticsProviderProps) {
const pathname = usePathname();
const searchParams = useSearchParams();
// Initialize client once
const client = useMemo(() => {
const config: AnalyticsConfig = {
apiBaseUrl: collectorUrl,
appName,
enabled,
enableDebugLogging: debug,
autoCapture: {
pageViews: false, // We handle this manually for route changes
clicks: true,
scrollDepth: true,
performance: true,
},
};
return new AnalyticsClient(config);
}, [collectorUrl, appName, enabled, debug]);
// Track route changes
useEffect(() => {
if (!enabled) return;
const url = pathname + (searchParams?.toString() ? `?${searchParams.toString()}` : '');
client.trackEngagement({
type: 'navigation',
action: 'page_view',
metadata: {
path: pathname,
url,
referrer: typeof document !== 'undefined' ? document.referrer : undefined,
title: typeof document !== 'undefined' ? document.title : undefined,
},
});
}, [pathname, searchParams, client, enabled]);
// Build context value
const value = useMemo(
(): AnalyticsContextValue => ({
client,
trackEvent: (type, action, metadata) => {
client.trackEngagement({ type, action, metadata });
},
trackClick: (element, metadata) => {
client.trackEngagement({
type: 'click',
action: element,
metadata,
});
},
identify: (userId, traits) => {
client.identify(userId, traits);
},
}),
[client],
);
return <AnalyticsContext.Provider value={value}>{children}</AnalyticsContext.Provider>;
}
// ─────────────────────────────────────────────────────────────────────────────
// Hook
// ─────────────────────────────────────────────────────────────────────────────
export function useAnalytics(): AnalyticsContextValue {
const context = useContext(AnalyticsContext);
if (!context) {
throw new Error('useAnalytics must be used within an AnalyticsProvider');
}
return context;
}
/**
* Safe hook that returns null if not in provider (for optional tracking)
*/
export function useAnalyticsSafe(): AnalyticsContextValue | null {
return useContext(AnalyticsContext);
}