Profile follow client events (#9385)

* Add parameters to profile:follow

Track who was followed, whose profile generated the follow, and the position of the person who was followed in the list

* Add profileCard:seen event

* Don't send "profileCard:seen" event to Statsig

* Clean up

* prevent overzealous clearing

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
Alex Benzer
2025-11-17 05:04:11 -08:00
committed by GitHub
parent 2f678a3ccd
commit 084f60c88f
6 changed files with 125 additions and 3 deletions
+18
View File
@@ -48,11 +48,15 @@ export function Default({
moderationOpts,
logContext = 'ProfileCard',
testID,
position,
contextProfileDid,
}: {
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
testID?: string
position?: number
contextProfileDid?: string
}) {
return (
<Link testID={testID} profile={profile}>
@@ -60,6 +64,8 @@ export function Default({
profile={profile}
moderationOpts={moderationOpts}
logContext={logContext}
position={position}
contextProfileDid={contextProfileDid}
/>
</Link>
)
@@ -69,10 +75,14 @@ export function Card({
profile,
moderationOpts,
logContext = 'ProfileCard',
position,
contextProfileDid,
}: {
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
position?: number
contextProfileDid?: string
}) {
return (
<Outer>
@@ -83,6 +93,8 @@ export function Card({
profile={profile}
moderationOpts={moderationOpts}
logContext={logContext}
position={position}
contextProfileDid={contextProfileDid}
/>
</Header>
@@ -437,6 +449,8 @@ export type FollowButtonProps = {
colorInverted?: boolean
onFollow?: () => void
withIcon?: boolean
position?: number
contextProfileDid?: string
} & Partial<ButtonProps>
export function FollowButton(props: FollowButtonProps) {
@@ -453,6 +467,8 @@ export function FollowButtonInner({
onFollow,
colorInverted,
withIcon = true,
position,
contextProfileDid,
...rest
}: FollowButtonProps) {
const {_} = useLingui()
@@ -461,6 +477,8 @@ export function FollowButtonInner({
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
profile,
logContext,
position,
contextProfileDid,
)
const isRound = Boolean(rest.shape && rest.shape === 'round')
+8
View File
@@ -256,9 +256,12 @@ export type MetricEvents = {
'bookmarks:view': {}
'bookmarks:post-clicked': {}
'profile:follow': {
contextProfileDid?: string
didBecomeMutual: boolean | undefined
followeeClout: number | undefined
followeeDid: string
followerClout: number | undefined
position?: number
logContext:
| 'RecommendedFollowsItem'
| 'PostThreadItem'
@@ -276,6 +279,11 @@ export type MetricEvents = {
| 'ExploreSuggestedAccounts'
| 'OnboardingSuggestedAccounts'
}
'profileCard:seen': {
contextProfileDid?: string
profileDid: string
position?: number
}
'suggestedUser:follow': {
logContext:
| 'Explore'
+13 -1
View File
@@ -242,12 +242,19 @@ export function useProfileFollowMutationQueue(
profile: Shadow<bsky.profile.AnyProfileView>,
logContext: LogEvents['profile:follow']['logContext'] &
LogEvents['profile:follow']['logContext'],
position?: number,
contextProfileDid?: string,
) {
const agent = useAgent()
const queryClient = useQueryClient()
const did = profile.did
const initialFollowingUri = profile.viewer?.following
const followMutation = useProfileFollowMutation(logContext, profile)
const followMutation = useProfileFollowMutation(
logContext,
profile,
position,
contextProfileDid,
)
const unfollowMutation = useProfileUnfollowMutation(logContext)
const queueToggle = useToggleMutationQueue({
@@ -314,6 +321,8 @@ export function useProfileFollowMutationQueue(
function useProfileFollowMutation(
logContext: LogEvents['profile:follow']['logContext'],
profile: Shadow<bsky.profile.AnyProfileView>,
position?: number,
contextProfileDid?: string,
) {
const {currentAccount} = useSession()
const agent = useAgent()
@@ -336,7 +345,10 @@ function useProfileFollowMutation(
'followersCount' in profile
? toClout(profile.followersCount)
: undefined,
followeeDid: did,
followerClout: toClout(ownProfile?.followersCount),
position,
contextProfileDid,
})
return await agent.follow(did)
},
+6
View File
@@ -9,10 +9,14 @@ export function ProfileCardWithFollowBtn({
profile,
noBorder,
logContext = 'ProfileCard',
position,
contextProfileDid,
}: {
profile: AppBskyActorDefs.ProfileView
noBorder?: boolean
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
position?: number
contextProfileDid?: string
}) {
const t = useTheme()
const moderationOpts = useModerationOpts()
@@ -30,6 +34,8 @@ export function ProfileCardWithFollowBtn({
profile={profile}
moderationOpts={moderationOpts}
logContext={logContext}
position={position}
contextProfileDid={contextProfileDid}
/>
</View>
)
+40 -1
View File
@@ -16,15 +16,19 @@ import {ProfileCardWithFollowBtn} from './ProfileCard'
function renderItem({
item,
index,
contextProfileDid,
}: {
item: ActorDefs.ProfileView
index: number
contextProfileDid: string | undefined
}) {
return (
<ProfileCardWithFollowBtn
key={item.did}
profile={item}
noBorder={index === 0}
position={index + 1}
contextProfileDid={contextProfileDid}
/>
)
}
@@ -83,6 +87,40 @@ export function ProfileFollowers({name}: {name: string}) {
}
}, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
const renderItemWithContext = React.useCallback(
({item, index}: {item: ActorDefs.ProfileView; index: number}) =>
renderItem({item, index, contextProfileDid: resolvedDid}),
[resolvedDid],
)
// track seen items
const seenItemsRef = React.useRef<Set<string>>(new Set())
React.useEffect(() => {
seenItemsRef.current.clear()
}, [resolvedDid])
const onItemSeen = React.useCallback(
(item: ActorDefs.ProfileView) => {
if (seenItemsRef.current.has(item.did)) {
return
}
seenItemsRef.current.add(item.did)
const position = followers.findIndex(p => p.did === item.did) + 1
if (position === 0) {
return
}
logger.metric(
'profileCard:seen',
{
profileDid: item.did,
position,
...(resolvedDid !== undefined && {contextProfileDid: resolvedDid}),
},
{statsig: false},
)
},
[followers, resolvedDid],
)
if (followers.length < 1) {
return (
<ListMaybePlaceholder
@@ -104,12 +142,13 @@ export function ProfileFollowers({name}: {name: string}) {
return (
<List
data={followers}
renderItem={renderItem}
renderItem={renderItemWithContext}
keyExtractor={keyExtractor}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={4}
onItemSeen={onItemSeen}
ListFooterComponent={
<ListFooter
isFetchingNextPage={isFetchingNextPage}
+40 -1
View File
@@ -16,15 +16,19 @@ import {ProfileCardWithFollowBtn} from './ProfileCard'
function renderItem({
item,
index,
contextProfileDid,
}: {
item: ActorDefs.ProfileView
index: number
contextProfileDid: string | undefined
}) {
return (
<ProfileCardWithFollowBtn
key={item.did}
profile={item}
noBorder={index === 0}
position={index + 1}
contextProfileDid={contextProfileDid}
/>
)
}
@@ -83,6 +87,40 @@ export function ProfileFollows({name}: {name: string}) {
}
}, [error, fetchNextPage, hasNextPage, isFetchingNextPage])
const renderItemWithContext = React.useCallback(
({item, index}: {item: ActorDefs.ProfileView; index: number}) =>
renderItem({item, index, contextProfileDid: resolvedDid}),
[resolvedDid],
)
// track seen items
const seenItemsRef = React.useRef<Set<string>>(new Set())
React.useEffect(() => {
seenItemsRef.current.clear()
}, [resolvedDid])
const onItemSeen = React.useCallback(
(item: ActorDefs.ProfileView) => {
if (seenItemsRef.current.has(item.did)) {
return
}
seenItemsRef.current.add(item.did)
const position = follows.findIndex(p => p.did === item.did) + 1
if (position === 0) {
return
}
logger.metric(
'profileCard:seen',
{
profileDid: item.did,
position,
...(resolvedDid !== undefined && {contextProfileDid: resolvedDid}),
},
{statsig: false},
)
},
[follows, resolvedDid],
)
if (follows.length < 1) {
return (
<ListMaybePlaceholder
@@ -104,12 +142,13 @@ export function ProfileFollows({name}: {name: string}) {
return (
<List
data={follows}
renderItem={renderItem}
renderItem={renderItemWithContext}
keyExtractor={keyExtractor}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={4}
onItemSeen={onItemSeen}
ListFooterComponent={
<ListFooter
isFetchingNextPage={isFetchingNextPage}