Skip to content

CMM 1267 stats devices card#22620

Merged
adalpari merged 1 commit intotrunkfrom
feat/CMM-1267-stats-devices-card
Feb 25, 2026
Merged

CMM 1267 stats devices card#22620
adalpari merged 1 commit intotrunkfrom
feat/CMM-1267-stats-devices-card

Conversation

@adalpari
Copy link
Contributor

@adalpari adalpari commented Feb 23, 2026

Description

Add a new "Devices" card to the new stats screen that displays device statistics (screen size, browser, and platform) using a ring (donut) chart and a sorted list. The card
includes a segmented selector to switch between the three device categories. Items with zero views are filtered out.

The implementation follows the same architecture as the Locations card but is simpler — no detail screen ("Show All") and no previous-period comparison.

Changes

  • Data layer: Added fetchDevicesScreensize, fetchDevicesBrowser, fetchDevicesPlatform to StatsDataSource interface and StatsDataSourceImpl, using the new
    StatsDevicesParams/StatsDevicesPeriod uniffi types
  • Repository layer: Added corresponding repository methods with zero-view filtering and descending sort
  • UI state: DevicesCardUiState (Loading/Loaded/Error), DeviceType enum, DeviceItem data class
  • ViewModel: DevicesViewModel with per-type lazy loading, period tracking, and refresh support
  • Ring chart: StatsRingChart — Canvas-based donut chart with proportional arcs and accessibility support
  • Card UI: DevicesCard composable with segmented type selector, ring chart, color-coded device list, shimmer loading state, and card management actions
  • Card registration: Added DEVICES to StatsCardType enum and integrated into NewStatsActivity

Testing instructions

Devices card on new stats screen:

  1. Enable FF and open the new stats screen
  2. Scroll down to the "Devices" card
  • Verify the card displays with the "Screen size" tab selected by default

  • Verify a ring (donut) chart is shown with color-coded segments

  • Verify a list of devices is shown below the chart with color dots, names, and view counts

  • Verify Browser and Platform tab works as well

Screen_recording_20260223_145007.mp4

@adalpari
Copy link
Contributor Author

@claude

@claude
Copy link
Contributor

claude bot commented Feb 23, 2026

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


Code Review

  • Read changed files
  • Review data layer (StatsDataSource, StatsDataSourceImpl, StatsRepository)
  • Review UI state and ViewModel
  • Review Composable UI (DevicesCard, StatsRingChart)
  • Review tests
  • Post comprehensive feedback

Overall this is a solid implementation that follows the existing architecture patterns well. The code is clean and the test coverage is reasonable. A few items worth addressing:


Issues

1. Unnecessary computation in repository — previous date range discarded

In StatsRepository.kt (lines 1016–1043), all three device methods call calculateComparisonDateRanges but immediately discard the previous range:

suspend fun fetchDevicesScreensize(siteId: Long, period: StatsPeriod): DevicesResult =
    withContext(ioDispatcher) {
        val (currentDateRange, _) = calculateComparisonDateRanges(period)  // _ is wasted
        fetchDevicesData { statsDataSource.fetchDevicesScreensize(siteId, currentDateRange) }
    }

calculateComparisonDateRanges computes and allocates both current and previous date ranges. Since devices has no comparison data, a lightweight helper that calculates only the current range would avoid this. This is minor but it's a consistency concern since it reuses a concept the feature doesn't need.

Fix this →


2. Double item cap at 10

Items are capped at 10 in two separate places:

  • StatsDataSourceImpl.buildDevicesParams (line 511): max = max.coerceAtLeast(1).toUInt() with the default max: Int = 10 in StatsDataSource.
  • DevicesViewModel.mapToLoadedState (line 314): .take(CARD_MAX_ITEMS) where CARD_MAX_ITEMS = 10.

The .take(CARD_MAX_ITEMS) in the ViewModel is effectively dead code since the API will never return more than 10 items (it was already capped at the request level). Either remove the .take() in the ViewModel or remove the max parameter from the data source interface for devices (since it's a presentation concern). As-is it's not a bug, but it's confusing — which layer owns the limit?


3. ViewModel fetch methods are nearly identical — code duplication

fetchDevicesScreensize, fetchDevicesBrowser, and fetchDevicesPlatform in DevicesViewModel.kt (lines 215–302) are structurally identical: try/catch, call repository, update a state flow, or set error state. The repository already solved the same problem elegantly with fetchDevicesData. Consider a private helper like:

private suspend fun fetchAndUpdate(
    stateFlow: MutableStateFlow<DevicesCardUiState>,
    setPeriod: () -> Unit,
    fetch: suspend () -> DevicesResult
) {
    try {
        when (val result = fetch()) {
            is DevicesResult.Success -> {
                setPeriod()
                stateFlow.value = mapToLoadedState(result)
            }
            is DevicesResult.Error -> stateFlow.value = DevicesCardUiState.Error(...)
        }
    } catch (e: Exception) { ... }
}

Fix this →


4. Missing test: exception path in ViewModel

The three fetchDevices* methods in DevicesViewModel all have @Suppress("TooGenericExceptionCaught") with catch (e: Exception) blocks that emit a generic error state. There are no tests covering this path in DevicesViewModelTest.kt. Consider adding a test where the repository throws an exception to verify the ViewModel emits DevicesCardUiState.Error with R.string.stats_error_unknown.


5. Missing test: item truncation at CARD_MAX_ITEMS

There is no test verifying that more than 10 items are truncated to 10 in DevicesViewModelTest. If the double-capping in issue #2 is resolved by removing the data source cap, this test becomes critical.


Observations / Minor Points

6. ringChartColors() is public but only used within the devices package

StatsRingChart.kt exposes ringChartColors() as a public @Composable function, but it's only called from DevicesCard.kt. Marking it internal would better express intent. Alternatively, since it's tightly coupled to how DevicesCard assigns colors, moving it into DevicesCard.kt keeps the coloring logic co-located.

7. Accessibility description lacks units

In StatsRingChart.kt (line 36–37), the content description is:

val description = entries.joinToString(", ") {
    "${it.label}: ${it.value.toLong()}"
}

This produces e.g. "Desktop: 500, Mobile: 300". Adding "views" makes this more meaningful for screen reader users: "${it.label}: ${it.value.toLong()} views".

8. mapToLoadedState silently depends on sort order

DevicesViewModel.mapToLoadedState (line 316) reads:

val maxViewsForBar = cardItems.firstOrNull()?.views ?: 0L

This implicitly assumes items arrive sorted descending. The sort is done in the repository, which is correct, but this assumption is unguarded and undocumented. If the sorting in the repository were ever changed or bypassed (e.g., in tests), this would silently produce incorrect bar sizing. A comment or an explicit maxByOrNull call would make this safer to read.

9. StatsDevicesParams does not pass locale (unlike other endpoints)

In StatsDataSourceImpl.buildDevicesParams (lines 503–521), StatsDevicesParams is constructed without a locale parameter. All other similar endpoint params (StatsTopPostsParams, StatsCountryViewsParams, StatsReferrersParams, etc.) pass locale = wpComLanguage. If the uniffi StatsDevicesParams supports locale, this omission could affect localized device names. If it doesn't support locale, a comment clarifying that would be helpful.


Summary

The implementation is well-structured and consistent with the codebase. The main actionable items are:

@dangermattic
Copy link
Collaborator

dangermattic commented Feb 23, 2026

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

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Feb 23, 2026

Project dependencies changes

list
! Upgraded Dependencies
rs.wordpress.api:android:1179-5909d11d98d0438c6ee896a45e3fab3720b395a1, (changed from trunk-9edcee430afd18d7d440baf497a763f9b1bb83d9)
rs.wordpress.api:kotlin:1179-5909d11d98d0438c6ee896a45e3fab3720b395a1, (changed from trunk-9edcee430afd18d7d440baf497a763f9b1bb83d9)
tree
 +--- project :libs:fluxc
-|    \--- rs.wordpress.api:android:trunk-9edcee430afd18d7d440baf497a763f9b1bb83d9
-|         +--- 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-9edcee430afd18d7d440baf497a763f9b1bb83d9
-|         |    +--- 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:1179-5909d11d98d0438c6ee896a45e3fab3720b395a1
+|         +--- 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:1179-5909d11d98d0438c6ee896a45e3fab3720b395a1
+|         |    +--- 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-9edcee430afd18d7d440baf497a763f9b1bb83d9 (*)
+\--- rs.wordpress.api:android:1179-5909d11d98d0438c6ee896a45e3fab3720b395a1 (*)

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Feb 23, 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
FlavorJalapeno
Build TypeDebug
Versionpr22620-a5ca3b3
Build Number1485
Application IDorg.wordpress.android.prealpha
Commita5ca3b3
Installation URL75204m1j07340
Note: Google Login is not supported on these builds.

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Feb 23, 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
FlavorJalapeno
Build TypeDebug
Versionpr22620-a5ca3b3
Build Number1485
Application IDcom.jetpack.android.prealpha
Commita5ca3b3
Installation URL0hti7f3tgkgto
Note: Google Login is not supported on these builds.

@adalpari
Copy link
Contributor Author

2. Double item cap at 10

Items are capped at 10 in two separate places:

  • StatsDataSourceImpl.buildDevicesParams (line 511): max = max.coerceAtLeast(1).toUInt() with the default max: Int = 10 in StatsDataSource.
  • DevicesViewModel.mapToLoadedState (line 314): .take(CARD_MAX_ITEMS) where CARD_MAX_ITEMS = 10.

The .take(CARD_MAX_ITEMS) in the ViewModel is effectively dead code since the API will never return more than 10 items (it was already capped at the request level). Either remove the .take() in the ViewModel or remove the max parameter from the data source interface for devices (since it's a presentation concern). As-is it's not a bug, but it's confusing — which layer owns the limit?

The API is not always following the 10 items cap. So, this is necessary

wordpress-lint = '2.2.0'
wordpress-persistent-edittext = '1.0.2'
wordpress-rs = 'trunk-9edcee430afd18d7d440baf497a763f9b1bb83d9'
wordpress-rs = '1179-e4582399b9d0c8735d2ddb16894e94668781927f'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note: The PR won't pass until this is reverted to trunk

@adalpari adalpari marked this pull request as ready for review February 23, 2026 14:41
@adalpari adalpari requested a review from a team as a code owner February 23, 2026 14:41
@adalpari adalpari requested review from nbradbury and removed request for a team February 23, 2026 14:41
@nbradbury
Copy link
Contributor

@adalpari When there's no data in the response the devices cards shows an error message.

Screenshot 2026-02-23 at 11 18 25 AM

@nbradbury
Copy link
Contributor

@adalpari The UI is showing only two items for the screen sizes, but the response contains three items.

Screenshot_20260223_114701 response

@nbradbury
Copy link
Contributor

@adalpari When using a larger font size, "Screen size" looks a little too tight.

Screenshot_20260223_115214

Maybe just use "Size" like we do on the web?

web

@adalpari
Copy link
Contributor Author

@nbradbury thank you for the checks!

  1. Missed this one. I see the error is in the rs layer. So, I need to change it. I'll let you know when it's ready

  2. Good catch. I took for granted that we were using integers. So, the data was not properly rounded

  3. Fixed!

Screenshot 2026-02-24 at 09 35 15

@adalpari
Copy link
Contributor Author

Fixed the empty results case
Screenshot 2026-02-24 at 10 56 59
Screenshot 2026-02-24 at 10 56 59

So, @nbradbury, can you take another look?

In other notes, I noticed that the webapp is not always skipping the empty values. So, I've made the Android app to always show them to be consiostent.

Two different sites in the webapp:

Screenshot 2026-02-24 at 12 04 04 Screenshot 2026-02-24 at 12 04 18

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.

@adalpari This looks good! I'll approve and you can merge when the wp-rs hash is updated :shipit:

Adds screen size, browser, and platform device stats cards with
ring chart visualization. Also includes clicks, search terms,
video plays, and file downloads stats cards.
@adalpari adalpari force-pushed the feat/CMM-1267-stats-devices-card branch from e8703ef to a5ca3b3 Compare February 25, 2026 18:23
@adalpari adalpari enabled auto-merge (squash) February 25, 2026 18:24
@sonarqubecloud
Copy link

@adalpari adalpari merged commit 1025cfd into trunk Feb 25, 2026
21 of 22 checks passed
@adalpari adalpari deleted the feat/CMM-1267-stats-devices-card branch February 25, 2026 18:51
@codecov
Copy link

codecov bot commented Feb 25, 2026

Codecov Report

❌ Patch coverage is 29.67213% with 429 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.20%. Comparing base (628bf67) to head (a5ca3b3).
⚠️ Report is 2 commits behind head on trunk.

Files with missing lines Patch % Lines
...rdpress/android/ui/newstats/devices/DevicesCard.kt 0.00% 235 Missing ⚠️
...roid/ui/newstats/datasource/StatsDataSourceImpl.kt 0.00% 76 Missing ⚠️
...ress/android/ui/newstats/devices/StatsRingChart.kt 0.00% 61 Missing ⚠️
.../wordpress/android/ui/newstats/NewStatsActivity.kt 0.00% 33 Missing ⚠️
.../android/ui/newstats/repository/StatsRepository.kt 74.50% 11 Missing and 2 partials ⚠️
...ss/android/ui/newstats/devices/DevicesViewModel.kt 92.91% 3 Missing and 6 partials ⚠️
...ess/android/ui/newstats/locations/LocationsCard.kt 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            trunk   #22620      +/-   ##
==========================================
- Coverage   38.24%   38.20%   -0.05%     
==========================================
  Files        2259     2263       +4     
  Lines      114979   115584     +605     
  Branches    15980    16041      +61     
==========================================
+ Hits        43978    44157     +179     
- Misses      67384    67802     +418     
- Partials     3617     3625       +8     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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