diff --git a/src/analytics/PassiveAnalytics.tsx b/src/analytics/PassiveAnalytics.tsx new file mode 100644 index 0000000000..25dfea929a --- /dev/null +++ b/src/analytics/PassiveAnalytics.tsx @@ -0,0 +1,32 @@ +import {useEffect, useRef} from 'react' + +import {getCurrentState, onAppStateChange} from '#/lib/appState' +import {useAnalytics} from '#/analytics' + +/** + * Tracks passive analytics like app foreground/background time. + */ +export function PassiveAnalytics() { + const ax = useAnalytics() + const lastActive = useRef( + getCurrentState() === 'active' ? performance.now() : null, + ) + + useEffect(() => { + const sub = onAppStateChange(state => { + if (state === 'active') { + lastActive.current = performance.now() + ax.metric('state:foreground', {}) + } else if (lastActive.current !== null) { + ax.metric('state:background', { + secondsActive: Math.round( + (performance.now() - lastActive.current) / 1e3, + ), + }) + } + }) + return () => sub.remove() + }, [ax]) + + return null +} diff --git a/src/analytics/index.tsx b/src/analytics/index.tsx index 58deb4051b..95dd33705b 100644 --- a/src/analytics/index.tsx +++ b/src/analytics/index.tsx @@ -61,8 +61,17 @@ const Context = createContext({ }, }) +/** + * Ensures that deviceId is set and migrated from legacy storage. Handled on + * startup in `App..tsx`. This must be awaited prior to the app + * booting up. + */ export const setupDeviceId = getAndMigrateDeviceId() +/** + * Analytics context provider. Decorates the parent analytics context with + * additional metadata. Nesting should be done carefully and sparingly. + */ export function AnalyticsContext({ children, metadata, @@ -102,6 +111,11 @@ export function AnalyticsContext({ return {children} } +/** + * Feature gates provider. Decorates the parent analytics context with + * feature gate capabilities. Should be mounted within `AnalyticsContext`, + * and below the `` breaker in `App..tsx`. + */ export function AnalyticsFeaturesContext({ children, }: { @@ -133,10 +147,18 @@ export function AnalyticsFeaturesContext({ return {children} } +/** + * Basic analytics context without feature gates. Should really only be used + * above the `AnalyticsFeaturesContext` provider. + */ export function useAnalyticsBase() { return useContext(Context) } +/** + * The main analytics context, including feature gates. Use this everywhere you + * need metrics, features, or logging within the React tree. + */ export function useAnalytics() { const ctx = useContext(Context) if (!('feature' in ctx) || !('Features' in ctx)) { diff --git a/src/analytics/metrics/client.ts b/src/analytics/metrics/client.ts index c882636d97..2f7206e137 100644 --- a/src/analytics/metrics/client.ts +++ b/src/analytics/metrics/client.ts @@ -3,7 +3,7 @@ import {isNetworkError} from '#/lib/strings/errors' import {Sentry} from '#/logger/sentry/lib' import * as env from '#/env' -// TODO just fucken use logger in here +// TODO debug logging type Event> = { time: number diff --git a/src/analytics/metrics/index.ts b/src/analytics/metrics/index.ts index dc64ec6f63..42de5e72e9 100644 --- a/src/analytics/metrics/index.ts +++ b/src/analytics/metrics/index.ts @@ -3,18 +3,3 @@ import {type Events} from '#/analytics/metrics/types' export type {Events as Metrics} from '#/analytics/metrics/types' export const metrics = new MetricsClient() - -/* - * TODO -let lastActive = getCurrentState() === 'active' ? performance.now() : null -onAppStateChange(state => { - if (state === 'active') { - lastActive = performance.now() - metrics.track('state:foreground', {}) - } else if (lastActive !== null) { - metrics.track('state:background', { - secondsActive: Math.round((performance.now() - lastActive) / 1e3), - }) - } -}) -*/ diff --git a/src/analytics/metrics/types.ts b/src/analytics/metrics/types.ts index 057bf090c4..0d2119da92 100644 --- a/src/analytics/metrics/types.ts +++ b/src/analytics/metrics/types.ts @@ -1,3 +1,7 @@ +/* + * Do not import runtime code into this file + */ + import {type NotificationReason} from '#/lib/hooks/useNotificationHandler' import {type FeedDescriptor} from '#/state/queries/post-feed' import {type LiveEventFeedMetricContext} from '#/features/liveEvents/types' diff --git a/src/analytics/utils.ts b/src/analytics/utils.ts index be8b67c925..3638ae5f9a 100644 --- a/src/analytics/utils.ts +++ b/src/analytics/utils.ts @@ -4,6 +4,10 @@ import {BSKY_SERVICE} from '#/lib/constants' import {type SessionAccount} from '#/state/session' import {type MergeableMetadata, type SessionMetadata} from '#/analytics/types' +/** + * Thin `useMemo` wrapper that marks the metadata as memoized and provides a + * type guard. + */ export function useMeta(metadata: MergeableMetadata) { const m = useMemo(() => metadata, [metadata]) // @ts-ignore diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index a5e956fceb..38eeb5b75b 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -42,6 +42,7 @@ import {Outlet as PortalOutlet} from '#/components/Portal' import {useAgeAssurance} from '#/ageAssurance' import {NoAccessScreen} from '#/ageAssurance/components/NoAccessScreen' import {RedirectOverlay} from '#/ageAssurance/components/RedirectOverlay' +import {PassiveAnalytics} from '#/analytics/PassiveAnalytics' import {IS_ANDROID, IS_IOS} from '#/env' import {RoutesContainer, TabsNavigator} from '#/Navigation' import {BottomSheetOutlet} from '../../../modules/bottom-sheet' @@ -245,6 +246,8 @@ export function Shell() { )} + + ) } diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 1f687a1b98..28fed7152d 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -34,6 +34,7 @@ import {WelcomeModal} from '#/components/WelcomeModal' import {useAgeAssurance} from '#/ageAssurance' import {NoAccessScreen} from '#/ageAssurance/components/NoAccessScreen' import {RedirectOverlay} from '#/ageAssurance/components/RedirectOverlay' +import {PassiveAnalytics} from '#/analytics/PassiveAnalytics' import {FlatNavigator, RoutesContainer} from '#/Navigation' import {Composer} from './Composer.web' import {DrawerContent} from './Drawer' @@ -181,6 +182,8 @@ export function Shell() { )} + + ) }