Fix navigation context issue
This commit is contained in:
+36
-113
@@ -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 {Linking} from 'react-native'
|
||||||
import * as Notifications from 'expo-notifications'
|
import * as Notifications from 'expo-notifications'
|
||||||
import {i18n, type MessageDescriptor} from '@lingui/core'
|
import {i18n, type MessageDescriptor} from '@lingui/core'
|
||||||
@@ -14,9 +14,7 @@ import {
|
|||||||
DefaultTheme,
|
DefaultTheme,
|
||||||
type LinkingOptions,
|
type LinkingOptions,
|
||||||
NavigationContainer,
|
NavigationContainer,
|
||||||
type NavigationState,
|
|
||||||
StackActions,
|
StackActions,
|
||||||
useNavigation,
|
|
||||||
} from '@react-navigation/native'
|
} from '@react-navigation/native'
|
||||||
|
|
||||||
import {timeout} from '#/lib/async/timeout'
|
import {timeout} from '#/lib/async/timeout'
|
||||||
@@ -138,12 +136,8 @@ import {
|
|||||||
EmailDialogScreenID,
|
EmailDialogScreenID,
|
||||||
useEmailDialogControl,
|
useEmailDialogControl,
|
||||||
} from '#/components/dialogs/EmailDialog'
|
} from '#/components/dialogs/EmailDialog'
|
||||||
import {
|
import {useAnalytics} from '#/analytics'
|
||||||
AnalyticsContext,
|
import {setNavigationMetadata} from '#/analytics/metadata'
|
||||||
type AnalyticsContextType,
|
|
||||||
useAnalytics,
|
|
||||||
utils,
|
|
||||||
} from '#/analytics'
|
|
||||||
import {IS_NATIVE, IS_WEB} from '#/env'
|
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||||
import {router} from '#/routes'
|
import {router} from '#/routes'
|
||||||
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
import {Referrer} from '../modules/expo-bluesky-swiss-army'
|
||||||
@@ -892,7 +886,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
|||||||
const {currentAccount, accounts} = useSession()
|
const {currentAccount, accounts} = useSession()
|
||||||
const {onPressSwitchAccount} = useAccountSwitcher()
|
const {onPressSwitchAccount} = useAccountSwitcher()
|
||||||
const {setShowLoggedOut} = useLoggedOutViewControls()
|
const {setShowLoggedOut} = useLoggedOutViewControls()
|
||||||
const prevLoggedRouteName = useRef<string | undefined>(undefined)
|
const previousScreen = useRef<string | undefined>(undefined)
|
||||||
const emailDialogControl = useEmailDialogControl()
|
const emailDialogControl = useEmailDialogControl()
|
||||||
const closeAllActiveElements = useCloseAllActiveElements()
|
const closeAllActiveElements = useCloseAllActiveElements()
|
||||||
|
|
||||||
@@ -954,16 +948,10 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
|||||||
const payload = getNotificationPayload(response.notification)
|
const payload = getNotificationPayload(response.notification)
|
||||||
|
|
||||||
if (payload) {
|
if (payload) {
|
||||||
ax.metric(
|
ax.metric('notifications:openApp', {
|
||||||
'notifications:openApp',
|
reason: payload.reason,
|
||||||
{reason: payload.reason, causedBoot: true},
|
causedBoot: true,
|
||||||
{
|
})
|
||||||
navigation: {
|
|
||||||
previousScreen: prevLoggedRouteName.current,
|
|
||||||
currentScreen: getCurrentRouteName(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (payload.reason === 'chat-message') {
|
if (payload.reason === 'chat-message') {
|
||||||
handleChatMessage(payload)
|
handleChatMessage(payload)
|
||||||
@@ -988,19 +976,16 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onNavigationReady = useCallOnce(() => {
|
const onNavigationReady = useCallOnce(() => {
|
||||||
prevLoggedRouteName.current = getCurrentRouteName()
|
const currentScreen = getCurrentRouteName()
|
||||||
|
setNavigationMetadata({
|
||||||
|
previousScreen: currentScreen,
|
||||||
|
currentScreen,
|
||||||
|
})
|
||||||
|
previousScreen.current = currentScreen
|
||||||
|
|
||||||
handlePushNotificationEntry()
|
handlePushNotificationEntry()
|
||||||
|
|
||||||
ax.metric(
|
ax.metric('router:navigate', {})
|
||||||
'router:navigate',
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
navigation: {
|
|
||||||
previousScreen: prevLoggedRouteName.current,
|
|
||||||
currentScreen: getCurrentRouteName(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (currentAccount && shouldRequestEmailConfirmation(currentAccount)) {
|
if (currentAccount && shouldRequestEmailConfirmation(currentAccount)) {
|
||||||
emailDialogControl.open({
|
emailDialogControl.open({
|
||||||
@@ -1009,39 +994,21 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
|||||||
snoozeEmailConfirmationPrompt()
|
snoozeEmailConfirmationPrompt()
|
||||||
}
|
}
|
||||||
|
|
||||||
ax.metric(
|
ax.metric('init', {
|
||||||
'init',
|
initMs: Math.round(
|
||||||
{
|
// @ts-ignore Emitted by Metro in the bundle prelude
|
||||||
initMs: Math.round(
|
performance.now() - global.__BUNDLE_START_TIME__,
|
||||||
// @ts-ignore Emitted by Metro in the bundle prelude
|
),
|
||||||
performance.now() - global.__BUNDLE_START_TIME__,
|
})
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
navigation: {
|
|
||||||
previousScreen: prevLoggedRouteName.current,
|
|
||||||
currentScreen: getCurrentRouteName(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (IS_WEB) {
|
if (IS_WEB) {
|
||||||
const referrerInfo = Referrer.getReferrerInfo()
|
const referrerInfo = Referrer.getReferrerInfo()
|
||||||
if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
|
if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
|
||||||
ax.metric(
|
ax.metric('deepLink:referrerReceived', {
|
||||||
'deepLink:referrerReceived',
|
to: window.location.href,
|
||||||
{
|
referrer: referrerInfo?.referrer,
|
||||||
to: window.location.href,
|
hostname: referrerInfo?.hostname,
|
||||||
referrer: referrerInfo?.referrer,
|
})
|
||||||
hostname: referrerInfo?.hostname,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
navigation: {
|
|
||||||
previousScreen: prevLoggedRouteName.current,
|
|
||||||
currentScreen: getCurrentRouteName(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1052,17 +1019,14 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
|||||||
linking={LINKING}
|
linking={LINKING}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
onStateChange={() => {
|
onStateChange={() => {
|
||||||
ax.metric(
|
const currentScreen = getCurrentRouteName()
|
||||||
'router:navigate',
|
// do this before metric
|
||||||
{from: prevLoggedRouteName.current},
|
setNavigationMetadata({
|
||||||
{
|
previousScreen: previousScreen.current,
|
||||||
navigation: {
|
currentScreen,
|
||||||
previousScreen: prevLoggedRouteName.current,
|
})
|
||||||
currentScreen: getCurrentRouteName(),
|
ax.metric('router:navigate', {from: previousScreen.current})
|
||||||
},
|
previousScreen.current = currentScreen
|
||||||
},
|
|
||||||
)
|
|
||||||
prevLoggedRouteName.current = getCurrentRouteName()
|
|
||||||
}}
|
}}
|
||||||
onReady={onNavigationReady}
|
onReady={onNavigationReady}
|
||||||
// WARNING: Implicit navigation to nested navigators is depreciated in React Navigation 7.x
|
// 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
|
// We will need to confirm we handle nested navigators correctly by the time we migrate to React Navigation 8.x
|
||||||
// -sfn
|
// -sfn
|
||||||
navigationInChildEnabled>
|
navigationInChildEnabled>
|
||||||
<NavigationAnalyticsContext>{children}</NavigationAnalyticsContext>
|
|
||||||
</NavigationContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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<string | undefined>(
|
|
||||||
() => getActiveRouteFromNavigationState(nav.getState()) ?? 'Home',
|
|
||||||
)
|
|
||||||
const [metadata, setMetadata] = useState<
|
|
||||||
Pick<AnalyticsContextType['metadata'], 'navigation'>
|
|
||||||
>(() => {
|
|
||||||
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 (
|
|
||||||
<AnalyticsContext metadata={utils.useMeta(metadata)}>
|
|
||||||
{children}
|
{children}
|
||||||
</AnalyticsContext>
|
</NavigationContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {GrowthBook} from '@growthbook/growthbook-react'
|
import {GrowthBook} from '@growthbook/growthbook-react'
|
||||||
|
|
||||||
import {type Metadata} from '#/analytics/types'
|
import {type Metadata} from '#/analytics/metadata'
|
||||||
import * as env from '#/env'
|
import * as env from '#/env'
|
||||||
|
|
||||||
export {Features} from '#/analytics/features/types'
|
export {Features} from '#/analytics/features/types'
|
||||||
|
|||||||
@@ -15,9 +15,13 @@ import {
|
|||||||
getInitialSessionId,
|
getInitialSessionId,
|
||||||
useSessionId,
|
useSessionId,
|
||||||
} from '#/analytics/identifiers'
|
} from '#/analytics/identifiers'
|
||||||
|
import {
|
||||||
|
getNavigationMetadata,
|
||||||
|
type MergeableMetadata,
|
||||||
|
type Metadata,
|
||||||
|
} from '#/analytics/metadata'
|
||||||
import {type Metrics, metrics} from '#/analytics/metrics'
|
import {type Metrics, metrics} from '#/analytics/metrics'
|
||||||
import * as refParams from '#/analytics/misc/refParams'
|
import * as refParams from '#/analytics/misc/refParams'
|
||||||
import {type MergeableMetadata, type Metadata} from '#/analytics/types'
|
|
||||||
import {getMetadataForLogger} from '#/analytics/utils'
|
import {getMetadataForLogger} from '#/analytics/utils'
|
||||||
import * as env from '#/env'
|
import * as env from '#/env'
|
||||||
import {useGeolocation} from '#/geolocation'
|
import {useGeolocation} from '#/geolocation'
|
||||||
@@ -86,7 +90,10 @@ const Context = createContext<AnalyticsBaseContextType>({
|
|||||||
if (metadata && '__meta' in metadata) {
|
if (metadata && '__meta' in metadata) {
|
||||||
delete metadata.__meta
|
delete metadata.__meta
|
||||||
}
|
}
|
||||||
metrics.track(event, payload, metadata)
|
metrics.track(event, payload, {
|
||||||
|
...metadata,
|
||||||
|
navigation: getNavigationMetadata(),
|
||||||
|
})
|
||||||
},
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
base: {
|
base: {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -52,7 +52,7 @@ export class MetricsClient<M extends Record<string, any>> {
|
|||||||
|
|
||||||
logger.info(`event: ${event as string}`, {
|
logger.info(`event: ${event as string}`, {
|
||||||
payload,
|
payload,
|
||||||
metadata,
|
__metadata__: metadata, // special logger field
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.queue.length > this.maxBatchSize) {
|
if (this.queue.length > this.maxBatchSize) {
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
type MergeableMetadata,
|
type MergeableMetadata,
|
||||||
type Metadata,
|
type Metadata,
|
||||||
type SessionMetadata,
|
type SessionMetadata,
|
||||||
} from '#/analytics/types'
|
} from '#/analytics/metadata'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thin `useMemo` wrapper that marks the metadata as memoized and provides a
|
* Thin `useMemo` wrapper that marks the metadata as memoized and provides a
|
||||||
@@ -14,9 +14,9 @@ import {
|
|||||||
*/
|
*/
|
||||||
export function useMeta(metadata?: MergeableMetadata) {
|
export function useMeta(metadata?: MergeableMetadata) {
|
||||||
const m = useMemo(() => metadata, [metadata])
|
const m = useMemo(() => metadata, [metadata])
|
||||||
|
if (!m) return
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
m.__meta = true
|
m.__meta = true
|
||||||
console.log('useMeta', JSON.stringify(m, null, 2))
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export class Logger {
|
|||||||
level: LogLevel
|
level: LogLevel
|
||||||
context: LogContext | undefined = undefined
|
context: LogContext | undefined = undefined
|
||||||
contextFilter: string = ''
|
contextFilter: string = ''
|
||||||
inheritedMetadata: Record<string, unknown> = {}
|
ambientMetadata: Record<string, unknown> = {}
|
||||||
|
|
||||||
protected debugContextRegexes: RegExp[] = []
|
protected debugContextRegexes: RegExp[] = []
|
||||||
protected transports: Transport[] = []
|
protected transports: Transport[] = []
|
||||||
@@ -55,7 +55,7 @@ export class Logger {
|
|||||||
level,
|
level,
|
||||||
context,
|
context,
|
||||||
contextFilter,
|
contextFilter,
|
||||||
metadata: inheritedMetadata = {},
|
metadata: ambientMetadata = {},
|
||||||
}: {
|
}: {
|
||||||
level?: LogLevel
|
level?: LogLevel
|
||||||
context?: LogContext
|
context?: LogContext
|
||||||
@@ -65,7 +65,7 @@ export class Logger {
|
|||||||
this.context = context
|
this.context = context
|
||||||
this.level = level || LogLevel.Info
|
this.level = level || LogLevel.Info
|
||||||
this.contextFilter = contextFilter || ''
|
this.contextFilter = contextFilter || ''
|
||||||
this.inheritedMetadata = inheritedMetadata
|
this.ambientMetadata = ambientMetadata
|
||||||
if (this.contextFilter) {
|
if (this.contextFilter) {
|
||||||
this.level = LogLevel.Debug
|
this.level = LogLevel.Debug
|
||||||
}
|
}
|
||||||
@@ -122,8 +122,8 @@ export class Logger {
|
|||||||
|
|
||||||
const timestamp = Date.now()
|
const timestamp = Date.now()
|
||||||
const meta: Metadata = {
|
const meta: Metadata = {
|
||||||
|
__metadata__: this.ambientMetadata,
|
||||||
...metadata,
|
...metadata,
|
||||||
metadata: this.inheritedMetadata,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// send every log to syslog
|
// send every log to syslog
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ export type Metadata = {
|
|||||||
*/
|
*/
|
||||||
__context__?: undefined
|
__context__?: undefined
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reserved for inherited metadata gathered in ambient context
|
||||||
|
*/
|
||||||
|
__metadata__?: Record<string, unknown>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applied as Sentry breadcrumb types. Defaults to `default`.
|
* Applied as Sentry breadcrumb types. Defaults to `default`.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user