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 usezio/stable
parent
a924df4dcd
commit
07fe058577
|
@ -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,15 +70,13 @@ 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}>
|
<TestCtrls />
|
||||||
<TestCtrls />
|
<Shell />
|
||||||
<Shell />
|
</GestureHandlerRootView>
|
||||||
</GestureHandlerRootView>
|
</RootSiblingParent>
|
||||||
</RootSiblingParent>
|
|
||||||
</AnalyticsProvider>
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</UnreadNotifsProvider>
|
</UnreadNotifsProvider>
|
||||||
</LoggedOutViewProvider>
|
</LoggedOutViewProvider>
|
||||||
|
|
|
@ -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,15 +57,13 @@ 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>
|
<Shell />
|
||||||
<Shell />
|
</SafeAreaProvider>
|
||||||
</SafeAreaProvider>
|
</RootSiblingParent>
|
||||||
</RootSiblingParent>
|
<ToastContainer />
|
||||||
<ToastContainer />
|
|
||||||
</AnalyticsProvider>
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</UnreadNotifsProvider>
|
</UnreadNotifsProvider>
|
||||||
</LoggedOutViewProvider>
|
</LoggedOutViewProvider>
|
||||||
|
|
|
@ -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.
|
||||||
writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
|
let segmentClient: SegmentClient | null = null
|
||||||
trackAppLifecycleEvents: false,
|
function getClient(): SegmentClient {
|
||||||
proxy: 'https://api.events.bsky.app/v1',
|
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 {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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,65 +1,68 @@
|
||||||
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>
|
||||||
{
|
|
||||||
writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
integrations: {
|
|
||||||
'Segment.io': {
|
|
||||||
apiHost: 'api.events.bsky.app/v1',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
export const track = segmentClient?.track?.bind?.(segmentClient)
|
|
||||||
|
|
||||||
export function useAnalytics() {
|
// Delay creating until first actual use.
|
||||||
|
let segmentClient: SegmentClient | null = null
|
||||||
|
function getClient(): SegmentClient {
|
||||||
|
if (!segmentClient) {
|
||||||
|
segmentClient = createClient(
|
||||||
|
{
|
||||||
|
writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
integrations: {
|
||||||
|
'Segment.io': {
|
||||||
|
apiHost: 'api.events.bsky.app/v1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return segmentClient
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const client = getClient()
|
||||||
if (account.did) {
|
if (account.did) {
|
||||||
if (account.did) {
|
const did_hashed = sha256(account.did)
|
||||||
const did_hashed = sha256(account.did)
|
client.identify(did_hashed, {did_hashed})
|
||||||
segmentClient.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')
|
client.identify()
|
||||||
segmentClient.identify()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
|
||||||
return (
|
|
||||||
<AnalyticsProvider client={segmentClient}>{children}</AnalyticsProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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'
|
||||||
|
|
Loading…
Reference in New Issue