Bluesky app fork with some witchin' additions 💫

Fire the post:view client event in more places, anywhere a post can be seen (#9467)

* Fire the post:view client event in more places

* Fix bad import

---------

Co-authored-by: Eric Bailey <git@esb.lol>

authored by

Alex Benzer
Eric Bailey
and committed by
GitHub
9a0cb09d db81cefb

+120 -3
+38
src/lib/hooks/usePostViewTracking.ts
···
··· 1 + import {useCallback, useRef} from 'react' 2 + import {type AppBskyFeedDefs} from '@atproto/api' 3 + 4 + import {logger} from '#/logger' 5 + import {type MetricEvents} from '#/logger/metrics' 6 + 7 + /** 8 + * Hook that returns a callback to track post:view events. 9 + * Handles deduplication so the same post URI is only tracked once per mount. 10 + * 11 + * @param logContext - The context where the post is being viewed 12 + * @returns A callback that accepts a post and logs the view event 13 + */ 14 + export function usePostViewTracking( 15 + logContext: MetricEvents['post:view']['logContext'], 16 + ) { 17 + const seenUrisRef = useRef(new Set<string>()) 18 + 19 + const trackPostView = useCallback( 20 + (post: AppBskyFeedDefs.PostView) => { 21 + if (seenUrisRef.current.has(post.uri)) return 22 + seenUrisRef.current.add(post.uri) 23 + 24 + logger.metric( 25 + 'post:view', 26 + { 27 + uri: post.uri, 28 + authorDid: post.author.did, 29 + logContext, 30 + }, 31 + {statsig: false}, 32 + ) 33 + }, 34 + [logContext], 35 + ) 36 + 37 + return trackPostView 38 + }
+11 -1
src/logger/metrics.ts
··· 271 'post:view': { 272 uri: string 273 authorDid: string 274 - logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 275 feedDescriptor?: string 276 position?: number 277 }
··· 271 'post:view': { 272 uri: string 273 authorDid: string 274 + logContext: 275 + | 'FeedItem' 276 + | 'PostThreadItem' 277 + | 'Post' 278 + | 'ImmersiveVideo' 279 + | 'SearchResults' 280 + | 'Bookmarks' 281 + | 'Notifications' 282 + | 'Hashtag' 283 + | 'Topic' 284 + | 'PostQuotes' 285 feedDescriptor?: string 286 position?: number 287 }
+7
src/screens/Bookmarks/index.tsx
··· 15 16 import {useCleanError} from '#/lib/hooks/useCleanError' 17 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 18 import { 19 type CommonNavigatorParams, 20 type NativeStackScreenProps, ··· 94 const initialNumToRender = useInitialNumToRender() 95 const cleanError = useCleanError() 96 const [isPTRing, setIsPTRing] = useState(false) 97 const { 98 data, 99 isLoading, ··· 176 onRefresh={onRefresh} 177 onEndReached={onEndReached} 178 onEndReachedThreshold={4} 179 ListFooterComponent={ 180 <ListFooter 181 isFetchingNextPage={isFetchingNextPage}
··· 15 16 import {useCleanError} from '#/lib/hooks/useCleanError' 17 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 18 + import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking' 19 import { 20 type CommonNavigatorParams, 21 type NativeStackScreenProps, ··· 95 const initialNumToRender = useInitialNumToRender() 96 const cleanError = useCleanError() 97 const [isPTRing, setIsPTRing] = useState(false) 98 + const trackPostView = usePostViewTracking('Bookmarks') 99 const { 100 data, 101 isLoading, ··· 178 onRefresh={onRefresh} 179 onEndReached={onEndReached} 180 onEndReachedThreshold={4} 181 + onItemSeen={item => { 182 + if (item.type === 'bookmark') { 183 + trackPostView(item.bookmark.item) 184 + } 185 + }} 186 ListFooterComponent={ 187 <ListFooter 188 isFetchingNextPage={isFetchingNextPage}
+3
src/screens/Hashtag.tsx
··· 8 9 import {HITSLOP_10} from '#/lib/constants' 10 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 11 import {type CommonNavigatorParams} from '#/lib/routes/types' 12 import {shareUrl} from '#/lib/sharing' 13 import {cleanError} from '#/lib/strings/errors' ··· 169 const [isPTR, setIsPTR] = React.useState(false) 170 const t = useTheme() 171 const {hasSession} = useSession() 172 173 const queryParam = React.useMemo(() => { 174 if (!author) return fullTag ··· 264 onRefresh={onRefresh} 265 onEndReached={onEndReached} 266 onEndReachedThreshold={4} 267 // @ts-ignore web only -prf 268 desktopFixedHeight 269 ListFooterComponent={
··· 8 9 import {HITSLOP_10} from '#/lib/constants' 10 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 11 + import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking' 12 import {type CommonNavigatorParams} from '#/lib/routes/types' 13 import {shareUrl} from '#/lib/sharing' 14 import {cleanError} from '#/lib/strings/errors' ··· 170 const [isPTR, setIsPTR] = React.useState(false) 171 const t = useTheme() 172 const {hasSession} = useSession() 173 + const trackPostView = usePostViewTracking('Hashtag') 174 175 const queryParam = React.useMemo(() => { 176 if (!author) return fullTag ··· 266 onRefresh={onRefresh} 267 onEndReached={onEndReached} 268 onEndReachedThreshold={4} 269 + onItemSeen={trackPostView} 270 // @ts-ignore web only -prf 271 desktopFixedHeight 272 ListFooterComponent={
+10
src/screens/PostThread/index.tsx
··· 5 6 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 7 import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 8 import {logger} from '#/logger' 9 import {useFeedFeedback} from '#/state/feed-feedback' 10 import {type ThreadViewOption} from '#/state/queries/preferences/useThreadPreferences' ··· 96 ) 97 } 98 }, [anchor, feedFeedback.feedDescriptor]) 99 100 const {openComposer} = useOpenComposer() 101 const optimisticOnPostReply = useCallback( ··· 557 onEndReached={onEndReached} 558 onEndReachedThreshold={4} 559 onStartReachedThreshold={1} 560 /** 561 * NATIVE ONLY 562 * {@link https://reactnative.dev/docs/scrollview#maintainvisiblecontentposition}
··· 5 6 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 7 import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 8 + import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking' 9 import {logger} from '#/logger' 10 import {useFeedFeedback} from '#/state/feed-feedback' 11 import {type ThreadViewOption} from '#/state/queries/preferences/useThreadPreferences' ··· 97 ) 98 } 99 }, [anchor, feedFeedback.feedDescriptor]) 100 + 101 + // Track post:view events for parent posts and replies (non-anchor posts) 102 + const trackThreadItemView = usePostViewTracking('PostThreadItem') 103 104 const {openComposer} = useOpenComposer() 105 const optimisticOnPostReply = useCallback( ··· 561 onEndReached={onEndReached} 562 onEndReachedThreshold={4} 563 onStartReachedThreshold={1} 564 + onItemSeen={item => { 565 + // Track post:view for parent posts and replies (non-anchor posts) 566 + if (item.type === 'threadPost' && item.depth !== 0) { 567 + trackThreadItemView(item.value.post) 568 + } 569 + }} 570 /** 571 * NATIVE ONLY 572 * {@link https://reactnative.dev/docs/scrollview#maintainvisiblecontentposition}
+7
src/screens/Search/SearchResults.tsx
··· 5 import {useLingui} from '@lingui/react' 6 7 import {urls} from '#/lib/constants' 8 import {cleanError} from '#/lib/strings/errors' 9 import {augmentSearchQuery} from '#/lib/strings/helpers' 10 import {useActorSearch} from '#/state/queries/actor-search' ··· 215 const {_} = useLingui() 216 const {currentAccount, hasSession} = useSession() 217 const [isPTR, setIsPTR] = useState(false) 218 219 const augmentedQuery = useMemo(() => { 220 return augmentSearchQuery(query || '', {did: currentAccount?.did}) ··· 339 refreshing={isPTR} 340 onRefresh={onPullToRefresh} 341 onEndReached={onEndReached} 342 desktopFixedHeight 343 ListFooterComponent={ 344 <ListFooter
··· 5 import {useLingui} from '@lingui/react' 6 7 import {urls} from '#/lib/constants' 8 + import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking' 9 import {cleanError} from '#/lib/strings/errors' 10 import {augmentSearchQuery} from '#/lib/strings/helpers' 11 import {useActorSearch} from '#/state/queries/actor-search' ··· 216 const {_} = useLingui() 217 const {currentAccount, hasSession} = useSession() 218 const [isPTR, setIsPTR] = useState(false) 219 + const trackPostView = usePostViewTracking('SearchResults') 220 221 const augmentedQuery = useMemo(() => { 222 return augmentSearchQuery(query || '', {did: currentAccount?.did}) ··· 341 refreshing={isPTR} 342 onRefresh={onPullToRefresh} 343 onEndReached={onEndReached} 344 + onItemSeen={item => { 345 + if (item.type === 'post') { 346 + trackPostView(item.post) 347 + } 348 + }} 349 desktopFixedHeight 350 ListFooterComponent={ 351 <ListFooter
+3
src/screens/Topic.tsx
··· 8 9 import {HITSLOP_10} from '#/lib/constants' 10 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 11 import {type CommonNavigatorParams} from '#/lib/routes/types' 12 import {shareUrl} from '#/lib/sharing' 13 import {cleanError} from '#/lib/strings/errors' ··· 135 const {_} = useLingui() 136 const initialNumToRender = useInitialNumToRender() 137 const [isPTR, setIsPTR] = React.useState(false) 138 139 const { 140 data, ··· 186 onRefresh={onRefresh} 187 onEndReached={onEndReached} 188 onEndReachedThreshold={4} 189 // @ts-ignore web only -prf 190 desktopFixedHeight 191 ListFooterComponent={
··· 8 9 import {HITSLOP_10} from '#/lib/constants' 10 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 11 + import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking' 12 import {type CommonNavigatorParams} from '#/lib/routes/types' 13 import {shareUrl} from '#/lib/sharing' 14 import {cleanError} from '#/lib/strings/errors' ··· 136 const {_} = useLingui() 137 const initialNumToRender = useInitialNumToRender() 138 const [isPTR, setIsPTR] = React.useState(false) 139 + const trackPostView = usePostViewTracking('Topic') 140 141 const { 142 data, ··· 188 onRefresh={onRefresh} 189 onEndReached={onEndReached} 190 onEndReachedThreshold={4} 191 + onItemSeen={trackPostView} 192 // @ts-ignore web only -prf 193 desktopFixedHeight 194 ListFooterComponent={
+26 -2
src/screens/VideoFeed/index.tsx
··· 492 }): React.ReactNode => { 493 const postShadow = usePostShadow(post) 494 const {width, height} = useSafeAreaFrame() 495 - const {sendInteraction} = useFeedFeedbackContext() 496 497 useEffect(() => { 498 if (active) { ··· 502 feedContext, 503 reqId, 504 }) 505 } 506 - }, [active, post.uri, feedContext, reqId, sendInteraction]) 507 508 // TODO: high-performance android phones should also 509 // be capable of rendering 3 video players, but currently
··· 492 }): React.ReactNode => { 493 const postShadow = usePostShadow(post) 494 const {width, height} = useSafeAreaFrame() 495 + const {sendInteraction, feedDescriptor} = useFeedFeedbackContext() 496 + const hasTrackedView = useRef(false) 497 498 useEffect(() => { 499 if (active) { ··· 503 feedContext, 504 reqId, 505 }) 506 + 507 + // Track post:view event 508 + if (!hasTrackedView.current) { 509 + hasTrackedView.current = true 510 + logger.metric( 511 + 'post:view', 512 + { 513 + uri: post.uri, 514 + authorDid: post.author.did, 515 + logContext: 'ImmersiveVideo', 516 + feedDescriptor, 517 + }, 518 + {statsig: false}, 519 + ) 520 + } 521 } 522 + }, [ 523 + active, 524 + post.uri, 525 + post.author.did, 526 + feedContext, 527 + reqId, 528 + sendInteraction, 529 + feedDescriptor, 530 + ]) 531 532 // TODO: high-performance android phones should also 533 // be capable of rendering 3 video players, but currently
+12
src/view/com/notifications/NotificationFeed.tsx
··· 9 import {useLingui} from '@lingui/react' 10 11 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 12 import {cleanError} from '#/lib/strings/errors' 13 import {s} from '#/lib/styles' 14 import {logger} from '#/logger' ··· 47 const [isPTRing, setIsPTRing] = React.useState(false) 48 const {_} = useLingui() 49 const moderationOpts = useModerationOpts() 50 const { 51 data, 52 isFetching, ··· 181 onEndReached={onEndReached} 182 onEndReachedThreshold={2} 183 onScrolledDownChange={onScrolledDownChange} 184 contentContainerStyle={s.contentContainer} 185 desktopFixedHeight 186 initialNumToRender={initialNumToRender}
··· 9 import {useLingui} from '@lingui/react' 10 11 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 12 + import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking' 13 import {cleanError} from '#/lib/strings/errors' 14 import {s} from '#/lib/styles' 15 import {logger} from '#/logger' ··· 48 const [isPTRing, setIsPTRing] = React.useState(false) 49 const {_} = useLingui() 50 const moderationOpts = useModerationOpts() 51 + const trackPostView = usePostViewTracking('Notifications') 52 const { 53 data, 54 isFetching, ··· 183 onEndReached={onEndReached} 184 onEndReachedThreshold={2} 185 onScrolledDownChange={onScrolledDownChange} 186 + onItemSeen={item => { 187 + if ( 188 + (item.type === 'reply' || 189 + item.type === 'mention' || 190 + item.type === 'quote') && 191 + item.subject 192 + ) { 193 + trackPostView(item.subject) 194 + } 195 + }} 196 contentContainerStyle={s.contentContainer} 197 desktopFixedHeight 198 initialNumToRender={initialNumToRender}
+3
src/view/com/post-thread/PostQuotes.tsx
··· 9 import {useLingui} from '@lingui/react' 10 11 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 12 import {cleanError} from '#/lib/strings/errors' 13 import {logger} from '#/logger' 14 import {useModerationOpts} from '#/state/preferences/moderation-opts' ··· 44 const {_} = useLingui() 45 const initialNumToRender = useInitialNumToRender() 46 const [isPTRing, setIsPTRing] = useState(false) 47 48 const { 49 data: resolvedUri, ··· 123 onRefresh={onRefresh} 124 onEndReached={onEndReached} 125 onEndReachedThreshold={4} 126 ListFooterComponent={ 127 <ListFooter 128 isFetchingNextPage={isFetchingNextPage}
··· 9 import {useLingui} from '@lingui/react' 10 11 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 12 + import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking' 13 import {cleanError} from '#/lib/strings/errors' 14 import {logger} from '#/logger' 15 import {useModerationOpts} from '#/state/preferences/moderation-opts' ··· 45 const {_} = useLingui() 46 const initialNumToRender = useInitialNumToRender() 47 const [isPTRing, setIsPTRing] = useState(false) 48 + const trackPostView = usePostViewTracking('PostQuotes') 49 50 const { 51 data: resolvedUri, ··· 125 onRefresh={onRefresh} 126 onEndReached={onEndReached} 127 onEndReachedThreshold={4} 128 + onItemSeen={item => trackPostView(item.post)} 129 ListFooterComponent={ 130 <ListFooter 131 isFetchingNextPage={isFetchingNextPage}