Skip to content

[TV] Add onboarding analytics - #5773

Merged
sztomek merged 10 commits into
mainfrom
feat/tv-onboarding-analytics
Aug 27, 2026
Merged

sztomek merged 10 commits into
mainfrom
feat/tv-onboarding-analytics

Conversation

@sztomek

@sztomek sztomek commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

Description

Ports the Apple TV onboarding analytics to Android TV. The Android TV onboarding flow (Welcome, Sign In, Create Account, Syncing) previously fired no analytics; this brings it to parity with tvOS so both platforms report the same onboarding funnel.

Events added:

Screen Event Properties
Welcome setup_account_shown flow
Welcome setup_account_button_tapped flow, button = sign_in
Welcome setup_account_button_tapped flow, button = create_account
Welcome browse_no_account_tapped —
Sign In sign_in_shown —
Create Account create_account_shown flow
Syncing sign_in_sync_shown —

Implementation mirrors the existing merged TV analytics pattern (EventHorizon injected into a @HiltViewModel exposing trackXxx() methods, invoked from the screen via CallOnce { }).

The catalog bump also required adapting two unrelated call sites to a newly-required flow property (user_account_created, create_account_next_button_tapped); both pass OnboardingFlowType.Unknown as that layer has no onboarding flow in scope.

The pin is pocket-casts-2026-08-27_07-40-16 rather than the 08-25 build so it also carries the new SearchResultFilterType.Folders value (EventHorizonSchemas#125). Going 08-25 → 08-27 adds no further required-property breakage — only the enum value.

With #5769 (TV Search analytics) now merged, this branch also flips the TV Search folder filter chip from the placeholder SearchResultFilterType.Unknown to the real SearchResultFilterType.Folders (TvSearchViewModel), so focusing the Folders chip reports filter = folders instead of unknown — closing the blocking finding from that thread. Covered by a new TvSearchViewModelTest case.

Decisions / notes

  • flow = OnboardingFlowType.Unknown on the onboarding screen events. tvOS sends no flow, but the Android generated events require a non-null OnboardingFlowType.
  • sign_in_shown fires when the Sign In screen is shown (not on the Welcome "Sign In" tap), which covers both entry points into sign-in: the Welcome screen and the browse-without-account Home.
  • Sign-in success/failure are not fired from the TV screens — user_signed_in / user_signin_failed are emitted centrally by SyncManagerImpl; duplicating them from the screens would double-count.
  • sign_in_type_tapped is omitted — Android TV sign-in is QR-only (no QR/password picker).
  • Create Account fires only create_account_shown — Android TV "create account" is a QR to the web /create page, and the device-auth path signs in an existing account (isNewAccount = false), so user_account_created does not apply on TV.
  • setup_account_button_tapped fires only from the Welcome screen (matching tvOS, where it is Welcome-only). The Create Account screen's "Sign In" button and the browse-without-account profile-modal entry points navigate without firing it, so setup_account_button_tapped undercounts relative to sign_in_shown / create_account_shown — i.e. "tapped → shown" is not a clean drop-off rate for these events.

Fixes PCDROID-731 https://linear.app/a8c/issue/PCDROID-731/onboarding-analytics

Testing Instructions

  1. Build and run the tv app on an Android TV device/emulator, signed out.
  2. On the Welcome screen, confirm setup_account_shown fires.
  3. Tap Sign In → confirm setup_account_button_tapped (button = sign_in), then sign_in_shown on the Sign In screen.
  4. Go back, tap Create Free Account → confirm setup_account_button_tapped (button = create_account) and create_account_shown.
  5. Go back, tap Browse without account → confirm browse_no_account_tapped.
  6. Complete a QR sign-in → confirm sign_in_sync_shown on the Syncing screen, and that user_signed_in fires exactly once (from SyncManagerImpl) with source = qr_code and no duplicate.

Unit tests: ./gradlew :tv:testDebugUnitTest (new TvWelcomeViewModelTest, TvCreateAccountViewModelTest; updated TvSignInViewModelTest, TvSyncingViewModelTest). Catalog bump verified by compiling :app, :tv, :automotive, :wear and running :modules:services:repositories, :modules:services:servers, :modules:features:account unit tests.

Screenshots or Screencast

No UI changes — analytics only.

Checklist

  • If this is a user-facing change, I have added an entry in CHANGELOG.md
  • Ensure the linter passes (./gradlew spotlessApply to automatically apply formatting/linting)
  • I have considered whether it makes sense to add tests for my changes
  • All strings that need to be localized are in modules/services/localization/src/main/res/values/strings.xml
  • Any jetpack compose components I added or changed are covered by compose previews
  • I have updated (or requested that someone edit) the Event Horizon schema to reflect any new or changed analytics.

@claude

claude Bot commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @sztomek's task in 4m 51s —— View job


Code review: TV onboarding analytics

  • Gather context (diff, changed files, existing TV/mobile analytics patterns)
  • Review implementation for bugs / correctness
  • Review tests
  • Post review (4 inline comments + summary below)

Summary

Clean, well-scoped PR. The implementation follows the pattern already established in this repo (EventHorizon → @HiltViewModel → CallOnce { viewModel.trackX() }), matching both TvHomeScreen/TvHomeViewModel on TV and OnboardingCreateAccountPage on mobile. Tracking calls fire before navigation, so no events are lost to the transition. The TvWelcomeContent / TvCreateAccountContent extraction is the right call — it keeps the previews free of Hilt without a nullable-ViewModel hack.

No correctness bugs found. No security or performance concerns: every event is either param-free or enum-valued, so there's no PII or user-supplied data in the payloads, and tracking is synchronous fire-and-forget off the existing EventHorizon singleton.

The decisions documented in the PR description all hold up against the code:

  • Not re-firing user_signed_in is correct — SyncManagerImpl.loginWithDeviceAuth already emits it, so duplicating from the TV screens would double-count.
  • sign_in_shown on screen-show rather than on the Welcome tap does correctly cover both entry points (Welcome and the browse-without-account profile modal).

Inline comments

# File Severity Issue
1 TvWelcomeViewModel.kt:19 discuss flow = OnboardingFlowType.Unknown — worth confirming vs InitialOnboarding; unknown ≠ absent
2 TvWelcomeViewModel.kt:22 nit trackXxxClicked vs the module's trackXxxTapped convention
3 TvCreateAccountScreen.kt:50 minor Create Account → Sign In fires no setup_account_button_tapped; mobile does
4 TvSignInViewModelTest.kt:35-44 nit Add createViewModel() helper; the stubs' purpose is non-obvious

Two additional notes (no action required)

CallOnce semantics differ from tvOS onAppear. CallOnce uses rememberSaveable, and NavHost restores each back-stack entry's saveable state, so Welcome → Sign In → back will not re-fire setup_account_shown. Same for create_account_shown. This is deliberate and matches mobile (OnboardingCreateAccountPage.kt:80 uses the identical pattern), so I wouldn't change it — but if tvOS fires on every appearance, the two platforms' *_shown counts aren't directly comparable, which slightly undercuts the parity goal. Worth knowing before comparing dashboards.

setup_account_dismissed / create_account_dismissed are absent. Mobile fires these on back-press (OnboardingCreateAccountViewModel.kt:80-91); TV back-press from these screens fires nothing. Outside the stated tvOS-parity scope, but it's the natural next event if you want onboarding drop-off.

Verification I could not do

The tv module wasn't compiled here — this environment has no Gradle cache for com.automattic:eventhorizon:pocket-casts-2026-08-13_07-58-34, so I couldn't confirm that OnboardingFlowType.Unknown exists in that catalog version (no existing usage in the repo to cross-check against; every other call site uses a mapped value). CI on this PR currently shows only Validate Gradle Wrapper passing, with no compile or :tv:testDebugUnitTest run. Please confirm ./gradlew :tv:testDebugUnitTest and spotlessCheck pass locally before merge.

--- • branch feat/tv-onboarding-analytics

@sztomek sztomek changed the title [TV] Add onboarding analytics (Apple TV parity) [TV] Add onboarding analytics Aug 25, 2026
@sztomek sztomek added [Type] Feature Adding a new feature. [Area] Analytics Analytics related issues [Area] Onboarding [Area] TV labels Aug 25, 2026
@sztomek sztomek added this to the 8.20 milestone Aug 25, 2026
@sztomek
sztomek marked this pull request as ready for review August 25, 2026 19:37
@sztomek
sztomek requested a review from a team as a code owner August 25, 2026 19:37
@sztomek
sztomek requested review from geekygecko and removed request for a team August 25, 2026 19:37
@claude

claude Bot commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @sztomek's task in 3m 8s —— View job


Code review: TV onboarding analytics (re-review at ready_for_review)

  • Fetch base & diff the full PR
  • Re-check the 4 previous inline comments against current HEAD
  • Review the new qr_code / EventHorizon-bump commit
  • Post updated review (2 new inline comments)

Previous round — status

# Was Now
1 flow = OnboardingFlowType.Unknown on TV events Accepted as-is. No // comment added, but the PR description now documents the reasoning. Fine — see the new finding below, which is the mobile half of the same question.
2 trackXxxClicked naming ✅ Fixed — trackSignInTapped / trackCreateAccountTapped / trackBrowseNoAccountTapped
3 Create Account → Sign In fires no setup_account_button_tapped ✅ Documented — the undercount caveat is now spelled out in the PR description. Deliberate, not an omission.
4 TvSignInViewModelTest needs a createViewModel() helper ✅ Fixed (TvSignInViewModelTest.kt:157). The "why are these stubs here" comment wasn't added — harmless.

New finding this round

The 2026-08-25 catalog bump makes flow a required property on UserAccountCreatedEvent / CreateAccountNextButtonTappedEvent, and this PR satisfies that by hardcoding OnboardingFlowType.Unknown at all four new call sites. For the two TV sites and CreateEmailFragment that's correct — there is genuinely no flow in scope.

For the two in SyncManagerImpl it isn't: the callers do have the flow. OnboardingCreateAccountViewModel takes flow: OnboardingFlow on every other tracking method it exposes, and GoogleSignInButtonViewModel.startGoogleOneTapSignIn(flow, …) already uses flow.analyticsValue immediately before calling into loginWithGoogle. The result is that on mobile, the whole account-creation funnel carries a real flow except the terminal conversion event, which is now permanently unknown — so flow-segmented funnels break exactly at the conversion step. Details and a suggested fix are in the inline comment on SyncManagerImpl.kt:586; a follow-up issue would be an acceptable resolution if you want to keep this PR TV-scoped.

qr_code identity — traced, no bug

I walked the persistence paths for the new LoginIdentity.QrCode and everything holds (written up inline on LoginIdentity.kt:21):

  • Downgrade-safe — an older build's valueOf("QrCode") returns null, but getLoginIdentity() falls back to PocketCasts (SyncAccountManagerImpl.kt:86), so it degrades rather than crashes.
  • Token refresh unaffected — downloadTokens switches on getSignInType() (SIGN_IN_TYPE_KEY, always Tokens), never on LoginIdentity. Description is accurate.
  • isGoogleLogin() is the only behavioural consumer and is unchanged (QrCode != Google, as PocketCasts != Google was).
  • One theoretical, currently unreachable gap: LoginIdentity.Adapter.fromJson feeds a non-null WatchSyncAuthData.loginIdentity, so an older watch build receiving "QrCode" would throw rather than degrade. Device auth is TV-only (TvDeviceAuth.kt:36 is the sole caller) and TV has no watch sync — only relevant if device auth ever ships on phone.

Also worth a heads-up to dashboard owners: TV device-auth sign-ins move from source = password to source = qr_code, so saved segments filtering on password will show a step change at this release.

Everything else

The TV analytics work itself is clean and I found no correctness bugs. The TvWelcomeContent / TvCreateAccountContent extraction keeps previews Hilt-free, tracking fires before navigation so nothing is lost to the transition, and the new tests cover each trackXxx method. No security or performance concerns — every payload is param-free or enum-valued, and tracking is fire-and-forget off the existing singleton.

Two carry-over notes from last round, still true and still no action needed: CallOnce uses rememberSaveable, so *_shown won't re-fire on back-navigation the way a tvOS onAppear would (matches mobile, but the two platforms' *_shown counts aren't strictly comparable); and setup_account_dismissed / create_account_dismissed have no TV equivalent, which is the natural next step if you want drop-off.

Verification I could not do

The tv module still wasn't compiled here — no Gradle cache for com.automattic:eventhorizon:pocket-casts-2026-08-25_18-30-35, so I can't confirm OnboardingFlowType.Unknown, LoginIdentityType.QrCode, or the new required flow parameters exist as used. GitHub Actions on this PR shows only Validate Gradle Wrapper and the Danger trigger; the actual build runs on Buildkite. Please confirm ./gradlew :tv:testDebugUnitTest, the :modules:services:repositories / :servers / :features:account unit tests, and spotlessCheck are green there before merge.

--- • branch feat/tv-onboarding-analytics

@wpmobilebot

wpmobilebot commented Aug 25, 2026 •

Copy link
Copy Markdown
Collaborator

Project dependencies changes

list
! Upgraded Dependencies
com.automattic:eventhorizon:pocket-casts-2026-08-27_07-40-16, (changed from pocket-casts-2026-08-13_07-58-34)
tree
 +--- project ':modules:features:account'
 |    \--- project ':modules:features:search'
 |         \--- project ':modules:services:analytics'
-|              +--- com.automattic:eventhorizon:pocket-casts-2026-08-13_07-58-34
+|              +--- com.automattic:eventhorizon:pocket-casts-2026-08-27_07-40-16
 |              +--- project ':modules:services:model'
-|              |    +--- com.automattic:eventhorizon:pocket-casts-2026-08-13_07-58-34 (*)
+|              |    +--- com.automattic:eventhorizon:pocket-casts-2026-08-27_07-40-16 (*)
 |              |    \--- project ':modules:services:utils'
 |              |         \--- project ':modules:services:payment'
-|              |              \--- com.automattic:eventhorizon:pocket-casts-2026-08-13_07-58-34 (*)
+|              |              \--- com.automattic:eventhorizon:pocket-casts-2026-08-27_07-40-16 (*)
 |              \--- project ':modules:services:preferences'
-|                   \--- com.automattic:eventhorizon:pocket-casts-2026-08-13_07-58-34 (*)
+|                   \--- com.automattic:eventhorizon:pocket-casts-2026-08-27_07-40-16 (*)
 \--- 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-08-13_07-58-34 (*)
+                         \--- com.automattic:eventhorizon:pocket-casts-2026-08-27_07-40-16 (*)

@claude

claude Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @sztomek's task in 2m 34s —— View job


Code review: TV onboarding analytics (re-review after initial_onboarding commit)

  • Diff the new commit since last review (60c791a)
  • Re-check outstanding findings against current HEAD
  • Review full PR diff for new issues
  • Post updated review (1 new inline comment)

The new commit

60c791a swaps OnboardingFlowType.Unknown → OnboardingFlowType.InitialOnboarding at all three TV sites plus their tests. This resolves round-1 finding #1 — and I can now confirm the value compiles: OnboardingFlowType.InitialOnboarding is already referenced at OnboardingFlow.kt:23, so it exists in the catalog regardless of which version resolves. Diff is mechanical and the tests were updated in lockstep.

One nuance worth knowing rather than fixing. On mobile the two values are distinguished by when onboarding is entered:

  • InitialOnboarding — MainActivity.kt:477-483, gated on !hasCompletedOnboarding && !isLoggedIn at cold start, i.e. genuinely first-run.
  • LoggedOut — every later re-entry from a signed-out state (MainActivity.kt:1063, ProfileFragment.kt:104, …).

TV has no hasCompletedOnboarding equivalent, and TvOnboardingNavHost.kt:84 routes onSignedOut back to LANDING via navigateClearingBackStack. Since that pop is inclusive, the new LANDING entry gets fresh rememberSaveable state, so CallOnce re-fires and a post-sign-out Welcome reports flow = initial_onboarding where mobile would report logged_out. Given TV tracks no onboarding-completion state, InitialOnboarding is the right pragmatic pick — just don't read TV's initial_onboarding as "first ever launch" when comparing against mobile.

Also: the PR description is now stale — the Decisions / notes section still says flow = OnboardingFlowType.Unknown for the onboarding screen events and cites "tvOS sends no flow" as the reason. Worth updating so the description matches what merges.

New finding this round

TvCreateAccountModal is a third, untracked account-setup surface. Full detail inline on TvCreateAccountViewModel.kt:16. In short: a signed-out user tapping Follow on podcast details (TvPodcastDetailsScreen.kt:137-142) gets TvCreateAccountModal — same create-account framing, same QR device-auth flow — but it fires neither create_account_shown nor sign_in_shown. The sign-in still completes through SyncManagerImpl.loginWithDeviceAuth, so you get user_signed_in(source = qr_code) with no preceding impression event.

That's a different shape of problem from the setup_account_button_tapped undercount already documented: an undercounted tap makes the numerator lossy, but a conversion with no impression makes the denominator wrong the other way. Reusing this ViewModel at the modal's call site is a few lines; a follow-up issue is a fine resolution if you want to keep this PR scoped.

Previous findings — status

Round Finding Status
1 flow = Unknown on TV events ✅ Fixed in 60c791a → InitialOnboarding
1 trackXxxClicked naming ✅ Fixed
1 Create Account → Sign In fires no setup_account_button_tapped ✅ Documented as deliberate
1 TvSignInViewModelTest needs a createViewModel() helper ✅ Fixed
2 SyncManagerImpl hardcodes flow = Unknown on user_account_created ⏳ Still open — unchanged at SyncManagerImpl.kt:586 and :632. Mobile's whole account-creation funnel carries a real flow except the terminal conversion event. Follow-up is acceptable; leaving it silently isn't, since unknown on a required property is sticky once dashboards exist.
2 LoginIdentity.QrCode persistence trace ✅ No bug — reasoning recorded inline

Everything else

Re-read the full diff; nothing else changed since round 2 and I found no new correctness bugs. Tracking still fires before navigation, the TvWelcomeContent / TvCreateAccountContent split keeps previews Hilt-free, every new event is param-free or enum-valued (no PII), and tracking is fire-and-forget off the existing EventHorizon singleton — no security or performance concerns.

Carry-over notes, still true, still no action needed: CallOnce uses rememberSaveable so *_shown won't re-fire on back-navigation the way a tvOS onAppear would; and setup_account_dismissed / create_account_dismissed have no TV equivalent.

Verification I could not do

The tv module still wasn't compiled here — no Gradle cache for com.automattic:eventhorizon:pocket-casts-2026-08-25_18-30-35, so I can't confirm LoginIdentityType.QrCode or the new required flow parameters exist as used (OnboardingFlowType.InitialOnboarding I can confirm, via the pre-existing OnboardingFlow.kt:23 reference). GitHub Actions on this PR shows only Validate Gradle Wrapper (passing) and this review job; the real build runs on Buildkite. Please confirm ./gradlew :tv:testDebugUnitTest, the :modules:services:repositories / :servers / :features:account unit tests, and spotlessCheck are green there before merge.
• branch feat/tv-onboarding-analytics

@claude

claude Bot commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @sztomek's task in 1m 35s —— View job


Code review: incremental (since 60c791a)

  • Diff new commits since last review
  • Re-check still-open findings against HEAD
  • Post review

The only change since my last review is 9c743d2, a one-line EventHorizon catalog bump 2026-08-25_18-30-35 → 2026-08-27_07-40-16. No Kotlin changed, so the TV analytics work is as reviewed. The bump itself is inert here — nothing on this branch reads a folders search filter value — but an unrelated catalog bump is exactly what silently made flow required repo-wide last round, and I can't compile against the new catalog (no Gradle cache for it in this environment), so only the Buildkite build will confirm.

Non-blocking

  • gradle/libs.versions.toml:295 — catalog bump is unused by this PR; consider landing it with the search change that needs the folders value, or confirm the full build is green since it can add newly-required properties elsewhere. thread

Still open from earlier rounds

  • modules/services/repositories/src/main/java/au/com/shiftyjelly/pocketcasts/repositories/sync/SyncManagerImpl.kt:586 and :632 — user_account_created still hardcodes flow = OnboardingFlowType.Unknown while the mobile callers have the real flow; thread the flow through or open a follow-up before dashboards depend on it.
  • tv/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/TvPodcastDetailsScreen.kt:172 — TvCreateAccountModal still fires no *_shown event, so its QR sign-ins land as user_signed_in with no impression.
    • branch feat/tv-onboarding-analytics

Comment thread gradle/libs.versions.toml
@sztomek
sztomek force-pushed the feat/tv-onboarding-analytics branch from 9c743d2 to 1fb257a Compare August 27, 2026 08:07
@claude

claude Bot commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @sztomek's task in 41s —— View job


Code review: incremental (since the last round)

Since my last review the branch adds 7613ff8 (catalog bump, same version as before — the branch was force-pushed, so this is the same one-line change re-authored) and 1fb257a, which maps TvSearchFilter.Folders -> SearchResultFilterType.Folders and adds a test for it. That's a one-line enum swap on an existing, already-tracked code path, with a test covering it. No correctness issues; it also resolves my earlier non-blocking note that the bump was unused on this branch — it now is.

Still open from earlier rounds

  • modules/services/repositories/src/main/java/au/com/shiftyjelly/pocketcasts/repositories/sync/SyncManagerImpl.kt:586 and :632 — user_account_created still hardcodes flow = OnboardingFlowType.Unknown while the mobile callers have the real flow; thread it through or open a follow-up before dashboards depend on it.
  • tv/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/TvPodcastDetailsScreen.kt:172 — TvCreateAccountModal still fires no *_shown event, so its QR sign-ins land as user_signed_in with no impression.
    • branch feat/tv-onboarding-analytics

@sztomek
sztomek merged commit bc027fb into main Aug 27, 2026
20 checks passed
@sztomek
sztomek deleted the feat/tv-onboarding-analytics branch August 27, 2026 08:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Area] Analytics Analytics related issues [Area] Onboarding [Area] TV [Type] Feature Adding a new feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants