diff --git a/src/App.native.tsx b/src/App.native.tsx index 442d7fe5..9b928778 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -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,15 +70,13 @@ function InnerApp() { - - {/* All components should be within this provider */} - - - - - - - + {/* All components should be within this provider */} + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index 37115320..e29eec0d 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -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,15 +57,13 @@ function InnerApp() { - - {/* All components should be within this provider */} - - - - - - - + {/* All components should be within this provider */} + + + + + + diff --git a/src/lib/analytics/analytics.tsx b/src/lib/analytics/analytics.tsx index 1650dfd3..64b66904 100644 --- a/src/lib/analytics/analytics.tsx +++ b/src/lib/analytics/analytics.tsx @@ -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({ - writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI', - trackAppLifecycleEvents: false, - proxy: 'https://api.events.bsky.app/v1', -}) +// 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, - track: () => Promise, - identify: () => Promise, - flush: () => Promise, - group: () => Promise, - alias: () => Promise, - reset: () => Promise, + 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 ( - {children} - ) -} - async function writeAppInfo(value: AppInfo) { await AsyncStorage.setItem('BSKY_APP_INFO', JSON.stringify(value)) } diff --git a/src/lib/analytics/analytics.web.tsx b/src/lib/analytics/analytics.web.tsx index df03ee13..71c32d75 100644 --- a/src/lib/analytics/analytics.web.tsx +++ b/src/lib/analytics/analytics.web.tsx @@ -1,65 +1,68 @@ 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( - { - writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI', - }, - { - integrations: { - 'Segment.io': { - apiHost: 'api.events.bsky.app/v1', - }, - }, - }, -) -export const track = segmentClient?.track?.bind?.(segmentClient) +type SegmentClient = ReturnType -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 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) { + const client = getClient() if (account.did) { - if (account.did) { - const did_hashed = sha256(account.did) - segmentClient.identify(did_hashed, {did_hashed}) - logger.debug('Ping w/hash') - } else { - logger.debug('Ping w/o hash') - segmentClient.identify() - } + const did_hashed = sha256(account.did) + client.identify(did_hashed, {did_hashed}) + logger.debug('Ping w/hash') + } else { + logger.debug('Ping w/o hash') + client.identify() } } } - -export function Provider({children}: React.PropsWithChildren<{}>) { - return ( - {children} - ) -} diff --git a/src/lib/analytics/types.ts b/src/lib/analytics/types.ts index d677f321..9883cc36 100644 --- a/src/lib/analytics/types.ts +++ b/src/lib/analytics/types.ts @@ -7,6 +7,7 @@ export type ScreenEvent = ( name: keyof ScreenPropertiesMap, properties?: ScreenPropertiesMap[keyof ScreenPropertiesMap], ) => Promise + 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 +} diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index 3a7781ea..3624e656 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -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'