about summary refs log tree commit diff
diff options
context:
space:
mode:
authordan <dan.abramov@gmail.com>2023-12-06 21:06:54 +0000
committerGitHub <noreply@github.com>2023-12-06 13:06:54 -0800
commit07fe0585775f1c9d89399b00dc1849a6d0c5d71f (patch)
tree1ef0968dbfbc9dffe25176ddb1ab666f74ebe898
parenta924df4dcd87b9963699643d04475dc89c926633 (diff)
downloadvoidsky-07fe0585775f1c9d89399b00dc1849a6d0c5d71f.tar.zst
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
-rw-r--r--src/App.native.tsx17
-rw-r--r--src/App.web.tsx17
-rw-r--r--src/lib/analytics/analytics.tsx85
-rw-r--r--src/lib/analytics/analytics.web.tsx87
-rw-r--r--src/lib/analytics/types.ts6
-rw-r--r--src/view/com/feeds/FeedPage.tsx2
6 files changed, 105 insertions, 109 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx
index 442d7fe58..9b9287789 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() {
       <LoggedOutViewProvider>
         <UnreadNotifsProvider>
           <ThemeProvider theme={colorMode}>
-            <AnalyticsProvider>
-              {/* All components should be within this provider */}
-              <RootSiblingParent>
-                <GestureHandlerRootView style={s.h100pct}>
-                  <TestCtrls />
-                  <Shell />
-                </GestureHandlerRootView>
-              </RootSiblingParent>
-            </AnalyticsProvider>
+            {/* All components should be within this provider */}
+            <RootSiblingParent>
+              <GestureHandlerRootView style={s.h100pct}>
+                <TestCtrls />
+                <Shell />
+              </GestureHandlerRootView>
+            </RootSiblingParent>
           </ThemeProvider>
         </UnreadNotifsProvider>
       </LoggedOutViewProvider>
diff --git a/src/App.web.tsx b/src/App.web.tsx
index 371153209..e29eec0dc 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() {
       <LoggedOutViewProvider>
         <UnreadNotifsProvider>
           <ThemeProvider theme={colorMode}>
-            <AnalyticsProvider>
-              {/* All components should be within this provider */}
-              <RootSiblingParent>
-                <SafeAreaProvider>
-                  <Shell />
-                </SafeAreaProvider>
-              </RootSiblingParent>
-              <ToastContainer />
-            </AnalyticsProvider>
+            {/* All components should be within this provider */}
+            <RootSiblingParent>
+              <SafeAreaProvider>
+                <Shell />
+              </SafeAreaProvider>
+            </RootSiblingParent>
+            <ToastContainer />
           </ThemeProvider>
         </UnreadNotifsProvider>
       </LoggedOutViewProvider>
diff --git a/src/lib/analytics/analytics.tsx b/src/lib/analytics/analytics.tsx
index 1650dfd3f..64b66904b 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<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))
 }
diff --git a/src/lib/analytics/analytics.web.tsx b/src/lib/analytics/analytics.web.tsx
index df03ee13c..71c32d758 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',
+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',
+      },
+      {
+        integrations: {
+          'Segment.io': {
+            apiHost: 'api.events.bsky.app/v1',
+          },
+        },
       },
-    },
-  },
-)
-export const track = segmentClient?.track?.bind?.(segmentClient)
+    )
+  }
+  return segmentClient
+}
+
+export const track: TrackEvent = async (...args) => {
+  await getClient().track(...args)
+}
 
-export function useAnalytics() {
+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 (
-    <AnalyticsProvider client={segmentClient}>{children}</AnalyticsProvider>
-  )
-}
diff --git a/src/lib/analytics/types.ts b/src/lib/analytics/types.ts
index d677f3217..9883cc36e 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<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
+}
diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx
index 3a7781ea1..3624e6565 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'