Show the new Stats Today card on the My Site dashboard - #25917
Show the new Stats Today card on the My Site dashboard#25917crazytonyli wants to merge 2 commits into
Conversation
The Stats "Today" card (TodayCard/TodayCardViewModel) is internal to JetpackStats and only loads on first appearance, with no reload path. To embed it outside the Stats screen (the My Site dashboard), expose a small controller that owns the card's lifecycle instead of leaking the internals. StatsTodayCardController drives refreshIfNeeded()/cancel() and reports load failures; StatsTodayCardView renders the unchanged TodayCard with the controller's context injected and optional host-supplied more-menu items, passed as a @ViewBuilder (the card still draws the ellipsis button, so only the items differ). The view model gains additive internal refreshIfNeeded()/cancelLoading(): refreshIfNeeded reloads the current period only on retry-after-failure, midnight rollover, or TTL staleness, and is otherwise a no-op that preserves the service cache. The Stats screen does not call these, so its behavior is unchanged.
When FeatureFlag.newStats is on, the My Site dashboard rendered the legacy Today card but tapping it landed on the new, differently styled Stats screen. Render the new Stats Today card (sparkline included) on the dashboard so both surfaces match. A separate .todaysStatsNew DashboardCard case, declared adjacent to .todaysStats and mutually exclusive with it, carries the rendering mode in the diffable item identity so a flag flip swaps the cell on the next dashboard appearance. Eligibility uses a side-effect-free predicate over stored properties (flag, dotComID, stored authToken) so dashboard parsing never triggers the sign-in notification or the context factory's assertion; both cases share one personalization key and report the todays_stats analytics identity. A new frameless DashboardTodayStatsNewCardCell hosts StatsTodayCardView with a site-keyed lifecycle (controller rebuilt and in-flight loads cancelled when the site changes; refreshIfNeeded on configure, on reappearance, and on app foreground so a failed or stale card recovers). It builds the context via StatsContext.dashboard(blog:), which reuses the Stats-screen factory but installs no analytics tracker, so only the dashboard's own card-shown/tapped events fire (parity with the legacy card) and load failures are logged via swift-log. The whole card is a plain Button (mirroring the Stats screen) for correct tap and accessibility semantics, with the existing showStats navigation and the shared hide action. The legacy card remains the fallback when the flag is off or the context cannot be built.
Generated by 🚫 Danger |
|
| App Name | WordPress | |
| Configuration | Release-Alpha | |
| Build Number | 33859 | |
| Version | PR #25917 | |
| Bundle ID | org.wordpress.alpha | |
| Commit | b62cf47 | |
| Installation URL | 3f4l7cbq579p8 |
|
| App Name | Jetpack | |
| Configuration | Release-Alpha | |
| Build Number | 33859 | |
| Version | PR #25917 | |
| Bundle ID | com.jetpack.alpha | |
| Commit | b62cf47 | |
| Installation URL | 1tdfokj47chf0 |
🤖 Build Failure AnalysisThis build has failures. Claude has analyzed them - check the build annotations for details. |
|
|
||
| // MARK: - BlogDashboardCardConfigurable | ||
|
|
||
| func configure(blog: Blog, viewController: BlogDashboardViewController?, apiResponse: BlogDashboardRemoteEntity?) { |
There was a problem hiding this comment.
apiResponse is passed in here but never used.
The card decides to appear using the dashboard's stats, but loads the numbers it shows from a separate request. If that request fails (e.g. a shaky connection at launch), the card still appears — with only dashes and a greyed-out chart. The old card can't, since it shows the same data it checked.
Suggestion: fill the totals in from apiResponse so numbers appear at once; only the chart waits on the fetch.
| // MARK: - Dashboard menu | ||
|
|
||
| @ViewBuilder | ||
| private func makeMenu(for blog: Blog) -> some View { |
There was a problem hiding this comment.
Opening this menu doesn't log the "menu opened" event that the old card — and every other card — logs (my_site_dashboard_contextual_menu_accessed). Once the feature ships to everyone, that event will look like it's vanishing even though people still open the menu. ("Shown" and "tapped" are covered; only this is missing.)
Fix: log it when the menu is built. Note: SwiftUI can rebuild the menu more than once, so guard against double-counting.
| /// a site switch) so a stale response cannot land on the wrong site. | ||
| func cancelLoading() { | ||
| loadingTask?.cancel() | ||
| loadingTask = nil |
There was a problem hiding this comment.
cancel() stops the load but leaves the card marked as still loading, so it can never refresh again. Harmless today (the card is thrown away right after cancel()), but this one line removes the trap in case the card is ever reused instead of rebuilt.
| loadingTask = nil | |
| loadingTask = nil | |
| isLoading = false |


Note
Reviewable commit by commit: the
JetpackStatsseam first, then the dashboard integration.Description
Fixes https://linear.app/a8c/issue/CMM-2317
When
FeatureFlag.newStatsis on, the My Site dashboard still showed the legacy "Today's Stats" card, but tapping it landed on the new, differently styled Stats screen. This renders the new Stats Today card (with the hourly sparkline) on the dashboard so both surfaces match.The main changes:
.todaysStatsNewDashboardCardcase, mutually exclusive with.todaysStatsand rendered by a new framelessDashboardTodayStatsNewCardCell. The rendering mode is part of the diffable item identity, so flipping the flag swaps the cell on the next dashboard appearance. Both cases share one personalization key and report thetodays_statsanalytics identity.JetpackStats,StatsTodayCardController+StatsTodayCardView, wrapping the internalTodayCard/TodayCardViewModelwith an explicit refresh/cancel lifecycle (the internal view model only loads on first appearance). The Stats screen is untouched.StatsContextis built viaStatsContext.dashboard(blog:), which installs no analytics tracker, so the embedded card fires only the dashboard's own card-shown/tapped events, matching the legacy card.Worth a closer look: the new card fetches its own hourly series through the module (the batched
todays_statspayload has no hourly data, though it still gates visibility), and eligibility (newStatsActive) is a side-effect-free predicate so dashboard parsing never triggers the sign-in notification or theStatsContext(blog:)assertion. The legacy card stays as the fallback when the flag is off or the context can't be built.Testing instructions
Enable
FeatureFlag.newStats(fresh installs enable it) and open My Site for a WordPress.com or Jetpack site:I verified this with unit tests and on the WordPress app's sign-in and navigation, but not yet with a full visual pass on the live Jetpack dashboard, so screenshots are still to come.