Post view client event (#9408)
* Adds post:view client event tracking in feeds * Add post:view event on the post page itself * Don't send post:view to statsig for now * convert to non reactive callback to reduce rerenders --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
@@ -263,6 +263,13 @@ export type MetricEvents = {
|
|||||||
'post:unbookmark': {
|
'post:unbookmark': {
|
||||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||||
}
|
}
|
||||||
|
'post:view': {
|
||||||
|
uri: string
|
||||||
|
authorDid: string
|
||||||
|
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||||
|
feedDescriptor?: string
|
||||||
|
position?: number
|
||||||
|
}
|
||||||
'bookmarks:view': {}
|
'bookmarks:view': {}
|
||||||
'bookmarks:post-clicked': {}
|
'bookmarks:post-clicked': {}
|
||||||
'profile:follow': {
|
'profile:follow': {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import {useCallback, useMemo, useRef, useState} from 'react'
|
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
||||||
import {useWindowDimensions, View} from 'react-native'
|
import {useWindowDimensions, View} from 'react-native'
|
||||||
import Animated, {useAnimatedStyle} from 'react-native-reanimated'
|
import Animated, {useAnimatedStyle} from 'react-native-reanimated'
|
||||||
import {Trans} from '@lingui/macro'
|
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 {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'
|
||||||
import {
|
import {
|
||||||
@@ -73,6 +74,29 @@ export function PostThread({uri}: {uri: string}) {
|
|||||||
return {hasParents}
|
return {hasParents}
|
||||||
}, [thread.data.items])
|
}, [thread.data.items])
|
||||||
|
|
||||||
|
// Track post:view event when anchor post is viewed
|
||||||
|
const seenPostUriRef = useRef<string | null>(null)
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
anchor?.type === 'threadPost' &&
|
||||||
|
anchor.value.post.uri !== seenPostUriRef.current
|
||||||
|
) {
|
||||||
|
const post = anchor.value.post
|
||||||
|
seenPostUriRef.current = post.uri
|
||||||
|
|
||||||
|
logger.metric(
|
||||||
|
'post:view',
|
||||||
|
{
|
||||||
|
uri: post.uri,
|
||||||
|
authorDid: post.author.did,
|
||||||
|
logContext: 'Post',
|
||||||
|
feedDescriptor: feedFeedback.feedDescriptor,
|
||||||
|
},
|
||||||
|
{statsig: false},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [anchor, feedFeedback.feedDescriptor])
|
||||||
|
|
||||||
const {openComposer} = useOpenComposer()
|
const {openComposer} = useOpenComposer()
|
||||||
const optimisticOnPostReply = useCallback(
|
const optimisticOnPostReply = useCallback(
|
||||||
(payload: OnPostSuccessData) => {
|
(payload: OnPostSuccessData) => {
|
||||||
|
|||||||
@@ -840,12 +840,67 @@ let PostFeed = ({
|
|||||||
const liveNowConfig = useLiveNowConfig()
|
const liveNowConfig = useLiveNowConfig()
|
||||||
|
|
||||||
const seenActorWithStatusRef = useRef<Set<string>>(new Set())
|
const seenActorWithStatusRef = useRef<Set<string>>(new Set())
|
||||||
|
const seenPostUrisRef = useRef<Set<string>>(new Set())
|
||||||
|
|
||||||
|
// Helper to calculate position in feed (count only root posts, not interstitials or thread replies)
|
||||||
|
const getPostPosition = useNonReactiveCallback(
|
||||||
|
(type: FeedRow['type'], key: string) => {
|
||||||
|
// Calculate position: find the row index in feedItems, then calculate position
|
||||||
|
const rowIndex = feedItems.findIndex(
|
||||||
|
row => row.type === 'sliceItem' && row.key === key,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (rowIndex >= 0) {
|
||||||
|
let position = 0
|
||||||
|
for (let i = 0; i < rowIndex && i < feedItems.length; i++) {
|
||||||
|
const row = feedItems[i]
|
||||||
|
if (row.type === 'sliceItem') {
|
||||||
|
// Only count root posts (indexInSlice === 0), not thread replies
|
||||||
|
if (row.indexInSlice === 0) {
|
||||||
|
position++
|
||||||
|
}
|
||||||
|
} else if (row.type === 'videoGridRow') {
|
||||||
|
// Count each video in the grid row
|
||||||
|
position += row.items.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return position
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const onItemSeen = useCallback(
|
const onItemSeen = useCallback(
|
||||||
(item: FeedRow) => {
|
(item: FeedRow) => {
|
||||||
feedFeedback.onItemSeen(item)
|
feedFeedback.onItemSeen(item)
|
||||||
if (item.type === 'sliceItem') {
|
|
||||||
const actor = item.slice.items[item.indexInSlice].post.author
|
|
||||||
|
|
||||||
|
// Track post:view events
|
||||||
|
if (item.type === 'sliceItem') {
|
||||||
|
const slice = item.slice
|
||||||
|
const indexInSlice = item.indexInSlice
|
||||||
|
const postItem = slice.items[indexInSlice]
|
||||||
|
const post = postItem.post
|
||||||
|
|
||||||
|
// Only track the root post of each slice (index 0) to avoid double-counting thread items
|
||||||
|
if (indexInSlice === 0 && !seenPostUrisRef.current.has(post.uri)) {
|
||||||
|
seenPostUrisRef.current.add(post.uri)
|
||||||
|
|
||||||
|
const position = getPostPosition('sliceItem', item.key)
|
||||||
|
|
||||||
|
logger.metric(
|
||||||
|
'post:view',
|
||||||
|
{
|
||||||
|
uri: post.uri,
|
||||||
|
authorDid: post.author.did,
|
||||||
|
logContext: 'FeedItem',
|
||||||
|
feedDescriptor: feedFeedback.feedDescriptor || feed,
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{statsig: false},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live status tracking (existing code)
|
||||||
|
const actor = post.author
|
||||||
if (
|
if (
|
||||||
actor.status &&
|
actor.status &&
|
||||||
validateStatus(actor.did, actor.status, liveNowConfig) &&
|
validateStatus(actor.did, actor.status, liveNowConfig) &&
|
||||||
@@ -863,9 +918,33 @@ let PostFeed = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (item.type === 'videoGridRow') {
|
||||||
|
// Track each video in the grid row
|
||||||
|
for (let i = 0; i < item.items.length; i++) {
|
||||||
|
const postItem = item.items[i]
|
||||||
|
const post = postItem.post
|
||||||
|
|
||||||
|
if (!seenPostUrisRef.current.has(post.uri)) {
|
||||||
|
seenPostUrisRef.current.add(post.uri)
|
||||||
|
|
||||||
|
const position = getPostPosition('videoGridRow', item.key)
|
||||||
|
|
||||||
|
logger.metric(
|
||||||
|
'post:view',
|
||||||
|
{
|
||||||
|
uri: post.uri,
|
||||||
|
authorDid: post.author.did,
|
||||||
|
logContext: 'FeedItem',
|
||||||
|
feedDescriptor: feedFeedback.feedDescriptor || feed,
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
{statsig: false},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[feedFeedback, feed, liveNowConfig],
|
[feedFeedback, feed, liveNowConfig, getPostPosition],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user