[TV] Home analytics - #5734
Conversation
Project dependencies changeslist! Upgraded Dependencies
com.automattic:eventhorizon:pocket-casts-2026-08-13_07-58-34, (changed from pocket-casts-2026-07-14_22-15-37)tree +--- project ':modules:features:account'
| \--- project ':modules:features:search'
| \--- project ':modules:services:analytics'
-| +--- com.automattic:eventhorizon:pocket-casts-2026-07-14_22-15-37
+| +--- com.automattic:eventhorizon:pocket-casts-2026-08-13_07-58-34
| +--- project ':modules:services:model'
-| | +--- com.automattic:eventhorizon:pocket-casts-2026-07-14_22-15-37 (*)
+| | +--- com.automattic:eventhorizon:pocket-casts-2026-08-13_07-58-34 (*)
| | \--- project ':modules:services:utils'
| | \--- project ':modules:services:payment'
-| | \--- com.automattic:eventhorizon:pocket-casts-2026-07-14_22-15-37 (*)
+| | \--- com.automattic:eventhorizon:pocket-casts-2026-08-13_07-58-34 (*)
| \--- project ':modules:services:preferences'
-| \--- com.automattic:eventhorizon:pocket-casts-2026-07-14_22-15-37 (*)
+| \--- com.automattic:eventhorizon:pocket-casts-2026-08-13_07-58-34 (*)
\--- project ':modules:features:discover'
\--- project ':modules:features:podcasts'
\--- project ':modules:features:player'
\--- project ':modules:features:transcripts'
\--- project ':modules:services:sharing'
- \--- com.automattic:eventhorizon:pocket-casts-2026-07-14_22-15-37 (*)
+ \--- com.automattic:eventhorizon:pocket-casts-2026-08-13_07-58-34 (*) |
e560903 to
e542fc9
Compare
|
@claude please review this |
|
Claude finished @sztomek's task in 5m 36s —— View job Code review
Nice, well-scoped foundation. The additive no-op callbacks on 1. Every TV event will be reported as
|
44cb8bc to
e9ef950
Compare
e542fc9 to
ae937f4
Compare
|
Claude finished @sztomek's task in 3m 13s —— View job Code review
Nothing changed in the code since my last review — the update was a rebase (the base 1. Every TV event will be reported as
|
|
Claude finished @sztomek's task in 3m 11s —— View job Code review — re-check after
|
| Earlier finding | Status |
|---|---|
#1 TV events reported as platform: "phone" |
Fixed — TracksAnalyticsTracker.kt:105 now branches on Util.isTv first. Confirmed the value reaches Tracks: tv/build.gradle.kts:33 builds a release variant and ReleaseTrackerModule.kt:18 binds TracksAnalyticsTracker. |
#5 trackDiscoverPodcastTapped didn't early-return for local rows |
Fixed — TvHomeViewModel.kt:194 is now ?: return, consistent with the other three trackers. |
#6 indexOf linear scan on structural equality |
Fixed — indexOfFirst { it.id == category.id } (TvDiscoverRows.kt:151). |
| Missing tests | Fixed — trackDiscoverEpisodePodcastTapped now covered, plus a verifyNoInteractions test asserting Banner/Categories rows fire no impression. |
Two follow-ups on the platform fix, then the items still open.
1. AppPlatform still maps TV → DeviceType.Phone (latent, not live)
The fix bypasses AppPlatform rather than extending it, so AppPlatform.kt still has no TV entry and AppPlatform.analyticsValue (an EventHorizon DeviceType) still resolves to DeviceType.Phone on TV. The one analytics consumer is PlaybackStatsSyncWorker.kt:47 (deviceType on ListeningTimeEvent).
I checked whether that's reachable: the worker is only scheduled from app/.../PocketCastsApplication.kt:320-321, so no TV data is wrong today — correcting my earlier framing, this isn't a live bug. But the two now disagree by construction: platform = "tv" on every event while device_type would say phone the moment listening-stats sync is enabled on TV. A comment at AppPlatform.kt noting that TV is deliberately handled outside the enum (or adding the enum value once the catalog has DeviceType.Tv) would stop that from being rediscovered later.
Two smaller things in the same hunk:
Util.isTv(appContext)does aPackageManager.getApplicationInfo(GET_META_DATA)IPC on every call, unlikegetAppPlatformwhich caches (Util.kt:37). It runs once perrefreshMetadata(), so it's not hot — but folding the TV check into the cachedgetAppPlatformwould both avoid the IPC and keep one source of truth for "what platform is this".- The nested
when {} → when () {}with a blank line between the two branches reads awkwardly for what is conceptually one four-way choice; a flatwhen { Util.isTv(..) -> "tv"; isAutomotive -> ... }or thegetAppPlatformroute would be tidier. Style only.
Worth confirming with whoever owns the Tracks schema that "tv" is an accepted value for platform — dashboards that filter on an enumerated set will silently drop unknown values.
Still open (unchanged — flagging so they're an explicit decision, not an oversight)
region is the stored preference, not the region actually browsed — TvHomeViewModel.kt:249 sends settings.discoverCountryCode.value, but TvDiscoverFeedLoader.kt:204-207 resolves with discover.regions[stored] ?: discover.regions[defaultRegionCode]. The phone sends the resolved code for the same event (DiscoverFragment.kt:179 ← DiscoverViewModel.kt:146), so when the stored code isn't in the feed the two Android surfaces report different region for the same user. resolveRegionOrNull already computes the right value; exposing the last-resolved DiscoverRegion.code from the loader would close it.
discover_categories_pill_tapped.index means something different than on phone — TV sends the display position, phone sends category.featuredIndex ?: -1 (DiscoverFragment.kt:180) and featuredIndex is never populated on the TV feed path. Fine if tvOS parity is the display index — just needs to be a conscious choice plus a note for whoever queries it.
home_shown and impressions re-fire on every Home-tab visit — TvScaffold.kt:164 renders tab content in a Crossfade with no SaveableStateHolder, so leaving Home disposes TvHomeScreen; returning re-runs CallOnce { trackHomeShown() } and each LaunchedEffect(row.id) { onListImpression(row) } for the visible rows. Plausibly correct parity with tvOS viewDidAppear/willDisplay — please confirm. Either way, testing step 1 in the description should say only the initially visible rows fire on open, not one per Discover row.
BookmarkViewModel isNewBookmark sources disagree — onShown takes it from the fragment args (BookmarkFragment.kt:60) while onClose/onSubmitBookmark read uiState.value.isNewBookmark, which is derived from bookmarkUuid (:52) and gets overwritten in load() when findByEpisodeTime resolves an existing bookmark at the same timestamp (:77-81). One form session can emit shown(isNewBookmark = true) → submitted(isNewBookmark = false). Deliberate?
"none" redirect path — now a named constant in SyncManagerImpl.kt:111 but still a bare literal in AccountActivity.kt:164 and OnboardingLogInViewModel.kt:128.
discover_list_episode_play fires on tap, not on playback start — onEpisodePlay(row, episode) runs before onPlayEpisode(episode) (TvDiscoverRows.kt:105), and playEpisode can fail into an error toast. Depends on what tvOS does.
Nothing blocking. The one that mattered most is fixed and I verified the wiring; the rest are consistency/intent calls. Reviewed statically — no build or test run in this job, so the bumped EventHorizon constructor signatures remain CI-verified only.
• branch feat/tv-analytics-home
|
Claude finished @sztomek's task in 2m 57s —— View job Code review — re-check after
|
Description
First screen of the Android TV → Apple TV analytics parity program (EventHorizon only). Lays the analytics foundation for the TV app and completely mirrors the Home screen — on both platforms there is no separate Discover screen; the Discover feed is the Home tab body, so all its interactions are Home events (tvOS stamps them
source = "home"). Based onfeat/tv-search-category-detailsso the banner/category features exist to instrument.Foundation
:modules:services:analyticsto thetvmodule and injectsEventHorizon(the TV app previously fired no analytics).2026-07-14→2026-08-13_07-58-34, produced by Automattic/EventHorizonSchemas#118 (adds theandroidplatform to the Apple-TV events, plus an optionalsourceon the Discover list/category events). The bump is app-wide, so it adapts phone call sites whose event constructors changed:redirectPath = "none"onUserAccountCreatedEvent/UserSignedInEvent/SigninShownEvent, andsource/isNewBookmarkonBookmarkEditForm*Event.Home analytics — verified against the tvOS app across every tracking layer (
Analytics.track,AnalyticsHelper,DiscoverAnalytics, and the cell/model methods):home_shownHomeShownEventCallOnce)discover_list_impressionDiscoverListImpressionEventlist_id, sourcediscover_list_podcast_tappedDiscoverListPodcastTappedEventlist_id, podcast_uuid, sourcediscover_featured_podcast_tappedDiscoverFeaturedPodcastTappedEventpodcast_uuiddiscover_list_episode_tappedDiscoverListEpisodeTappedEventlist_id, podcast_uuid, episode_uuid, sourcediscover_list_episode_playDiscoverListEpisodePlayEventlist_id, podcast_uuiddiscover_ad_category_tappedDiscoverAdCategoryTappedEventname, region, id, podcast_idbanner_row_tappedBannerRowTappedEventtypediscover_categories_pill_tappedDiscoverCategoriesPillTappedEventname, region, index, visits, sponsored, sourceAnalytics is added via additive no-op callbacks on the shared
tvDiscoverRow/TvCategoryPodcastsScreen, so the Search screen is untouched (its analytics land in the Search PR, where it will stampsource = "search"). Subscribe actions (discover_featured_podcast_subscribed,discover_list_podcast_subscribed) fire from Podcast Details and are out of scope here.Fidelity notes
source = "home"is stamped ondiscover_list_impression/discover_list_podcast_tapped/discover_list_episode_tapped/discover_categories_pill_tapped, mirroring tvOS — enabled by the optionalsourceadded in Automattic/EventHorizonSchemas#118.regionis the resolved Discover region (settings.discoverCountryCode.value) ondiscover_categories_pill_tappedand bothdiscover_ad_category_tappedvariants.visits/sponsoredcome fromDiscoverCategory; the category-page ad carries the real categoryname/id. The only remaining unknown is the list-context ad'sid(0) — an injected sponsored podcast in a normal list has no category id, and tvOS has the same gap there (it only hasitem?.categoryID, and likewise hardcodesname = "unknown").Fixes PCDROID-719 https://linear.app/a8c/issue/PCDROID-719/home-screen-analytics
Testing Instructions
./gradlew :tv:installDebug; open Home → confirm onehome_shownand adiscover_list_impressionper Discover row inadb logcat(debug logs viaLoggingAnalyticsListener), all withsource = "home".discover_list_podcast_tapped+discover_featured_podcast_tapped. Open a non-featured one → onlydiscover_list_podcast_tapped.discover_list_podcast_tapped+discover_ad_category_tapped; a sponsored featured one → nodiscover_ad_category_tapped.discover_list_episode_tapped+discover_list_episode_play.banner_row_tapped; tap a category pill →discover_categories_pill_tapped; open a podcast inside a category →discover_list_podcast_tapped(with the category's reallist_id).discover_list_*events.Screenshots or Screencast
Checklist
./gradlew spotlessApply)