From 5c551e1ad9b6acdd8ecdd927c886fd166aafbde7 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 8 Jun 2026 10:50:06 -0500 Subject: [PATCH] Fire impressions on vis change --- src/components/Post/Embed/ImageEmbed.tsx | 16 +------ src/view/com/posts/PostFeed.tsx | 60 +++++++++++++++++++----- 2 files changed, 50 insertions(+), 26 deletions(-) diff --git a/src/components/Post/Embed/ImageEmbed.tsx b/src/components/Post/Embed/ImageEmbed.tsx index 7016147a10..207105b31a 100644 --- a/src/components/Post/Embed/ImageEmbed.tsx +++ b/src/components/Post/Embed/ImageEmbed.tsx @@ -1,4 +1,4 @@ -import {useEffect, useRef} from 'react' +import {useRef} from 'react' import {InteractionManager, View} from 'react-native' import {type AnimatedRef} from 'react-native-reanimated' import {Image} from 'expo-image' @@ -57,20 +57,6 @@ export function ImageEmbed({ ? {layout, ...postContext} : undefined - // Impression: one per mount of a post photo embed. Covers all three layouts - // identically so the opens/impressions CTR is unbiased by layout. - useEffect(() => { - if (images.length > 0 && postContext) { - ax.metric('post:photoEmbed:impression', { - layout, - totalImages: images.length, - ...postContext, - }) - } - // Fire once per mount; intentionally not reactive to post context changes. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - // Captured from AutoSizedImage so the peek-commit handler can reuse the same // ref + dims that a tap would — keeps the lightbox's return animation intact. const singleContainerRef = useRef | null>(null) diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 48754d2753..227fddde74 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -13,6 +13,8 @@ import { import { type AppBskyActorDefs, AppBskyEmbedExternal, + AppBskyEmbedGallery, + AppBskyEmbedImages, AppBskyEmbedVideo, type AppBskyFeedDefs, } from '@atproto/api' @@ -907,7 +909,9 @@ let PostFeed = ({ const seenActorWithStatusRef = useRef>(new Set()) const seenPostUrisRef = useRef>(new Set()) - const seenStandardSiteUrisRef = useRef>(new Set()) + // Tracks every post we've seen so we can fire per-post events exactly once, + // regardless of the post's position within its slice. + const seenPerPostUrisRef = useRef>(new Set()) // Helper to calculate position in feed (count only root posts, not interstitials or thread replies) const getPostPosition = useNonReactiveCallback( @@ -940,6 +944,48 @@ let PostFeed = ({ (item: FeedRow) => { feedFeedback.onItemSeen(item) + // Events that should fire exactly once for every new post, regardless of + // its position within a slice or video grid row. + const onPostSeen = (post: AppBskyFeedDefs.PostView) => { + if (seenPerPostUrisRef.current.has(post.uri)) return + seenPerPostUrisRef.current.add(post.uri) + + // Standard site embed view tracking + if ( + AppBskyEmbedExternal.isView(post.embed) && + isStandardSiteEmbed(post.embed.external) + ) { + ax.metric('embed:standardSite:view', {url: post.embed.external.uri}) + } + + // Photo embed impression tracking + if ( + AppBskyEmbedImages.isView(post.embed) || + AppBskyEmbedGallery.isView(post.embed) + ) { + const totalImages = AppBskyEmbedGallery.isView(post.embed) + ? post.embed.items.filter(AppBskyEmbedGallery.isViewImage).length + : post.embed.images.length + const useExpandedLayout = AppBskyEmbedGallery.isView(post.embed) + ? totalImages > 4 + : ax.features.enabled(ax.features.PostGalleryEmbedEnable) + const layout = + totalImages === 1 + ? 'single' + : useExpandedLayout + ? 'carousel' + : 'grid' + + ax.metric('post:photoEmbed:impression', { + layout, + totalImages, + uri: post.uri, + authorDid: post.author.did, + feedDescriptor: feedFeedback.feedDescriptor || feed, + }) + } + } + // Track post:view events if (item.type === 'sliceItem') { const slice = item.slice @@ -947,6 +993,8 @@ let PostFeed = ({ const postItem = slice.items[indexInSlice] const post = postItem.post + onPostSeen(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) @@ -977,16 +1025,6 @@ let PostFeed = ({ }) } } - - // Standard site embed view tracking - if ( - AppBskyEmbedExternal.isView(post.embed) && - isStandardSiteEmbed(post.embed.external) && - !seenStandardSiteUrisRef.current.has(post.embed.external.uri) - ) { - seenStandardSiteUrisRef.current.add(post.embed.external.uri) - ax.metric('embed:standardSite:view', {url: post.embed.external.uri}) - } } else if (item.type === 'videoGridRow') { // Track each video in the grid row for (let i = 0; i < item.items.length; i++) {