| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778 |
- import { useEffect, useLayoutEffect, useRef } from 'react';
- import { AppState } from 'react-native';
- import { useGlobalSearchParams, usePathname } from 'expo-router';
- import { useCurrentUserStore } from 'app/context/user';
- import { parseTabKey } from 'app/lib/navigation/tab-path';
- import { resolveTabPageUrl } from 'app/components/nav/tab-page-url';
- import { installGlobalErrorCapture, sendStoredErrors } from './global-errors';
- import { setHealthView, startScreenLoad } from './health';
- import { setTrackerVisible, startTracker, trackPageview } from './tracker';
- /** A screen shown for less than this isn't a screen view (web waits as long for the title). */
- const SETTLE_MS = 400;
- /** `/view-post?id=5` from a UNA page URL or path (`https://site/view-post?id=5`, `view-post`). */
- function toPagePath(url: string): string {
- const absolute = /^https?:\/\/[^/]+(\/[^#]*)?/i.exec(url);
- if (absolute) return absolute[1] || '/';
- const path = url.split('#')[0] ?? '';
- return path.startsWith('/') ? path : `/${path}`;
- }
- /**
- * The UNA page a screen shows, resolved as the tab Screen resolves it (`url` param, the More
- * tab's last page, the tab's menu item). Other routes (`/pg`) are their own path.
- * `user` is the signed-in profile, `false` for a guest, `null` while unknown.
- */
- function getScreenPath(pathname: string, url: string | undefined, user: { url?: string } | false | null): string {
- const tabKey = parseTabKey(pathname);
- if (!tabKey) return pathname;
- if (user === null) return '';
- const page = resolveTabPageUrl(tabKey, url, user, pathname !== tabKey);
- return page ? toPagePath(page) : '';
- }
- /**
- * Sends screen views, engaged time, errors and vitals to UNA's analytics. Mounted once, in the
- * tabs layout. A screen's `screen_load` starts when its path changes and ends when its page is
- * laid out (`ScreenReadyMarker` in the page layout).
- */
- export function AnalyticsTracker() {
- const pathname = usePathname();
- const params = useGlobalSearchParams<{ url?: string | string[] }>();
- const url = Array.isArray(params.url) ? params.url[0] : params.url;
- const user = useCurrentUserStore((store) => store.currentUser);
- const screenPath = getScreenPath(pathname, url, user);
- const firstScreen = useRef(true);
- // A layout effect: it runs before the new screen's marker can report its layout.
- useLayoutEffect(() => {
- if (!screenPath) return;
- setHealthView(screenPath);
- if (firstScreen.current) firstScreen.current = false;
- else startScreenLoad();
- }, [screenPath]);
- useEffect(() => {
- // already installed in apps/expo/index.js; here in case that didn't run
- installGlobalErrorCapture();
- void sendStoredErrors();
- startTracker(AppState.currentState === 'active');
- // `inactive` (iOS app switcher, Control Center) passes quickly: only background counts as hidden
- const subscription = AppState.addEventListener('change', (appState) => {
- if (appState === 'active') setTrackerVisible(true);
- else if (appState === 'background') setTrackerVisible(false);
- });
- return () => subscription.remove();
- }, []);
- useEffect(() => {
- if (!screenPath) return undefined;
- // A screen left before this fires isn't counted, as on web: the tab root a deep
- // link lands on just before its page is pushed.
- const timer = setTimeout(() => trackPageview(screenPath), SETTLE_MS);
- return () => clearTimeout(timer);
- }, [screenPath]);
- return null;
- }
|