/** * Automatic screen tracking for React Navigation (v5+ / v6+). * * The simplest integration — just spread onto your NavigationContainer: * * ```tsx * import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native'; * import { datalyrScreenTracking } from '@datalyr/react-native'; * * const navigationRef = useNavigationContainerRef(); * const screenTracking = datalyrScreenTracking(navigationRef); * * * ``` * * Note: If you enable automatic screen tracking, avoid also calling * `Datalyr.screen()` / `datalyr.screen()` manually for the same screens, * as this will produce duplicate events. * * For Expo Router (file-based routing), automatic tracking is not needed. * Use the `datalyr.screen()` method in your layout files instead. */ import { debugLog, errorLog } from './utils'; // --------------------------------------------------------------------------- // Minimal React Navigation types — avoids adding @react-navigation as a // peer dependency. Only the methods we actually call are listed here. // --------------------------------------------------------------------------- /** Minimal subset of React Navigation's NavigationContainerRef that we need. */ export interface NavigationContainerRef { getCurrentRoute(): { name: string; params?: Record } | undefined; } // --------------------------------------------------------------------------- // Configuration // --------------------------------------------------------------------------- export interface ScreenTrackingConfig { /** * Transform the route name before tracking. * Useful for cleaning up or grouping screen names. * @example (name) => name.replace('Screen', '') */ transformScreenName?: (routeName: string) => string; /** * Filter which screens should be tracked. * Return false to skip tracking for a given screen. * @example (name) => !['Loading', 'Splash'].includes(name) */ shouldTrackScreen?: (routeName: string) => boolean; /** * Extract additional properties from the route to include in the screen event. * @example (name, params) => ({ product_id: params?.productId }) */ extractProperties?: (routeName: string, params?: Record) => Record; } // --------------------------------------------------------------------------- // Core implementation // --------------------------------------------------------------------------- /** * Create `onReady` and `onStateChange` callbacks that automatically * fire screen events through the Datalyr SDK whenever the active * React Navigation route changes. * * @param navigationRef A React Navigation `NavigationContainerRef` * (from `useNavigationContainerRef()` or * `createNavigationContainerRef()`). * @param trackScreen The function that records a screen event. * Pass `datalyr.screen.bind(datalyr)` or the * Datalyr static class's `Datalyr.screen`. * If omitted, the default singleton is used. * @param config Optional filtering / transform config. */ export function createScreenTrackingListeners( navigationRef: NavigationContainerRef, trackScreen: (screenName: string, properties?: Record) => Promise, config?: ScreenTrackingConfig, ): { onReady: () => void; onStateChange: () => void } { let currentRouteName: string | undefined; const resolveScreenName = (routeName: string): string => config?.transformScreenName ? config.transformScreenName(routeName) : routeName; const buildProperties = ( routeName: string, params?: Record, previousRouteName?: string, ): Record => { const props: Record = { source: 'auto_navigation' }; if (previousRouteName) { props.previous_screen = resolveScreenName(previousRouteName); } if (config?.extractProperties) { Object.assign(props, config.extractProperties(routeName, params)); } return props; }; const shouldTrack = (routeName: string): boolean => !config?.shouldTrackScreen || config.shouldTrackScreen(routeName); const safeTrack = (screenName: string, properties: Record) => { try { trackScreen(screenName, properties).catch((err) => { errorLog('Auto screen tracking failed:', err as Error); }); } catch (err) { errorLog('Auto screen tracking failed:', err as Error); } }; const onReady = () => { const route = navigationRef.getCurrentRoute(); if (!route) return; currentRouteName = route.name; if (shouldTrack(route.name)) { const screenName = resolveScreenName(route.name); safeTrack(screenName, buildProperties(route.name, route.params)); debugLog('Auto screen tracking: initial screen', screenName); } }; const onStateChange = () => { const route = navigationRef.getCurrentRoute(); if (!route) return; const previousRouteName = currentRouteName; currentRouteName = route.name; // Don't fire for same-screen param changes if (previousRouteName === currentRouteName) return; if (!shouldTrack(route.name)) return; const screenName = resolveScreenName(route.name); safeTrack(screenName, buildProperties(route.name, route.params, previousRouteName)); debugLog('Auto screen tracking: navigated to', screenName); }; return { onReady, onStateChange }; } // --------------------------------------------------------------------------- // Convenience wrapper — wires to the default Datalyr singleton // --------------------------------------------------------------------------- /** * Auto-wire screen tracking to the default Datalyr singleton. * This is the recommended API for most users. * * ```tsx * const navigationRef = useNavigationContainerRef(); * const screenTracking = datalyrScreenTracking(navigationRef); * * * ``` * * @param navigationRef React Navigation container ref * @param config Optional screen name transforms and filters */ export function datalyrScreenTracking( navigationRef: NavigationContainerRef, config?: ScreenTrackingConfig, ): { onReady: () => void; onStateChange: () => void } { // Lazily resolved reference to the SDK singleton. // We avoid a top-level import to prevent circular deps (index.ts re-exports this file). let sdk: { screen: (name: string, props?: Record) => Promise } | null = null; const getSdk = () => { if (!sdk) { // eslint-disable-next-line @typescript-eslint/no-var-requires sdk = (require('./datalyr-sdk') as { default: typeof sdk }).default; } return sdk!; }; return createScreenTrackingListeners( navigationRef, (screenName, properties) => getSdk().screen(screenName, properties), config, ); }