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:
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user