From cb35f71826ac887e58c711e489c38d0f85e42dcb Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Thu, 16 Jul 2026 13:52:22 -0700 Subject: [PATCH] feat(analytics): add feed_type to Playback: Play events from the feed Plays originating from the feed lineup now carry a feed_type property (FOR_YOU / LATEST, matching Feed: Change view's view values) so engagement on the two feed views can be measured separately. The feed view is stamped onto playback queue entries when the feed lineup builds them (same pattern as collectionId), so passive plays from queue auto-advance report the view the track actually came from even if the user has since switched tabs. Track-tile plays on the feed page include it directly. Co-Authored-By: Claude Fable 5 --- packages/common/src/models/Analytics.ts | 3 ++ .../common/src/store/playback/selectors.ts | 3 ++ packages/common/src/store/playback/types.ts | 7 ++++- .../web/src/common/store/playback/sagas.ts | 9 ++++-- .../web/src/components/lineup/TrackLineup.tsx | 30 ++++++++++++++----- .../components/desktop/FeedPageContent.tsx | 1 + .../components/mobile/FeedPageContent.tsx | 1 + 7 files changed, 44 insertions(+), 10 deletions(-) diff --git a/packages/common/src/models/Analytics.ts b/packages/common/src/models/Analytics.ts index d2fbb4e0c8a..65dddbbddd8 100644 --- a/packages/common/src/models/Analytics.ts +++ b/packages/common/src/models/Analytics.ts @@ -1172,6 +1172,9 @@ type PlaybackPlay = { isPreview?: boolean source: PlaybackSource collectionId?: string + // Which feed view the play originated from (matches FEED_CHANGE_VIEW's + // `view` values). Only present for plays coming from the feed lineup. + feed_type?: FeedTab } type PlaybackPause = { eventName: Name.PLAYBACK_PAUSE diff --git a/packages/common/src/store/playback/selectors.ts b/packages/common/src/store/playback/selectors.ts index d25706131fa..092f80eede6 100644 --- a/packages/common/src/store/playback/selectors.ts +++ b/packages/common/src/store/playback/selectors.ts @@ -29,6 +29,9 @@ export const getCurrentSource = (state: CommonState) => export const getCollectionId = (state: CommonState) => getCurrentPlaybackTrack(state)?.collectionId ?? null +export const getFeedType = (state: CommonState) => + getCurrentPlaybackTrack(state)?.feedType ?? null + export const getCurrentPlayerBehavior = (state: CommonState) => getCurrentPlaybackTrack(state)?.playerBehavior ?? PlayerBehavior.FULL_OR_PREVIEW diff --git a/packages/common/src/store/playback/types.ts b/packages/common/src/store/playback/types.ts index e782260f893..ab9a10da0de 100644 --- a/packages/common/src/store/playback/types.ts +++ b/packages/common/src/store/playback/types.ts @@ -1,4 +1,4 @@ -import { ID, Track, User } from '../../models' +import { FeedTab, ID, Track, User } from '../../models' export const PLAYBACK_RATE_LS_KEY = 'playbackRate' @@ -92,6 +92,11 @@ export type PlaybackTrack = { // Set by collection-page when building its queue; consumed by analytics // (PLAYBACK_PLAY events). collectionId?: ID + // The feed view (FOR_YOU / LATEST) this entry was queued from, if any. + // Set by the feed lineup when building its queue; consumed by analytics + // (PLAYBACK_PLAY events). Stamped at queue time so passive plays report + // the view the track actually came from even if the user switches tabs. + feedType?: FeedTab playerBehavior?: PlayerBehavior } diff --git a/packages/web/src/common/store/playback/sagas.ts b/packages/web/src/common/store/playback/sagas.ts index 164181d547f..331ae4ecf9c 100644 --- a/packages/web/src/common/store/playback/sagas.ts +++ b/packages/web/src/common/store/playback/sagas.ts @@ -69,6 +69,7 @@ const { getCurrentPlayerBehavior, getCurrentTrackId, getCollectionId, + getFeedType, getOvershot, getPlaybackIndex, getPlaybackQueue, @@ -772,11 +773,13 @@ function* watchNext() { } else { yield* call(playCurrent) const collId = yield* select(getCollectionId) + const feedType = yield* select(getFeedType) yield* put( make(Name.PLAYBACK_PLAY, { id: `${trackId}`, source: AnalyticsPlaybackSource.PASSIVE, - ...(collId ? { collectionId: collId } : {}) + ...(collId ? { collectionId: collId } : {}), + ...(feedType ? { feed_type: feedType } : {}) }) ) } @@ -811,11 +814,13 @@ function* watchPrevious() { if (track) { yield* call(playCurrent) const collId = yield* select(getCollectionId) + const feedType = yield* select(getFeedType) yield* put( make(Name.PLAYBACK_PLAY, { id: `${trackId}`, source: AnalyticsPlaybackSource.PASSIVE, - ...(collId ? { collectionId: collId } : {}) + ...(collId ? { collectionId: collId } : {}), + ...(feedType ? { feed_type: feedType } : {}) }) ) } else { diff --git a/packages/web/src/components/lineup/TrackLineup.tsx b/packages/web/src/components/lineup/TrackLineup.tsx index 86618f0da4a..a99ef5b9fbf 100644 --- a/packages/web/src/components/lineup/TrackLineup.tsx +++ b/packages/web/src/components/lineup/TrackLineup.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import type { LineupData } from '@audius/common/api' import { usePlayTrack, usePauseTrack } from '@audius/common/hooks' -import { ID, PlaybackSource, Name } from '@audius/common/models' +import { FeedTab, ID, PlaybackSource, Name } from '@audius/common/models' import { playbackActions, playbackSelectors } from '@audius/common/store' import type { PlaybackTrack, @@ -96,6 +96,11 @@ export type TrackLineupProps = { onClickTile?: (trackId: ID) => void playbackSource?: PlaybackSource + + // The feed view (FOR_YOU / LATEST) this lineup renders, when it's the + // feed. Stamped onto queue entries and attached to PLAYBACK_PLAY events + // as `feed_type` so plays can be attributed to a feed view. + feedType?: FeedTab } /** @@ -134,7 +139,8 @@ export const TrackLineup = ({ delineatorMap, elementAdornment, onClickTile, - playbackSource = PlaybackSource.TRACK_TILE_LINEUP + playbackSource = PlaybackSource.TRACK_TILE_LINEUP, + feedType }: TrackLineupProps) => { const dispatch = useDispatch() const isMobile = useIsMobile() @@ -181,9 +187,10 @@ export const TrackLineup = ({ () => playbackTrackIds.map((id) => ({ trackId: id, - source + source, + ...(feedType ? { feedType } : {}) })), - [playbackTrackIds, source] + [playbackTrackIds, source, feedType] ) const togglePlay = useCallback( @@ -204,7 +211,11 @@ export const TrackLineup = ({ if (isSameTile && !isPlaying) { dispatch(playbackActions.play()) dispatch( - make(Name.PLAYBACK_PLAY, { id: `${trackId}`, source: analytics }) + make(Name.PLAYBACK_PLAY, { + id: `${trackId}`, + source: analytics, + ...(feedType ? { feed_type: feedType } : {}) + }) ) return } @@ -218,7 +229,11 @@ export const TrackLineup = ({ }) ) dispatch( - make(Name.PLAYBACK_PLAY, { id: `${trackId}`, source: analytics }) + make(Name.PLAYBACK_PLAY, { + id: `${trackId}`, + source: analytics, + ...(feedType ? { feed_type: feedType } : {}) + }) ) }, [ @@ -230,7 +245,8 @@ export const TrackLineup = ({ currentLegacy?.source, source, isPlaying, - playbackSource + playbackSource, + feedType ] ) diff --git a/packages/web/src/pages/feed-page/components/desktop/FeedPageContent.tsx b/packages/web/src/pages/feed-page/components/desktop/FeedPageContent.tsx index a654be8936e..4fd92aebc3a 100644 --- a/packages/web/src/pages/feed-page/components/desktop/FeedPageContent.tsx +++ b/packages/web/src/pages/feed-page/components/desktop/FeedPageContent.tsx @@ -159,6 +159,7 @@ const FeedPageContent = ({ containerRef }: FeedPageContentProps) => { key={`feed-${feedTab}`} aria-label='feed' source='DISCOVER_FEED' + feedType={feedTab} variant={LineupVariant.MAIN} scrollParent={containerRef?.current ?? null} emptyElement={} diff --git a/packages/web/src/pages/feed-page/components/mobile/FeedPageContent.tsx b/packages/web/src/pages/feed-page/components/mobile/FeedPageContent.tsx index 68d64c862e4..a1c0da371f8 100644 --- a/packages/web/src/pages/feed-page/components/mobile/FeedPageContent.tsx +++ b/packages/web/src/pages/feed-page/components/mobile/FeedPageContent.tsx @@ -162,6 +162,7 @@ const FeedPageMobileContent = ({ key={`feed-${feedTab}`} aria-label='feed' source='DISCOVER_FEED' + feedType={feedTab} ordered variant={LineupVariant.MAIN} scrollParent={containerRef?.current ?? null}