Refactor photo embed analytics to post:photoEmbed:* namespace (#10784)
Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,7 @@ import {type Dimensions} from '#/lib/media/types'
|
||||
import {useTheme} from '#/alf'
|
||||
import {setSystemUITheme} from '#/alf/util/systemUI'
|
||||
import {type Lightbox} from '#/components/Lightbox/state'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
import {IS_IOS} from '#/env'
|
||||
import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army'
|
||||
import {Footer} from '../chrome/Footer'
|
||||
@@ -228,7 +229,8 @@ function ImageView({
|
||||
openProgress: SharedValue<number>
|
||||
thumbRects: SharedValue<Record<number, MeasuredDimensions | null>>
|
||||
}) {
|
||||
const {images, index: initialImageIndex} = lightbox
|
||||
const {images, index: initialImageIndex, metricsContext} = lightbox
|
||||
const ax = useAnalytics()
|
||||
const isAnimated = useMemo(() => canAnimate(lightbox), [lightbox])
|
||||
const [isScaled, setIsScaled] = useState(false)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
@@ -377,7 +379,21 @@ function ImageView({
|
||||
scrollEnabled={!isScaled}
|
||||
initialPage={initialImageIndex}
|
||||
onPageSelected={e => {
|
||||
setImageIndex(e.nativeEvent.position)
|
||||
const next = e.nativeEvent.position
|
||||
setImageIndex(prev => {
|
||||
if (metricsContext && prev !== next) {
|
||||
ax.metric('post:photoEmbed:lightboxSwipe', {
|
||||
layout: metricsContext.layout,
|
||||
fromImage: prev + 1,
|
||||
toImage: next + 1,
|
||||
totalImages: images.length,
|
||||
postUri: metricsContext.postUri,
|
||||
postAuthorDid: metricsContext.postAuthorDid,
|
||||
feedDescriptor: metricsContext.feedDescriptor,
|
||||
})
|
||||
}
|
||||
return next
|
||||
})
|
||||
setIsScaled(false)
|
||||
}}
|
||||
onPageScrollStateChanged={e => {
|
||||
|
||||
@@ -11,10 +11,20 @@ import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||
import {useHotkeysContext} from '#/lib/hotkeys'
|
||||
import {type ImageSource} from '#/components/Lightbox/types'
|
||||
|
||||
export type LightboxMetricsContext = {
|
||||
layout: 'single' | 'grid' | 'carousel'
|
||||
postUri: string
|
||||
postAuthorDid: string
|
||||
feedDescriptor?: string
|
||||
}
|
||||
|
||||
export type Lightbox = {
|
||||
id: string
|
||||
images: ImageSource[]
|
||||
index: number
|
||||
// Set for post photo embeds so the lightbox can emit post:photoEmbed:lightboxSwipe.
|
||||
// Left unset for non-post contexts (e.g. profile avatar/banner lightbox).
|
||||
metricsContext?: LightboxMetricsContext
|
||||
}
|
||||
|
||||
const LightboxContext = createContext<{
|
||||
|
||||
@@ -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,20 @@ 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.post
|
||||
? {
|
||||
postUri: rest.post.uri,
|
||||
postAuthorDid: rest.post.author.did,
|
||||
feedDescriptor: rest.feedDescriptor,
|
||||
}
|
||||
: undefined
|
||||
const metricsContext: LightboxMetricsContext | undefined = postContext
|
||||
? {layout, ...postContext}
|
||||
: undefined
|
||||
|
||||
// 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 +74,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 +92,7 @@ export function ImageEmbed({
|
||||
type: 'image',
|
||||
})),
|
||||
index,
|
||||
metricsContext,
|
||||
})
|
||||
}
|
||||
const onPressIn = (_: number) => {
|
||||
@@ -132,6 +158,7 @@ export function ImageEmbed({
|
||||
onPressIn={onPressIn}
|
||||
viewContext={rest.viewContext}
|
||||
isWithinQuote={rest.isWithinQuote}
|
||||
metricsPostContext={postContext}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -345,6 +345,9 @@ 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.
|
||||
post={quote}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -15,6 +15,13 @@ export type CommonProps = {
|
||||
viewContext?: PostEmbedViewContext
|
||||
isWithinQuote?: boolean
|
||||
allowNestedQuotes?: boolean
|
||||
/**
|
||||
* The post that contains this embed. Used for analytics on photo embed
|
||||
* events (post:photoEmbed:*). When the embed has no owning post (e.g.
|
||||
* composer previews), leave this undefined and no events will be emitted.
|
||||
*/
|
||||
post?: AppBskyFeedDefs.PostView
|
||||
feedDescriptor?: string
|
||||
}
|
||||
|
||||
export type EmbedProps = CommonProps & {
|
||||
|
||||
@@ -55,6 +55,13 @@ interface GalleryProps {
|
||||
onPressIn?: (index: number) => void
|
||||
viewContext?: PostEmbedViewContext
|
||||
isWithinQuote?: boolean
|
||||
// Post context for the in-feed carousel swipe metric. Omit for non-post
|
||||
// contexts (no event will be emitted).
|
||||
metricsPostContext?: {
|
||||
postUri: string
|
||||
postAuthorDid: string
|
||||
feedDescriptor?: string
|
||||
}
|
||||
}
|
||||
|
||||
const Context = createContext<{
|
||||
@@ -99,6 +106,7 @@ export function Gallery({
|
||||
onPressIn,
|
||||
viewContext,
|
||||
isWithinQuote,
|
||||
metricsPostContext,
|
||||
}: GalleryProps) {
|
||||
const {t: l} = useLingui()
|
||||
const ax = useAnalytics()
|
||||
@@ -169,13 +177,17 @@ export function Gallery({
|
||||
const emitSwipeMetric = useMemo(
|
||||
() =>
|
||||
debounce((fromIndex: number, toIndex: number) => {
|
||||
ax.metric('post:gallery:swipe', {
|
||||
if (!metricsPostContext) return
|
||||
ax.metric('post:photoEmbed:carouselSwipe', {
|
||||
fromImage: fromIndex + 1, // convert to 1-based index for easier analysis
|
||||
toImage: toIndex + 1, // convert to 1-based index for easier analysis
|
||||
totalImages: images.length,
|
||||
postUri: metricsPostContext.postUri,
|
||||
postAuthorDid: metricsPostContext.postAuthorDid,
|
||||
feedDescriptor: metricsPostContext.feedDescriptor,
|
||||
})
|
||||
}, 200),
|
||||
[ax, images.length],
|
||||
[ax, images.length, metricsPostContext],
|
||||
)
|
||||
|
||||
const setCurrentIndex = (index: number) => {
|
||||
@@ -277,10 +289,6 @@ export function Gallery({
|
||||
renderItem={({item, index}) => {
|
||||
const openLightboxAtIndex = onPress
|
||||
? () => {
|
||||
ax.metric('post:gallery:openLightbox', {
|
||||
fromImage: index + 1, // convert to 1-based index for easier analysis
|
||||
totalImages: images.length,
|
||||
})
|
||||
const refs: AnimatedRef<any>[] = []
|
||||
const dims: (Dimensions | null)[] = []
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
|
||||
Reference in New Issue
Block a user