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>
This commit is contained in:
Alex Benzer
2025-12-05 10:32:19 -08:00
committed by GitHub
parent db81cefb86
commit 9a0cb09d3a
10 changed files with 120 additions and 3 deletions
+38
View File
@@ -0,0 +1,38 @@
import {useCallback, useRef} from 'react'
import {type AppBskyFeedDefs} from '@atproto/api'
import {logger} from '#/logger'
import {type MetricEvents} from '#/logger/metrics'
/**
* Hook that returns a callback to track post:view events.
* Handles deduplication so the same post URI is only tracked once per mount.
*
* @param logContext - The context where the post is being viewed
* @returns A callback that accepts a post and logs the view event
*/
export function usePostViewTracking(
logContext: MetricEvents['post:view']['logContext'],
) {
const seenUrisRef = useRef(new Set<string>())
const trackPostView = useCallback(
(post: AppBskyFeedDefs.PostView) => {
if (seenUrisRef.current.has(post.uri)) return
seenUrisRef.current.add(post.uri)
logger.metric(
'post:view',
{
uri: post.uri,
authorDid: post.author.did,
logContext,
},
{statsig: false},
)
},
[logContext],
)
return trackPostView
}
+11 -1
View File
@@ -271,7 +271,17 @@ export type MetricEvents = {
'post:view': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
logContext:
| 'FeedItem'
| 'PostThreadItem'
| 'Post'
| 'ImmersiveVideo'
| 'SearchResults'
| 'Bookmarks'
| 'Notifications'
| 'Hashtag'
| 'Topic'
| 'PostQuotes'
feedDescriptor?: string
position?: number
}
+7
View File
@@ -15,6 +15,7 @@ import {
import {useCleanError} from '#/lib/hooks/useCleanError'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
import {
type CommonNavigatorParams,
type NativeStackScreenProps,
@@ -94,6 +95,7 @@ function BookmarksInner() {
const initialNumToRender = useInitialNumToRender()
const cleanError = useCleanError()
const [isPTRing, setIsPTRing] = useState(false)
const trackPostView = usePostViewTracking('Bookmarks')
const {
data,
isLoading,
@@ -176,6 +178,11 @@ function BookmarksInner() {
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={4}
onItemSeen={item => {
if (item.type === 'bookmark') {
trackPostView(item.bookmark.item)
}
}}
ListFooterComponent={
<ListFooter
isFetchingNextPage={isFetchingNextPage}
+3
View File
@@ -8,6 +8,7 @@ import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {HITSLOP_10} from '#/lib/constants'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
import {type CommonNavigatorParams} from '#/lib/routes/types'
import {shareUrl} from '#/lib/sharing'
import {cleanError} from '#/lib/strings/errors'
@@ -169,6 +170,7 @@ function HashtagScreenTab({
const [isPTR, setIsPTR] = React.useState(false)
const t = useTheme()
const {hasSession} = useSession()
const trackPostView = usePostViewTracking('Hashtag')
const queryParam = React.useMemo(() => {
if (!author) return fullTag
@@ -264,6 +266,7 @@ function HashtagScreenTab({
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={4}
onItemSeen={trackPostView}
// @ts-ignore web only -prf
desktopFixedHeight
ListFooterComponent={
+10
View File
@@ -5,6 +5,7 @@ import {Trans} from '@lingui/macro'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
import {logger} from '#/logger'
import {useFeedFeedback} from '#/state/feed-feedback'
import {type ThreadViewOption} from '#/state/queries/preferences/useThreadPreferences'
@@ -97,6 +98,9 @@ export function PostThread({uri}: {uri: string}) {
}
}, [anchor, feedFeedback.feedDescriptor])
// Track post:view events for parent posts and replies (non-anchor posts)
const trackThreadItemView = usePostViewTracking('PostThreadItem')
const {openComposer} = useOpenComposer()
const optimisticOnPostReply = useCallback(
(payload: OnPostSuccessData) => {
@@ -557,6 +561,12 @@ export function PostThread({uri}: {uri: string}) {
onEndReached={onEndReached}
onEndReachedThreshold={4}
onStartReachedThreshold={1}
onItemSeen={item => {
// Track post:view for parent posts and replies (non-anchor posts)
if (item.type === 'threadPost' && item.depth !== 0) {
trackThreadItemView(item.value.post)
}
}}
/**
* NATIVE ONLY
* {@link https://reactnative.dev/docs/scrollview#maintainvisiblecontentposition}
+7
View File
@@ -5,6 +5,7 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {urls} from '#/lib/constants'
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
import {cleanError} from '#/lib/strings/errors'
import {augmentSearchQuery} from '#/lib/strings/helpers'
import {useActorSearch} from '#/state/queries/actor-search'
@@ -215,6 +216,7 @@ let SearchScreenPostResults = ({
const {_} = useLingui()
const {currentAccount, hasSession} = useSession()
const [isPTR, setIsPTR] = useState(false)
const trackPostView = usePostViewTracking('SearchResults')
const augmentedQuery = useMemo(() => {
return augmentSearchQuery(query || '', {did: currentAccount?.did})
@@ -339,6 +341,11 @@ let SearchScreenPostResults = ({
refreshing={isPTR}
onRefresh={onPullToRefresh}
onEndReached={onEndReached}
onItemSeen={item => {
if (item.type === 'post') {
trackPostView(item.post)
}
}}
desktopFixedHeight
ListFooterComponent={
<ListFooter
+3
View File
@@ -8,6 +8,7 @@ import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {HITSLOP_10} from '#/lib/constants'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
import {type CommonNavigatorParams} from '#/lib/routes/types'
import {shareUrl} from '#/lib/sharing'
import {cleanError} from '#/lib/strings/errors'
@@ -135,6 +136,7 @@ function TopicScreenTab({
const {_} = useLingui()
const initialNumToRender = useInitialNumToRender()
const [isPTR, setIsPTR] = React.useState(false)
const trackPostView = usePostViewTracking('Topic')
const {
data,
@@ -186,6 +188,7 @@ function TopicScreenTab({
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={4}
onItemSeen={trackPostView}
// @ts-ignore web only -prf
desktopFixedHeight
ListFooterComponent={
+26 -2
View File
@@ -492,7 +492,8 @@ let VideoItem = ({
}): React.ReactNode => {
const postShadow = usePostShadow(post)
const {width, height} = useSafeAreaFrame()
const {sendInteraction} = useFeedFeedbackContext()
const {sendInteraction, feedDescriptor} = useFeedFeedbackContext()
const hasTrackedView = useRef(false)
useEffect(() => {
if (active) {
@@ -502,8 +503,31 @@ let VideoItem = ({
feedContext,
reqId,
})
// Track post:view event
if (!hasTrackedView.current) {
hasTrackedView.current = true
logger.metric(
'post:view',
{
uri: post.uri,
authorDid: post.author.did,
logContext: 'ImmersiveVideo',
feedDescriptor,
},
{statsig: false},
)
}
}
}, [active, post.uri, feedContext, reqId, sendInteraction])
}, [
active,
post.uri,
post.author.did,
feedContext,
reqId,
sendInteraction,
feedDescriptor,
])
// TODO: high-performance android phones should also
// be capable of rendering 3 video players, but currently
@@ -9,6 +9,7 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
import {cleanError} from '#/lib/strings/errors'
import {s} from '#/lib/styles'
import {logger} from '#/logger'
@@ -47,6 +48,7 @@ export function NotificationFeed({
const [isPTRing, setIsPTRing] = React.useState(false)
const {_} = useLingui()
const moderationOpts = useModerationOpts()
const trackPostView = usePostViewTracking('Notifications')
const {
data,
isFetching,
@@ -181,6 +183,16 @@ export function NotificationFeed({
onEndReached={onEndReached}
onEndReachedThreshold={2}
onScrolledDownChange={onScrolledDownChange}
onItemSeen={item => {
if (
(item.type === 'reply' ||
item.type === 'mention' ||
item.type === 'quote') &&
item.subject
) {
trackPostView(item.subject)
}
}}
contentContainerStyle={s.contentContainer}
desktopFixedHeight
initialNumToRender={initialNumToRender}
+3
View File
@@ -9,6 +9,7 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
@@ -44,6 +45,7 @@ export function PostQuotes({uri}: {uri: string}) {
const {_} = useLingui()
const initialNumToRender = useInitialNumToRender()
const [isPTRing, setIsPTRing] = useState(false)
const trackPostView = usePostViewTracking('PostQuotes')
const {
data: resolvedUri,
@@ -123,6 +125,7 @@ export function PostQuotes({uri}: {uri: string}) {
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={4}
onItemSeen={item => trackPostView(item.post)}
ListFooterComponent={
<ListFooter
isFetchingNextPage={isFetchingNextPage}