/** * Automatic screen tracking for Expo Router. * * Expo Router uses file-based routing and does not expose a * NavigationContainerRef, so we use the `usePathname()` hook to * detect route changes and fire pageview events automatically. * * ```tsx * // app/_layout.tsx * import { useDatalyrScreenTracking } from '@datalyr/react-native/expo'; * * export default function RootLayout() { * useDatalyrScreenTracking(); * return ; * } * ``` * * Screen names are the raw pathname (e.g. "/onboarding/paywall", "/(app)/chat"). * These are consistent and easy to filter in the Datalyr dashboard. */ import { useEffect, useRef } from 'react'; // --------------------------------------------------------------------------- // Configuration // --------------------------------------------------------------------------- export interface ExpoRouterTrackingConfig { /** * Map specific pathnames to friendly screen names. * Paths not in this map use the raw pathname (or `transformPathname` if set). * @example { '/onboarding/paywall': 'Paywall', '/(app)/chat': 'Chat' } */ screenNames?: Record; /** * Transform the pathname before tracking. * Applied only when the path is NOT in `screenNames`. * @example (path) => path.replace(/\(.*?\)\//g, '') // strip route groups */ transformPathname?: (pathname: string) => string; /** * Filter which paths should be tracked. * Return false to skip tracking for a given path. * @example (path) => !path.startsWith('/modal') */ shouldTrackPath?: (pathname: string) => boolean; } // --------------------------------------------------------------------------- // Core hook // --------------------------------------------------------------------------- /** * React hook that automatically tracks screen views when the Expo Router * pathname changes. Drop this into your root `_layout.tsx`. * * @param trackScreen The function that records a screen event. * Receives `(pathname, properties)`. * @param usePathname The `usePathname` hook from `expo-router`. * Passed in to avoid a hard dependency on expo-router. * @param config Optional filtering / transform config. */ export function useExpoRouterTracking( trackScreen: (screenName: string, properties?: Record) => Promise, usePathname: () => string, config?: ExpoRouterTrackingConfig, ): void { const pathname = usePathname(); const previousPathname = useRef(undefined); // Keep mutable refs so the effect always sees the latest values // without needing them in the dependency array (which would re-fire on every render). const trackScreenRef = useRef(trackScreen); trackScreenRef.current = trackScreen; const configRef = useRef(config); configRef.current = config; useEffect(() => { if (!pathname) return; if (previousPathname.current === pathname) return; const prevPath = previousPathname.current; previousPathname.current = pathname; const cfg = configRef.current; // Apply filter if (cfg?.shouldTrackPath && !cfg.shouldTrackPath(pathname)) return; // Resolve screen name: screenNames map → transformPathname → raw pathname const resolve = (path: string): string => cfg?.screenNames?.[path] ?? (cfg?.transformPathname ? cfg.transformPathname(path) : path); const screenName = resolve(pathname); const properties: Record = { source: 'auto_expo_router' }; if (prevPath) { properties.previous_screen = resolve(prevPath); } trackScreenRef.current(screenName, properties).catch(() => { // Silently ignore — SDK logs internally }); }, [pathname]); }