Skip to content

CMM-1936: Add Insights tab with Year in Review card#22666

Open
adalpari wants to merge 9 commits intotrunkfrom
feat/CMM-1936-create-insights-tab
Open

CMM-1936: Add Insights tab with Year in Review card#22666
adalpari wants to merge 9 commits intotrunkfrom
feat/CMM-1936-create-insights-tab

Conversation

@adalpari
Copy link
Contributor

@adalpari adalpari commented Mar 9, 2026

Description

Populates the Insights tab in the new stats screen with its first card: Year in Review.

This follows the same architecture as the Traffic tab: card management (add/remove/move), data layer, ViewModel pattern, and comprehensive tests.

What's included:

  • Card type system: InsightsCardType enum, InsightsCardsConfiguration, and InsightsCardsConfigurationRepository for card management with persistence via AppPrefs
  • Data layer: StatsDataSource.fetchStatsInsights() and StatsRepository.fetchInsights() for fetching yearly insights data
  • Year in Review card: Compose UI showing the current year's stats (posts, words, likes, comments) in a 2×2 mini-card grid with shimmer loading and error states
  • Detail screen: YearInReviewDetailActivity showing all years with full stats (totals + averages) in a table layout, accessible via "View all" link
  • Current year guarantee: The current year is always shown in the card, even if not returned by the API (displayed with zero values)
  • Card management: InsightsViewModel handles add/remove/move card operations with the same pattern as NewStatsViewModel

Testing instructions

NOTE: You will see the period selector in the Insights screen, but it's useless. I've not hidden it because I already hid it in this PR (waiting for the RS layer to be approved) and I don't want to create any conflict.

Year in Review card:

  1. Navigate to Stats → Insights tab
  2. Verify the Year in Review card loads with the current year's stats
  • Card shows current year title (e.g., "2026 in review")
  • Stats display in a 2×2 grid (Posts, Words, Likes, Comments)
  • "View all" link is visible

View all detail screen:

  1. Tap "View all" on the Year in Review card
  • Detail screen opens with all years listed newest-first
  • Each year shows 7 stats: Posts, Total comments, Avg comments/post, Total likes, Avg likes/post, Total words, Avg words/post
  • Back button returns to Insights tab
Screen_recording_20260309_170610.mp4

adalpari and others added 7 commits March 9, 2026 11:13
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Populate the Insights tab with card management (add, remove, move)
mirroring the Traffic tab architecture. Add Year in Review as the
first Insights card, fetching yearly summaries (posts, words, likes,
comments) via the wordpress-rs statsInsights endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update the card title to show "YEAR in review" instead of a generic
title. Replace the table layout with a 2x2 grid of mini cards, each
displaying an icon, label, and formatted value for Posts, Words,
Likes, and Comments, matching the web design.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a detail screen showing all years with full stats (posts, comments,
avg comments/post, likes, avg likes/post, words, avg words/post). The
card shows the most recent year and a "View all" link when multiple
years are available. Years are sorted newest first.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ow All

Ensure the current year is always present in the data, adding it with
zero values if not returned by the API. The Show All footer is now
always visible regardless of the number of years available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use resource string for exception errors instead of raw e.message
- Add duplicate guard in addCard()
- Change Error from data class to class to fix lambda equality
- Use Year.now() per-call instead of cached static val
- Fix isValidConfiguration to check for null entries
- Remove 0dp Spacer from detail screen
- Add StatsFormatterTest with 12 tests
- Add repository moveCard and addCard duplicate tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dangermattic
Copy link
Collaborator

dangermattic commented Mar 9, 2026

1 Error
🚫 This PR includes a PR version of wordpress-rs (1219-9cb722b47cbf8380f7e7fd40d076773e1f87cea0). Please merge the corresponding wordpress-rs PR and update to a trunk version before merging.
2 Warnings
⚠️ This PR is larger than 300 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.
⚠️ PR is not assigned to a milestone.

Generated by 🚫 Danger

@adalpari adalpari changed the base branch from trunk to feat/CMM-1936-new-stats-create-insights-tab-feature-branch March 9, 2026 13:01
@adalpari
Copy link
Contributor Author

adalpari commented Mar 9, 2026

@claude

@claude
Copy link
Contributor

claude bot commented Mar 9, 2026

Claude finished @adalpari's task in 3m 28s —— View job


Code Review: CMM-1936 – Insights Tab with Year in Review Card

  • Gather context and understand the PR
  • Review data layer (StatsDataSource, StatsRepository)
  • Review card configuration and management
  • Review ViewModels
  • Review UI components (YearInReviewCard, YearInReviewDetailActivity)
  • Review tests
  • Review AppPrefs changes
  • Compile and post final review

Overall the PR is well-structured and follows the established Traffic-tab patterns closely. The code is clean, the test coverage is solid, and the architecture is consistent. Below are the issues worth addressing before merging.


Bugs / Correctness

1. isLoadedSuccessfully stays true after a failed refresh — error state persists on re-entry

YearInReviewViewModel.kt:41,106

When refresh() succeeds once, isLoadedSuccessfully = true. If a subsequent refresh() throws or returns an error, the state becomes Error, but isLoadedSuccessfully is never reset to false. When the user navigates away and back, loadDataIfNeeded() sees isLoadedSuccessfully = true and does nothing — the card stays stuck in the error state with no automatic recovery path.

// loadDataInternal – sets true on success but never resets on failure
is InsightsResult.Success -> {
    isLoadedSuccessfully = true   // ← never reset on subsequent failure
    ...
}

Fix: reset isLoadedSuccessfully = false when the error state is set inside loadDataInternal, or clear the flag at the start of refresh(). Fix this →


2. Race condition in InsightsCardsConfigurationRepository mutation operations

InsightsCardsConfigurationRepository.kt:53-128

Every mutation method (removeCard, addCard, moveCardUp, …) follows a read-modify-write pattern:

suspend fun removeCard(siteId: Long, cardType: InsightsCardType) =
    withContext(ioDispatcher) {
        val current = getConfiguration(siteId)   // read
        ...
        saveConfiguration(siteId, ...)            // write
    }

withContext is not a mutex — if two coroutines call mutations concurrently (e.g. the user rapidly taps remove and the OS also triggers a config reload), the second read can return stale data and overwrite the first write. The equivalent StatsCardsConfigurationRepository for the Traffic tab likely has the same pattern.

Recommended fix: use a Mutex to serialize mutations. Fix this →


3. siteId silently falls back to 0L — operations proceed with an invalid site ID

InsightsViewModel.kt:43-45

private val siteId: Long
    get() = selectedSiteRepository.getSelectedSite()?.siteId ?: 0L

If getSelectedSite() returns null (e.g. during logout), all card mutation calls (removeCard, addCard, move operations) will execute with siteId = 0L, silently writing to the wrong preferences key. Consider guarding early and returning/logging an error instead of using 0. Fix this →


Design / API Inconsistency

4. fetchStatsInsights takes siteId: String while all other interface methods take siteId: Long

StatsDataSource.kt:217-219

// All other methods:
suspend fun fetchStatsVisits(siteId: Long, ...): ...

// Insights:
suspend fun fetchStatsInsights(siteId: String): StatsInsightsDataResult

The conversion to String happens in StatsRepository.fetchInsights (siteId.toString()). The inconsistency leaks through the interface and can confuse future implementors. Consider aligning the interface to Long and letting StatsDataSourceImpl handle the toULong() conversion internally (as it does for all other methods). Fix this →


UX Issues

5. Period selector (date/calendar toolbar button) is visible on the Insights tab

NewStatsActivity.kt:138-208

The TopAppBar period selector is always rendered regardless of the active tab. Insights data is year-based and not period-dependent, so showing a period picker on the Insights tab is misleading. The action should be hidden or disabled when the Insights (or Subscribers) tab is active.


Minor Issues

6. resetToDefault does not emit to configurationFlow

InsightsCardsConfigurationRepository.kt:188-196

When an invalid config is found and reset, _configurationFlow is not updated, so any active observers won't be notified of the reset. Since resetToDefault is only called from loadConfiguration (which is invoked from getConfiguration), and getConfiguration returns the result directly to the caller, this is not a practical problem today — but it makes the flow semantics inconsistent. Consider emitting there as well.

7. YearInReviewCardUiState.Error is a plain class, not data class

YearInReviewCardUiState.kt:13-16

Loading and Loaded use data object/data class, but Error is a plain class because it holds a lambda. This is correct (lambdas can't be compared by value), but it means Error has no toString(), no structural equality, and no copy(). This asymmetry is worth documenting with a comment explaining why it can't be a data class.

8. Loading shimmer has no accessibility semantics

YearInReviewCard.kt:112-181

The shimmer LoadingContent composable has no contentDescription or semantic annotation. Screen readers will announce nothing while the card loads. Consider adding a semantics { contentDescription = stringResource(R.string.loading) } to the outer container.


Test Coverage

The test suite is comprehensive and covers the key scenarios well (success, error, retry, refresh, loadDataIfNeeded deduplication, current-year guarantee). The main gap is:

  • No test for the "error after successful load → navigate away → return" scenario described in bug Menu Drawer Item refactor #1 above.
  • No test for concurrent mutations in InsightsCardsConfigurationRepositoryTest (the existing mutation tests are sequential).

Summary

Severity Count
Bug 2 (#1 stale error state, #2 race condition)
Correctness concern 1 (#3 siteId fallback)
Design inconsistency 1 (#4 String vs Long siteId)
UX 1 (#5 period selector on Insights tab)
Minor/polish 3 (#6–8)

@adalpari adalpari changed the base branch from feat/CMM-1936-new-stats-create-insights-tab-feature-branch to trunk March 9, 2026 13:03
@wpmobilebot
Copy link
Contributor

wpmobilebot commented Mar 9, 2026

Project manifest changes for WordPress

The following changes in the WordPress's merged AndroidManifest.xml file were detected (build variant: wordpressRelease):

--- ./build/reports/diff_manifest/WordPress/wordpressRelease/base_manifest.txt	2026-03-09 17:02:48.377165720 +0000
+++ ./build/reports/diff_manifest/WordPress/wordpressRelease/head_manifest.txt	2026-03-09 17:03:06.407203272 +0000
@@ -203,6 +203,10 @@
         <activity
             android:name="org.wordpress.android.ui.newstats.authors.AuthorsDetailActivity"
             android:exported="false"
+            android:theme="@style/WordPress.NoActionBar" />
+        <activity
+            android:name="org.wordpress.android.ui.newstats.yearinreview.YearInReviewDetailActivity"
+            android:exported="false"
             android:theme="@style/WordPress.NoActionBar" /> <!-- Account activities -->
         <activity
             android:name="org.wordpress.android.ui.main.MeActivity"

Go to https://buildkite.com/automattic/wordpress-android/builds/25372/canvas?sid=019cd383-1bb6-4fe1-88aa-dede8de9dfed, click on the Artifacts tab and audit the files.

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Mar 9, 2026

Project manifest changes for WordPress

The following changes in the WordPress's merged AndroidManifest.xml file were detected (build variant: jetpackRelease):

--- ./build/reports/diff_manifest/WordPress/jetpackRelease/base_manifest.txt	2026-03-09 17:02:57.130784216 +0000
+++ ./build/reports/diff_manifest/WordPress/jetpackRelease/head_manifest.txt	2026-03-09 17:03:12.870816913 +0000
@@ -395,6 +395,10 @@
         <activity
             android:name="org.wordpress.android.ui.newstats.authors.AuthorsDetailActivity"
             android:exported="false"
+            android:theme="@style/WordPress.NoActionBar" />
+        <activity
+            android:name="org.wordpress.android.ui.newstats.yearinreview.YearInReviewDetailActivity"
+            android:exported="false"
             android:theme="@style/WordPress.NoActionBar" /> <!-- Account activities -->
         <activity
             android:name="org.wordpress.android.ui.main.MeActivity"

Go to https://buildkite.com/automattic/wordpress-android/builds/25372/canvas?sid=019cd383-1bb8-4aac-87ac-2ec3e34bea9c, click on the Artifacts tab and audit the files.

@wpmobilebot
Copy link
Contributor

Project dependencies changes

list
! Upgraded Dependencies
rs.wordpress.api:android:1219-9cb722b47cbf8380f7e7fd40d076773e1f87cea0, (changed from trunk-4fac42b1a262e93adcbc0cd0353aafa0369671d2)
rs.wordpress.api:kotlin:1219-9cb722b47cbf8380f7e7fd40d076773e1f87cea0, (changed from trunk-4fac42b1a262e93adcbc0cd0353aafa0369671d2)
tree
 +--- project :libs:fluxc
-|    \--- rs.wordpress.api:android:trunk-4fac42b1a262e93adcbc0cd0353aafa0369671d2
-|         +--- com.squareup.okhttp3:okhttp:5.3.2 (*)
-|         +--- com.squareup.okhttp3:okhttp-tls:5.3.2
-|         |    +--- com.squareup.okhttp3:okhttp:5.3.2 (*)
-|         |    +--- com.squareup.okio:okio:3.16.4 (*)
-|         |    \--- org.jetbrains.kotlin:kotlin-stdlib:2.2.21 -> 2.3.10 (*)
-|         +--- net.java.dev.jna:jna:5.18.1
-|         +--- rs.wordpress.api:kotlin:trunk-4fac42b1a262e93adcbc0cd0353aafa0369671d2
-|         |    +--- com.squareup.okhttp3:okhttp:5.3.2 (*)
-|         |    +--- com.squareup.okhttp3:okhttp-tls:5.3.2 (*)
-|         |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2 (*)
-|         |    \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.3.10 (*)
-|         \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.3.10 (*)
+|    \--- rs.wordpress.api:android:1219-9cb722b47cbf8380f7e7fd40d076773e1f87cea0
+|         +--- com.squareup.okhttp3:okhttp:5.3.2 (*)
+|         +--- com.squareup.okhttp3:okhttp-tls:5.3.2
+|         |    +--- com.squareup.okhttp3:okhttp:5.3.2 (*)
+|         |    +--- com.squareup.okio:okio:3.16.4 (*)
+|         |    \--- org.jetbrains.kotlin:kotlin-stdlib:2.2.21 -> 2.3.10 (*)
+|         +--- net.java.dev.jna:jna:5.18.1
+|         +--- rs.wordpress.api:kotlin:1219-9cb722b47cbf8380f7e7fd40d076773e1f87cea0
+|         |    +--- com.squareup.okhttp3:okhttp:5.3.2 (*)
+|         |    +--- com.squareup.okhttp3:okhttp-tls:5.3.2 (*)
+|         |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2 (*)
+|         |    \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.3.10 (*)
+|         \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.3.10 (*)
-\--- rs.wordpress.api:android:trunk-4fac42b1a262e93adcbc0cd0353aafa0369671d2 (*)
+\--- rs.wordpress.api:android:1219-9cb722b47cbf8380f7e7fd40d076773e1f87cea0 (*)

…, siteId fallback, API type inconsistency

- Reset isLoadedSuccessfully on error/exception so loadDataIfNeeded recovers after failed refresh
- Add Mutex to InsightsCardsConfigurationRepository to prevent concurrent mutation races
- Guard siteId in InsightsViewModel mutations to avoid operating with invalid 0L
- Align fetchStatsInsights parameter from String to Long for interface consistency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@adalpari
Copy link
Contributor Author

adalpari commented Mar 9, 2026

Claude sugestions achieved here

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Mar 9, 2026

App Icon📲 You can test the changes from this Pull Request in Jetpack Android by scanning the QR code below to install the corresponding build.

App NameJetpack Android
Build TypeDebug
Versionpr22666-36ff869
Build Number1486
Application IDcom.jetpack.android.prealpha
Commit36ff869
Installation URL055n3h1b7t8io
Note: Google Login is not supported on these builds.

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Mar 9, 2026

App Icon📲 You can test the changes from this Pull Request in WordPress Android by scanning the QR code below to install the corresponding build.

App NameWordPress Android
Build TypeDebug
Versionpr22666-36ff869
Build Number1486
Application IDorg.wordpress.android.prealpha
Commit36ff869
Installation URL12jfap6al16so
Note: Google Login is not supported on these builds.

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Mar 9, 2026

🤖 Build Failure Analysis

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

@adalpari adalpari marked this pull request as ready for review March 9, 2026 16:09
@adalpari adalpari requested a review from a team as a code owner March 9, 2026 16:09
@adalpari adalpari requested review from nbradbury and removed request for a team March 9, 2026 16:09
@nbradbury
Copy link
Contributor

I found it odd that the four labels are all totals for the year, but only the Words and Likes labels include the word "Total." The web app doesn't say "Total" anywhere and I think we can do the same.

total

Card uses short labels (Words, Likes) without "Total" prefix.
Detail screen uses exact web labels: Total posts, Total comments,
Avg comments per post, Total likes, Avg likes per post, Total words,
Avg words per post.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@adalpari
Copy link
Contributor Author

adalpari commented Mar 9, 2026

I found it odd that the four labels are all totals for the year, but only the Words and Likes labels include the word "Total." The web app doesn't say "Total" anywhere and I think we can do the same.

Good call! This was because of reusing words. Changed!
share_908030740176707496

@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 9, 2026

Copy link
Contributor

@nbradbury nbradbury left a comment

Choose a reason for hiding this comment

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

Looks good! I'll approve this and it can be merged when Buildkite is feeling better :shipit:

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants