[Statsig] Send Discover aggregate interactions (#4599)

zio/stable
dan 2024-06-22 03:54:47 +03:00 committed by GitHub
parent 7db8dd8980
commit 1715afd80e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 125 additions and 1 deletions

View File

@ -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': {}

View File

@ -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

View File

@ -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
}
}