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