diff --git a/website2/src/app/layout.tsx b/website2/src/app/layout.tsx index fbaf6747e8..06c424892c 100644 --- a/website2/src/app/layout.tsx +++ b/website2/src/app/layout.tsx @@ -4,7 +4,7 @@ import localFont from 'next/font/local'; import { ReactNode, Suspense } from 'react'; import EngagementDialog from '@/components/dialogs/EngagementDialog'; -import { GoogleAnalytics } from '@/components/GoogleAnalytics'; +import GoogleAnalytics from '@/components/GoogleAnalytics'; import Loading from '@/components/loading'; import { ErrorBoundary } from '@/components/ui'; import { ReduxDataProvider } from '@/context/ReduxDataProvider'; @@ -38,11 +38,6 @@ export default async function RootLayout({ return ( - - {GA_MEASUREMENT_ID && ( - - )} - @@ -58,6 +53,9 @@ export default async function RootLayout({ + + {/* Initialize & Track Google Analytics */} + ); diff --git a/website2/src/components/GoogleAnalytics.tsx b/website2/src/components/GoogleAnalytics.tsx index cd0c12bff1..60166de11f 100644 --- a/website2/src/components/GoogleAnalytics.tsx +++ b/website2/src/components/GoogleAnalytics.tsx @@ -1,96 +1,94 @@ 'use client'; import { usePathname, useSearchParams } from 'next/navigation'; +import Script from 'next/script'; import { useEffect } from 'react'; declare global { interface Window { - gtag: ( - option: string, - gaTrackingId: string, - options: Record, - ) => void; - dataLayer: Record[]; + dataLayer?: Record[]; + gtag?: (...args: any[]) => void; } } interface GoogleAnalyticsProps { - measurementId: string; + measurementId?: string; } -// Validate measurement ID format -const isValidMeasurementId = (id: string): boolean => { - return /^G-[A-Z0-9]+$/.test(id); -}; - -export function GoogleAnalytics({ measurementId }: GoogleAnalyticsProps) { +/** + * Single component to initialize Google Analytics and + * track page views on route changes using the Next.js App Router. + */ +export default function GoogleAnalytics({ + measurementId, +}: GoogleAnalyticsProps) { const pathname = usePathname(); const searchParams = useSearchParams(); useEffect(() => { - if (!measurementId || !isValidMeasurementId(measurementId)) return; + if ( + typeof window === 'undefined' || + !measurementId || + typeof window.gtag === 'undefined' + ) { + return; + } - const url = pathname + searchParams.toString(); + // Construct page path with query strings (if any) + const pagePath = searchParams.toString() + ? `${pathname}?${searchParams.toString()}` + : pathname; - window.gtag?.('config', measurementId, { - page_path: url, + window.gtag('config', measurementId, { + page_path: pagePath, }); - }, [pathname, searchParams, measurementId]); + }, [measurementId, pathname, searchParams]); - if (!measurementId || !isValidMeasurementId(measurementId)) { - console.warn('Invalid or missing Google Analytics Measurement ID'); + if (!measurementId) { + // If you don't provide a measurement ID, no GA scripts will load return null; } return ( <> - ); } -// Safe event tracking utility with input validation -export const trackEvent = ( - action: string, - category: string, - label: string, - value?: number, -) => { - // Validate inputs - if (!action || typeof action !== 'string') return; - if (!category || typeof category !== 'string') return; - if (!label || typeof label !== 'string') return; - if (value !== undefined && typeof value !== 'number') return; - - // Sanitize inputs - const sanitizedAction = action.slice(0, 100); - const sanitizedCategory = category.slice(0, 100); - const sanitizedLabel = label.slice(0, 100); - - if (typeof window.gtag !== 'undefined') { - try { - window.gtag('event', sanitizedAction, { - event_category: sanitizedCategory, - event_label: sanitizedLabel, - value: value, - }); - } catch (error) { - console.error('Error tracking event:', error); - } +/** + * Example helper function to track custom GA events. + */ +export function trackEvent({ + action, + category, + label, + value, +}: { + action: string; + category: string; + label: string; + value?: number; +}) { + if (typeof window !== 'undefined' && typeof window.gtag !== 'undefined') { + window.gtag('event', action, { + event_category: category, + event_label: label, + value, + }); } -}; +}