Move analytics out of critical path (#2117)

* Remove analytics provider, simplify hook

* Fix wrong import being used by feed

* Remove early bind

* Create client lazy on first use
zio/stable
dan 2023-12-06 21:06:54 +00:00 committed by GitHub
parent a924df4dcd
commit 07fe058577
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 106 additions and 110 deletions

View File

@ -16,7 +16,6 @@ import {ThemeProvider} from 'lib/ThemeContext'
import {s} from 'lib/styles'
import {Shell} from 'view/shell'
import * as notifications from 'lib/notifications/notifications'
import {Provider as AnalyticsProvider} from 'lib/analytics/analytics'
import * as Toast from 'view/com/util/Toast'
import {queryClient} from 'lib/react-query'
import {TestCtrls} from 'view/com/testing/TestCtrls'
@ -71,7 +70,6 @@ function InnerApp() {
<LoggedOutViewProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={colorMode}>
<AnalyticsProvider>
{/* All components should be within this provider */}
<RootSiblingParent>
<GestureHandlerRootView style={s.h100pct}>
@ -79,7 +77,6 @@ function InnerApp() {
<Shell />
</GestureHandlerRootView>
</RootSiblingParent>
</AnalyticsProvider>
</ThemeProvider>
</UnreadNotifsProvider>
</LoggedOutViewProvider>

View File

@ -9,7 +9,6 @@ import 'view/icons'
import {init as initPersistedState} from '#/state/persisted'
import {useColorMode} from 'state/shell'
import {Provider as AnalyticsProvider} from 'lib/analytics/analytics'
import {Shell} from 'view/shell/index'
import {ToastContainer} from 'view/com/util/Toast.web'
import {ThemeProvider} from 'lib/ThemeContext'
@ -58,7 +57,6 @@ function InnerApp() {
<LoggedOutViewProvider>
<UnreadNotifsProvider>
<ThemeProvider theme={colorMode}>
<AnalyticsProvider>
{/* All components should be within this provider */}
<RootSiblingParent>
<SafeAreaProvider>
@ -66,7 +64,6 @@ function InnerApp() {
</SafeAreaProvider>
</RootSiblingParent>
<ToastContainer />
</AnalyticsProvider>
</ThemeProvider>
</UnreadNotifsProvider>
</LoggedOutViewProvider>

View File

@ -1,15 +1,10 @@
import React from 'react'
import {AppState, AppStateStatus} from 'react-native'
import AsyncStorage from '@react-native-async-storage/async-storage'
import {
createClient,
AnalyticsProvider,
useAnalytics as useAnalyticsOrig,
ClientMethods,
} from '@segment/analytics-react-native'
import {createClient, SegmentClient} from '@segment/analytics-react-native'
import {useSession, SessionAccount} from '#/state/session'
import {sha256} from 'js-sha256'
import {ScreenEvent, TrackEvent} from './types'
import {TrackEvent, AnalyticsMethods} from './types'
import {logger} from '#/logger'
type AppInfo = {
@ -19,53 +14,56 @@ type AppInfo = {
version?: string | undefined
}
const segmentClient = createClient({
// Delay creating until first actual use.
let segmentClient: SegmentClient | null = null
function getClient(): SegmentClient {
if (!segmentClient) {
segmentClient = createClient({
writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
trackAppLifecycleEvents: false,
proxy: 'https://api.events.bsky.app/v1',
})
})
}
return segmentClient
}
export const track = segmentClient?.track?.bind?.(segmentClient) as TrackEvent
export const track: TrackEvent = async (...args) => {
await getClient().track(...args)
}
export function useAnalytics() {
export function useAnalytics(): AnalyticsMethods {
const {hasSession} = useSession()
const methods: ClientMethods = useAnalyticsOrig()
return React.useMemo(() => {
if (hasSession) {
return {
screen: methods.screen as ScreenEvent, // ScreenEvents defines all the possible screen names
track: methods.track as TrackEvent, // TrackEvents defines all the possible track events and their properties
identify: methods.identify,
flush: methods.flush,
group: methods.group,
alias: methods.alias,
reset: methods.reset,
async screen(...args) {
await getClient().screen(...args)
},
async track(...args) {
await getClient().track(...args)
},
}
}
// dont send analytics pings for anonymous users
return {
screen: () => Promise<void>,
track: () => Promise<void>,
identify: () => Promise<void>,
flush: () => Promise<void>,
group: () => Promise<void>,
alias: () => Promise<void>,
reset: () => Promise<void>,
screen: async () => {},
track: async () => {},
}
}, [hasSession, methods])
}, [hasSession])
}
export function init(account: SessionAccount | undefined) {
setupListenersOnce()
if (account) {
const client = getClient()
if (account.did) {
const did_hashed = sha256(account.did)
segmentClient.identify(did_hashed, {did_hashed})
client.identify(did_hashed, {did_hashed})
logger.debug('Ping w/hash')
} else {
logger.debug('Ping w/o hash')
segmentClient.identify()
client.identify()
}
}
}
@ -80,12 +78,13 @@ function setupListenersOnce() {
// this is a copy of segment's own lifecycle event tracking
// we handle it manually to ensure that it never fires while the app is backgrounded
// -prf
segmentClient.isReady.onChange(async () => {
const client = getClient()
client.isReady.onChange(async () => {
if (AppState.currentState !== 'active') {
logger.debug('Prevented a metrics ping while the app was backgrounded')
return
}
const context = segmentClient.context.get()
const context = client.context.get()
if (typeof context?.app === 'undefined') {
logger.debug('Aborted metrics ping due to unavailable context')
return
@ -97,19 +96,19 @@ function setupListenersOnce() {
logger.debug('Recording app info', {new: newAppInfo, old: oldAppInfo})
if (typeof oldAppInfo === 'undefined') {
segmentClient.track('Application Installed', {
client.track('Application Installed', {
version: newAppInfo.version,
build: newAppInfo.build,
})
} else if (newAppInfo.version !== oldAppInfo.version) {
segmentClient.track('Application Updated', {
client.track('Application Updated', {
version: newAppInfo.version,
build: newAppInfo.build,
previous_version: oldAppInfo.version,
previous_build: oldAppInfo.build,
})
}
segmentClient.track('Application Opened', {
client.track('Application Opened', {
from_background: false,
version: newAppInfo.version,
build: newAppInfo.build,
@ -119,25 +118,19 @@ function setupListenersOnce() {
let lastState: AppStateStatus = AppState.currentState
AppState.addEventListener('change', (state: AppStateStatus) => {
if (state === 'active' && lastState !== 'active') {
const context = segmentClient.context.get()
segmentClient.track('Application Opened', {
const context = client.context.get()
client.track('Application Opened', {
from_background: true,
version: context?.app?.version,
build: context?.app?.build,
})
} else if (state !== 'active' && lastState === 'active') {
segmentClient.track('Application Backgrounded')
client.track('Application Backgrounded')
}
lastState = state
})
}
export function Provider({children}: React.PropsWithChildren<{}>) {
return (
<AnalyticsProvider client={segmentClient}>{children}</AnalyticsProvider>
)
}
async function writeAppInfo(value: AppInfo) {
await AsyncStorage.setItem('BSKY_APP_INFO', JSON.stringify(value))
}

View File

@ -1,15 +1,18 @@
import React from 'react'
import {
createClient,
AnalyticsProvider,
useAnalytics as useAnalyticsOrig,
} from '@segment/analytics-react'
import {createClient} from '@segment/analytics-react'
import {sha256} from 'js-sha256'
import {TrackEvent, AnalyticsMethods} from './types'
import {useSession, SessionAccount} from '#/state/session'
import {logger} from '#/logger'
const segmentClient = createClient(
type SegmentClient = ReturnType<typeof createClient>
// Delay creating until first actual use.
let segmentClient: SegmentClient | null = null
function getClient(): SegmentClient {
if (!segmentClient) {
segmentClient = createClient(
{
writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
},
@ -20,46 +23,46 @@ const segmentClient = createClient(
},
},
},
)
export const track = segmentClient?.track?.bind?.(segmentClient)
)
}
return segmentClient
}
export function useAnalytics() {
export const track: TrackEvent = async (...args) => {
await getClient().track(...args)
}
export function useAnalytics(): AnalyticsMethods {
const {hasSession} = useSession()
const methods = useAnalyticsOrig()
return React.useMemo(() => {
if (hasSession) {
return methods
return {
async screen(...args) {
await getClient().screen(...args)
},
async track(...args) {
await getClient().track(...args)
},
}
}
// dont send analytics pings for anonymous users
return {
screen: () => {},
track: () => {},
identify: () => {},
flush: () => {},
group: () => {},
alias: () => {},
reset: () => {},
screen: async () => {},
track: async () => {},
}
}, [hasSession, methods])
}, [hasSession])
}
export function init(account: SessionAccount | undefined) {
if (account) {
if (account.did) {
const client = getClient()
if (account.did) {
const did_hashed = sha256(account.did)
segmentClient.identify(did_hashed, {did_hashed})
client.identify(did_hashed, {did_hashed})
logger.debug('Ping w/hash')
} else {
logger.debug('Ping w/o hash')
segmentClient.identify()
}
client.identify()
}
}
}
export function Provider({children}: React.PropsWithChildren<{}>) {
return (
<AnalyticsProvider client={segmentClient}>{children}</AnalyticsProvider>
)
}

View File

@ -7,6 +7,7 @@ export type ScreenEvent = (
name: keyof ScreenPropertiesMap,
properties?: ScreenPropertiesMap[keyof ScreenPropertiesMap],
) => Promise<void>
interface TrackPropertiesMap {
// LOGIN / SIGN UP events
'Sign In': {resumedSession: boolean} // CAN BE SERVER
@ -150,3 +151,8 @@ interface ScreenPropertiesMap {
MutedAccounts: {}
SavedFeeds: {}
}
export type AnalyticsMethods = {
screen: ScreenEvent
track: TrackEvent
}

View File

@ -4,7 +4,7 @@ import {
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native'
import {useAnalytics} from '@segment/analytics-react-native'
import {useAnalytics} from 'lib/analytics/analytics'
import {useQueryClient} from '@tanstack/react-query'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'