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

View File

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

View File

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

View File

@ -1,15 +1,18 @@
import React from 'react' import React from 'react'
import { import {createClient} from '@segment/analytics-react'
createClient,
AnalyticsProvider,
useAnalytics as useAnalyticsOrig,
} from '@segment/analytics-react'
import {sha256} from 'js-sha256' import {sha256} from 'js-sha256'
import {TrackEvent, AnalyticsMethods} from './types'
import {useSession, SessionAccount} from '#/state/session' import {useSession, SessionAccount} from '#/state/session'
import {logger} from '#/logger' 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', writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
}, },
@ -21,45 +24,45 @@ 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 {hasSession} = useSession()
const methods = useAnalyticsOrig()
return React.useMemo(() => { return React.useMemo(() => {
if (hasSession) { 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 // dont send analytics pings for anonymous users
return { return {
screen: () => {}, screen: async () => {},
track: () => {}, track: async () => {},
identify: () => {},
flush: () => {},
group: () => {},
alias: () => {},
reset: () => {},
} }
}, [hasSession, methods]) }, [hasSession])
} }
export function init(account: SessionAccount | undefined) { export function init(account: SessionAccount | undefined) {
if (account) { if (account) {
if (account.did) { const client = getClient()
if (account.did) { if (account.did) {
const did_hashed = sha256(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') logger.debug('Ping w/hash')
} else { } else {
logger.debug('Ping w/o hash') 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, name: keyof ScreenPropertiesMap,
properties?: ScreenPropertiesMap[keyof ScreenPropertiesMap], properties?: ScreenPropertiesMap[keyof ScreenPropertiesMap],
) => Promise<void> ) => Promise<void>
interface TrackPropertiesMap { interface TrackPropertiesMap {
// LOGIN / SIGN UP events // LOGIN / SIGN UP events
'Sign In': {resumedSession: boolean} // CAN BE SERVER 'Sign In': {resumedSession: boolean} // CAN BE SERVER
@ -150,3 +151,8 @@ interface ScreenPropertiesMap {
MutedAccounts: {} MutedAccounts: {}
SavedFeeds: {} SavedFeeds: {}
} }
export type AnalyticsMethods = {
screen: ScreenEvent
track: TrackEvent
}

View File

@ -4,7 +4,7 @@ import {
FontAwesomeIconStyle, FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome' } from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native' 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 {useQueryClient} from '@tanstack/react-query'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'