diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx
index 41626ff399..55bef226b3 100644
--- a/src/components/ProfileCard.tsx
+++ b/src/components/ProfileCard.tsx
@@ -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 (
@@ -60,6 +64,8 @@ export function Default({
profile={profile}
moderationOpts={moderationOpts}
logContext={logContext}
+ position={position}
+ contextProfileDid={contextProfileDid}
/>
)
@@ -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 (
@@ -83,6 +93,8 @@ export function Card({
profile={profile}
moderationOpts={moderationOpts}
logContext={logContext}
+ position={position}
+ contextProfileDid={contextProfileDid}
/>
@@ -437,6 +449,8 @@ export type FollowButtonProps = {
colorInverted?: boolean
onFollow?: () => void
withIcon?: boolean
+ position?: number
+ contextProfileDid?: string
} & Partial
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')
diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts
index c7bac2fecd..37b8e21f74 100644
--- a/src/logger/metrics.ts
+++ b/src/logger/metrics.ts
@@ -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'
diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts
index eb65fef7c2..9d30288d40 100644
--- a/src/state/queries/profile.ts
+++ b/src/state/queries/profile.ts
@@ -242,12 +242,19 @@ export function useProfileFollowMutationQueue(
profile: Shadow,
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,
+ 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)
},
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index cee9507030..f200a62cb6 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -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}
/>
)
diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx
index dfb63909e4..b5838f0c19 100644
--- a/src/view/com/profile/ProfileFollowers.tsx
+++ b/src/view/com/profile/ProfileFollowers.tsx
@@ -16,15 +16,19 @@ import {ProfileCardWithFollowBtn} from './ProfileCard'
function renderItem({
item,
index,
+ contextProfileDid,
}: {
item: ActorDefs.ProfileView
index: number
+ contextProfileDid: string | undefined
}) {
return (
)
}
@@ -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>(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 (
)
}
@@ -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>(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 (