Refactor photo embed analytics to post:photoEmbed:* namespace

Replaces the legacy post:gallery:* events, which only instrumented the
carousel layout (and so were biased by the PostGalleryEmbedEnable rollout)
with a coherent, layout-agnostic family that fires identically across the
single image, 2-4 image grid, and carousel renderers.

The new events all carry uri, authorDid, and an optional feedDescriptor,
and a layout discriminator ('single' | 'grid' | 'carousel') so opens and
impressions are directly comparable across layouts.

- post:photoEmbed:impression: fires once per mount of a post photo embed
- post:photoEmbed:open: fires from the shared onPress in ImageEmbed, so
  all three layouts emit it without per-layout duplication
- post:photoEmbed:carouselSwipe: in-feed carousel swipe (debounced)
- post:photoEmbed:lightboxSwipe: fires from the lightbox pager regardless
  of which layout opened it

The dead post:gallery:impression definition is removed, and
post:gallery:openLightbox / post:gallery:swipe are hard-removed with no
dual-emit period.

Quoted-post photos are attributed to the quoted post (quote.uri /
quote.author.did), not the surrounding post.
This commit is contained in:
vineyardbovines
2026-06-08 10:43:36 -04:00
parent 5c0429a3f7
commit 28bd708c6b
12 changed files with 136 additions and 17 deletions
+44 -2
View File
@@ -1,4 +1,4 @@
import {useRef} from 'react'
import {useEffect, useRef} from 'react'
import {InteractionManager, View} from 'react-native'
import {type AnimatedRef} from 'react-native-reanimated'
import {Image} from 'expo-image'
@@ -8,7 +8,10 @@ import {atoms as a, tokens} from '#/alf'
import {AutoSizedImage} from '#/components/images/AutoSizedImage'
import {Gallery} from '#/components/images/Gallery'
import {ImageLayoutGrid} from '#/components/images/ImageLayoutGrid'
import {useLightboxControls} from '#/components/Lightbox/state'
import {
type LightboxMetricsContext,
useLightboxControls,
} from '#/components/Lightbox/state'
import {type Dimensions} from '#/components/Lightbox/types'
import {ImageContextMenu} from '#/components/Post/Embed/ImageContextMenu'
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
@@ -40,6 +43,35 @@ export function ImageEmbed({
? images.length > MAX_GRID_IMAGES
: ax.features.enabled(ax.features.PostGalleryEmbedEnable)
const layout: 'single' | 'grid' | 'carousel' =
images.length === 1 ? 'single' : useExpandedLayout ? 'carousel' : 'grid'
const postContext =
rest.uri && rest.authorDid
? {
uri: rest.uri,
authorDid: rest.authorDid,
feedDescriptor: rest.feedDescriptor,
}
: undefined
const metricsContext: LightboxMetricsContext | undefined = postContext
? {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<AnimatedRef<any> | null>(null)
@@ -57,6 +89,14 @@ export function ImageEmbed({
refs: AnimatedRef<any>[],
fetchedDims: (Dimensions | null)[],
) => {
if (postContext) {
ax.metric('post:photoEmbed:open', {
layout,
fromImage: index + 1,
totalImages: images.length,
...postContext,
})
}
openLightbox({
images: items.map((item, i) => ({
...item,
@@ -67,6 +107,7 @@ export function ImageEmbed({
type: 'image',
})),
index,
metricsContext,
})
}
const onPressIn = (_: number) => {
@@ -132,6 +173,7 @@ export function ImageEmbed({
onPressIn={onPressIn}
viewContext={rest.viewContext}
isWithinQuote={rest.isWithinQuote}
metricsPostContext={postContext}
/>
</View>
)
+4
View File
@@ -345,6 +345,10 @@ export function QuoteEmbed({
allowNestedQuotes={
parentIsWithinQuote ? false : parentAllowNestedQuotes
}
// The photo embed belongs to the quoted post, so attribute its
// analytics to the quoted post rather than the parent.
uri={quote.uri}
authorDid={quote.author.did}
/>
)}
</>
+6
View File
@@ -15,6 +15,12 @@ export type CommonProps = {
viewContext?: PostEmbedViewContext
isWithinQuote?: boolean
allowNestedQuotes?: boolean
// Post context for analytics on photo embed events (post:photoEmbed:*).
// When the embed has no owning post (e.g. previews), leave these undefined
// and no events will be emitted.
uri?: string
authorDid?: string
feedDescriptor?: string
}
export type EmbedProps = CommonProps & {