Standardize metadata for client events in feeds (#9653)

This commit is contained in:
Alex Benzer
2026-01-13 02:42:26 -08:00
committed by GitHub
parent c540dae4e7
commit 2d8de1ddc3
10 changed files with 194 additions and 38 deletions
+14 -2
View File
@@ -8,6 +8,7 @@ import type React from 'react'
import {useCleanError} from '#/lib/hooks/useCleanError' import {useCleanError} from '#/lib/hooks/useCleanError'
import {logger} from '#/logger' import {logger} from '#/logger'
import {type Shadow} from '#/state/cache/post-shadow' import {type Shadow} from '#/state/cache/post-shadow'
import {useFeedFeedbackContext} from '#/state/feed-feedback'
import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation' import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation'
import {useRequireAuth} from '#/state/session' import {useRequireAuth} from '#/state/session'
import {useTheme} from '#/alf' import {useTheme} from '#/alf'
@@ -32,6 +33,7 @@ export const BookmarkButton = memo(function BookmarkButton({
const {mutateAsync: bookmark} = useBookmarkMutation() const {mutateAsync: bookmark} = useBookmarkMutation()
const cleanError = useCleanError() const cleanError = useCleanError()
const requireAuth = useRequireAuth() const requireAuth = useRequireAuth()
const {feedDescriptor} = useFeedFeedbackContext()
const {viewer} = post const {viewer} = post
const isBookmarked = !!viewer?.bookmarked const isBookmarked = !!viewer?.bookmarked
@@ -50,7 +52,12 @@ export const BookmarkButton = memo(function BookmarkButton({
post, post,
}) })
logger.metric('post:bookmark', {logContext}) logger.metric('post:bookmark', {
uri: post.uri,
authorDid: post.author.did,
logContext,
feedDescriptor,
})
toast.show( toast.show(
<toast.Outer> <toast.Outer>
@@ -85,7 +92,12 @@ export const BookmarkButton = memo(function BookmarkButton({
uri: post.uri, uri: post.uri,
}) })
logger.metric('post:unbookmark', {logContext}) logger.metric('post:unbookmark', {
uri: post.uri,
authorDid: post.author.did,
logContext,
feedDescriptor,
})
toast.show( toast.show(
<toast.Outer> <toast.Outer>
@@ -98,6 +98,7 @@ let PostMenuItems = ({
richText, richText,
threadgateRecord, threadgateRecord,
onShowLess, onShowLess,
logContext,
}: { }: {
testID: string testID: string
post: Shadow<AppBskyFeedDefs.PostView> post: Shadow<AppBskyFeedDefs.PostView>
@@ -111,6 +112,7 @@ let PostMenuItems = ({
timestamp: string timestamp: string
threadgateRecord?: AppBskyFeedThreadgate.Record threadgateRecord?: AppBskyFeedThreadgate.Record
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
}): React.ReactNode => { }): React.ReactNode => {
const {hasSession, currentAccount} = useSession() const {hasSession, currentAccount} = useSession()
const {_} = useLingui() const {_} = useLingui()
@@ -210,9 +212,21 @@ let PostMenuItems = ({
try { try {
if (isThreadMuted) { if (isThreadMuted) {
unmuteThread() unmuteThread()
logger.metric('post:unmute', {
uri: postUri,
authorDid: postAuthor.did,
logContext,
feedDescriptor: feedFeedback.feedDescriptor,
})
Toast.show(_(msg`You will now receive notifications for this thread`)) Toast.show(_(msg`You will now receive notifications for this thread`))
} else { } else {
muteThread() muteThread()
logger.metric('post:mute', {
uri: postUri,
authorDid: postAuthor.did,
logContext,
feedDescriptor: feedFeedback.feedDescriptor,
})
Toast.show( Toast.show(
_(msg`You will no longer receive notifications for this thread`), _(msg`You will no longer receive notifications for this thread`),
) )
@@ -272,6 +286,12 @@ let PostMenuItems = ({
feedContext: postFeedContext, feedContext: postFeedContext,
reqId: postReqId, reqId: postReqId,
}) })
logger.metric('post:showMore', {
uri: postUri,
authorDid: postAuthor.did,
logContext,
feedDescriptor: feedFeedback.feedDescriptor,
})
Toast.show( Toast.show(
_(msg({message: 'Feedback sent to feed operator', context: 'toast'})), _(msg({message: 'Feedback sent to feed operator', context: 'toast'})),
) )
@@ -284,6 +304,12 @@ let PostMenuItems = ({
feedContext: postFeedContext, feedContext: postFeedContext,
reqId: postReqId, reqId: postReqId,
}) })
logger.metric('post:showLess', {
uri: postUri,
authorDid: postAuthor.did,
logContext,
feedDescriptor: feedFeedback.feedDescriptor,
})
if (onShowLess) { if (onShowLess) {
onShowLess({ onShowLess({
item: postUri, item: postUri,
@@ -29,6 +29,7 @@ let PostMenuButton = ({
threadgateRecord, threadgateRecord,
onShowLess, onShowLess,
hitSlop, hitSlop,
logContext,
}: { }: {
testID: string testID: string
post: Shadow<AppBskyFeedDefs.PostView> post: Shadow<AppBskyFeedDefs.PostView>
@@ -41,6 +42,7 @@ let PostMenuButton = ({
threadgateRecord?: AppBskyFeedThreadgate.Record threadgateRecord?: AppBskyFeedThreadgate.Record
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
hitSlop?: Insets hitSlop?: Insets
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
}): React.ReactNode => { }): React.ReactNode => {
const {_} = useLingui() const {_} = useLingui()
@@ -87,6 +89,7 @@ let PostMenuButton = ({
timestamp={timestamp} timestamp={timestamp}
threadgateRecord={threadgateRecord} threadgateRecord={threadgateRecord}
onShowLess={onShowLess} onShowLess={onShowLess}
logContext={logContext}
/> />
)} )}
</Menu.Root> </Menu.Root>
@@ -16,6 +16,7 @@ import {useGate} from '#/lib/statsig/statsig'
import {toShareUrl} from '#/lib/strings/url-helpers' import {toShareUrl} from '#/lib/strings/url-helpers'
import {logger} from '#/logger' import {logger} from '#/logger'
import {type Shadow} from '#/state/cache/post-shadow' import {type Shadow} from '#/state/cache/post-shadow'
import {useFeedFeedbackContext} from '#/state/feed-feedback'
import {EventStopper} from '#/view/com/util/EventStopper' import {EventStopper} from '#/view/com/util/EventStopper'
import {native} from '#/alf' import {native} from '#/alf'
import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox' import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox'
@@ -35,6 +36,7 @@ let ShareMenuButton = ({
threadgateRecord, threadgateRecord,
onShare, onShare,
hitSlop, hitSlop,
logContext,
}: { }: {
testID: string testID: string
post: Shadow<AppBskyFeedDefs.PostView> post: Shadow<AppBskyFeedDefs.PostView>
@@ -45,9 +47,11 @@ let ShareMenuButton = ({
threadgateRecord?: AppBskyFeedThreadgate.Record threadgateRecord?: AppBskyFeedThreadgate.Record
onShare: () => void onShare: () => void
hitSlop?: Insets hitSlop?: Insets
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
}): React.ReactNode => { }): React.ReactNode => {
const {_} = useLingui() const {_} = useLingui()
const gate = useGate() const gate = useGate()
const {feedDescriptor} = useFeedFeedbackContext()
const ShareIcon = gate('alt_share_icon') const ShareIcon = gate('alt_share_icon')
? ArrowShareRightIcon ? ArrowShareRightIcon
@@ -65,13 +69,27 @@ let ShareMenuButton = ({
setTimeout(menuControl.open) setTimeout(menuControl.open)
logger.metric( logger.metric(
'share:open', 'post:share',
{context: big ? 'thread' : 'feed'}, {
uri: post.uri,
authorDid: post.author.did,
logContext,
feedDescriptor,
postContext: big ? 'thread' : 'feed',
},
{statsig: true}, {statsig: true},
) )
}, },
}), }),
[menuControl, setHasBeenOpen, big], [
menuControl,
setHasBeenOpen,
big,
logContext,
feedDescriptor,
post.uri,
post.author.did,
],
) )
const onNativeLongPress = () => { const onNativeLongPress = () => {
+19 -1
View File
@@ -13,6 +13,7 @@ import {CountWheel} from '#/lib/custom-animations/CountWheel'
import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon' import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {logger} from '#/logger'
import {type Shadow} from '#/state/cache/types' import {type Shadow} from '#/state/cache/types'
import {useFeedFeedbackContext} from '#/state/feed-feedback' import {useFeedFeedbackContext} from '#/state/feed-feedback'
import { import {
@@ -174,6 +175,12 @@ let PostControls = ({
feedContext, feedContext,
reqId, reqId,
}) })
logger.metric('post:clickQuotePost', {
uri: post.uri,
authorDid: post.author.did,
logContext,
feedDescriptor,
})
openComposer({ openComposer({
quote: post, quote: post,
onPost: onPostReply, onPost: onPostReply,
@@ -217,7 +224,16 @@ let PostControls = ({
testID="replyBtn" testID="replyBtn"
onPress={ onPress={
!replyDisabled !replyDisabled
? () => requireAuth(() => onPressReply()) ? () =>
requireAuth(() => {
logger.metric('post:clickReply', {
uri: post.uri,
authorDid: post.author.did,
logContext,
feedDescriptor,
})
onPressReply()
})
: undefined : undefined
} }
label={_( label={_(
@@ -315,6 +331,7 @@ let PostControls = ({
left: secondaryControlSpacingStyles.gap / 2, left: secondaryControlSpacingStyles.gap / 2,
right: secondaryControlSpacingStyles.gap / 2, right: secondaryControlSpacingStyles.gap / 2,
}} }}
logContext={logContext}
/> />
<PostMenuButton <PostMenuButton
testID="postDropdownBtn" testID="postDropdownBtn"
@@ -330,6 +347,7 @@ let PostControls = ({
hitSlop={{ hitSlop={{
left: secondaryControlSpacingStyles.gap / 2, left: secondaryControlSpacingStyles.gap / 2,
}} }}
logContext={logContext}
/> />
</View> </View>
</View> </View>
+77 -9
View File
@@ -176,13 +176,19 @@ export type MetricEvents = {
'feed:suggestion:press': { 'feed:suggestion:press': {
feedUrl: string feedUrl: string
} }
'feed:showMore': { 'post:showMore': {
feed: string uri: string
feedContext: string authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
} }
'feed:showLess': { 'post:showLess': {
feed: string uri: string
feedContext: string authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
} }
'feed:clickthrough': { 'feed:clickthrough': {
feed: string feed: string
@@ -257,15 +263,70 @@ export type MetricEvents = {
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string feedDescriptor?: string
} }
'post:mute': {} 'post:mute': {
'post:unmute': {} uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
}
'post:unmute': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
}
'post:pin': {} 'post:pin': {}
'post:unpin': {} 'post:unpin': {}
'post:bookmark': { 'post:bookmark': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
} }
'post:unbookmark': { 'post:unbookmark': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
}
'post:clickReply': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
}
'post:clickQuotePost': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
}
'post:clickthroughAuthor': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
}
'post:clickthroughItem': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
}
'post:clickthroughEmbed': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
position?: number
} }
'post:view': { 'post:view': {
uri: string uri: string
@@ -565,7 +626,14 @@ export type MetricEvents = {
'live:view:profile': {subject: string} 'live:view:profile': {subject: string}
'live:view:post': {subject: string; feed?: string} 'live:view:post': {subject: string; feed?: string}
'share:open': {context: 'feed' | 'thread'} 'post:share': {
uri: string
authorDid: string
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
feedDescriptor?: string
postContext: 'feed' | 'thread'
position?: number
}
'share:press:copyLink': {} 'share:press:copyLink': {}
'share:press:nativeShare': {} 'share:press:nativeShare': {}
'share:press:openDmSearch': {} 'share:press:openDmSearch': {}
@@ -281,6 +281,12 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
]) ])
const onOpenAuthor = () => { const onOpenAuthor = () => {
logger.metric('post:clickthroughAuthor', {
uri: post.uri,
authorDid: post.author.did,
logContext: 'PostThreadItem',
feedDescriptor: feedFeedback.feedDescriptor,
})
if (postSource) { if (postSource) {
feedFeedback.sendInteraction({ feedFeedback.sendInteraction({
item: post.uri, item: post.uri,
@@ -292,6 +298,12 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
} }
const onOpenEmbed = () => { const onOpenEmbed = () => {
logger.metric('post:clickthroughEmbed', {
uri: post.uri,
authorDid: post.author.did,
logContext: 'PostThreadItem',
feedDescriptor: feedFeedback.feedDescriptor,
})
if (postSource) { if (postSource) {
feedFeedback.sendInteraction({ feedFeedback.sendInteraction({
item: post.uri, item: post.uri,
+1 -20
View File
@@ -137,7 +137,6 @@ export function useFeedFeedback(
sendOrAggregateInteractionsForStats( sendOrAggregateInteractionsForStats(
aggregatedStats.current, aggregatedStats.current,
interactionsToSend, interactionsToSend,
feed?.feedDescriptor ?? 'unknown',
) )
throttledFlushAggregatedStats() throttledFlushAggregatedStats()
logger.debug('flushed') logger.debug('flushed')
@@ -274,28 +273,10 @@ function createAggregatedStats(): AggregatedStats {
function sendOrAggregateInteractionsForStats( function sendOrAggregateInteractionsForStats(
stats: AggregatedStats, stats: AggregatedStats,
interactions: AppBskyFeedDefs.Interaction[], interactions: AppBskyFeedDefs.Interaction[],
feed: string,
) { ) {
for (let interaction of interactions) { for (let interaction of interactions) {
switch (interaction.event) { switch (interaction.event) {
// Pressing "Show more" / "Show less" is relatively uncommon so we won't aggregate them. // The events are aggregated and sent later in batches.
// This lets us send the feed context together with them.
case 'app.bsky.feed.defs#requestLess': {
logger.metric('feed:showLess', {
feed,
feedContext: interaction.feedContext ?? '',
})
break
}
case 'app.bsky.feed.defs#requestMore': {
logger.metric('feed:showMore', {
feed,
feedContext: interaction.feedContext ?? '',
})
break
}
// The rest of the events are aggregated and sent later in batches.
case 'app.bsky.feed.defs#clickthroughAuthor': case 'app.bsky.feed.defs#clickthroughAuthor':
case 'app.bsky.feed.defs#clickthroughEmbed': case 'app.bsky.feed.defs#clickthroughEmbed':
case 'app.bsky.feed.defs#clickthroughItem': case 'app.bsky.feed.defs#clickthroughItem':
-2
View File
@@ -373,7 +373,6 @@ function useThreadMuteMutation() {
{uri: string} // the root post's uri {uri: string} // the root post's uri
>({ >({
mutationFn: ({uri}) => { mutationFn: ({uri}) => {
logger.metric('post:mute', {})
return agent.api.app.bsky.graph.muteThread({root: uri}) return agent.api.app.bsky.graph.muteThread({root: uri})
}, },
}) })
@@ -383,7 +382,6 @@ function useThreadUnmuteMutation() {
const agent = useAgent() const agent = useAgent()
return useMutation<{}, Error, {uri: string}>({ return useMutation<{}, Error, {uri: string}>({
mutationFn: ({uri}) => { mutationFn: ({uri}) => {
logger.metric('post:unmute', {})
return agent.api.app.bsky.graph.unmuteThread({root: uri}) return agent.api.app.bsky.graph.unmuteThread({root: uri})
}, },
}) })
+21 -1
View File
@@ -21,6 +21,7 @@ import {makeProfileLink} from '#/lib/routes/links'
import {type NavigationProp} from '#/lib/routes/types' import {type NavigationProp} from '#/lib/routes/types'
import {useGate} from '#/lib/statsig/statsig' import {useGate} from '#/lib/statsig/statsig'
import {countLines} from '#/lib/strings/helpers' import {countLines} from '#/lib/strings/helpers'
import {logger} from '#/logger'
import { import {
POST_TOMBSTONE, POST_TOMBSTONE,
type Shadow, type Shadow,
@@ -173,7 +174,8 @@ let FeedItemInner = ({
const urip = new AtUri(post.uri) const urip = new AtUri(post.uri)
return [makeProfileLink(post.author, 'post', urip.rkey), urip.rkey] return [makeProfileLink(post.author, 'post', urip.rkey), urip.rkey]
}, [post.uri, post.author]) }, [post.uri, post.author])
const {sendInteraction, feedSourceInfo} = useFeedFeedbackContext() const {sendInteraction, feedSourceInfo, feedDescriptor} =
useFeedFeedbackContext()
const onPressReply = () => { const onPressReply = () => {
sendInteraction({ sendInteraction({
@@ -209,6 +211,12 @@ let FeedItemInner = ({
feedContext, feedContext,
reqId, reqId,
}) })
logger.metric('post:clickthroughAuthor', {
uri: post.uri,
authorDid: post.author.did,
logContext: 'FeedItem',
feedDescriptor,
})
} }
const onOpenReposter = () => { const onOpenReposter = () => {
@@ -227,6 +235,12 @@ let FeedItemInner = ({
feedContext, feedContext,
reqId, reqId,
}) })
logger.metric('post:clickthroughEmbed', {
uri: post.uri,
authorDid: post.author.did,
logContext: 'FeedItem',
feedDescriptor,
})
} }
const onBeforePress = () => { const onBeforePress = () => {
@@ -236,6 +250,12 @@ let FeedItemInner = ({
feedContext, feedContext,
reqId, reqId,
}) })
logger.metric('post:clickthroughItem', {
uri: post.uri,
authorDid: post.author.did,
logContext: 'FeedItem',
feedDescriptor,
})
unstableCacheProfileView(queryClient, post.author) unstableCacheProfileView(queryClient, post.author)
setUnstablePostSource(buildPostSourceKey(post.uri, post.author.handle), { setUnstablePostSource(buildPostSourceKey(post.uri, post.author.handle), {
feedSourceInfo, feedSourceInfo,