Skip to content

Show the new Stats Today card on the My Site dashboard - #25917

Open
crazytonyli wants to merge 2 commits into
trunkfrom
task/cmm-2317-my-site-new-stats-today-card
Open

Show the new Stats Today card on the My Site dashboard#25917
crazytonyli wants to merge 2 commits into
trunkfrom
task/cmm-2317-my-site-new-stats-today-card

Conversation

@crazytonyli

@crazytonyli crazytonyli commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Note

Reviewable commit by commit: the JetpackStats seam first, then the dashboard integration.

Description

Fixes https://linear.app/a8c/issue/CMM-2317

When FeatureFlag.newStats is 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:

  1. A new .todaysStatsNew DashboardCard case, mutually exclusive with .todaysStats and rendered by a new frameless DashboardTodayStatsNewCardCell. 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 the todays_stats analytics identity.
  2. A small public seam in JetpackStats, StatsTodayCardController + StatsTodayCardView, wrapping the internal TodayCard/TodayCardViewModel with an explicit refresh/cancel lifecycle (the internal view model only loads on first appearance). The Stats screen is untouched.
  3. The dashboard's StatsContext is built via StatsContext.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_stats payload 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 the StatsContext(blog:) assertion. The legacy card stays as the fallback when the flag is off or the context can't be built.

Before After

Testing instructions

Enable FeatureFlag.newStats (fresh installs enable it) and open My Site for a WordPress.com or Jetpack site:

  • The Today card renders in the new style with the sparkline and matches the Stats screen; tapping it opens the Stats Traffic tab.
  • The more menu shows "View stats" and "Hide this"; hiding also toggles via Personalize Home Tab.
  • Toggle the flag from the Stats screen and return: the card swaps rendering both ways. Switch sites: only the current site's data shows. Flag off, or no usable WP.com context: the legacy card shows, with no sign-in prompt.

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.

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.
@dangermattic

Copy link
Copy Markdown
Collaborator
1 Warning
⚠️ This PR is larger than 500 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.
1 Message
📖 This PR is still a Draft: some checks will be skipped.

Generated by 🚫 Danger

@wpmobilebot

Copy link
Copy Markdown
Contributor
App Icon📲 You can test the changes from this Pull Request in WordPress by scanning the QR code below to install the corresponding build.
App NameWordPress
ConfigurationRelease-Alpha
Build Number33859
VersionPR #25917
Bundle IDorg.wordpress.alpha
Commitb62cf47
Installation URL3f4l7cbq579p8
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot

Copy link
Copy Markdown
Contributor
App Icon📲 You can test the changes from this Pull Request in Jetpack by scanning the QR code below to install the corresponding build.
App NameJetpack
ConfigurationRelease-Alpha
Build Number33859
VersionPR #25917
Bundle IDcom.jetpack.alpha
Commitb62cf47
Installation URL1tdfokj47chf0
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot

Copy link
Copy Markdown
Contributor

🤖 Build Failure Analysis

This build has failures. Claude has analyzed them - check the build annotations for details.

@crazytonyli
crazytonyli requested a review from jkmassel August 20, 2026 07:21
@crazytonyli crazytonyli added this to the 27.3 milestone Aug 20, 2026
@crazytonyli
crazytonyli marked this pull request as ready for review August 20, 2026 07:22

// MARK: - BlogDashboardCardConfigurable

func configure(blog: Blog, viewController: BlogDashboardViewController?, apiResponse: BlogDashboardRemoteEntity?) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
loadingTask = nil
loadingTask = nil
isLoading = false

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants