Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[Statsig] Send Discover aggregate interactions (#4599)

authored by

dan and committed by
GitHub
1715afd8 7db8dd89

+125 -1
+16
src/lib/statsig/events.ts
··· 73 73 feedType: string 74 74 reason: 'pull-to-refresh' | 'soft-reset' | 'load-latest' 75 75 } 76 + 'discover:showMore': { 77 + feedContext: string 78 + } 79 + 'discover:showLess': { 80 + feedContext: string 81 + } 82 + 'discover:clickthrough:sampled': { 83 + count: number 84 + } 85 + 'discover:engaged:sampled': { 86 + count: number 87 + } 88 + 'discover:seen:sampled': { 89 + count: number 90 + } 91 + 76 92 'composer:gif:open': {} 77 93 'composer:gif:select': {} 78 94
+3
src/lib/statsig/statsig.tsx
··· 115 115 'home:feedDisplayed:sampled', 116 116 'feed:endReached:sampled', 117 117 'feed:refresh:sampled', 118 + 'discover:clickthrough:sampled', 119 + 'discover:engaged:sampled', 120 + 'discover:seen:sampled', 118 121 ]) 119 122 const isDownsampledSession = Math.random() < 0.9 // 90% likely 120 123
+106 -1
src/state/feed-feedback.tsx
··· 4 4 import throttle from 'lodash.throttle' 5 5 6 6 import {PROD_DEFAULT_FEED} from '#/lib/constants' 7 + import {logEvent} from '#/lib/statsig/statsig' 7 8 import {logger} from '#/logger' 8 9 import { 9 10 FeedDescriptor, ··· 33 34 // This assumes that referential identity of slice items maps 1:1 to feed (re)fetches. 34 35 WeakSet<FeedPostSliceItem | AppBskyFeedDefs.Interaction> 35 36 >(new WeakSet()) 37 + 38 + const aggregatedStats = React.useRef<AggregatedStats | null>(null) 39 + const throttledFlushAggregatedStats = React.useMemo( 40 + () => 41 + throttle(() => flushToStatsig(aggregatedStats.current), 45e3, { 42 + leading: true, // The outer call is already throttled somewhat. 43 + trailing: true, 44 + }), 45 + [], 46 + ) 36 47 37 48 const sendToFeedNoDelay = React.useCallback(() => { 38 49 const proxyAgent = agent.withProxy( ··· 45 56 const interactions = Array.from(queue.current).map(toInteraction) 46 57 queue.current.clear() 47 58 59 + // Send to the feed 48 60 proxyAgent.app.bsky.feed 49 61 .sendInteractions({interactions}) 50 62 .catch((e: any) => { 51 63 logger.warn('Failed to send feed interactions', {error: e}) 52 64 }) 53 - }, [agent]) 65 + 66 + // Send to Statsig 67 + if (aggregatedStats.current === null) { 68 + aggregatedStats.current = createAggregatedStats() 69 + } 70 + sendOrAggregateInteractionsForStats(aggregatedStats.current, interactions) 71 + throttledFlushAggregatedStats() 72 + }, [agent, throttledFlushAggregatedStats]) 54 73 55 74 const sendToFeed = React.useMemo( 56 75 () => ··· 149 168 const [item, event, feedContext] = str.split('|') 150 169 return {item, event, feedContext} 151 170 } 171 + 172 + type AggregatedStats = { 173 + clickthroughCount: number 174 + engagedCount: number 175 + seenCount: number 176 + } 177 + 178 + function createAggregatedStats(): AggregatedStats { 179 + return { 180 + clickthroughCount: 0, 181 + engagedCount: 0, 182 + seenCount: 0, 183 + } 184 + } 185 + 186 + function sendOrAggregateInteractionsForStats( 187 + stats: AggregatedStats, 188 + interactions: AppBskyFeedDefs.Interaction[], 189 + ) { 190 + for (let interaction of interactions) { 191 + switch (interaction.event) { 192 + // Pressing "Show more" / "Show less" is relatively uncommon so we won't aggregate them. 193 + // This lets us send the feed context together with them. 194 + case 'app.bsky.feed.defs#requestLess': { 195 + logEvent('discover:showLess', { 196 + feedContext: interaction.feedContext ?? '', 197 + }) 198 + break 199 + } 200 + case 'app.bsky.feed.defs#requestMore': { 201 + logEvent('discover:showMore', { 202 + feedContext: interaction.feedContext ?? '', 203 + }) 204 + break 205 + } 206 + 207 + // The rest of the events are aggregated and sent later in batches. 208 + case 'app.bsky.feed.defs#clickthroughAuthor': 209 + case 'app.bsky.feed.defs#clickthroughEmbed': 210 + case 'app.bsky.feed.defs#clickthroughItem': 211 + case 'app.bsky.feed.defs#clickthroughReposter': { 212 + stats.clickthroughCount++ 213 + break 214 + } 215 + case 'app.bsky.feed.defs#interactionLike': 216 + case 'app.bsky.feed.defs#interactionQuote': 217 + case 'app.bsky.feed.defs#interactionReply': 218 + case 'app.bsky.feed.defs#interactionRepost': 219 + case 'app.bsky.feed.defs#interactionShare': { 220 + stats.engagedCount++ 221 + break 222 + } 223 + case 'app.bsky.feed.defs#interactionSeen': { 224 + stats.seenCount++ 225 + break 226 + } 227 + } 228 + } 229 + } 230 + 231 + function flushToStatsig(stats: AggregatedStats | null) { 232 + if (stats === null) { 233 + return 234 + } 235 + 236 + if (stats.clickthroughCount > 0) { 237 + logEvent('discover:clickthrough:sampled', { 238 + count: stats.clickthroughCount, 239 + }) 240 + stats.clickthroughCount = 0 241 + } 242 + 243 + if (stats.engagedCount > 0) { 244 + logEvent('discover:engaged:sampled', { 245 + count: stats.engagedCount, 246 + }) 247 + stats.engagedCount = 0 248 + } 249 + 250 + if (stats.seenCount > 0) { 251 + logEvent('discover:seen:sampled', { 252 + count: stats.seenCount, 253 + }) 254 + stats.seenCount = 0 255 + } 256 + }