Bluesky app fork with some witchin' additions 💫

Standardize metadata for client events in feeds (#9653)

authored by

Alex Benzer and committed by
GitHub
2d8de1dd c540dae4

+194 -38
+14 -2
src/components/PostControls/BookmarkButton.tsx
··· 8 8 import {useCleanError} from '#/lib/hooks/useCleanError' 9 9 import {logger} from '#/logger' 10 10 import {type Shadow} from '#/state/cache/post-shadow' 11 + import {useFeedFeedbackContext} from '#/state/feed-feedback' 11 12 import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation' 12 13 import {useRequireAuth} from '#/state/session' 13 14 import {useTheme} from '#/alf' ··· 32 33 const {mutateAsync: bookmark} = useBookmarkMutation() 33 34 const cleanError = useCleanError() 34 35 const requireAuth = useRequireAuth() 36 + const {feedDescriptor} = useFeedFeedbackContext() 35 37 36 38 const {viewer} = post 37 39 const isBookmarked = !!viewer?.bookmarked ··· 50 52 post, 51 53 }) 52 54 53 - logger.metric('post:bookmark', {logContext}) 55 + logger.metric('post:bookmark', { 56 + uri: post.uri, 57 + authorDid: post.author.did, 58 + logContext, 59 + feedDescriptor, 60 + }) 54 61 55 62 toast.show( 56 63 <toast.Outer> ··· 85 92 uri: post.uri, 86 93 }) 87 94 88 - logger.metric('post:unbookmark', {logContext}) 95 + logger.metric('post:unbookmark', { 96 + uri: post.uri, 97 + authorDid: post.author.did, 98 + logContext, 99 + feedDescriptor, 100 + }) 89 101 90 102 toast.show( 91 103 <toast.Outer>
+26
src/components/PostControls/PostMenu/PostMenuItems.tsx
··· 98 98 richText, 99 99 threadgateRecord, 100 100 onShowLess, 101 + logContext, 101 102 }: { 102 103 testID: string 103 104 post: Shadow<AppBskyFeedDefs.PostView> ··· 111 112 timestamp: string 112 113 threadgateRecord?: AppBskyFeedThreadgate.Record 113 114 onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void 115 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 114 116 }): React.ReactNode => { 115 117 const {hasSession, currentAccount} = useSession() 116 118 const {_} = useLingui() ··· 210 212 try { 211 213 if (isThreadMuted) { 212 214 unmuteThread() 215 + logger.metric('post:unmute', { 216 + uri: postUri, 217 + authorDid: postAuthor.did, 218 + logContext, 219 + feedDescriptor: feedFeedback.feedDescriptor, 220 + }) 213 221 Toast.show(_(msg`You will now receive notifications for this thread`)) 214 222 } else { 215 223 muteThread() 224 + logger.metric('post:mute', { 225 + uri: postUri, 226 + authorDid: postAuthor.did, 227 + logContext, 228 + feedDescriptor: feedFeedback.feedDescriptor, 229 + }) 216 230 Toast.show( 217 231 _(msg`You will no longer receive notifications for this thread`), 218 232 ) ··· 272 286 feedContext: postFeedContext, 273 287 reqId: postReqId, 274 288 }) 289 + logger.metric('post:showMore', { 290 + uri: postUri, 291 + authorDid: postAuthor.did, 292 + logContext, 293 + feedDescriptor: feedFeedback.feedDescriptor, 294 + }) 275 295 Toast.show( 276 296 _(msg({message: 'Feedback sent to feed operator', context: 'toast'})), 277 297 ) ··· 283 303 item: postUri, 284 304 feedContext: postFeedContext, 285 305 reqId: postReqId, 306 + }) 307 + logger.metric('post:showLess', { 308 + uri: postUri, 309 + authorDid: postAuthor.did, 310 + logContext, 311 + feedDescriptor: feedFeedback.feedDescriptor, 286 312 }) 287 313 if (onShowLess) { 288 314 onShowLess({
+3
src/components/PostControls/PostMenu/index.tsx
··· 29 29 threadgateRecord, 30 30 onShowLess, 31 31 hitSlop, 32 + logContext, 32 33 }: { 33 34 testID: string 34 35 post: Shadow<AppBskyFeedDefs.PostView> ··· 41 42 threadgateRecord?: AppBskyFeedThreadgate.Record 42 43 onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void 43 44 hitSlop?: Insets 45 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 44 46 }): React.ReactNode => { 45 47 const {_} = useLingui() 46 48 ··· 87 89 timestamp={timestamp} 88 90 threadgateRecord={threadgateRecord} 89 91 onShowLess={onShowLess} 92 + logContext={logContext} 90 93 /> 91 94 )} 92 95 </Menu.Root>
+21 -3
src/components/PostControls/ShareMenu/index.tsx
··· 16 16 import {toShareUrl} from '#/lib/strings/url-helpers' 17 17 import {logger} from '#/logger' 18 18 import {type Shadow} from '#/state/cache/post-shadow' 19 + import {useFeedFeedbackContext} from '#/state/feed-feedback' 19 20 import {EventStopper} from '#/view/com/util/EventStopper' 20 21 import {native} from '#/alf' 21 22 import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox' ··· 35 36 threadgateRecord, 36 37 onShare, 37 38 hitSlop, 39 + logContext, 38 40 }: { 39 41 testID: string 40 42 post: Shadow<AppBskyFeedDefs.PostView> ··· 45 47 threadgateRecord?: AppBskyFeedThreadgate.Record 46 48 onShare: () => void 47 49 hitSlop?: Insets 50 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 48 51 }): React.ReactNode => { 49 52 const {_} = useLingui() 50 53 const gate = useGate() 54 + const {feedDescriptor} = useFeedFeedbackContext() 51 55 52 56 const ShareIcon = gate('alt_share_icon') 53 57 ? ArrowShareRightIcon ··· 65 69 setTimeout(menuControl.open) 66 70 67 71 logger.metric( 68 - 'share:open', 69 - {context: big ? 'thread' : 'feed'}, 72 + 'post:share', 73 + { 74 + uri: post.uri, 75 + authorDid: post.author.did, 76 + logContext, 77 + feedDescriptor, 78 + postContext: big ? 'thread' : 'feed', 79 + }, 70 80 {statsig: true}, 71 81 ) 72 82 }, 73 83 }), 74 - [menuControl, setHasBeenOpen, big], 84 + [ 85 + menuControl, 86 + setHasBeenOpen, 87 + big, 88 + logContext, 89 + feedDescriptor, 90 + post.uri, 91 + post.author.did, 92 + ], 75 93 ) 76 94 77 95 const onNativeLongPress = () => {
+19 -1
src/components/PostControls/index.tsx
··· 13 13 import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon' 14 14 import {useHaptics} from '#/lib/haptics' 15 15 import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 16 + import {logger} from '#/logger' 16 17 import {type Shadow} from '#/state/cache/types' 17 18 import {useFeedFeedbackContext} from '#/state/feed-feedback' 18 19 import { ··· 174 175 feedContext, 175 176 reqId, 176 177 }) 178 + logger.metric('post:clickQuotePost', { 179 + uri: post.uri, 180 + authorDid: post.author.did, 181 + logContext, 182 + feedDescriptor, 183 + }) 177 184 openComposer({ 178 185 quote: post, 179 186 onPost: onPostReply, ··· 217 224 testID="replyBtn" 218 225 onPress={ 219 226 !replyDisabled 220 - ? () => requireAuth(() => onPressReply()) 227 + ? () => 228 + requireAuth(() => { 229 + logger.metric('post:clickReply', { 230 + uri: post.uri, 231 + authorDid: post.author.did, 232 + logContext, 233 + feedDescriptor, 234 + }) 235 + onPressReply() 236 + }) 221 237 : undefined 222 238 } 223 239 label={_( ··· 315 331 left: secondaryControlSpacingStyles.gap / 2, 316 332 right: secondaryControlSpacingStyles.gap / 2, 317 333 }} 334 + logContext={logContext} 318 335 /> 319 336 <PostMenuButton 320 337 testID="postDropdownBtn" ··· 330 347 hitSlop={{ 331 348 left: secondaryControlSpacingStyles.gap / 2, 332 349 }} 350 + logContext={logContext} 333 351 /> 334 352 </View> 335 353 </View>
+77 -9
src/logger/metrics.ts
··· 176 176 'feed:suggestion:press': { 177 177 feedUrl: string 178 178 } 179 - 'feed:showMore': { 180 - feed: string 181 - feedContext: string 179 + 'post:showMore': { 180 + uri: string 181 + authorDid: string 182 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 183 + feedDescriptor?: string 184 + position?: number 182 185 } 183 - 'feed:showLess': { 184 - feed: string 185 - feedContext: string 186 + 'post:showLess': { 187 + uri: string 188 + authorDid: string 189 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 190 + feedDescriptor?: string 191 + position?: number 186 192 } 187 193 'feed:clickthrough': { 188 194 feed: string ··· 257 263 logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 258 264 feedDescriptor?: string 259 265 } 260 - 'post:mute': {} 261 - 'post:unmute': {} 266 + 'post:mute': { 267 + uri: string 268 + authorDid: string 269 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 270 + feedDescriptor?: string 271 + position?: number 272 + } 273 + 'post:unmute': { 274 + uri: string 275 + authorDid: string 276 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 277 + feedDescriptor?: string 278 + position?: number 279 + } 262 280 'post:pin': {} 263 281 'post:unpin': {} 264 282 'post:bookmark': { 283 + uri: string 284 + authorDid: string 265 285 logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 286 + feedDescriptor?: string 287 + position?: number 266 288 } 267 289 'post:unbookmark': { 290 + uri: string 291 + authorDid: string 268 292 logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 293 + feedDescriptor?: string 294 + position?: number 295 + } 296 + 'post:clickReply': { 297 + uri: string 298 + authorDid: string 299 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 300 + feedDescriptor?: string 301 + position?: number 302 + } 303 + 'post:clickQuotePost': { 304 + uri: string 305 + authorDid: string 306 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 307 + feedDescriptor?: string 308 + position?: number 309 + } 310 + 'post:clickthroughAuthor': { 311 + uri: string 312 + authorDid: string 313 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 314 + feedDescriptor?: string 315 + position?: number 316 + } 317 + 'post:clickthroughItem': { 318 + uri: string 319 + authorDid: string 320 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 321 + feedDescriptor?: string 322 + position?: number 323 + } 324 + 'post:clickthroughEmbed': { 325 + uri: string 326 + authorDid: string 327 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 328 + feedDescriptor?: string 329 + position?: number 269 330 } 270 331 'post:view': { 271 332 uri: string ··· 565 626 'live:view:profile': {subject: string} 566 627 'live:view:post': {subject: string; feed?: string} 567 628 568 - 'share:open': {context: 'feed' | 'thread'} 629 + 'post:share': { 630 + uri: string 631 + authorDid: string 632 + logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 633 + feedDescriptor?: string 634 + postContext: 'feed' | 'thread' 635 + position?: number 636 + } 569 637 'share:press:copyLink': {} 570 638 'share:press:nativeShare': {} 571 639 'share:press:openDmSearch': {}
+12
src/screens/PostThread/components/ThreadItemAnchor.tsx
··· 281 281 ]) 282 282 283 283 const onOpenAuthor = () => { 284 + logger.metric('post:clickthroughAuthor', { 285 + uri: post.uri, 286 + authorDid: post.author.did, 287 + logContext: 'PostThreadItem', 288 + feedDescriptor: feedFeedback.feedDescriptor, 289 + }) 284 290 if (postSource) { 285 291 feedFeedback.sendInteraction({ 286 292 item: post.uri, ··· 292 298 } 293 299 294 300 const onOpenEmbed = () => { 301 + logger.metric('post:clickthroughEmbed', { 302 + uri: post.uri, 303 + authorDid: post.author.did, 304 + logContext: 'PostThreadItem', 305 + feedDescriptor: feedFeedback.feedDescriptor, 306 + }) 295 307 if (postSource) { 296 308 feedFeedback.sendInteraction({ 297 309 item: post.uri,
+1 -20
src/state/feed-feedback.tsx
··· 137 137 sendOrAggregateInteractionsForStats( 138 138 aggregatedStats.current, 139 139 interactionsToSend, 140 - feed?.feedDescriptor ?? 'unknown', 141 140 ) 142 141 throttledFlushAggregatedStats() 143 142 logger.debug('flushed') ··· 274 273 function sendOrAggregateInteractionsForStats( 275 274 stats: AggregatedStats, 276 275 interactions: AppBskyFeedDefs.Interaction[], 277 - feed: string, 278 276 ) { 279 277 for (let interaction of interactions) { 280 278 switch (interaction.event) { 281 - // Pressing "Show more" / "Show less" is relatively uncommon so we won't aggregate them. 282 - // This lets us send the feed context together with them. 283 - case 'app.bsky.feed.defs#requestLess': { 284 - logger.metric('feed:showLess', { 285 - feed, 286 - feedContext: interaction.feedContext ?? '', 287 - }) 288 - break 289 - } 290 - case 'app.bsky.feed.defs#requestMore': { 291 - logger.metric('feed:showMore', { 292 - feed, 293 - feedContext: interaction.feedContext ?? '', 294 - }) 295 - break 296 - } 297 - 298 - // The rest of the events are aggregated and sent later in batches. 279 + // The events are aggregated and sent later in batches. 299 280 case 'app.bsky.feed.defs#clickthroughAuthor': 300 281 case 'app.bsky.feed.defs#clickthroughEmbed': 301 282 case 'app.bsky.feed.defs#clickthroughItem':
-2
src/state/queries/post.ts
··· 373 373 {uri: string} // the root post's uri 374 374 >({ 375 375 mutationFn: ({uri}) => { 376 - logger.metric('post:mute', {}) 377 376 return agent.api.app.bsky.graph.muteThread({root: uri}) 378 377 }, 379 378 }) ··· 383 382 const agent = useAgent() 384 383 return useMutation<{}, Error, {uri: string}>({ 385 384 mutationFn: ({uri}) => { 386 - logger.metric('post:unmute', {}) 387 385 return agent.api.app.bsky.graph.unmuteThread({root: uri}) 388 386 }, 389 387 })
+21 -1
src/view/com/posts/PostFeedItem.tsx
··· 21 21 import {type NavigationProp} from '#/lib/routes/types' 22 22 import {useGate} from '#/lib/statsig/statsig' 23 23 import {countLines} from '#/lib/strings/helpers' 24 + import {logger} from '#/logger' 24 25 import { 25 26 POST_TOMBSTONE, 26 27 type Shadow, ··· 173 174 const urip = new AtUri(post.uri) 174 175 return [makeProfileLink(post.author, 'post', urip.rkey), urip.rkey] 175 176 }, [post.uri, post.author]) 176 - const {sendInteraction, feedSourceInfo} = useFeedFeedbackContext() 177 + const {sendInteraction, feedSourceInfo, feedDescriptor} = 178 + useFeedFeedbackContext() 177 179 178 180 const onPressReply = () => { 179 181 sendInteraction({ ··· 209 211 feedContext, 210 212 reqId, 211 213 }) 214 + logger.metric('post:clickthroughAuthor', { 215 + uri: post.uri, 216 + authorDid: post.author.did, 217 + logContext: 'FeedItem', 218 + feedDescriptor, 219 + }) 212 220 } 213 221 214 222 const onOpenReposter = () => { ··· 227 235 feedContext, 228 236 reqId, 229 237 }) 238 + logger.metric('post:clickthroughEmbed', { 239 + uri: post.uri, 240 + authorDid: post.author.did, 241 + logContext: 'FeedItem', 242 + feedDescriptor, 243 + }) 230 244 } 231 245 232 246 const onBeforePress = () => { ··· 235 249 event: 'app.bsky.feed.defs#clickthroughItem', 236 250 feedContext, 237 251 reqId, 252 + }) 253 + logger.metric('post:clickthroughItem', { 254 + uri: post.uri, 255 + authorDid: post.author.did, 256 + logContext: 'FeedItem', 257 + feedDescriptor, 238 258 }) 239 259 unstableCacheProfileView(queryClient, post.author) 240 260 setUnstablePostSource(buildPostSourceKey(post.uri, post.author.handle), {