diff --git a/src/Navigation.tsx b/src/Navigation.tsx index de286ad9bb..a49ba5f248 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -1,4 +1,4 @@ -import {type JSX, useCallback, useEffect, useRef, useState} from 'react' +import {type JSX, useCallback, useRef} from 'react' import {Linking} from 'react-native' import * as Notifications from 'expo-notifications' import {i18n, type MessageDescriptor} from '@lingui/core' @@ -14,9 +14,7 @@ import { DefaultTheme, type LinkingOptions, NavigationContainer, - type NavigationState, StackActions, - useNavigation, } from '@react-navigation/native' import {timeout} from '#/lib/async/timeout' @@ -138,12 +136,8 @@ import { EmailDialogScreenID, useEmailDialogControl, } from '#/components/dialogs/EmailDialog' -import { - AnalyticsContext, - type AnalyticsContextType, - useAnalytics, - utils, -} from '#/analytics' +import {useAnalytics} from '#/analytics' +import {setNavigationMetadata} from '#/analytics/metadata' import {IS_NATIVE, IS_WEB} from '#/env' import {router} from '#/routes' import {Referrer} from '../modules/expo-bluesky-swiss-army' @@ -892,7 +886,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { const {currentAccount, accounts} = useSession() const {onPressSwitchAccount} = useAccountSwitcher() const {setShowLoggedOut} = useLoggedOutViewControls() - const prevLoggedRouteName = useRef(undefined) + const previousScreen = useRef(undefined) const emailDialogControl = useEmailDialogControl() const closeAllActiveElements = useCloseAllActiveElements() @@ -954,16 +948,10 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { const payload = getNotificationPayload(response.notification) if (payload) { - ax.metric( - 'notifications:openApp', - {reason: payload.reason, causedBoot: true}, - { - navigation: { - previousScreen: prevLoggedRouteName.current, - currentScreen: getCurrentRouteName(), - }, - }, - ) + ax.metric('notifications:openApp', { + reason: payload.reason, + causedBoot: true, + }) if (payload.reason === 'chat-message') { handleChatMessage(payload) @@ -988,19 +976,16 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { } const onNavigationReady = useCallOnce(() => { - prevLoggedRouteName.current = getCurrentRouteName() + const currentScreen = getCurrentRouteName() + setNavigationMetadata({ + previousScreen: currentScreen, + currentScreen, + }) + previousScreen.current = currentScreen + handlePushNotificationEntry() - ax.metric( - 'router:navigate', - {}, - { - navigation: { - previousScreen: prevLoggedRouteName.current, - currentScreen: getCurrentRouteName(), - }, - }, - ) + ax.metric('router:navigate', {}) if (currentAccount && shouldRequestEmailConfirmation(currentAccount)) { emailDialogControl.open({ @@ -1009,39 +994,21 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { snoozeEmailConfirmationPrompt() } - ax.metric( - 'init', - { - initMs: Math.round( - // @ts-ignore Emitted by Metro in the bundle prelude - performance.now() - global.__BUNDLE_START_TIME__, - ), - }, - { - navigation: { - previousScreen: prevLoggedRouteName.current, - currentScreen: getCurrentRouteName(), - }, - }, - ) + ax.metric('init', { + initMs: Math.round( + // @ts-ignore Emitted by Metro in the bundle prelude + performance.now() - global.__BUNDLE_START_TIME__, + ), + }) if (IS_WEB) { const referrerInfo = Referrer.getReferrerInfo() if (referrerInfo && referrerInfo.hostname !== 'bsky.app') { - ax.metric( - 'deepLink:referrerReceived', - { - to: window.location.href, - referrer: referrerInfo?.referrer, - hostname: referrerInfo?.hostname, - }, - { - navigation: { - previousScreen: prevLoggedRouteName.current, - currentScreen: getCurrentRouteName(), - }, - }, - ) + ax.metric('deepLink:referrerReceived', { + to: window.location.href, + referrer: referrerInfo?.referrer, + hostname: referrerInfo?.hostname, + }) } } }) @@ -1052,17 +1019,14 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { linking={LINKING} theme={theme} onStateChange={() => { - ax.metric( - 'router:navigate', - {from: prevLoggedRouteName.current}, - { - navigation: { - previousScreen: prevLoggedRouteName.current, - currentScreen: getCurrentRouteName(), - }, - }, - ) - prevLoggedRouteName.current = getCurrentRouteName() + const currentScreen = getCurrentRouteName() + // do this before metric + setNavigationMetadata({ + previousScreen: previousScreen.current, + currentScreen, + }) + ax.metric('router:navigate', {from: previousScreen.current}) + previousScreen.current = currentScreen }} onReady={onNavigationReady} // WARNING: Implicit navigation to nested navigators is depreciated in React Navigation 7.x @@ -1072,49 +1036,8 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { // We will need to confirm we handle nested navigators correctly by the time we migrate to React Navigation 8.x // -sfn navigationInChildEnabled> - {children} - - ) -} - -function getActiveRouteFromNavigationState(state?: NavigationState) { - if (!state) return undefined - const currentRoute = state?.routes[state.index] - return currentRoute.name -} - -function NavigationAnalyticsContext({children}: {children: React.ReactNode}) { - const nav = useNavigation() - const [previousScreen, setPreviousScreen] = useState( - () => getActiveRouteFromNavigationState(nav.getState()) ?? 'Home', - ) - const [metadata, setMetadata] = useState< - Pick - >(() => { - return { - navigation: { - previousScreen, - currentScreen: previousScreen, - }, - } - }) - useEffect(() => { - return nav.addListener('state', payload => { - const curr = - getActiveRouteFromNavigationState(payload.data.state) ?? 'Home' - setMetadata({ - navigation: { - previousScreen, - currentScreen: curr, - }, - }) - setPreviousScreen(curr) - }) - }, [nav, previousScreen]) - return ( - {children} - + ) } diff --git a/src/analytics/features/index.ts b/src/analytics/features/index.ts index 2edd75fed5..b28ba88474 100644 --- a/src/analytics/features/index.ts +++ b/src/analytics/features/index.ts @@ -1,6 +1,6 @@ import {GrowthBook} from '@growthbook/growthbook-react' -import {type Metadata} from '#/analytics/types' +import {type Metadata} from '#/analytics/metadata' import * as env from '#/env' export {Features} from '#/analytics/features/types' diff --git a/src/analytics/index.tsx b/src/analytics/index.tsx index 9208999150..7777dbbcbb 100644 --- a/src/analytics/index.tsx +++ b/src/analytics/index.tsx @@ -15,9 +15,13 @@ import { getInitialSessionId, useSessionId, } from '#/analytics/identifiers' +import { + getNavigationMetadata, + type MergeableMetadata, + type Metadata, +} from '#/analytics/metadata' import {type Metrics, metrics} from '#/analytics/metrics' import * as refParams from '#/analytics/misc/refParams' -import {type MergeableMetadata, type Metadata} from '#/analytics/types' import {getMetadataForLogger} from '#/analytics/utils' import * as env from '#/env' import {useGeolocation} from '#/geolocation' @@ -86,7 +90,10 @@ const Context = createContext({ if (metadata && '__meta' in metadata) { delete metadata.__meta } - metrics.track(event, payload, metadata) + metrics.track(event, payload, { + ...metadata, + navigation: getNavigationMetadata(), + }) }, metadata: { base: { diff --git a/src/analytics/metadata.ts b/src/analytics/metadata.ts new file mode 100644 index 0000000000..f097c17a76 --- /dev/null +++ b/src/analytics/metadata.ts @@ -0,0 +1,61 @@ +import {type Geolocation} from '#/geolocation' + +export type BaseMetadata = { + deviceId: string + sessionId: string + platform: string + appVersion: string + bundleIdentifier: string + bundleDate: number + referrerSrc: string + referrerUrl: string +} + +export type GeolocationMetadata = Geolocation + +export type SessionMetadata = { + did: string + isBskyPds: boolean +} + +export type PreferencesMetadata = { + appLanguage: string + contentLanguages: string[] +} + +export type MergeableMetadata = { + session?: SessionMetadata + preferences?: PreferencesMetadata + /** + * Navigation metadata is not actually available on this object, instead it's + * merged in at time-of-log/metric. See `#/analytics/metadata.ts` for details. + */ + navigation?: NavigationMetadata +} + +export type Metadata = { + base: BaseMetadata + geolocation: GeolocationMetadata +} & MergeableMetadata + +/* + * Navigation metadata is handle out-of-band from React, since we don't want to + * slow down screen transitions in any way, and there doesn't seem to be a nice + * way to get current navigation state without an additional re-render between + * navigations. + * + * So instead of this data being available on the Metadata object, it's stored + * here and merged in at time-of-log/metric. + */ +export type NavigationMetadata = { + previousScreen?: string + currentScreen?: string +} +let navigationMetadata: NavigationMetadata | undefined +export function getNavigationMetadata() { + console.log('metadata', JSON.stringify(navigationMetadata, null, 2)) + return navigationMetadata +} +export function setNavigationMetadata(meta: NavigationMetadata | undefined) { + navigationMetadata = meta +} diff --git a/src/analytics/metrics/client.ts b/src/analytics/metrics/client.ts index 7965dc5466..adff84ce50 100644 --- a/src/analytics/metrics/client.ts +++ b/src/analytics/metrics/client.ts @@ -52,7 +52,7 @@ export class MetricsClient> { logger.info(`event: ${event as string}`, { payload, - metadata, + __metadata__: metadata, // special logger field }) if (this.queue.length > this.maxBatchSize) { diff --git a/src/analytics/types.ts b/src/analytics/types.ts deleted file mode 100644 index 90f268a543..0000000000 --- a/src/analytics/types.ts +++ /dev/null @@ -1,40 +0,0 @@ -import {type Geolocation} from '#/geolocation' - -export type BaseMetadata = { - deviceId: string - sessionId: string - platform: string - appVersion: string - bundleIdentifier: string - bundleDate: number - referrerSrc: string - referrerUrl: string -} - -export type GeolocationMetadata = Geolocation - -export type NavigationMetadata = { - previousScreen?: string - currentScreen?: string -} - -export type SessionMetadata = { - did: string - isBskyPds: boolean -} - -export type PreferencesMetadata = { - appLanguage: string - contentLanguages: string[] -} - -export type MergeableMetadata = { - navigation?: NavigationMetadata - session?: SessionMetadata - preferences?: PreferencesMetadata -} - -export type Metadata = { - base: BaseMetadata - geolocation: GeolocationMetadata -} & MergeableMetadata diff --git a/src/analytics/utils.ts b/src/analytics/utils.ts index 941dcf5cd5..38f7528824 100644 --- a/src/analytics/utils.ts +++ b/src/analytics/utils.ts @@ -6,7 +6,7 @@ import { type MergeableMetadata, type Metadata, type SessionMetadata, -} from '#/analytics/types' +} from '#/analytics/metadata' /** * Thin `useMemo` wrapper that marks the metadata as memoized and provides a @@ -14,9 +14,9 @@ import { */ export function useMeta(metadata?: MergeableMetadata) { const m = useMemo(() => metadata, [metadata]) + if (!m) return // @ts-ignore m.__meta = true - console.log('useMeta', JSON.stringify(m, null, 2)) return m } diff --git a/src/logger/index.tsx b/src/logger/index.tsx index fc5b19c4fe..99ac79d4ed 100644 --- a/src/logger/index.tsx +++ b/src/logger/index.tsx @@ -33,7 +33,7 @@ export class Logger { level: LogLevel context: LogContext | undefined = undefined contextFilter: string = '' - inheritedMetadata: Record = {} + ambientMetadata: Record = {} protected debugContextRegexes: RegExp[] = [] protected transports: Transport[] = [] @@ -55,7 +55,7 @@ export class Logger { level, context, contextFilter, - metadata: inheritedMetadata = {}, + metadata: ambientMetadata = {}, }: { level?: LogLevel context?: LogContext @@ -65,7 +65,7 @@ export class Logger { this.context = context this.level = level || LogLevel.Info this.contextFilter = contextFilter || '' - this.inheritedMetadata = inheritedMetadata + this.ambientMetadata = ambientMetadata if (this.contextFilter) { this.level = LogLevel.Debug } @@ -122,8 +122,8 @@ export class Logger { const timestamp = Date.now() const meta: Metadata = { + __metadata__: this.ambientMetadata, ...metadata, - metadata: this.inheritedMetadata, } // send every log to syslog diff --git a/src/logger/types.ts b/src/logger/types.ts index 19e12c5045..ab9707b41b 100644 --- a/src/logger/types.ts +++ b/src/logger/types.ts @@ -49,6 +49,11 @@ export type Metadata = { */ __context__?: undefined + /** + * Reserved for inherited metadata gathered in ambient context + */ + __metadata__?: Record + /** * Applied as Sentry breadcrumb types. Defaults to `default`. *