[TV] Search analytics - #5769
Conversation
|
Claude finished @sztomek's task in 6m 31s —— View job Code review
Overall this is well-executed. The Worth addressing1. This is my main finding. The pills use focus-to-select, and on a D-pad you must focus an element before you can click it — so The traversal concern that motivated the split is real, but a settle-debounce on focus solves it without discarding the signal: cancel a pending track job on each focus change, fire after ~500ms if the filter is unchanged and differs from the last tracked value, and have 2. Navigation callbacks silently became optional — inline
3. Search-side discover coverage gap — inline The whole point of the extraction is Smaller points4. 5. Episode 6. 7. Timing semantics differ from the phone. Phone fires 8. History vs. suggestion asymmetry. Verified as correct
I did not build the module or run the tests (no Gradle dependency cache available in this environment), so the above is static analysis only. CI on this run shows the Gradle wrapper validation passing; the unit-test and |
Generated by 🚫 Danger |
|
Claude finished @sztomek's task in 3m 49s —— View job Code review
Re-reviewed at Previous findings — status
New points (both minor, both inline)Search's discover Episode-actions path emits nothing — inline. Nit: Verified in this pass
I did not build the module or run the tests — no Gradle dependency cache in this environment — so this remains static analysis. CI here shows Gradle wrapper validation green; the Buildkite unit-test and Nothing blocking from my side. |
0b71717 to
c071478
Compare
|
Claude finished @sztomek's task in 1m 40s —— View job Code reviewReviewed Non-blocking
I did not build the module or run the tests (no Gradle dependency cache in this environment), so this is static analysis only. |
c071478 to
b06170b
Compare
|
Good catch @geekygecko — you're right, tvOS tracks this via |
b06170b to
4b494a4
Compare
|
Claude finished @sztomek's task in 3m 21s —— View job Code reviewReviewed the new commit The rebase pulled in the new Blocking
Non-blocking
I did not build the module or run the tests (no Gradle dependency cache in this environment), so this is static analysis only — in particular I could not confirm that |

Description
Brings the Android TV Search screen to analytics parity with the Apple TV app (
Pocket Casts TV App/UI/Search/*+Analytics/SearchAnalytics.swift). The TV Search screen previously fired zero analytics.Apple TV is the source of truth for which events fire, their properties, and their triggers. Each event maps to the existing generated EventHorizon Kotlin class (no schema change is needed — every event already includes the
androidplatform).sourceisSourceViewType.Searchfor thesearch_*events and the string"search"for the reused discover events.This also extracts a reusable
TvDiscoverFeedAnalyticstracker (parameterised bysource) out ofTvHomeViewModel, so the search idle body — which reuses the shared discover grid, exactly like Apple TV'sDiscoverAllView(source: .search)— fires the same discover family withsource="search". The Home refactor is behaviour-preserving (source="home", local-row gating intact).Core search events (
source = search)search_shownCallOnce)search_performedsearch_filter_tappedtop_results→allResults,podcasts,episodes; the TV-onlyfolderspill maps tounknown— the schema enum has no folders value)search_predictive_term_tappedsearch_history_item_tappedsearch_termsearch_empty_resultssearch_failedsearch_result_tappedpodcast_local_result/podcast_remote_result) or episode result is opened/played (incl. Go to podcast from an episode result’s actions modal)Discover idle-body family (
source = search, via the shared tracker)discover_list_impressiondiscover_list_podcast_tappeddiscover_featured_podcast_tappeddiscover_ad_category_tappeddiscover_categories_pill_tappedNotable parity decisions / divergences
search_failedcarriesterm, noterror_code— the schema contract forsearch_failedissource+term(checked againsttrunk); there is noerror_codeproperty on this event for any platform. Apple TV happens to send an undeclarederror_code(its Tracks pipeline ships arbitrary property dicts) and omits the declaredterm, so it's the off-contract one. Android'ssource+termis fully compliant and matches the phone app. (Extending the contract witherror_code: textwas considered and declined — Android's failure path catches a genericThrowablewith no uniform numeric code.)search_filter_tapped— top results — Apple TV emitsfilter="top_results"; the Android typedSearchResultFilterTypeenum has no such value, so the top-results scope emits its nearest equivalentallResults(the same mapping the phone app uses for its "All" filter).search_filter_tapped— fires on filter change, like tvOS — tvOS tracks this viaonChange(of: model.scope), so merely moving between the chips fires the event (confirmed on device by @geekygecko). Android TV mirrors that: the pills are focus-to-select, and the event fires whenever the selected filter actually changes (focus move or click), deduped so re-selecting the current filter doesn't re-fire.includeHomeSections = false, andTvDiscoverFeedLoaderdropsListType.Categoriesrows in that mode, so only the browse-categories row is a live source ofdiscover_categories_pill_tappedon Search. The categories-row wiring is kept so it becomes correct the moment home sections are ever enabled here — don't expect two feeds of that event in the data.discover_list_episode_tapped/discover_list_episode_playare not wired from the search idle yet (the shared tracker supports them; Home already fires them).Testing Instructions
./gradlew :tv:installDebug(or run thetvconfig).LoggingAnalyticsListenerevents (debug builds log every EventHorizon event).search_shown; the idle body logsdiscover_list_impressionper row.search_performed.search_empty_results; a network failure logssearch_failed.search_predictive_term_tapped; tapping a recent search logssearch_history_item_tapped.search_filter_tappedper filter change (matching tvOS); staying on the same pill does not re-log it.search_result_tappedwithpodcast_local_result; a non-subscribed one withpodcast_remote_result; playing an episode result logsepisode.discover_list_podcast_tapped; tapping a category pill logsdiscover_categories_pill_tapped(all withsource=search).Screenshots or Screencast
Checklist
./gradlew spotlessApply)modules/services/localization— n/a, no new stringsandroidplatform