about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authordan <dan.abramov@gmail.com>2024-06-22 03:54:47 +0300
committerGitHub <noreply@github.com>2024-06-22 03:54:47 +0300
commit1715afd80ed7d9de1f2d82befa04815015d34a3a (patch)
treebcc50f37a04e7567c9a7fd45de575b569a20447c /src
parent7db8dd8980c71e189315d89289196820db8b7875 (diff)
downloadvoidsky-1715afd80ed7d9de1f2d82befa04815015d34a3a.tar.zst
[Statsig] Send Discover aggregate interactions (#4599)
Diffstat (limited to 'src')
-rw-r--r--src/lib/statsig/events.ts16
-rw-r--r--src/lib/statsig/statsig.tsx3
-rw-r--r--src/state/feed-feedback.tsx107
3 files changed, 125 insertions, 1 deletions
diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts
index 0d77ec8a3..2e8cedb54 100644
--- a/src/lib/statsig/events.ts
+++ b/src/lib/statsig/events.ts
@@ -73,6 +73,22 @@ export type LogEvents = {
     feedType: string
     reason: 'pull-to-refresh' | 'soft-reset' | 'load-latest'
   }
+  'discover:showMore': {
+    feedContext: string
+  }
+  'discover:showLess': {
+    feedContext: string
+  }
+  'discover:clickthrough:sampled': {
+    count: number
+  }
+  'discover:engaged:sampled': {
+    count: number
+  }
+  'discover:seen:sampled': {
+    count: number
+  }
+
   'composer:gif:open': {}
   'composer:gif:select': {}
 
diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx
index b5a239c3a..94a1e63d0 100644
--- a/src/lib/statsig/statsig.tsx
+++ b/src/lib/statsig/statsig.tsx
@@ -115,6 +115,9 @@ const DOWNSAMPLED_EVENTS: Set<keyof LogEvents> = new Set([
   'home:feedDisplayed:sampled',
   'feed:endReached:sampled',
   'feed:refresh:sampled',
+  'discover:clickthrough:sampled',
+  'discover:engaged:sampled',
+  'discover:seen:sampled',
 ])
 const isDownsampledSession = Math.random() < 0.9 // 90% likely
 
diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx
index 64bdd4b89..88f50daca 100644
--- a/src/state/feed-feedback.tsx
+++ b/src/state/feed-feedback.tsx
@@ -4,6 +4,7 @@ import {AppBskyFeedDefs, BskyAgent} from '@atproto/api'
 import throttle from 'lodash.throttle'
 
 import {PROD_DEFAULT_FEED} from '#/lib/constants'
+import {logEvent} from '#/lib/statsig/statsig'
 import {logger} from '#/logger'
 import {
   FeedDescriptor,
@@ -34,6 +35,16 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
     WeakSet<FeedPostSliceItem | AppBskyFeedDefs.Interaction>
   >(new WeakSet())
 
+  const aggregatedStats = React.useRef<AggregatedStats | null>(null)
+  const throttledFlushAggregatedStats = React.useMemo(
+    () =>
+      throttle(() => flushToStatsig(aggregatedStats.current), 45e3, {
+        leading: true, // The outer call is already throttled somewhat.
+        trailing: true,
+      }),
+    [],
+  )
+
   const sendToFeedNoDelay = React.useCallback(() => {
     const proxyAgent = agent.withProxy(
       // @ts-ignore TODO need to update withProxy() to support this key -prf
@@ -45,12 +56,20 @@ export function useFeedFeedback(feed: FeedDescriptor, hasSession: boolean) {
     const interactions = Array.from(queue.current).map(toInteraction)
     queue.current.clear()
 
+    // Send to the feed
     proxyAgent.app.bsky.feed
       .sendInteractions({interactions})
       .catch((e: any) => {
         logger.warn('Failed to send feed interactions', {error: e})
       })
-  }, [agent])
+
+    // Send to Statsig
+    if (aggregatedStats.current === null) {
+      aggregatedStats.current = createAggregatedStats()
+    }
+    sendOrAggregateInteractionsForStats(aggregatedStats.current, interactions)
+    throttledFlushAggregatedStats()
+  }, [agent, throttledFlushAggregatedStats])
 
   const sendToFeed = React.useMemo(
     () =>
@@ -149,3 +168,89 @@ function toInteraction(str: string): AppBskyFeedDefs.Interaction {
   const [item, event, feedContext] = str.split('|')
   return {item, event, feedContext}
 }
+
+type AggregatedStats = {
+  clickthroughCount: number
+  engagedCount: number
+  seenCount: number
+}
+
+function createAggregatedStats(): AggregatedStats {
+  return {
+    clickthroughCount: 0,
+    engagedCount: 0,
+    seenCount: 0,
+  }
+}
+
+function sendOrAggregateInteractionsForStats(
+  stats: AggregatedStats,
+  interactions: AppBskyFeedDefs.Interaction[],
+) {
+  for (let interaction of interactions) {
+    switch (interaction.event) {
+      // Pressing "Show more" / "Show less" is relatively uncommon so we won't aggregate them.
+      // This lets us send the feed context together with them.
+      case 'app.bsky.feed.defs#requestLess': {
+        logEvent('discover:showLess', {
+          feedContext: interaction.feedContext ?? '',
+        })
+        break
+      }
+      case 'app.bsky.feed.defs#requestMore': {
+        logEvent('discover:showMore', {
+          feedContext: interaction.feedContext ?? '',
+        })
+        break
+      }
+
+      // The rest of the events are aggregated and sent later in batches.
+      case 'app.bsky.feed.defs#clickthroughAuthor':
+      case 'app.bsky.feed.defs#clickthroughEmbed':
+      case 'app.bsky.feed.defs#clickthroughItem':
+      case 'app.bsky.feed.defs#clickthroughReposter': {
+        stats.clickthroughCount++
+        break
+      }
+      case 'app.bsky.feed.defs#interactionLike':
+      case 'app.bsky.feed.defs#interactionQuote':
+      case 'app.bsky.feed.defs#interactionReply':
+      case 'app.bsky.feed.defs#interactionRepost':
+      case 'app.bsky.feed.defs#interactionShare': {
+        stats.engagedCount++
+        break
+      }
+      case 'app.bsky.feed.defs#interactionSeen': {
+        stats.seenCount++
+        break
+      }
+    }
+  }
+}
+
+function flushToStatsig(stats: AggregatedStats | null) {
+  if (stats === null) {
+    return
+  }
+
+  if (stats.clickthroughCount > 0) {
+    logEvent('discover:clickthrough:sampled', {
+      count: stats.clickthroughCount,
+    })
+    stats.clickthroughCount = 0
+  }
+
+  if (stats.engagedCount > 0) {
+    logEvent('discover:engaged:sampled', {
+      count: stats.engagedCount,
+    })
+    stats.engagedCount = 0
+  }
+
+  if (stats.seenCount > 0) {
+    logEvent('discover:seen:sampled', {
+      count: stats.seenCount,
+    })
+    stats.seenCount = 0
+  }
+}