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': {
|
'post:view': {
|
||||||
uri: string
|
uri: string
|
||||||
authorDid: string
|
authorDid: string
|
||||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
logContext:
|
||||||
|
| 'FeedItem'
|
||||||
|
| 'PostThreadItem'
|
||||||
|
| 'Post'
|
||||||
|
| 'ImmersiveVideo'
|
||||||
|
| 'SearchResults'
|
||||||
|
| 'Bookmarks'
|
||||||
|
| 'Notifications'
|
||||||
|
| 'Hashtag'
|
||||||
|
| 'Topic'
|
||||||
|
| 'PostQuotes'
|
||||||
feedDescriptor?: string
|
feedDescriptor?: string
|
||||||
position?: number
|
position?: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
|
|
||||||
import {useCleanError} from '#/lib/hooks/useCleanError'
|
import {useCleanError} from '#/lib/hooks/useCleanError'
|
||||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||||
|
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
|
||||||
import {
|
import {
|
||||||
type CommonNavigatorParams,
|
type CommonNavigatorParams,
|
||||||
type NativeStackScreenProps,
|
type NativeStackScreenProps,
|
||||||
@@ -94,6 +95,7 @@ function BookmarksInner() {
|
|||||||
const initialNumToRender = useInitialNumToRender()
|
const initialNumToRender = useInitialNumToRender()
|
||||||
const cleanError = useCleanError()
|
const cleanError = useCleanError()
|
||||||
const [isPTRing, setIsPTRing] = useState(false)
|
const [isPTRing, setIsPTRing] = useState(false)
|
||||||
|
const trackPostView = usePostViewTracking('Bookmarks')
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -176,6 +178,11 @@ function BookmarksInner() {
|
|||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
onEndReachedThreshold={4}
|
onEndReachedThreshold={4}
|
||||||
|
onItemSeen={item => {
|
||||||
|
if (item.type === 'bookmark') {
|
||||||
|
trackPostView(item.bookmark.item)
|
||||||
|
}
|
||||||
|
}}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
<ListFooter
|
<ListFooter
|
||||||
isFetchingNextPage={isFetchingNextPage}
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
|||||||
|
|
||||||
import {HITSLOP_10} from '#/lib/constants'
|
import {HITSLOP_10} from '#/lib/constants'
|
||||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||||
|
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
|
||||||
import {type CommonNavigatorParams} from '#/lib/routes/types'
|
import {type CommonNavigatorParams} from '#/lib/routes/types'
|
||||||
import {shareUrl} from '#/lib/sharing'
|
import {shareUrl} from '#/lib/sharing'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
@@ -169,6 +170,7 @@ function HashtagScreenTab({
|
|||||||
const [isPTR, setIsPTR] = React.useState(false)
|
const [isPTR, setIsPTR] = React.useState(false)
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
|
const trackPostView = usePostViewTracking('Hashtag')
|
||||||
|
|
||||||
const queryParam = React.useMemo(() => {
|
const queryParam = React.useMemo(() => {
|
||||||
if (!author) return fullTag
|
if (!author) return fullTag
|
||||||
@@ -264,6 +266,7 @@ function HashtagScreenTab({
|
|||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
onEndReachedThreshold={4}
|
onEndReachedThreshold={4}
|
||||||
|
onItemSeen={trackPostView}
|
||||||
// @ts-ignore web only -prf
|
// @ts-ignore web only -prf
|
||||||
desktopFixedHeight
|
desktopFixedHeight
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {Trans} from '@lingui/macro'
|
|||||||
|
|
||||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||||
|
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useFeedFeedback} from '#/state/feed-feedback'
|
import {useFeedFeedback} from '#/state/feed-feedback'
|
||||||
import {type ThreadViewOption} from '#/state/queries/preferences/useThreadPreferences'
|
import {type ThreadViewOption} from '#/state/queries/preferences/useThreadPreferences'
|
||||||
@@ -97,6 +98,9 @@ export function PostThread({uri}: {uri: string}) {
|
|||||||
}
|
}
|
||||||
}, [anchor, feedFeedback.feedDescriptor])
|
}, [anchor, feedFeedback.feedDescriptor])
|
||||||
|
|
||||||
|
// Track post:view events for parent posts and replies (non-anchor posts)
|
||||||
|
const trackThreadItemView = usePostViewTracking('PostThreadItem')
|
||||||
|
|
||||||
const {openComposer} = useOpenComposer()
|
const {openComposer} = useOpenComposer()
|
||||||
const optimisticOnPostReply = useCallback(
|
const optimisticOnPostReply = useCallback(
|
||||||
(payload: OnPostSuccessData) => {
|
(payload: OnPostSuccessData) => {
|
||||||
@@ -557,6 +561,12 @@ export function PostThread({uri}: {uri: string}) {
|
|||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
onEndReachedThreshold={4}
|
onEndReachedThreshold={4}
|
||||||
onStartReachedThreshold={1}
|
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
|
* NATIVE ONLY
|
||||||
* {@link https://reactnative.dev/docs/scrollview#maintainvisiblecontentposition}
|
* {@link https://reactnative.dev/docs/scrollview#maintainvisiblecontentposition}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {msg, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {urls} from '#/lib/constants'
|
import {urls} from '#/lib/constants'
|
||||||
|
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {augmentSearchQuery} from '#/lib/strings/helpers'
|
import {augmentSearchQuery} from '#/lib/strings/helpers'
|
||||||
import {useActorSearch} from '#/state/queries/actor-search'
|
import {useActorSearch} from '#/state/queries/actor-search'
|
||||||
@@ -215,6 +216,7 @@ let SearchScreenPostResults = ({
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {currentAccount, hasSession} = useSession()
|
const {currentAccount, hasSession} = useSession()
|
||||||
const [isPTR, setIsPTR] = useState(false)
|
const [isPTR, setIsPTR] = useState(false)
|
||||||
|
const trackPostView = usePostViewTracking('SearchResults')
|
||||||
|
|
||||||
const augmentedQuery = useMemo(() => {
|
const augmentedQuery = useMemo(() => {
|
||||||
return augmentSearchQuery(query || '', {did: currentAccount?.did})
|
return augmentSearchQuery(query || '', {did: currentAccount?.did})
|
||||||
@@ -339,6 +341,11 @@ let SearchScreenPostResults = ({
|
|||||||
refreshing={isPTR}
|
refreshing={isPTR}
|
||||||
onRefresh={onPullToRefresh}
|
onRefresh={onPullToRefresh}
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
|
onItemSeen={item => {
|
||||||
|
if (item.type === 'post') {
|
||||||
|
trackPostView(item.post)
|
||||||
|
}
|
||||||
|
}}
|
||||||
desktopFixedHeight
|
desktopFixedHeight
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
<ListFooter
|
<ListFooter
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
|||||||
|
|
||||||
import {HITSLOP_10} from '#/lib/constants'
|
import {HITSLOP_10} from '#/lib/constants'
|
||||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||||
|
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
|
||||||
import {type CommonNavigatorParams} from '#/lib/routes/types'
|
import {type CommonNavigatorParams} from '#/lib/routes/types'
|
||||||
import {shareUrl} from '#/lib/sharing'
|
import {shareUrl} from '#/lib/sharing'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
@@ -135,6 +136,7 @@ function TopicScreenTab({
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const initialNumToRender = useInitialNumToRender()
|
const initialNumToRender = useInitialNumToRender()
|
||||||
const [isPTR, setIsPTR] = React.useState(false)
|
const [isPTR, setIsPTR] = React.useState(false)
|
||||||
|
const trackPostView = usePostViewTracking('Topic')
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -186,6 +188,7 @@ function TopicScreenTab({
|
|||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
onEndReachedThreshold={4}
|
onEndReachedThreshold={4}
|
||||||
|
onItemSeen={trackPostView}
|
||||||
// @ts-ignore web only -prf
|
// @ts-ignore web only -prf
|
||||||
desktopFixedHeight
|
desktopFixedHeight
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
|
|||||||
@@ -492,7 +492,8 @@ let VideoItem = ({
|
|||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const postShadow = usePostShadow(post)
|
const postShadow = usePostShadow(post)
|
||||||
const {width, height} = useSafeAreaFrame()
|
const {width, height} = useSafeAreaFrame()
|
||||||
const {sendInteraction} = useFeedFeedbackContext()
|
const {sendInteraction, feedDescriptor} = useFeedFeedbackContext()
|
||||||
|
const hasTrackedView = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (active) {
|
if (active) {
|
||||||
@@ -502,8 +503,31 @@ let VideoItem = ({
|
|||||||
feedContext,
|
feedContext,
|
||||||
reqId,
|
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
|
// TODO: high-performance android phones should also
|
||||||
// be capable of rendering 3 video players, but currently
|
// be capable of rendering 3 video players, but currently
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {msg} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||||
|
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {s} from '#/lib/styles'
|
import {s} from '#/lib/styles'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
@@ -47,6 +48,7 @@ export function NotificationFeed({
|
|||||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
|
const trackPostView = usePostViewTracking('Notifications')
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isFetching,
|
isFetching,
|
||||||
@@ -181,6 +183,16 @@ export function NotificationFeed({
|
|||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
onEndReachedThreshold={2}
|
onEndReachedThreshold={2}
|
||||||
onScrolledDownChange={onScrolledDownChange}
|
onScrolledDownChange={onScrolledDownChange}
|
||||||
|
onItemSeen={item => {
|
||||||
|
if (
|
||||||
|
(item.type === 'reply' ||
|
||||||
|
item.type === 'mention' ||
|
||||||
|
item.type === 'quote') &&
|
||||||
|
item.subject
|
||||||
|
) {
|
||||||
|
trackPostView(item.subject)
|
||||||
|
}
|
||||||
|
}}
|
||||||
contentContainerStyle={s.contentContainer}
|
contentContainerStyle={s.contentContainer}
|
||||||
desktopFixedHeight
|
desktopFixedHeight
|
||||||
initialNumToRender={initialNumToRender}
|
initialNumToRender={initialNumToRender}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {msg} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
|
||||||
|
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
@@ -44,6 +45,7 @@ export function PostQuotes({uri}: {uri: string}) {
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const initialNumToRender = useInitialNumToRender()
|
const initialNumToRender = useInitialNumToRender()
|
||||||
const [isPTRing, setIsPTRing] = useState(false)
|
const [isPTRing, setIsPTRing] = useState(false)
|
||||||
|
const trackPostView = usePostViewTracking('PostQuotes')
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: resolvedUri,
|
data: resolvedUri,
|
||||||
@@ -123,6 +125,7 @@ export function PostQuotes({uri}: {uri: string}) {
|
|||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
onEndReachedThreshold={4}
|
onEndReachedThreshold={4}
|
||||||
|
onItemSeen={item => trackPostView(item.post)}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
<ListFooter
|
<ListFooter
|
||||||
isFetchingNextPage={isFetchingNextPage}
|
isFetchingNextPage={isFetchingNextPage}
|
||||||
|
|||||||
Reference in New Issue
Block a user