Skip to content

Conversation

@adalpari
Copy link
Contributor

@adalpari adalpari commented Jan 30, 2026

Description

Implements a new Countries stats card for the new stats feature. The card displays visitor statistics by country with an interactive map visualization and a list of top countries. Users can tap "Show All" to see the complete list of countries in a detail screen.

Key components added:

  • CountriesCard - Jetpack Compose UI with map visualization (WebView-based), country list with flags, loading shimmer, and error states
  • CountriesViewModel - Manages UI state, fetches country data via StatsRepository, handles period changes and refresh
  • CountriesDetailActivity - Full-screen detail view showing all countries with the map
  • CountriesCardUiState - Sealed class representing Loading, Loaded, and Error states
  • Data layer integration via StatsDataSource and StatsRepository
Screen_recording_20260130_114034.mp4

Testing instructions

Countries card loading and display:

  1. Open the app and navigate to a site with stats data
  2. Go to the new Stats screen
  • Verify the Countries card displays with a map visualization at the top
  • Verify the card shows a list of countries with flags, names, and view counts

Countries detail screen:

  1. On the Stats screen, locate the Countries card
  2. Tap on "Show All" button
  • Verify the CountriesDetailActivity opens
  • Verify the detail screen shows the map at the top
  • Verify all countries are displayed in a scrollable list

Error handling:

  1. Enable airplane mode
  2. Pull to refresh or reopen the Stats screen
  • Verify the Countries card shows an error state with a retry button
  1. Disable airplane mode and tap retry
  • Verify the card reloads and displays data correctly

NOTE: I know I need to work in the per-card retry button :)

Period changes:

  1. On the Stats screen, change the time period (e.g., from Last 7 Days to Last 30 Days)
  • Verify the Countries card reloads with data for the new period

@adalpari
Copy link
Contributor Author

@claude

@claude
Copy link
Contributor

claude bot commented Jan 30, 2026

OUTDATED

@dangermattic
Copy link
Collaborator

dangermattic commented Jan 30, 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

ReferrersDataResult.Error("Unknown error")
}
}
return ReferrersDataResult.Error("Referrers API not available")
Copy link
Contributor

Choose a reason for hiding this comment

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

Unreachable code: This return statement is unreachable because the when expression above (lines 204-222) already returns in all branches. This line should be removed.

Comment on lines 230 to 232
val maxViews = state.countries.maxOfOrNull { it.views } ?: 1L
state.countries.forEachIndexed { index, country ->
val percentage = if (maxViews > 0) country.views.toFloat() / maxViews.toFloat() else 0f
Copy link
Contributor

Choose a reason for hiding this comment

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

Performance: maxOfOrNull is called inside the composable on every recomposition, scanning through all countries. Consider calculating maxViews in the ViewModel and including it in the UI state instead:

data class Loaded(
    val countries: List<CountryItem>,
    val maxCountryViews: Long,  // Pre-calculated in ViewModel
    // ...
)

Note: The division by zero check is also redundant since maxViews is initialized with ?: 1L.

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Jan 30, 2026

Project manifest changes for WordPress

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

--- ./build/reports/diff_manifest/WordPress/wordpressVanillaRelease/base_manifest.txt	2026-01-30 15:34:18.553867749 +0000
+++ ./build/reports/diff_manifest/WordPress/wordpressVanillaRelease/head_manifest.txt	2026-01-30 15:34:29.343894594 +0000
@@ -195,6 +195,10 @@
         <activity
             android:name="org.wordpress.android.ui.newstats.mostviewed.MostViewedDetailActivity"
             android:exported="false"
+            android:theme="@style/WordPress.NoActionBar" />
+        <activity
+            android:name="org.wordpress.android.ui.newstats.countries.CountriesDetailActivity"
+            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/24724/canvas?sid=019c0f84-4483-45fb-8732-af60f3e0109b, click on the Artifacts tab and audit the files.

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Jan 30, 2026

Project manifest changes for WordPress

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

--- ./build/reports/diff_manifest/WordPress/jetpackVanillaRelease/base_manifest.txt	2026-01-30 15:34:05.262294710 +0000
+++ ./build/reports/diff_manifest/WordPress/jetpackVanillaRelease/head_manifest.txt	2026-01-30 15:34:15.572412575 +0000
@@ -389,6 +389,10 @@
         <activity
             android:name="org.wordpress.android.ui.newstats.mostviewed.MostViewedDetailActivity"
             android:exported="false"
+            android:theme="@style/WordPress.NoActionBar" />
+        <activity
+            android:name="org.wordpress.android.ui.newstats.countries.CountriesDetailActivity"
+            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/24724/canvas?sid=019c0f84-4484-4d67-bca8-0944accf9e75, click on the Artifacts tab and audit the files.

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Jan 30, 2026

Project dependencies changes

list
! Upgraded Dependencies
rs.wordpress.api:android:1134-6cdddac9b99642742ae89bb5833bbe2ed1c7cdd1, (changed from 1119-c0b6b2248a27094599f6411ee52e68e7baafc469)
rs.wordpress.api:kotlin:1134-6cdddac9b99642742ae89bb5833bbe2ed1c7cdd1, (changed from 1119-c0b6b2248a27094599f6411ee52e68e7baafc469)
tree
 +--- project :libs:fluxc
-|    \--- rs.wordpress.api:android:1119-c0b6b2248a27094599f6411ee52e68e7baafc469
-|         +--- com.squareup.okhttp3:okhttp:4.12.0 -> 5.3.2 (*)
-|         +--- com.squareup.okhttp3:okhttp-tls:4.12.0
-|         |    +--- com.squareup.okhttp3:okhttp:4.12.0 -> 5.3.2 (*)
-|         |    +--- com.squareup.okio:okio:3.6.0 -> 3.16.4 (*)
-|         |    \--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.21 -> 1.9.24 (*)
-|         +--- net.java.dev.jna:jna:5.18.1
-|         +--- rs.wordpress.api:kotlin:1119-c0b6b2248a27094599f6411ee52e68e7baafc469
-|         |    +--- com.squareup.okhttp3:okhttp:4.12.0 -> 5.3.2 (*)
-|         |    +--- com.squareup.okhttp3:okhttp-tls:4.12.0 (*)
-|         |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2 (*)
-|         |    \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.2.21 (*)
-|         \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.2.21 (*)
+|    \--- rs.wordpress.api:android:1134-6cdddac9b99642742ae89bb5833bbe2ed1c7cdd1
+|         +--- com.squareup.okhttp3:okhttp:4.12.0 -> 5.3.2 (*)
+|         +--- com.squareup.okhttp3:okhttp-tls:4.12.0
+|         |    +--- com.squareup.okhttp3:okhttp:4.12.0 -> 5.3.2 (*)
+|         |    +--- com.squareup.okio:okio:3.6.0 -> 3.16.4 (*)
+|         |    \--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.21 -> 1.9.24 (*)
+|         +--- net.java.dev.jna:jna:5.18.1
+|         +--- rs.wordpress.api:kotlin:1134-6cdddac9b99642742ae89bb5833bbe2ed1c7cdd1
+|         |    +--- com.squareup.okhttp3:okhttp:4.12.0 -> 5.3.2 (*)
+|         |    +--- com.squareup.okhttp3:okhttp-tls:4.12.0 (*)
+|         |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2 (*)
+|         |    \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.2.21 (*)
+|         \--- org.jetbrains.kotlin:kotlin-stdlib:2.1.21 -> 2.2.21 (*)
-\--- rs.wordpress.api:android:1119-c0b6b2248a27094599f6411ee52e68e7baafc469 (*)
+\--- rs.wordpress.api:android:1134-6cdddac9b99642742ae89bb5833bbe2ed1c7cdd1 (*)

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Jan 30, 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
Versionpr22546-2872ca5
Build Number1483
Application IDcom.jetpack.android.prealpha
Commit2872ca5
Installation URL0efgumgdavca8
Note: Google Login is not supported on these builds.

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Jan 30, 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
Versionpr22546-2872ca5
Build Number1483
Application IDorg.wordpress.android.prealpha
Commit2872ca5
Installation URL187nm68141r40
Note: Google Login is not supported on these builds.

@adalpari
Copy link
Contributor Author

@claude

@claude
Copy link
Contributor

claude bot commented Jan 30, 2026

Claude finished @adalpari's task in 1m 39s —— View job


Code Review Complete ✓

I've reviewed the Countries card implementation. Here's my comprehensive feedback:

Summary

This PR implements a well-structured Countries stats feature with good separation of concerns, proper state management, and comprehensive testing. However, there are several important issues to address related to security, code duplication, and architecture.


Critical Issues

1. WebView Security Risk

Location: StatsGeoChartWebView.kt:73,126

The implementation enables JavaScript and loads content from an external CDN (https://www.gstatic.com/charts/loader.js). While the HTML is loaded as base64, the JavaScript library itself is fetched from the network, creating potential security risks:

  • If Google's CDN is compromised, malicious JavaScript could execute in the app
  • No Content Security Policy restrictions
  • JavaScript is enabled globally for the WebView

Recommendations:

  1. Consider using a native Android charting library (MPAndroidChart, Vico, etc.) to eliminate WebView entirely
  2. If WebView is necessary, implement strict URL validation via WebViewClient
  3. Add CSP restrictions
  4. Consider setMixedContentMode(MIXED_CONTENT_NEVER_ALLOW)

Fix security issues →


High Priority Issues

2. Code Duplication - Shared Components

Location: Multiple files

The map-related composables (StatsGeoChartWebView, StatsMapLegend, StatsChangeIndicator) were created but are currently only used by the Countries feature. While extracting them as reusable components is good architecture, ensure these are actually intended to be shared:

  • StatsGeoChartWebView.kt - 164 lines
  • StatsMapLegend.kt - 72 lines
  • StatsChangeIndicator.kt - 41 lines

Recommendation: Confirm that other stats cards will use maps. If not, consider moving these components into the countries package to maintain package cohesion.

3. Inefficient State Calculation

Location: CountriesCard.kt:182-184

The maxViewsForBar calculation is performed on every recomposition:

val percentage = if (state.maxViewsForBar > 0) {
    country.views.toFloat() / state.maxViewsForBar.toFloat()
} else 0f

Impact: This recalculates for each country row on every recomposition.

Recommendation: Pre-calculate maxViewsForBar in the ViewModel and include it in CountriesCardUiState.Loaded (already done correctly!)

Optimize performance →

4. Parcelable vs Serializable Performance

Location: CountriesDetailActivity.kt:141-147

The code uses Parcelable for CountriesDetailItem (good!), but the previous review comment suggested this as an issue. Actually, this is already correctly implemented with @Parcelize.

Status: ✓ Already correct


Medium Priority Issues

5. Unnecessary Division-by-Zero Check

Location: CountriesCard.kt:182

The check if (state.maxViewsForBar > 0) is redundant because:

  • maxViewsForBar is calculated from countries.firstOrNull()?.views ?: 1L
  • It's always ≥ 1

Recommendation: Remove the conditional or document why it's needed.

6. Unreachable Code in Data Source

Location: StatsDataSourceImpl.kt:223 (already flagged by previous review)

The return statement after the when expression is unreachable. This should be removed.

7. Empty State Handling Inconsistency

Location: CountriesCard.kt:143

When countries.isEmpty(), the card shows an empty state message. However, the map and legend are hidden. Consider whether showing an empty map with a message is better UX.


Code Quality Observations

Strengths

  1. Excellent Test Coverage: CountriesViewModelTest.kt has 542 lines of comprehensive tests covering:

    • Error states
    • Success states with various data conditions
    • Period changes
    • Refresh logic
    • Edge cases (empty data, same views, etc.)
  2. Clean Architecture: Clear separation between UI, ViewModel, Repository, and DataSource layers

  3. Proper State Management: Using StateFlow and sealed classes for UI state

  4. Good Compose Practices: Proper use of modifiers, preview functions, and composable structure

  5. Parallel Data Fetching: Repository uses coroutines properly with async for fetching current and previous period data

Areas for Improvement

  1. Documentation: While code is generally clear, some complex logic (like map data building, date range calculations) could benefit from additional comments

  2. Magic Numbers: Several constants like CARD_MAX_ITEMS = 10 are well-defined, but consider whether these should be configurable

  3. Error Messages: Generic error messages like "Failed to load country data" could be more specific (network error, API error, parsing error)


Security Review

Input Validation: Country codes and names from API are handled safely
SQL Injection: Not applicable (no direct DB queries)
⚠️ XSS: WebView JavaScript enabled (see Critical Issue #1)
Data Exposure: No sensitive data logged
Authentication: Proper token handling via AccountStore


Performance Review

Lazy Loading: List only shows 10 items in card, full list in detail screen
Parallel Requests: Current and previous period fetched in parallel
Caching: ViewModel caches data for detail screen
Recomposition: Proper use of remember and collectAsState
⚠️ WebView Overhead: Loading external JS library on every map render


Testing Review

Test Coverage: Excellent ✓

  • Unit Tests: Comprehensive ViewModel tests (541 lines)
  • Repository Tests: StatsRepositoryCountryViewsTest.kt (254 lines) covering data layer
  • Edge Cases: Empty data, single country, 15+ countries, change calculations
  • Error Cases: Network errors, no site selected

Missing Tests

  1. No UI/composable tests for CountriesCard.kt
  2. No integration tests for detail screen navigation
  3. No tests for map data HTML generation

Architecture Review

Design Pattern Adherence ✓

  • MVVM: Properly implemented with ViewModel and UI state
  • Repository Pattern: Clean data abstraction
  • Dependency Injection: Hilt properly used throughout
  • Unidirectional Data Flow: UI state flows down, events flow up

Suggestions

  1. Consider extracting map HTML generation logic into a separate utility class for testability
  2. The CountriesDetailData class could be part of the UI state instead of a separate data class

Overall Assessment

Code Quality: 8/10
Test Coverage: 9/10
Architecture: 8/10
Security: 6/10 (WebView concerns)
Performance: 8/10

This is a solid implementation with good engineering practices. The main concerns are:

  1. WebView security implications (critical)
  2. Code organization for shared components (medium)
  3. Minor performance optimizations (low)

The comprehensive test coverage and clean architecture demonstrate good software craftsmanship. Once the security concerns are addressed, this will be production-ready.


Recommendations Priority

  1. High: Address WebView security or use native charts
  2. Medium: Verify shared component organization
  3. Low: Remove unreachable code in StatsDataSourceImpl.kt:223
  4. Low: Improve error messages for better debugging

| Branch: feat/CMM-1145-Create-stats-Countries-card

@adalpari
Copy link
Contributor Author

6. Unreachable Code in Data Source

Location: StatsDataSourceImpl.kt:223 (already flagged by previous review)

The return statement after the when expression is unreachable. This should be removed.

This has already been fixed...

@adalpari
Copy link
Contributor Author

. Inefficient State Calculation

Location: CountriesCard.kt:182-184

The maxViewsForBar calculation is performed on every recomposition:

val percentage = if (state.maxViewsForBar > 0) {
    country.views.toFloat() / state.maxViewsForBar.toFloat()
} else 0f

Impact: This recalculates for each country row on every recomposition.

Recommendation: Pre-calculate maxViewsForBar in the ViewModel and include it in CountriesCardUiState.Loaded (already done correctly!)

This has already been fixed...

@adalpari
Copy link
Contributor Author

1. WebView Security Risk

Location: StatsGeoChartWebView.kt:73,126

The implementation enables JavaScript and loads content from an external CDN (https://www.gstatic.com/charts/loader.js). While the HTML is loaded as base64, the JavaScript library itself is fetched from the network, creating potential security risks:

  • If Google's CDN is compromised, malicious JavaScript could execute in the app
  • No Content Security Policy restrictions
  • JavaScript is enabled globally for the WebView

Recommendations:

  1. Consider using a native Android charting library (MPAndroidChart, Vico, etc.) to eliminate WebView entirely
  2. If WebView is necessary, implement strict URL validation via WebViewClient
  3. Add CSP restrictions
  4. Consider setMixedContentMode(MIXED_CONTENT_NEVER_ALLOW)

This is also already done: 0fda461

@codecov
Copy link

codecov bot commented Jan 30, 2026

Codecov Report

❌ Patch coverage is 22.41580% with 668 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.62%. Comparing base (051c5f6) to head (2872ca5).
⚠️ Report is 1 commits behind head on trunk.

Files with missing lines Patch % Lines
...ess/android/ui/newstats/countries/CountriesCard.kt 0.00% 245 Missing ⚠️
...d/ui/newstats/countries/CountriesDetailActivity.kt 0.00% 143 Missing ⚠️
...android/ui/newstats/components/StatsSummaryCard.kt 0.00% 78 Missing ⚠️
...roid/ui/newstats/datasource/StatsDataSourceImpl.kt 0.00% 64 Missing ⚠️
...roid/ui/newstats/countries/StatsGeoChartWebView.kt 0.00% 37 Missing ⚠️
...ss/android/ui/newstats/countries/StatsMapLegend.kt 0.00% 28 Missing ⚠️
.../wordpress/android/ui/newstats/NewStatsActivity.kt 0.00% 23 Missing ⚠️
...roid/ui/newstats/countries/StatsChangeIndicator.kt 0.00% 15 Missing and 1 partial ⚠️
...rdpress/android/ui/newstats/util/StatsFormatter.kt 12.50% 4 Missing and 3 partials ⚠️
...id/ui/newstats/mostviewed/MostViewedCardUiState.kt 33.33% 6 Missing ⚠️
... and 6 more
Additional details and impacted files
@@            Coverage Diff             @@
##            trunk   #22546      +/-   ##
==========================================
- Coverage   38.73%   38.62%   -0.11%     
==========================================
  Files        2211     2219       +8     
  Lines      108268   109055     +787     
  Branches    15228    15293      +65     
==========================================
+ Hits        41937    42125     +188     
- Misses      62842    63433     +591     
- Partials     3489     3497       +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.

@adalpari adalpari marked this pull request as ready for review January 30, 2026 13:30
@adalpari adalpari requested a review from a team as a code owner January 30, 2026 13:30
@adalpari adalpari requested review from nbradbury and removed request for a team and nbradbury January 30, 2026 13:30
@nbradbury
Copy link
Contributor

@adalpari When viewing country stats for a date that has no views, I see "Unknown error":

Screenshot_20260130_085652

Logcat shows "Error fetching country views: Unknown error," but the request succeeds with HTTP 200 and this response:

{"date":"2026-01-30","country-info":[],"summary":{"views":[],"other_views":0,"total_views":0}}

@adalpari
Copy link
Contributor Author

adalpari commented Jan 30, 2026

@adalpari When viewing country stats for a date that has no views, I see "Unknown error":

Logcat shows "Error fetching country views: Unknown error," but the request succeeds with HTTP 200 and this response:

{"date":"2026-01-30","country-info":[],"summary":{"views":[],"other_views":0,"total_views":0}}

Ahhhgggg, I forgot about that case! It will probably need a change in the RS library. Will work on it and ping you back.

@adalpari
Copy link
Contributor Author

adalpari commented Jan 30, 2026

@nbradbury The bug should have been fixed now :)
Thanks for reporting it!

Screenshot_20260130_162023

@sonarqubecloud
Copy link

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 great! :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.

5 participants